- Kubernetesのように設定対象が増える環境では、YAMLファイルを直接増やしていくやり方はすぐに限界に達し、YAMLテンプレートよりも設定データを生成するアプローチの方が適している
- Helm chartは
values.yamlとGoテンプレートで値を注入するが、オプションフィールド、配列、マップが入った瞬間に条件分岐とインデントの負担が大きくなる
- YAMLは空白ルールが厳密だが、HelmのテンプレートパーサーはYAML構造を理解しないため、
toYamlとindentの組み合わせは容易に脆い設定生成につながる
- YAMLはJSONの上位集合なので相互変換が単純であり、Jsonnetは外部変数、条件付きフィールド、マップ結合、オブジェクトマージによって設定オブジェクトの生成をコードのように扱う
- kr8はJsonnetベースの流れで複数のKubernetesクラスタ設定を作成・操作し、複雑なYAML文字列の組み立てではなくオブジェクトを直接生成・変形する方を選んでいる
設定の複雑さはYAMLファイル数が増えたときに始まる
- アプリケーションとインフラが一定規模を超えると、設定の複雑さは急速に増していく
- デプロイ対象が1〜2個ならYAML設定ファイルを直接書いても十分だが、それ以上に増えるなら設定を体系的に管理する必要がある
- 複数の設定ファイルが必要になる理由は、たいてい同じ対象でも一部の値が異なるからである
dev、stg、prodのような環境別デプロイ
- Europe、North Americaのようなリージョン別デプロイ
- すべての設定が異なるわけではないが、差分が十分に大きいなら共通部分と異なる部分を分けて管理する必要がある
- 設定管理の分野ではこうした問題を長く扱ってきており、さまざまなツールがそれぞれのやり方でYAMLを活用してきた
- Puppetに含まれるhieraは階層的に変数を参照できるため強力で柔軟であり、YAML自体をテンプレート化する必要性を大きく減らしてくれる
Helm chartで露呈するYAMLテンプレートの問題
- クラウドコンピューティングとKubernetesによって設定対象がOSより上のレイヤーにまで広がると、CloudFormationやHelmのようなツールが登場した
- Helm chartは
values.yamlで定義した外部パラメータを受け取ってレンダリングできる
- 単純な文字列値は比較的簡単である
image: "{{ .Values.image }}"
values.yamlにimage値を指定すれば、その値がテンプレートに入る
- オプションフィールドのような、より複雑な設定を扱い始めると問題が大きくなる
{{- with .resourceGroup }}
resourceGroup: {{ . }}
{{- end }}
- オプション値は空のままにはできないため、条件分岐やループが必要になり、テンプレートはすぐに煩雑になる
- 配列やマップを入れるときは
toYamlとindentを組み合わせなければならない
{{- with .Values.podAnnotations }}
annotations:
{{ toYaml . | indent 8 }}
{{- end }}
toYamlでYAMLを再びYAMLに変換する関数呼び出しも不自然だが、さらに大きな問題は空白処理である
YAMLの空白ルールとテンプレートエンジンの衝突
- YAMLはインデントと空白のルールが厳密である
- 次の例は有効でも完全でもないYAMLである
something: nothing
hello: goodbye
- 人が直接書くならバックスペースを数回押して直せるが、テンプレートシステムでYAMLを生成するときはそう単純ではない
- 設定ファイルが5〜10個を超える規模になるなら、直接記述より設定生成が必要になる
.Values.podAnnotationsの値をすでにインデントされたannotationsの下に入れるには、値そのものも正確なレベルでインデントしなければならない
- GoテンプレートパーサーはYAMLを理解しないため、テンプレート構文を見やすくインデントしようとしても問題が起きる
{{- with .Values.podAnnotations }}
annotations:
{{ toYaml . | indent 6 }}
{{- end }}
- テンプレートシステムがYAML構造を知らないまま空白と条件分岐を一緒に扱うと、複雑な設定生成はますます難しくなる
- JSONを直接書くやり方も、コメントがないことやカンマの抜け漏れの問題があるため適しておらず、こうした不便さゆえにYAMLが使われるようになった
JsonnetはJSON設定を生成するデータテンプレート言語である
- YAMLはJSONの上位集合なので、JSONとYAMLの相互変換は単純である
- 多くのアプリケーションやプログラミング言語はJSONとYAMLを標準的にパースまたは変換できる
- PythonでもYAMLを読み込んでJSONとして出力できる
python -c 'import json, sys, yaml ; y=yaml.safe_load(sys.stdin.read()) ; print(json.dumps(y))'
外部変数とオプションフィールドの処理
- Jsonnetは外部変数を使って設定値を注入できる
{
image: std.extVar('image'),
}
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フィールドは結果に含まれない
- 値を指定すればそのフィールドが出力される
マップとオブジェクト操作はYAMLのインデントより単純である
- 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テンプレートでインデントを合わせるよりはるかに単純である
- 生成結果は
metadata.annotationsの下にannotationマップが入ったJSONオブジェクトである
{
"metadata": {
"annotations": {
"nginx.ingress.kubernetes.io/app-root": "/",
"nginx.ingress.kubernetes.io/enable-cors": true
}
}
}
- 既存オブジェクトにannotationを追加する作業も、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',
},
},
}
- 結果オブジェクトには既存のannotationに
something: "nothing"が追加される
{
"metadata": {
"annotations": {
"nginx.ingress.kubernetes.io/app-root": "/",
"nginx.ingress.kubernetes.io/enable-cors": true,
"something": "nothing"
}
}
}
- 単純な例ではコードの方が長く見えるかもしれないが、設定が複雑になるほど、このようにオブジェクトを操作する機能は有用になる
kr8はJsonnet方式でKubernetes設定を扱う
- kr8は複数のKubernetesクラスタの設定を簡単かつシンプルに作成・操作するために、こうした方法を使っている
- 中核となる流れは、YAMLテンプレートを空白と条件分岐で組み立てる代わりに、JSON設定オブジェクトを生成し、必要な形に変形することである
1件のコメント
Hacker News の意見
YAML で書く設定にはもう完全にうんざりしている。GitHub Actionsで一番嫌いな部分で、安定性の問題よりもさらに悪い。
便利なツールが設定に YAML ファイルを要求してくるのを見ると、すぐ不安になる。Terraform の HCL や AWS Step Functions の ASL のような独自設定言語も同じ。
宣言型 API が欲しいというのは構わないが、その宣言をプログラムで生成できるようにしてほしい。コードで宣言して生成する設定のほうがずっと良い体験だったし、AWS CDK はそれを本当にうまくやっていた。
型安全な言語と優れた IDE サポートでクラウドインフラ定義を書けるし、2年前から更新されていないプラグインに頼らなくてもよい。
deno fmtには JSON フォーマッターがあるのに YAML フォーマッターがないことだった。JSON フォーマッターはミリ秒単位で動く単一バイナリだが、YAML の自動フォーマットは実質 Prettier を使うしかなく、Prettier は NPM の半分に依存し、起動と実行に2秒ほどかかる。
そこで会社のリポジトリで JSON に変えられる YAML ファイルはすべて JSON に移したところ、少なくとも自分はずっと満足している。誰も文句は言わなかった。
複数のエディタは JSON の
$schemaタグにも対応している。この機能を製品に追加したところ、ドキュメントを読まなくても Tab を押すだけで設定ファイルを作れるので本当に良い。YAML でも YAML language server で可能だが、Tab キーはインデントにも必要なので使い勝手はいまいち。JSON も完璧ではないが、少なくともテキストの
"no"が true になることはない。設定を読む前にコメントを取り除くフィルタを一つ挟めばいいのでは? YAML に変えるより難しいはずがない。
YAML は読みやすいという話もよく理解できない。設定を扱う苦痛は、中括弧や角括弧を数秒少なく解析できることではなく、数百行の設定の中で抜けたスペースやタブのせいで何が間違っているのか分かりにくいところから来る。
Ansible も同じ間違いをしているし、数多くのツールも同様だ。
CDK がそういう方式で動くのかはよく分からない。少し触ってみた限りでは、自分が作った「CloudFormation 生成」の体験とはかなり違っていて、CDK の良さを十分には理解できなかった。
YAML/テンプレート問題を継承/マジック問題に置き換えているように感じた。AWS CDK、Terraform CDK、Pulumi を使った人たちの経験をもっと聞きたい。
https://github.com/actions/runner/issues/1182
YAML テンプレートがかなり狂ったやり方だという点には同意するが、なぜ偽物の言語をやめて本物のプログラミング言語を使わないのかは、いつも理解できない。
複雑なロジックが必要なら、プログラミング言語で YAML/JSON/何でも生成すればいい。Ruby、Python、その他どんな言語でも、Jsonnet や Go テンプレートのような妙な疑似言語なしに必要なものを提供してくれる。
ただコードを書けば、テンプレートエンジンの不透明で奇妙な問題にずっと巻き込まれにくくなる。どんな本物の言語を使っても、はるかにましになる。
似たような作業で Chef を使ったことがあるが、Ruby なので望むロジックを簡単に定義でき、ループやちゃんとした変数を使えるのがよかった。
Ansible が非プログラマー向けに設計されているのは理解しているが、基本的なプログラミングに慣れた人にとって、条件付きタスクや反復が多い Ansible プレイブックを Jinja テンプレートの冗長な構文の中に閉じ込めることほどの地獄はない。
ところが今は、JSON/TOML/YAML パーサーを持ってきて
readConfig関数を作るのが普通で、組み込みインタプリタのほうが適している場面でもそう処理している。開発者にとっては、完全な言語埋め込みとアプリケーションバインディングを提供するより、設定フォーマットに複雑さを足すほうが簡単だ。だから、その方法自体を忘れてしまったか、可能だという発想すらないように見える。
Chef/Puppet の時代に IaC にロジックを入れ始め、アップグレードも保守もできない巨大な混沌になった場所は多かった。Chef/Pulumi 方式も可能ではあるが、スタイルと保守に非常に厳格な人が必要になる。
大きなチームと長期保守には Terraform/Puppet モデルのほうがよいと思う。HCL がいらだたしく、Python/TypeScript などを使うほうが解放感があるとしても、純粋な宣言型コードは多くのスパゲッティを防いでくれる。
いま流行している言語設計要素を入れたがり、セルフホスティングを望み、高速なマルチスレッド Web サーバーも書けるようにしようとするため、概念的に複雑になる。
システムエンジニア/DevOps 向けのLogo のような単純なおもちゃ言語が必要だ。本来は K&R の C 本くらいの大きさの本 1 冊で説明できるべきだ。
動的型付け、週末で学べる制御構造、スレッドや並行性なし、オブジェクト指向や継承なし、関数型/モジュール型の設計、そして他の言語やフレームワークから簡単に呼び出し・呼び出されできる FFI モデルが必要だ。
問題は、言語オタクが自分を抑えられず機能を追加し続け、それがコアライブラリやスタイルガイドに入り、初心者もすべて学ばなければならなくなる点にある。
自分でも配列/ハッシュマップに
each/map系の関数を追加し、第一級関数やクロージャを入れたくなるだろうが、それは間違いかもしれない。すでに設定用の不変な関数型言語はあるが、テンプレート YAML を使う 95% 以上の人はそのような方式でプログラミングを学びたがらないため、広く普及しにくい。設定が単純になり、利用しやすくなり、文書化もしやすくなる。しかし設定ファイルを書くときはプログラミング言語を使うべきで、できればエラーチェックや自動補完、インラインドキュメントを提供する静的型付け言語がよい。
AWS CDK がよい例だ。純粋な CloudFormation を書くのは苦痛だが、CDK は CloudFormation にプログラミング機能を付け足すのではなく、CloudFormation を生成してくれる。AWS が消費する入力は、今でも比較的単純で安定した CloudFormation である。
タイトルを見た瞬間、Kubernetes の話だと思った
Kubernetes API はかなり直感的で、よく定義された JSON スキーマがある。k8s を学ぶ時間の大半は API の使い方を理解することに使うべきなのに、実際には Helm チャートの使い方を突き止めることに費やしている
Jsonnet、Ksonnet、Nu、CUE がそこまで大きな人気を得たとは思わない。多くは Kustomize を使っているようで、その理由は比較的直感的で
kubectlに組み込まれているからだと思う欲しいツールは、定義の作成者に k8s スキーマに対する型チェック、検証、バージョン廃止の警告を提供し、ユーザーが簡単に検査できる単一の成果物を出力し、クラスタが何らかのオブジェクト/バージョンをサポートしていなければアトミックに失敗し、標準ツールチェーンに組み込まれているべき
Bun や Deno の TypeScript スクリプトが引数を受け取る関数をエクスポートし、定義のリストを返すようにすれば、
deno compileなどと相性がよさそうだが、標準ツールチェーン組み込みという条件に反する低レベルの詳細からは守られるが、問題が起きると、診断とデバッグを難しくする巨大な抽象化スタックを相手にしなければならない
実際に何が起きているのかを突き止めるのがはるかに難しくなり、抽象化レイヤーに依存するようになって、提供者が出すアップデートや依存関係グラフ内の別の問題まで背負い込むことになる
必要なことはほぼ問題なくこなしてくれるし、クロスプラットフォームで、複数の言語にまたがって使える。C++、.NET、JVM の実行ファイルに組み込んだことがある
結果として得られる JSON 設定は、代替の toml/yaml/hocon/ini などでは見つけにくい膨大なツール群と組み合わせて使える。HOCON を JVM 以外の言語で使おうとしたが、いつも何かしらの境界ケースに引っかかった
だが実際に大規模なシステムを管理していると、結局テンプレートの利点は避けられない
開発者が設定を適切に扱う方法について、いかに少ししか考えていないかを見ると笑ってしまう
単にファイルに保存されたりコードで生成されたりするキーと値の束のように見えるが、実はそれがすべてだ。プログラミングそのものだ
すべては設定であり、あらゆる関数引数も一種の設定だ。外部ファイルにあるすべての設定は、最終的に何らかの形で関数引数になる
問題はコードのプレーンテキスト表現にある。宣言的な設定ファイルはすべてを一か所で見られるのでよさそうに見えるが、設定をプログラムにすると、どこを変更すべきか見つけにくくなる
コードがリアルタイムに実行されて最終設定の表現を示し、それぞれの最終設定値がどのように生成されたかを追跡できるなら問題にはならない。ところがこの機能はかなり単純なのに、そのように設計されたシステムがない。設定はいつも後回しだ
この概念をプログラミング全体に拡張すると、1つの設定値に依存するすべてのコードとその変換を見られるべきだ
また、ほとんどの設定はリレーショナル/グラフ的なので、中央データベースに置いたほうがよい場合もある。異なる設定値同士は互いに関連している。だからデータベース/グラフエディタで設定を見るべきだ
プレーンテキストから離れるとずっと単純になり始めるが、先ほど述べた言語機能も依然として必要だ
設定は、関連する変数をまとめるために命名規則を使おうとしている
本物のネストしたデータ構造、おそらく JSON に移行したいが、エンジニアたちはコードを絶対に書きたがらないので、コードとしての設定は不可能だ。前述の欠点もある
次の考えは、設定をよりよく表示し、修正する方法が必要だということだ。最終製品の表現を探索し、部品を選んでその方法でパラメータを修正するビジュアル UIを考えていた
こういう方向性が正しいのか気になる。違うなら、もう少し説明してほしい。このアプリケーションの核心は設定なのだ
さらに悪いのは、CI/CD のような場所では YAML がほとんどプログラミング言語になる点だ。しかも非常に冗長で、直感的でなく、仕様がひどく、ベンダーごとに異なる言語だ
DTD と XML 検証があったにもかかわらず、遅れて爆発し、解釈しにくいエラーメッセージを出すというおなじみの性質があった
当時の多くの苛立ちは XML に向けられていたが、2020年代半ばの YAML 地獄を見ると、問題はマークアップ言語そのものではなかった
YAML テンプレートのどこかにロジックを埋め込むのは本当に嫌だ
[0] https://tanzu.vmware.com/developer/guides/ytt-gs/
Helm が勝ってしまったのは本当に悲しい。会社でオープンソースの k8s 関連の仕事をしているが、ユーザーの 100% が Helm チャートを作ってほしいと言うので、結局作らざるを得なかった。
作業していて惨めになる。ファイル名は
foo.yamlのようなのに、実際には YAML ではないのでエディタが助けてくれない。すべてのデータをindent 4に流し込んで YAML の整列を合わせなければならない。いちばん気が滅入るのは、Kubernetes の機能を全部、自分たちのやり方でもう一度公開し直さなければならない点だ。誰かが
deployment.spec.template.spec.fooBarsを追加したいと言えば、values.yamlにdeploymentFooBarsを追加して接続しなければならない。これをすべての機能で繰り返す。まさに「悪いもののほうが良い」が悪い方向に転がった例だ。自分もテンプレートを実装しようとして
sed -e s/$FOO/foo/gみたいなひどいことをしたことがあるし、Helm もたぶんそうやって始まったのだろう。結果はめちゃくちゃだ。個人的には
kubectlに入る前から Kustomize を使っていて、いつもかなり満足していた。変なところは多いが、少なくとも生成するオブジェクトの意味を理解してくれるので時間を節約できる。Jsonnet のほうがはるかに良い。自分たちの k8s アプリの一部として、複雑なトラフィックルーティングのための Envoy デプロイを同梱しているが、Envoy 設定は冗長でも Jsonnet だと扱いやすい: https://github.com/pachyderm/pachyderm/blob/master/etc/gener...
本気で jsonnet を Go テンプレート言語にトランスパイルして、全部 Jsonnet で実装することを検討している。少なくとも少しは保守可能になるし、
helm installはそのまま動くので誰にも分からないだろう。だが Helm は Kubernetes の終わりになる気がする。どこかの競合する計算資源割り当て/コンテナ実行ツールが、設定のためのまともな言語を持って出てきたら、一夜にして乗り換えられるだろう。
sed -e s/$FOO/foo/gを使いたくなったときは、もう少し標準的でましな解決策として envsubst を確認するとよい。jsonnet で Helm チャートをテンプレート化したり修正したりする話なら、Tanka も役に立つかもしれない: https://tanka.dev/helm
ただ、自分が働いた場所では今も変化を怖がって、tf/hcl と helm をそのまま使っている。少なくとも個人プロジェクトでは少し息がつける。
ここには問題があると思う。ただ、設定言語に YAML を選ぶようなタイプの人が、これを問題だと見るかはよく分からない。
人間中心のデータ表現とコンピュータ中心のデータ表現のあいだには、直接的な衝突がある。コンピュータは Lisp に似たものを好み、人間は Python に似たものを好む。
Kubernetes 設定をコンピュータで操作したい人なら、Kubernetes が YAML を使っていることに地味にいら立つはずだ。だが Kubernetes コミュニティは主に YAML 側の人たちに見えるので、プログラミングロジックが入ると設定ファイル作業がひどくなることを、なぜ気にするのだろうかと思う。
YAML の欠点がまさにこの状況であり、k8s 関係の人たちは概して、その程度は予想できるくらい十分に賢いと思う。
「YAML は JSON のスーパーセット」という言い方は、仕様の作者が文書に何を書こうと、実際には正しいとは思わない。YAML 設定を全部 JSON に変えたら DevOps チームは怒るだろう。
2 つのデータ形式は同じ意味表現を持ちうるが、同じ CPU アーキテクチャにコンパイルされるすべての言語だってそうだ。JSON と YAML は実務では別物であり、両者を混ぜるのは良い考えではない。
もちろん人々は結局手で書き、耐え難くなるとテンプレートを付け始めた。いつも物事はそう流れていくようだ。
手書きのテキストは、機械生成された設定のシリアライズ済みテキストに置き換わるというより、本来は相変わらず手書きのテキストにテンプレートが付いた形で置き換わる。
個人的な原則としては、機械が読むコードを生成するのに文字列補間を使うべきではない、というもの。テンプレート言語は、気の利いた文字列補間にすぎない
SQLインジェクションとクロスサイトスクリプティングの結果をどちらも見てきた。任意のテキストをインタプリタに入れ続ける限り、こうしたことは起こり続ける
だからHTMLを作るときも、テンプレートファイルは使うべきではないと思う
HTML向けのテンプレート言語の代替としては、RubyのHaml、JavaScriptのPugがある。これらの言語は、タグ、属性、テキストノードからなるツリー全体を指定するための定義された方法を提供する
Python式の意味のあるインデントが嫌なら、JavaScriptにはJSXがある。JSXのHTMLのように見える部分は、Web文書ツリーを作る
createElement式にコンパイルされ、そのツリーは必要ならHTMLとして出力できるHaml、Pug、JSXはHTMLを出力できるとしても、テンプレート言語ではない。同様に、
JSON.stringify(myObj)はJSON用のテンプレート言語ではない機械が読むコードは、可能であれば対象言語の既知の構造を理解し活用するツールで生成すべきだ
HamlはWeb文書内のインラインコードを避け、HTMLをよりすっきりさせるためのテンプレートシステムであり、PugはNode.js向けの機能豊富なテンプレートエンジンだ
JSXが厳密にはテンプレート言語ではない、という点には同意できる
結局これらはすべてHTMLにコンパイルされる。ただし文字列補間ではなく、構文木としてパースされ、有効な構造についての内部的な理解に基づいてHTMLへレンダリングされる言語だ
YAMLテンプレートは気の利いた文字列補間であり、テンプレート言語ではないか、少なくともひどく実装されたテンプレート言語だ
個人的なルールは、値が文字列に入るたびに必ず正しくエンコードしなければならない、というものだ
以前このテーマで記事を書いた: 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...
いくつかのエラーメッセージは少し解釈しづらいが、事前に捕まえてくれるエラーが非常に多いので許容できる。今では直接yamlを書かなければならない数少ない瞬間が、比べるととても退屈に感じる
こういうときはたいてい「私たちの救世主CUELangをご存じですか?」と割り込む: https://cuelang.org/
まだチューリング完全ではないが、重複排除には十分な表現力があり、同じ言語でスキーマとデータを同じファイルまたは別ファイルに定義でき、ユニオン型もある
YAMLやJSONを生成でき、自分自身またはYAML/JSONファイルを検証できる
最大の欠点は、現在の実装がGoだけなので、サブプロセスやFFIが必要になる場合があることだ
その後、各アプリケーション用のJSONファイルを作り、あるツールがXML定義を作成し、その定義がアーキテクトたちの所有するXLSに適用され、そこからHelmチャートに適用するYAMLが吐き出される
チャートはk8sクライアントをデプロイし、そのクライアントがAPI経由でJSONとしてメインクラスタとやり取りする
少し時間はかかったが、各作業に最適なツールを使っている