--set は便利オプションではなく小さな言語である
一言でいうと
--setは値を渡す近道ではなく、文法と型の規則を持った小さな言語であり、同じ値をvaluesファイルで渡すときと、結果が変わることがあります。
なぜこれが問題になるのか
デプロイスクリプトに、こんな行があるとします。
helm upgrade api ./api --set image.tag=8
レンダリング結果はimage: registry.local/api:8で、何も問題がないように見えます。ところが、チャートのテンプレートがこのようになっていたら、話が変わります。
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
--set image.tag=8は、値を数値の8として入れます。上のテンプレートは文字列の連結なので、結果が同じように見えますが、タグを別に出力する場所(アノテーション、ラベル、ConfigMapのdata)では、数値がそのまま出力されて、APIサーバーが拒否します。逆に、valuesファイルにtag: "8"と書けば、文字列が入ります。同じ値を書く2つの方法が、異なる型を作るというのが、このテーマの要です。
--setが型を決める規則
helm v3.16で直接確認した結果は、次のとおりです。
| 書いた値 | 入る型 |
|---|---|
8 |
数値 |
1.10 |
文字列(小数点があると、整数として読まれない) |
0755 |
文字列(先頭の0が生きている必要があるので) |
true |
ブール値 |
null |
そのキーを削除 |
ここで、よく誤解が生じます。「バージョンのような値は危険だ」とひとまとめに覚えると、1.10と8を同じものとして扱ってしまいますが、実際に問題になるのは、整数として読まれる値とブール値として読まれる値だけです。そのため、--set-stringが必要な場所も、そこです。規則を覚えるよりも、値のダンプを1回レンダリングして、型を目で確認するほうが速いです。
nullは特に注意が必要です。空の文字列を入れる--set key=と違い、--set key=nullは、キーそのものをなくします。チャートがif .Values.xだけで分岐するなら、2つは同じように動作しますが、キーの存在で判断する場所では、分かれます。
文法: ドット・カンマ・波括弧・角括弧・バックスラッシュ
--set image.repository=registry.local/web,replicas=5 # 점은 깊이, 쉼표는 구분
--set 'args={alpha,beta,gamma}' # 중괄호는 리스트 통째로
--set 'args[0].name=first,args[0].value=1' # 대괄호는 원소 자리
--set 'nodeSelector.kubernetes\.io/os=linux' # 역슬래시는 점의 의미를 끈다
キー名の中にドットが入ることは、Kubernetesでは非常によくあります。kubernetes.io/os、app.kubernetes.io/name、prometheus.io/scrapeが、すべてそうです。エスケープしないと、kubernetesの下にio/osというネストしたマップが、黙って作られ、レンダリングは成功しても、望んだラベルは付きません。
複雑な値には、専用のオプションが別にあります。--set-jsonは、値をJSONのまま受け取り、型まで意図したとおりに入れ、--set-fileは、ファイルの内容を値として入れます。証明書の本文や設定ファイルをまるごと渡すときは、--set-fileが唯一の現実的な方法です。--set config.ca=/path/ca.pemと書くと、パスの文字列が値になってしまいます。
何でデプロイしたかを、あとでわかるか
--setの本当のコストは、文法でも型でもなく、記録が残らないことにあります。デプロイが終わったあとで、「今、本番はどんな値で動いているのか」と尋ねられたとき、-f values-prod.yamlでデプロイしたチームは、リポジトリのファイル1つを開いて答えます。--setでデプロイしたチームは、releaseの中から取り出す必要があります。
helm get values api # 사용자가 준 값만
helm get values api --all # 차트 기본값까지 합쳐진 최종 값
取り出せるから大丈夫だと思いがちですが、この値はコードレビューを経ておらず、誰がなぜそう決めたのかがどこにもなく、クラスターがなくなれば、一緒になくなります。そのため、値が2–3個を超えたら、ファイルに移すほうがよいです。残しておく価値のある--setは、デプロイごとに必ず変わるもの1つか2つ、たいていはイメージタグです。
そして、まさにそのタグが、型の事故が起きる場所です。--set-string image.tag=$TAGで固定しておくか、チャート側のテンプレートで{{ .Values.image.tag | quote }}で囲んでおけば、値を渡す側が何をしても安全になります。値を渡す人は複数いて、チャートは1か所なので、防御はチャート側でするほうが、コストが少なくて済みます。
現場での姿
デプロイスクリプトが--setで長くなり始めると、2つの問題が一緒に来ます。1つ目は、何を渡したかの記録が残らないことです。helm get valuesで取り出せますが、リポジトリにないので、コードレビューを経ません。2つ目は、シェルの引用符とHelmの文法が重なって、読みにくくなることです。波括弧と角括弧は、シェルも特別に扱うので、シングルクォートで囲む必要がありますが、これを忘れると、シェルが先に展開した結果がHelmに渡されます。
そのため、実務の境界は、おおむね次のとおりです。構造のある値はvaluesファイルで、デプロイごとに変わる1つか2つの値だけを--setで。イメージタグは、デプロイごとに変わる代表的な値なので、--setに残ることが多いのですが、その場所が、まさに型の事故が起きる場所です。タグには--set-stringを使うか、チャート側で| quoteを掛けておけば、どちらから渡されても安全になります。チャートを作る人にできる防御があるなら、そちらを先にするほうがよいです。値を渡す人は複数人で、チャートは1か所だからです。
次のラボですること
渡された値をJSONのまま出力するチャートを作っておき、--setの文法を1つずつ掛けてみます。キーの中のドットをエスケープし、整数と小数点のある値の型がどう分かれるかを確認し、--set-jsonと--set-fileを使い、nullでキーを消します。最後に、同じ2つの値を、valuesファイルと--setと--set-stringの3通りで渡して、レンダリング結果を並べて比較します。