- ในสภาพแวดล้อมที่เป้าหมายของการตั้งค่าเพิ่มขึ้นอย่าง Kubernetes การเพิ่มไฟล์ YAML ด้วยมือจะถึงขีดจำกัดในไม่ช้า และแนวทางการสร้างข้อมูลคอนฟิกจะเหมาะสมกว่า YAML template
- Helm chart ใช้
values.yaml และ Go template เพื่อ inject ค่า แต่พอมี ฟิลด์แบบเลือกใส่ได้, array และ map เข้ามา ภาระเรื่องเงื่อนไขและการเยื้องก็จะเพิ่มขึ้นมาก
- YAML มีกฎเรื่องช่องว่างที่เข้มงวด แต่ตัว parser ของ Helm template ไม่เข้าใจโครงสร้าง YAML ทำให้การใช้
toYaml ร่วมกับ indent นำไปสู่ การสร้างคอนฟิกที่เปราะบาง ได้ง่าย
- YAML เป็น superset ของ JSON จึงแปลงไปมาระหว่างกันได้ง่าย และ Jsonnet จัดการ การสร้างอ็อบเจกต์คอนฟิก เหมือนเขียนโค้ด ด้วยตัวแปรภายนอก, ฟิลด์แบบมีเงื่อนไข, การรวม map และการ merge อ็อบเจกต์
- kr8 ใช้ flow ที่อิง Jsonnet เพื่อสร้างและปรับแต่งคอนฟิกสำหรับ Kubernetes cluster หลายชุด โดยเลือกสร้างและแปลงอ็อบเจกต์โดยตรง แทนการประกอบสตริง YAML ที่ซับซ้อน
ความซับซ้อนของคอนฟิกเริ่มขึ้นเมื่อจำนวนไฟล์ YAML เพิ่มขึ้น
- เมื่อแอปพลิเคชันและอินฟราสตรักเจอร์ขยายเกินระดับหนึ่ง ความซับซ้อนของคอนฟิก จะเพิ่มขึ้นอย่างรวดเร็ว
- ถ้าเป้าหมายการ deploy มีแค่ 1–2 แห่ง การเขียนไฟล์คอนฟิก YAML ด้วยมือก็เพียงพอ แต่ถ้าเพิ่มมากกว่านั้น จำเป็นต้องจัดการคอนฟิกอย่างเป็นระบบ
- เหตุผลที่ต้องมีไฟล์คอนฟิกหลายไฟล์โดยมากคือ แม้จะเป็นเป้าหมายเดียวกัน แต่ค่าบางส่วนแตกต่างกัน
- การ deploy แยกตาม environment เช่น
dev, stg, prod
- การ deploy แยกตามภูมิภาค เช่น Europe, North America
- ไม่ใช่ว่าคอนฟิกทุกอย่างจะแตกต่างกันทั้งหมด แต่ถ้าความแตกต่างมากพอ ก็ควรแยกส่วนที่ใช้ร่วมกันกับส่วนที่ต่างกันออกมาจัดการ
- วงการจัดการคอนฟิกเผชิญปัญหานี้มานาน และเครื่องมือต่าง ๆ ก็ใช้ YAML ในแนวทางของตนเอง
- hiera ที่รวมอยู่ใน Puppet สามารถค้นหาตัวแปรแบบลำดับชั้นได้ จึงทรงพลังและยืดหยุ่น และช่วยลดความจำเป็นในการทำ YAML เองให้เป็นเทมเพลตได้มาก
ปัญหาของ YAML template ที่เห็นได้จาก Helm chart
- เมื่อ cloud computing และ Kubernetes ทำให้เป้าหมายของคอนฟิกขยายขึ้นไปถึงชั้นที่อยู่เหนือระบบปฏิบัติการ เครื่องมืออย่าง CloudFormation และ Helm จึงเกิดขึ้น
- Helm chart สามารถรับพารามิเตอร์ภายนอกที่กำหนดไว้ใน
values.yaml แล้ว render ออกมาได้
- ค่าสตริงแบบง่าย ๆ ค่อนข้างจัดการได้ไม่ยาก
image: "{{ .Values.image }}"
- เมื่อกำหนดค่า
image ใน values.yaml ค่านั้นจะถูกใส่เข้าไปในเทมเพลต
- แต่เมื่อเริ่มจัดการคอนฟิกที่ซับซ้อนขึ้น เช่น ฟิลด์แบบเลือกใส่ได้ ปัญหาจะใหญ่ขึ้น
{{- with .resourceGroup }}
resourceGroup: {{ . }}
{{- end }}
- ค่าแบบเลือกใส่ได้ไม่สามารถปล่อยให้ว่างไว้ได้ จึงต้องใช้เงื่อนไขและลูป และเทมเพลตก็จะรกได้ง่าย
- เมื่อต้องใส่ array หรือ map ต้องใช้
toYaml ร่วมกับ indent
{{- with .Values.podAnnotations }}
annotations:
{{ toYaml . | indent 8 }}
{{- end }}
- การเรียกฟังก์ชันที่ใช้
toYaml เพื่อแปลง YAML กลับเป็น YAML อีกทีก็ดูฝืนอยู่แล้ว แต่ปัญหาที่ใหญ่กว่าคือ การจัดการช่องว่าง
กฎช่องว่างของ YAML กับความขัดแย้งกับ template engine
- YAML มีกฎเรื่องการเยื้องและช่องว่างที่เข้มงวด
- ตัวอย่างต่อไปนี้ไม่ใช่ YAML ที่ถูกต้องหรือสมบูรณ์
something: nothing
hello: goodbye
- ถ้าคนเขียนเอง ก็อาจกด backspace สองสามครั้งเพื่อแก้ได้ แต่เมื่อสร้าง YAML ด้วยระบบเทมเพลต เรื่องนี้ไม่ง่าย
- ถ้าจำนวนไฟล์คอนฟิกเกินระดับ 5–10 ไฟล์ ก็เริ่มจำเป็นต้องใช้ การสร้างคอนฟิก มากกว่าการเขียนเอง
- หากต้องใส่ค่า
.Values.podAnnotations ลงใต้ annotations ที่ถูกเยื้องไว้แล้ว ตัวค่านั้นเองก็ต้องถูกเยื้องให้ตรงระดับพอดีด้วย
- เพราะ Go template parser ไม่เข้าใจ YAML ต่อให้พยายามเยื้อง syntax ของเทมเพลตให้อ่านง่าย ก็อาจเกิดปัญหาได้
{{- with .Values.podAnnotations }}
annotations:
{{ toYaml . | indent 6 }}
{{- end }}
- เมื่อระบบเทมเพลตต้องจัดการช่องว่างและเงื่อนไขร่วมกัน โดยที่ไม่รู้โครงสร้าง YAML การสร้างคอนฟิกที่ซับซ้อนก็จะยิ่งยากขึ้นเรื่อย ๆ
- การเขียน JSON โดยตรงก็ไม่เหมาะ เพราะไม่มี comment และมักพลาด comma ได้ง่าย ความไม่สะดวกเหล่านี้จึงเป็นเหตุให้มีการใช้ YAML
Jsonnet คือภาษาเทมเพลตข้อมูลสำหรับสร้างคอนฟิก JSON
- YAML เป็น superset ของ JSON ดังนั้นการแปลงระหว่าง JSON กับ YAML จึงทำได้ง่าย
- แอปพลิเคชันและภาษาโปรแกรมจำนวนมากสามารถ parse หรือแปลง JSON และ YAML ได้โดยพื้นฐาน
- ใน Python ก็สามารถอ่าน YAML แล้วพิมพ์ออกเป็น JSON ได้
python -c 'import json, sys, yaml ; y=yaml.safe_load(sys.stdin.read()) ; print(json.dumps(y))'
- Jsonnet เรียกตัวเองว่า ภาษาเทมเพลตข้อมูล และเป้าหมายหลักคือการสร้างคอนฟิก JSON
- ที่มาของการออกแบบ Jsonnet ดูได้ใน design rationale
การจัดการตัวแปรภายนอกและฟิลด์แบบเลือกใส่ได้
- Jsonnet สามารถใช้ตัวแปรภายนอกเพื่อ inject ค่าคอนฟิกได้
{
image: std.extVar('image'),
}
- เมื่อส่งตัวแปรภายนอกจาก CLI ก็จะได้ผลลัพธ์ JSON
jsonnet image.jsonnet -V image="my-image"
{
"image": "my-image"
}
- ฟิลด์แบบเลือกใส่ได้สามารถเขียนเป็นนิพจน์เงื่อนไขในโค้ดได้ โดยไม่ต้องยัดเงื่อนไขของเทมเพลตเข้าไปในสตริง
// define a variable - yes, jsonnet also has comments
local rg = null;
{
image: std.extVar('image'),
// if the variable is null, this will be blank
[if rg != null then 'resourceGroup']: rg,
}
- ถ้า
rg เป็น null ฟิลด์ resourceGroup จะไม่ถูกรวมอยู่ในผลลัพธ์
- หากกำหนดค่าไว้ ฟิลด์นั้นก็จะถูกพิมพ์ออกมา
การจัดการ map และอ็อบเจกต์ง่ายกว่าการเยื้อง YAML
- ในกรณีที่ต้องใส่ map ลงในคอนฟิก เช่น Kubernetes pod annotation ใน Jsonnet สามารถกำหนดค่าเป็นตัวแปรแล้ววางลงในอ็อบเจกต์ได้
local annotations = {
'nginx.ingress.kubernetes.io/app-root': '/',
'nginx.ingress.kubernetes.io/enable-cors': true,
};
{
metadata: { // annotations are nested under the metadata of a pod
annotations: annotations,
},
}
- วิธีนี้ง่ายกว่าการจัดเยื้องใน YAML template มาก
- ผลลัพธ์ที่สร้างได้คืออ็อบเจกต์ JSON ที่มี map ของ annotation อยู่ใต้
metadata.annotations
{
"metadata": {
"annotations": {
"nginx.ingress.kubernetes.io/app-root": "/",
"nginx.ingress.kubernetes.io/enable-cors": true
}
}
}
- การเพิ่ม annotation เข้าไปในอ็อบเจกต์เดิมก็ทำได้ด้วย operator
+ ใน Jsonnet
local annotations = {
'nginx.ingress.kubernetes.io/app-root': '/',
'nginx.ingress.kubernetes.io/enable-cors': true,
};
{
metadata: {
annotations: annotations,
},
} + { // this adds another JSON object
metadata+: { // I'm using the + operator, so we'll append to the existing metadata
annotations+: { // same as above
something: 'nothing',
},
},
}
- ในอ็อบเจกต์ผลลัพธ์ จะมี
something: "nothing" เพิ่มเข้ามาใน annotation เดิม
{
"metadata": {
"annotations": {
"nginx.ingress.kubernetes.io/app-root": "/",
"nginx.ingress.kubernetes.io/enable-cors": true,
"something": "nothing"
}
}
}
- ในตัวอย่างง่าย ๆ โค้ดอาจดูยาวกว่า แต่ยิ่งคอนฟิกซับซ้อนขึ้น ความสามารถในการจัดการอ็อบเจกต์ด้วยวิธีนี้ก็ยิ่งมีประโยชน์
kr8 จัดการคอนฟิก Kubernetes ด้วยแนวทาง Jsonnet
- kr8 ใช้วิธีเหล่านี้เพื่อทำให้การสร้างและปรับแต่งคอนฟิกของ Kubernetes cluster หลายชุดง่ายและไม่ซับซ้อน
- flow หลักคือ แทนที่จะประกอบ YAML template ด้วยช่องว่างและเงื่อนไข ก็สร้าง อ็อบเจกต์คอนฟิก JSON แล้วแปลงรูปตามที่ต้องการ
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ผมเบื่อการตั้งค่าที่เขียนด้วย YAML จนเต็มทีแล้ว เป็นส่วนที่เกลียดที่สุดใน GitHub Actions และแย่ยิ่งกว่าความเสถียรเสียอีก
พอเห็นเครื่องมือเจ๋ง ๆ ตัวไหนบังคับให้ใช้ไฟล์ YAML สำหรับการตั้งค่า ผมก็เริ่มระแวงทันที ภาษา config เฉพาะทางอย่าง HCL ของ Terraform หรือ ASL ของ AWS Step Functions ก็เหมือนกัน
ถ้าอยากได้ API แบบ declarative ก็ไม่เป็นไร แต่อยากให้สามารถ สร้าง declaration นั้นด้วยโปรแกรม ได้ ประสบการณ์ของการประกาศด้วยโค้ดแล้วสร้าง config ออกมาดีกว่ามาก และ AWS CDK ทำเรื่องนี้ได้ดีจริง ๆ
เราสามารถเขียนนิยาม cloud infrastructure ด้วยภาษาที่ type-safe และมี IDE support ดี ๆ ได้ โดยไม่ต้องพึ่งปลั๊กอินที่ไม่ได้อัปเดตมาตั้งแต่ 2 ปีก่อน
deno fmtมี formatter สำหรับ JSON แต่ไม่มี formatter สำหรับ YAMLJSON formatter เป็นไบนารีเดี่ยวที่รันในระดับมิลลิวินาที แต่การ auto-format YAML แทบต้องใช้ Prettier และ Prettier ก็พึ่งพา NPM ไปครึ่งหนึ่ง แถมใช้เวลาประมาณ 2 วินาทีในการเริ่มและรัน
ดังนั้นใน repository ของบริษัท ผมย้ายไฟล์ YAML ทุกไฟล์ที่เปลี่ยนเป็น JSON ได้ไปเป็น JSON ทั้งหมด และอย่างน้อยผมก็พอใจกว่ามาก ไม่มีใครบ่นเลย
editor หลายตัวรองรับ แท็ก
$schemaของ JSON ด้วย เราเพิ่มฟีเจอร์นี้ในผลิตภัณฑ์แล้ว มันยอดเยี่ยมจริง ๆ เพราะสร้างไฟล์ config ได้แค่กด tab โดยไม่ต้องอ่านเอกสารYAML ก็ทำได้ด้วย YAML language server แต่ปุ่ม tab ก็ต้องใช้กับการ indent ด้วย เลยใช้งานไม่ค่อยดี JSON เองก็ไม่ได้สมบูรณ์แบบ แต่อย่างน้อยข้อความ
"no"ก็ไม่ใช่ค่าจริงแค่ใส่ filter สักตัวเพื่อลบ comment ก่อนอ่าน config ไม่ได้หรือ? ไม่น่าจะยากกว่าการเปลี่ยนไปใช้ YAML
ที่บอกว่า YAML อ่านง่าย ผมก็ไม่ค่อยเข้าใจ ความเจ็บปวดในการจัดการ config ไม่ได้มาจากการเสียเวลาน้อยลงไม่กี่วินาทีกับการ parse วงเล็บปีกกาและวงเล็บเหลี่ยม แต่มาจากการไม่รู้ว่าอะไรผิดพลาดเพราะช่องว่างหรือ tab ที่ขาดหายไปใน config หลายร้อยบรรทัดต่างหาก
Ansible ก็ทำผิดแบบเดียวกัน และเครื่องมืออีกมากมายก็เช่นกัน
ไม่ค่อยแน่ใจว่า CDK ทำงานในแนวทางนั้นหรือเปล่า ตอนลองแตะ ๆ ดู มันค่อนข้างต่างจากประสบการณ์ “สร้าง CloudFormation” ที่ผมทำเอง และผมยังไม่ค่อยเข้าใจข้อดีของ CDK อย่างแท้จริง
รู้สึกเหมือนเปลี่ยนปัญหา YAML/template ให้กลายเป็นปัญหา inheritance/magic แทน อยากฟังประสบการณ์เพิ่มเติมจากคนที่เคยใช้ AWS CDK, Terraform CDK และ Pulumi
https://github.com/actions/runner/issues/1182
เห็นด้วยว่าเทมเพลต YAML นั้นค่อนข้างบ้าบอ แต่ก็ไม่เคยเข้าใจเลยว่าทำไมถึงไม่เลิกใช้ภาษาปลอม ๆ แล้วหันไปใช้ ภาษาโปรแกรมจริง ๆ กัน
ถ้าต้องการลอจิกที่ซับซ้อน ก็ให้ภาษาโปรแกรมสร้าง YAML/JSON/อะไรก็ได้ออกมาก็พอ Ruby, Python หรือภาษาอะไรก็ตามให้สิ่งที่ต้องการได้โดยไม่ต้องมีภาษากึ่ง ๆ แปลก ๆ อย่าง Jsonnet หรือ Go templates
แค่เขียนโค้ดไปเลย ก็จะเจอปัญหาแปลก ๆ ที่ทึบของ template engine น้อยลงมาก ใช้ภาษาจริงภาษาไหนก็ดีกว่ามาก
เคยใช้ Chef กับงานคล้าย ๆ กันมาก่อน และเพราะมันเป็น Ruby จึงดีตรงที่กำหนดลอจิกที่ต้องการ ใช้ลูปและตัวแปรที่เป็นเรื่องเป็นราวได้ง่าย
เข้าใจว่า Ansible ออกแบบมาสำหรับคนที่ไม่ใช่โปรแกรมเมอร์ แต่สำหรับคนที่คุ้นกับการเขียนโปรแกรมพื้นฐานแล้ว ไม่มีอะไรนรกไปกว่าการเอา Ansible playbook ที่มี conditional task และ loop เยอะ ๆ ไปขังไว้ในไวยากรณ์เยิ่นเย้อของ Jinja template
แต่ตอนนี้มักจะเอา parser ของ JSON/TOML/YAML มาแล้วเขียนฟังก์ชัน
readConfigและแม้แต่จุดที่ embedded interpreter เหมาะกว่าก็จัดการแบบนั้นในมุมของนักพัฒนา การเพิ่มความซับซ้อนลงในรูปแบบ config นั้นง่ายกว่าการให้การฝังภาษาที่สมบูรณ์พร้อม binding กับแอปพลิเคชัน จึงดูเหมือนว่าลืมวิธีทำไปแล้ว หรือไม่ก็ไม่เคยคิดด้วยซ้ำว่ามันเป็นไปได้
ในยุค Chef/Puppet มีหลายที่เริ่มใส่ลอจิกลงใน IaC แล้วกลายเป็นกองความวุ่นวายขนาดใหญ่ที่ทั้งอัปเกรดและดูแลรักษาไม่ได้ วิธีแบบ Chef/Pulumi ก็ทำได้ แต่ต้องมีคนที่เข้มงวดกับ style และ maintenance มาก ๆ
สำหรับทีมใหญ่และการดูแลระยะยาว ผมคิดว่าโมเดล Terraform/Puppet ดีกว่า ต่อให้ HCL น่าหงุดหงิดและการใช้ Python/TypeScript ฯลฯ ให้ความรู้สึกปลดปล่อยกว่า แต่โค้ดแบบ declarative ล้วน ๆ ช่วยกันสปาเกตตีได้มาก
พวกเขาอยากใส่องค์ประกอบการออกแบบภาษาที่กำลังฮิต อยากให้ self-hosting ได้ และอยากให้เขียนเว็บเซิร์ฟเวอร์ multithreaded ที่เร็วได้ด้วย จนมันซับซ้อนในเชิงแนวคิด
เราต้องการ ภาษาของเล่นง่าย ๆ แบบ Logo สำหรับ system engineer/DevOps จริง ๆ แล้วควรอธิบายได้ด้วยหนังสือเล่มเดียวขนาดประมาณหนังสือ K&R C
ต้องเป็น dynamic typing, มี control structure ที่เรียนได้ในสุดสัปดาห์, ไม่มี threading หรือ concurrency, ไม่มี OOP หรือ inheritance, มีการออกแบบแบบ functional/modular และมีโมเดล FFI ที่เรียกใช้และถูกเรียกจากภาษาและ framework อื่นได้ง่าย
ปัญหาคือพวก language nerd คุมตัวเองไม่อยู่และเพิ่มฟีเจอร์ไปเรื่อย ๆ แล้วสิ่งนั้นก็เข้าไปอยู่ในไลบรารีหลักและ style guide จนมือใหม่ก็ต้องเรียนรู้ทั้งหมด
ตัวผมเองก็คงอยากเพิ่มฟังก์ชันจำพวก
each/mapให้กับ array/hashmap และใส่ first-class functions กับ closure เข้าไป แต่นั่นอาจเป็นความผิดพลาดก็ได้ จริง ๆ มีภาษาฟังก์ชันแบบ immutable สำหรับ config อยู่แล้ว แต่คนกว่า 95% ที่ใช้เทมเพลต YAML ไม่ได้อยากเรียนเขียนโปรแกรมด้วยวิธีนั้น จึงยากที่จะเผยแพร่ในวงกว้างconfig จะเรียบง่ายขึ้น บริโภคได้ง่ายขึ้น และทำเอกสารได้ดีขึ้นด้วย แต่ตอนเขียนไฟล์ config ควรใช้ภาษาโปรแกรม และถ้าเป็นไปได้ควรเป็นภาษาที่มี static typing ซึ่งมี error checking, autocomplete และ inline documentation
AWS CDK เป็นตัวอย่างที่ดี การเขียน CloudFormation ล้วน ๆ นั้นทรมาน แต่ CDK ไม่ได้เอาความสามารถด้านโปรแกรมมิงไปแปะบน CloudFormation หากแต่ช่วยสร้าง CloudFormation ให้ อินพุตที่ AWS บริโภคยังคงเป็น CloudFormation ที่ค่อนข้างเรียบง่ายและเสถียร
พอเห็นชื่อเรื่องก็คิดทันทีว่าน่าจะพูดถึง Kubernetes
Kubernetes API ค่อนข้างตรงไปตรงมาและมี JSON schema ที่นิยามไว้ดี เวลาส่วนใหญ่ในการเรียน k8s ควรถูกใช้ไปกับการทำความเข้าใจวิธีใช้ API แต่ในความเป็นจริงกลับถูกใช้ไปกับการหาวิธีใช้ Helm chart
ผมไม่คิดว่า Jsonnet, Ksonnet, Nu, CUE ได้รับความนิยมมากขนาดนั้น คนส่วนใหญ่น่าจะใช้ Kustomize เหตุผลคือค่อนข้างตรงไปตรงมาและฝังมากับ
kubectlเครื่องมือที่อยากได้ควรให้การตรวจสอบชนิดข้อมูลตาม k8s schema, การตรวจสอบความถูกต้อง และคำเตือนการเลิกใช้เวอร์ชันแก่ผู้เขียน definition, ให้ผลลัพธ์เดี่ยวที่ผู้ใช้ตรวจสอบได้ง่าย, ล้มเหลวแบบ atomic หากคลัสเตอร์ไม่รองรับ object/เวอร์ชันใด ๆ และต้องฝังอยู่ใน toolchain พื้นฐาน
ถ้าให้สคริปต์ TypeScript ของ Bun หรือ Deno export ฟังก์ชันที่รับอาร์กิวเมนต์และคืนรายการ definition ก็น่าจะเข้ากันได้ดีกับ
deno compileฯลฯ แต่จะผิดเงื่อนไขว่าต้องฝังอยู่ใน toolchain พื้นฐานเราถูกปกป้องจากรายละเอียดระดับล่างก็จริง แต่พอมีปัญหา ก็ต้องรับมือกับสแต็ก abstraction ขนาดใหญ่ที่ทำให้การวินิจฉัยและดีบักยากขึ้น
การหาว่าจริง ๆ แล้วเกิดอะไรขึ้นทำได้ยากขึ้นมาก และพอพึ่งพาชั้น abstraction ก็ต้องรับภาระทั้งอัปเดตจากผู้ให้บริการและปัญหาอื่น ๆ ใน dependency graph ไปด้วย
มันทำสิ่งที่เราต้องการได้แทบไม่มีปัญหา ใช้ข้ามแพลตฟอร์มได้ และใช้ได้ข้ามหลายภาษา เคยลองฝังใน executable ของ C++, .NET, JVM มาแล้ว
configuration JSON ที่ได้สามารถใช้ร่วมกับเครื่องมือจำนวนมาก ซึ่งหาได้ยากในทางเลือกอย่าง toml/yaml/hocon/ini ฯลฯ เคยพยายามใช้ HOCON ในภาษาที่ไม่ใช่ JVM แต่ก็มักติด edge case บางอย่างเสมอ
แต่พอจัดการระบบใหญ่จริง ๆ สุดท้ายก็หลีกเลี่ยงข้อดีของ template ไม่ได้
ตลกดีที่นักพัฒนาใช้ความคิดน้อยแค่ไหนกับวิธีจัดการ configuration ให้ถูกต้อง
มันดูเหมือนเป็นแค่ชุดของ key กับ value ที่ถูกเก็บในไฟล์หรือสร้างจากโค้ด แต่จริง ๆ แล้วนี่แหละคือทั้งหมด มันคือ การเขียนโปรแกรมเอง
ทุกอย่างคือ configuration และอาร์กิวเมนต์ของฟังก์ชันทุกตัวก็เป็น configuration ชนิดหนึ่ง configuration ทั้งหมดในไฟล์ภายนอกสุดท้ายก็จะกลายเป็นอาร์กิวเมนต์ของฟังก์ชันไม่ทางใดก็ทางหนึ่ง
ปัญหาคือ representation แบบข้อความธรรมดาของโค้ด ไฟล์ configuration แบบ declarative ดูดีเพราะเห็นทุกอย่างได้ในที่เดียว แต่พอทำ configuration ให้เป็นโปรแกรม ก็หาจุดที่ต้องแก้ได้ยาก
ถ้าโค้ดรันแบบเรียลไทม์เพื่อแสดง representation ของ configuration สุดท้าย และสามารถ trace ได้ว่าค่า configuration สุดท้ายแต่ละค่าถูกสร้างขึ้นมาอย่างไร ก็จะไม่เป็นปัญหา แต่ถึงฟีเจอร์นี้จะค่อนข้างเรียบง่าย ก็ไม่มีระบบที่ออกแบบมาแบบนั้นเลย configuration มักเป็นสิ่งที่คิดทีหลังเสมอ
ถ้าขยายแนวคิดนี้ไปสู่การเขียนโปรแกรมทั้งหมด เราควรเห็นโค้ดทั้งหมดที่พึ่งพาค่า configuration หนึ่งค่า รวมถึงการแปลงค่าของมันได้
อีกอย่าง configuration ส่วนใหญ่มีลักษณะเป็น relational/graph ดังนั้นอาจเหมาะกว่าที่จะเก็บไว้ในฐานข้อมูลกลาง ค่า configuration ต่าง ๆ มีความเกี่ยวข้องกัน ดังนั้นควรดู configuration ผ่าน database/graph editor
พอออกจากข้อความธรรมดา ทุกอย่างจะเริ่มเรียบง่ายขึ้นมาก แต่ฟีเจอร์ของภาษาที่กล่าวไปก่อนหน้าก็ยังจำเป็นอยู่
configuration เหล่านี้พยายามใช้ naming convention เพื่อจับกลุ่มตัวแปรที่เกี่ยวข้องกัน
จริง ๆ แล้วอยากย้ายไปใช้โครงสร้างข้อมูลแบบซ้อนกัน อาจเป็น JSON แต่เหล่าวิศวกรไม่ยอมเขียนโค้ดเด็ดขาด ดังนั้น configuration-as-code จึงเป็นไปไม่ได้ และยังมีข้อเสียที่กล่าวไปข้างต้นด้วย
ความคิดถัดมาคือต้องมีวิธีแสดงและแก้ไข configuration ให้ดีกว่านี้ คิดถึง UI แบบภาพ ที่ให้สำรวจ representation ของผลิตภัณฑ์สุดท้าย เลือกชิ้นส่วน แล้วแก้พารามิเตอร์ด้วยวิธีนั้น
อยากรู้ว่าแนวทางนี้ถูกต้องไหม ถ้าไม่ใช่ก็อยากให้ช่วยอธิบายเพิ่มอีกหน่อย แก่นของแอปพลิเคชันนี้คือ configuration
ที่แย่กว่านั้นคือ ในที่อย่าง CI/CD นั้น YAML แทบจะกลายเป็น ภาษาโปรแกรมมิง ไปแล้ว แถมยังเป็นภาษาที่เยิ่นเย้อมาก ไม่ตรงกับสัญชาตญาณ สเปกแย่ และแตกต่างกันไปตามผู้ให้บริการ
แม้จะมี DTD และการตรวจสอบ XML ก็ยังมีลักษณะที่คุ้นเคยคือไปพังทีหลัง และให้ข้อความผิดพลาดที่ตีความยาก
ตอนนั้นความหงุดหงิดจำนวนมากมุ่งไปที่ XML แต่เมื่อเห็นนรก YAML ในช่วงกลางทศวรรษ 2020 ก็ชัดว่าปัญหาไม่ได้อยู่ที่ markup language เอง
ผมเกลียดจริง ๆ กับการฝัง logic ไว้ที่ไหนสักแห่งใน YAML template
[0] https://tanzu.vmware.com/developer/guides/ytt-gs/
น่าเศร้าจริง ๆ ที่ Helm ชนะ ผมทำงานเกี่ยวกับ k8s โอเพนซอร์สในบริษัท และผู้ใช้ 100% ขอให้ทำ Helm chart ให้ สุดท้ายก็เลยต้องทำ
ทำแล้วหดหู่มาก ชื่อไฟล์ดูเหมือน
foo.yamlแต่จริง ๆ แล้วไม่ใช่ YAML ทำให้เอดิเตอร์ช่วยอะไรไม่ได้ ต้องปล่อยข้อมูลทั้งหมดผ่านindent 4เพื่อให้การจัดแนว YAML ถูกต้องสิ่งที่หดหู่ที่สุดคือ ต้องเอาฟีเจอร์ทั้งหมดของ Kubernetes มาเปิดเผยซ้ำในแบบของตัวเอง ถ้ามีใครอยากเพิ่ม
deployment.spec.template.spec.fooBarsก็ต้องเพิ่มdeploymentFooBarsในvalues.yamlแล้วเชื่อมมันเข้าด้วยกัน ต้องทำซ้ำแบบนี้กับทุกฟีเจอร์เป็นกรณีที่แนวคิด “ของแย่แต่ดีกว่า” พังไปจริง ๆ ผมเองก็เคยทำเรื่องน่ากลัวอย่าง
sed -e s/$FOO/foo/gเพื่อพยายามทำ template และ Helm ก็คงเริ่มมาแบบนั้นเหมือนกัน ผลลัพธ์คือเละเทะส่วนตัวใช้ Kustomize มาตั้งแต่ก่อนมันเข้าไปอยู่ใน
kubectlและก็พอใจกับมันมาตลอด มันมีจุดแปลก ๆ เยอะ แต่ อย่างน้อยมันก็เข้าใจความหมายของ object ที่สร้างขึ้น จึงช่วยประหยัดเวลาได้Jsonnet ดีกว่ามาก ในฐานะส่วนหนึ่งของแอป k8s ของเรา เราจัดส่ง deployment ของ Envoy สำหรับ traffic routing ที่ซับซ้อนมาด้วย และ config ของ Envoy แม้จะยืดยาว แต่จัดการด้วย Jsonnet ได้ง่าย: https://github.com/pachyderm/pachyderm/blob/master/etc/gener...
กำลังพิจารณาอย่างจริงจังว่าจะ transpile jsonnet เป็นภาษา template ของ Go แล้ว implement ทั้งหมดด้วย Jsonnet อย่างน้อยก็น่าจะพอบำรุงรักษาได้ขึ้นมาบ้าง และเพราะ
helm installจะทำงานได้ตามปกติ จึงคงไม่มีใครรู้แต่ผมคิดว่า Helm จะเป็นจุดจบของ Kubernetes ถ้ามีเครื่องมือคู่แข่งสำหรับจัดสรรคอมพิวเตอร์/รันคอนเทนเนอร์ที่มาพร้อมภาษาสำหรับ config ที่เหมาะสม ทุกคนคงย้ายไปในชั่วข้ามคืน
sed -e s/$FOO/foo/gลองดู envsubst ซึ่งเป็นทางออกที่มาตรฐานกว่าและดีกว่าถ้าเป็นเรื่องการทำ template หรือแก้ไข Helm chart ด้วย jsonnet, Tanka ก็อาจช่วยได้: https://tanka.dev/helm
แต่ที่ที่ผมเคยทำงานยังกลัวการเปลี่ยนแปลงอยู่ เลยยังใช้ tf/hcl กับ helm ตามเดิม อย่างน้อยในโปรเจกต์ส่วนตัวก็ยังพอหายใจหายคอได้บ้าง
ผมมองว่าตรงนี้มีปัญหาอยู่ แต่ก็ไม่แน่ใจว่าคนประเภทที่เลือก YAML เป็นภาษา config จะมองว่านี่เป็นปัญหาหรือเปล่า
มีความขัดแย้งโดยตรงระหว่างการแทนข้อมูลที่เน้นมนุษย์กับการแทนข้อมูลที่เน้นคอมพิวเตอร์ คอมพิวเตอร์ชอบอะไรที่คล้าย Lisp ส่วนมนุษย์ชอบอะไรที่คล้าย Python
ถ้าเป็นคนที่อยากจัดการ config ของ Kubernetes ด้วยโปรแกรม ก็คงหงุดหงิดอยู่ลึก ๆ ที่ Kubernetes ใช้ YAML แต่ชุมชน Kubernetes ดูเหมือนจะเป็นคนฝั่ง YAML เป็นหลัก แล้วทำไมพวกเขาจะต้องสนใจว่าพอมี programming logic เข้ามา งานกับไฟล์ config จะกลายเป็นฝันร้าย
ข้อเสียของ YAML ก็คือสถานการณ์แบบนี้ และผมคิดว่าคนที่เกี่ยวข้องกับ k8s โดยรวมก็ฉลาดพอที่จะคาดการณ์เรื่องระดับนี้ได้
คำพูดที่ว่า “YAML เป็น superset ของ JSON” ผมไม่คิดว่าถูกต้องในทางปฏิบัติ ไม่ว่าผู้เขียนสเปกจะเขียนอะไรไว้ในเอกสารก็ตาม ถ้าเปลี่ยน config YAML ทั้งหมดให้เป็น JSON ทีม DevOps คงโกรธแน่
รูปแบบข้อมูลทั้งสองอาจแทนความหมายเดียวกันได้ แต่ภาษาทั้งหมดที่ compile ไปเป็น CPU architecture เดียวกันก็เป็นแบบนั้นเหมือนกัน ในงานจริง JSON กับ YAML เป็นคนละอย่างกัน และการเอามาปนกันไม่ใช่ความคิดที่ดี
แน่นอนว่าผู้คนก็ลงเอยด้วยการเขียนด้วยมือ และพอทนไม่ไหวก็เริ่มใส่ template เข้าไป ดูเหมือนเรื่องจะเป็นแบบนี้เสมอ
ข้อความที่เขียนด้วยมือไม่ได้ถูกแทนที่ด้วยข้อความ serialization ของ config ที่เครื่องสร้างขึ้น แต่เดิมทีแล้วกลับถูกแทนที่ด้วยข้อความที่ยังเขียนด้วยมือเหมือนเดิม เพียงแต่มี template แปะเข้าไป
หลักส่วนตัวของผมคือไม่ควรใช้ string interpolation เพื่อสร้างโค้ดที่เครื่องอ่าน เราได้เห็นผลลัพธ์ของทั้ง SQL injection และ cross-site scripting กันมาแล้ว ตราบใดที่เรายังคอยป้อนข้อความตามอำเภอใจเข้าไปให้อินเทอร์พรีเตอร์ เรื่องแบบนี้ก็จะเกิดขึ้นต่อไป
ดังนั้นแม้แต่ตอนสร้าง HTML ผมก็คิดว่าไม่ควรใช้ไฟล์เทมเพลต
ทางเลือกแทนภาษาเทมเพลตสำหรับ HTML มี Haml ของ Ruby และ Pug ของ JavaScript ภาษาเหล่านี้มีวิธีที่กำหนดไว้ชัดเจนในการระบุทั้งทรีของแท็ก แอตทริบิวต์ และโหนดข้อความ
ถ้าไม่ชอบการย่อหน้าที่มีความหมายแบบ Python ใน JavaScript ก็มี JSX ส่วนที่ดูเหมือน HTML ของ JSX จะถูกคอมไพล์เป็นนิพจน์
createElementที่สร้างทรีเอกสารเว็บ และทรีนั้นสามารถส่งออกเป็น HTML ได้เมื่อจำเป็นแม้ Haml, Pug, JSX จะส่งออก HTML ได้ แต่ก็ไม่ใช่ภาษาเทมเพลต เช่นเดียวกัน
JSON.stringify(myObj)ก็ไม่ใช่ภาษาเทมเพลตสำหรับ JSONโค้ดที่เครื่องอ่านควรถูกสร้างด้วยเครื่องมือที่เข้าใจและใช้ประโยชน์จากโครงสร้างที่รู้จักของภาษาปลายทาง หากเป็นไปได้
Haml เป็นระบบเทมเพลตเพื่อหลีกเลี่ยงโค้ดอินไลน์ในเอกสารเว็บและทำให้ HTML สะอาดขึ้น ส่วน Pug เป็นเอนจินเทมเพลตที่มีฟีเจอร์ครบสำหรับ Node.js
ผมเห็นด้วยได้ว่า JSX ไม่ใช่ภาษาเทมเพลตอย่างเคร่งครัด
สุดท้ายทั้งหมดนี้ถูกคอมไพล์เป็น HTML เพียงแต่มันไม่ใช่ string interpolation แต่เป็นภาษาที่ถูกพาร์สเป็น syntax tree และเรนเดอร์เป็น HTML บนพื้นฐานของความเข้าใจภายในเกี่ยวกับโครงสร้างที่ถูกต้อง
เทมเพลต YAML คือ string interpolation แบบหรู และไม่ใช่ภาษาเทมเพลต หรืออย่างน้อยก็เป็นภาษาเทมเพลตที่ทำมาแย่มาก
กฎส่วนตัวของผมคือทุกครั้งที่ค่าใด ๆ ถูกใส่ลงในสตริง จะต้องเข้ารหัสให้ถูกต้องเสมอ
ผมเคยเขียนเรื่องนี้ไว้ก่อนหน้านี้: https://kevincox.ca/2022/02/08/escape-everything/
สรุปคือสตริงทุกตัวมีรูปแบบที่ต้องรักษาไว้ เช่น HTML, SQL, เอาต์พุตเทอร์มินัลที่มนุษย์อ่าน เมื่อใส่ค่าใด ๆ ลงในสตริง จะต้องเข้ารหัสให้ถูกต้องตามรูปแบบนั้น แต่เราแทบไม่เคยทำเช่นนั้น
เรากำลังย้ายไปใช้ cuelang [1] โดยส่วนตัวผมคิดว่าออกแบบมาดีกว่า Jsonette
Kubernetes มีการปรับสถานะให้ตรงกันอยู่แล้ว ดังนั้นสิ่งที่ขาดไปในการตั้งค่านี้มีแค่การลบ ซึ่งตอนนี้แก้ได้ด้วยฟีเจอร์ prune [2]
[1] https://cuelang.org/docs/integrations/k8s/
[2] https://kubernetes.io/blog/2023/05/09/introducing-kubectl-ap...
ข้อความ error บางอันตีความยากนิดหน่อย แต่เพราะมันจับ error ล่วงหน้าให้ได้เยอะมาก เลยพอรับได้ ตอนนี้ช่วงไม่กี่ครั้งที่ต้องเขียน yaml เอง เมื่อเทียบกันแล้วรู้สึกน่าเบื่อมาก
เวลามีเรื่องแบบนี้ ผมมักจะแทรกว่า “เคยได้ยินเรื่อง CUELang ผู้กอบกู้ของเราไหม?”: https://cuelang.org/
มันยังไม่ Turing-complete แต่มีความสามารถในการแสดงออกเพียงพอสำหรับการกำจัดความซ้ำซ้อน และสามารถนิยามสคีมากับข้อมูลด้วยภาษาเดียวกันในไฟล์เดียวกันหรือแยกไฟล์ได้ รวมถึงมี union type ด้วย
สามารถสร้าง YAML หรือ JSON ได้ และสามารถตรวจสอบความถูกต้องของตัวมันเองหรือไฟล์ YAML/JSON ได้
ข้อเสียใหญ่ที่สุดคือ implementation ปัจจุบันมีแค่ Go ดังนั้นอาจต้องใช้ subprocess หรือ FFI
จากนั้นมันสร้างไฟล์ JSON สำหรับแต่ละแอปพลิเคชัน มีเครื่องมือบางตัวสร้างนิยาม XML แล้วนิยามนั้นถูกนำไปใช้กับ XLS ที่สถาปนิกเป็นเจ้าของ จากนั้นจึงปล่อย YAML ที่จะนำไปใช้กับ Helm chart ออกมา
chart จะดีพลอย k8s client และ client นั้นโต้ตอบกับคลัสเตอร์หลักผ่าน API ด้วย JSON
ใช้เวลาพอสมควร แต่เรากำลังใช้เครื่องมือที่ดีที่สุดสำหรับแต่ละงาน