レンダリング結果が YAML ではなかった — スコープと空白
目標
with・rangeがドットを変えること、空白のトリムが行をくっつけてしまうこと、引用符を外した値が数値やブール値になることを、それぞれわざと起こしてみて、--debug・required・fail・helm lint --strictで防ぐ方法まで進みます。
なぜ重要なのか
テンプレートで起きる事故の大部分は、関数を知らないからではなく、結果がYAMLでなくなるために起きます。そのため、エラーメッセージが原因を指しません。mapping values are not allowed in this contextは、YAMLパーサーの言葉であって、テンプレートの言葉ではないので、その行をいくら見ても、-}}1つが犯人だということは見えません。同じ理由で、withの中で.Releaseがnilだというメッセージも、「ドットが変わった」という事実を知るまでは、読み解けません。ここに引用符の問題が重なると、レンダリングまではきちんとできて、クラスターが拒否します。デプロイパイプラインの最も遅い地点で爆発するタイプです。この4つを1回ずつ自分で作ってみれば、次からはメッセージ2行を見るだけで、どこを見るかを決められます。
ステップ
/root/hc-scope/frontierチャート(名前frontier、バージョン0.1.0)を作成し、values.yamlに、app(nameportal、teamcore、port8080)、notes(ownersre、runbookwiki/portal)、envs(STAGE、PROD)、release(tag"1.10"、enabled"no")を置いてください。templates/base.yamlは、名前が.Values.app.nameで、data.portが引用符で囲んだポートのConfigMapです。helm template web /root/hc-scope/frontierの結果を/root/hc-scope/out/base.yamlに保存してください。/root/hc-scope/frontier/templates/scoped.yamlを作成してください。名前は<app.name>-scopedで、その下を{{- with .Values.app }}ブロックで囲み、その中でdata.releaseを{{ .Release.Name }}、data.teamを{{ .team }}と書きます。レンダリングすると失敗します。その出力を/root/hc-scope/out/with-error.txtに保存してください。/root/hc-scope/frontier/templates/scoped.yamlで、withブロックはそのままにして、data.releaseだけを直し、release名がレンダリングされるようにしてください。レンダリング結果を/root/hc-scope/out/scoped.yamlに保存します(release名はweb)。dataには、release: webとteam: coreの2行が出力される必要があります。/root/hc-scope/frontier/templates/envs.yamlを作成してください。名前は<app.name>-envsで、.Values.envsをインデックスと値の2つの変数で受け取って回りながら、dataに<소문자 환경이름>: "<인덱스>-<app.port>"(プレースホルダーは順に、小文字の環境名とインデックスです)を1行ずつ出力します。レンダリング結果に、stage: "0-8080"とprod: "1-8080"が出力される必要があります。結果を/root/hc-scope/out/envs.yamlに保存してください。/root/hc-scope/brokenチャート(名前broken)を別に作成し、values.yamlにapp(nameportal、teamcore)とnotes(ownersre、runbookwiki/portal)を置いてください。templates/portal.yamlは、annotations:の下の行で{{- toYaml .Values.notes | indent 4 -}}でコメントを差し込み、そのあとにlabels:が来る形です。レンダリングすると失敗します。通常の出力は/root/hc-scope/out/ws-error.txtに、--debugを付けた出力は/root/hc-scope/out/ws-debug.txtに保存してください。このチャートは直さず、そのままにします。/root/hc-scope/brokenを/root/hc-scope/fixedにコピーし(チャート名はfixedに変えます)、templates/portal.yamlだけを直して、レンダリングが成功するようにしてください。コメントのブロックは、annotations:の下に4スペースインデントして2行で、labels.teamはcoreと出力される必要があります。結果を/root/hc-scope/out/fixed.yamlに保存してください。/root/hc-scope/frontier/templates/release.yamlを作成してください。名前は<릴리스이름>-release(プレースホルダーはrelease名です)、data.tagは.Values.release.tag、data.enabledは.Values.release.enabledを、引用符なしで差し込みます。このテンプレートだけをレンダリングして、yq -o=jsonを通した結果を/root/hc-scope/out/quote-yaml12.jsonに、そのレンダリングをkubectl apply --dry-run=serverに入れた出力を/root/hc-scope/out/quote-error.txtに保存してください。そのあと、2つの値にquoteを掛けて直し、もう一度サーバー検証を通過した出力を/root/hc-scope/out/quote-ok.txtに保存してください。/root/hc-scope/frontier/templates/guard.yamlを作成してください。ポートが1024未満ならfailで止め、data.teamはrequiredで値がないときに止めます。正常な値では、名前が<app.name>-guardのConfigMapが出力される必要があります。--set app.port=80でレンダリングした出力を/root/hc-scope/out/guard-fail.txtに、--set app.team=nullでレンダリングした出力を/root/hc-scope/out/guard-required.txtに保存し、オプションなしでレンダリングした全体の結果を/root/hc-scope/out/guard-ok.yamlに保存してください。/root/hc-scope/lintbadチャート(名前lintbad)を作成し、templates/cm.yamlのConfigMapの名前をBad_Nameにしてください。helm lintの出力と終了コードを/root/hc-scope/out/lint-plain.txtに、helm lint --strictの出力と終了コードを/root/hc-scope/out/lint-strict.txtに保存してください。2つのファイルの最後の行に、exit=<종료 코드>(プレースホルダーは終了コードです)を追記します。同じチャートなのに、通過するかどうかが分かれることが、このステップの答えです。
参考
helm template --debugは、YAMLとしてパースできない結果も、そのまま見せてくれますhelm template <릴리스> <차트> -s templates/<파일>(プレースホルダーは順にrelease名、チャート、ファイルです)で、1つのテンプレートだけをレンダリングします- キーのすぐ下にブロックを差し込むときは、
indentではなくnindentです - よくある間違い:
withブロックの中で.Release・.Chartをそのまま使うことです。ルートは$です - よくある間違い: タグの末尾の
-}}が、次の行を前の行にくっつけることです - 公式ドキュメント: https://helm.sh/docs/chart_template_guide/control_structures/ ・ https://helm.sh/docs/chart_template_guide/variables/ ・ https://helm.sh/docs/chart_template_guide/yaml_techniques/
値の構造を先に敷いて、一度レンダリングする
/root/hc-scope/frontierチャート(名前frontier、バージョン0.1.0)を作成し、values.yamlに、app(name portal、team core、port 8080)、notes(owner sre、runbook wiki/portal)、envs(STAGE、PROD)、release(tag "1.10"、enabled "no")を置いてください。templates/base.yamlは、名前が.Values.app.nameで、data.portが引用符で囲んだポートのConfigMapです。helm template web /root/hc-scope/frontierの結果を/root/hc-scope/out/base.yamlに保存してください。
helm createで作ると、スケルトンのテンプレートが一緒に付いてくるので、このラボでは、ディレクトリとファイルを直接作るほうがすっきりします。必要なのは、Chart.yaml、values.yaml、templates/の3つだけです。release名は、このラボの間ずっとwebを使います。
withの中で.Releaseが見つからない
/root/hc-scope/frontier/templates/scoped.yamlを作成してください。名前は<app.name>-scopedで、その下を{{- with .Values.app }}ブロックで囲み、その中でdata.releaseを{{ .Release.Name }}、data.teamを{{ .team }}と書きます。レンダリングすると失敗します。その出力を/root/hc-scope/out/with-error.txtに保存してください。
withは、条件が真のとき、ドット(.)が指す対象を変えます。ブロックの中でドットは、もはやルートではなく.Values.appです。そのため、.teamは見つかり、.Releaseはありません。エラーは標準エラー出力に出るので、2>&1で一緒に受け取ってください。メッセージの「何がnilなのか」をよく読んでみてください。
ドル記号でルートを取り直す
/root/hc-scope/frontier/templates/scoped.yamlで、withブロックはそのままにして、data.releaseだけを直し、release名がレンダリングされるようにしてください。レンダリング結果を/root/hc-scope/out/scoped.yamlに保存します(release名はweb)。dataには、release: webとteam: coreの2行が出力される必要があります。
テンプレートが始まったときのルートコンテキストは$に結び付けられていて、withやrangeの中でも変わりません。.Values.appに絞り込んだ便利さは維持したまま、ルートのものだけを取り出して使いたいときに使うノブです。
rangeの中でもルートの値を一緒に使う
/root/hc-scope/frontier/templates/envs.yamlを作成してください。名前は<app.name>-envsで、.Values.envsをインデックスと値の2つの変数で受け取って回りながら、dataに<소문자 환경이름>: "<인덱스>-<app.port>"(プレースホルダーは順に、小文字の環境名とインデックスです)を1行ずつ出力します。レンダリング結果に、stage: "0-8080"とprod: "1-8080"が出力される必要があります。結果を/root/hc-scope/out/envs.yamlに保存してください。
range $i, $e := .Values.envsのように変数を2つ受け取れば、インデックスと要素を一緒に使え、ドットが変わっても、$i・$eはそのまま生きています。ルートのポートは$.Values.app.portで取り出します。小文字に変える関数はlowerです。
空白トリム1つがYAMLを壊す
/root/hc-scope/brokenチャート(名前broken)を別に作成し、values.yamlにapp(name portal、team core)とnotes(owner sre、runbook wiki/portal)を置いてください。templates/portal.yamlは、annotations:の下の行で{{- toYaml .Values.notes | indent 4 -}}でコメントを差し込み、そのあとにlabels:が来る形です。レンダリングすると失敗します。通常の出力は/root/hc-scope/out/ws-error.txtに、--debugを付けた出力は/root/hc-scope/out/ws-debug.txtに保存してください。このチャートは直さず、そのままにします。
indent 4は、改行なしで4スペースだけを入れ、末尾の-}}は、続く改行と空白を消します。そのため、コメントのブロックはannotations:と同じ行にくっつき、次の行のlabels:も前の行の末尾にくっつきます。エラーメッセージはYAMLパーサーの言葉なので、原因を指しません。--debugを付けると、Helmが壊れた結果をそのまま見せてくれます。そこで、どの行がくっついたかを目で確認してください。
nindentで改行まで渡す
/root/hc-scope/brokenを/root/hc-scope/fixedにコピーし(チャート名はfixedに変えます)、templates/portal.yamlだけを直して、レンダリングが成功するようにしてください。コメントのブロックは、annotations:の下に4スペースインデントして2行で、labels.teamはcoreと出力される必要があります。結果を/root/hc-scope/out/fixed.yamlに保存してください。
nindent 4は、改行を先に入れて4スペースインデントします。前の{{-は、テンプレートタグの前の空白を消す役割だけをすればよく、末尾には-}}を書きません。ルールとして覚えると楽です。キーのすぐ下の行にブロックを差し込むときは、常にnindentです。
引用符を忘れたら、クラスターが拒否した
/root/hc-scope/frontier/templates/release.yamlを作成してください。名前は<릴리스이름>-release(プレースホルダーはrelease名です)、data.tagは.Values.release.tag、data.enabledは.Values.release.enabledを、引用符なしで差し込みます。このテンプレートだけをレンダリングして、yq -o=jsonを通した結果を/root/hc-scope/out/quote-yaml12.jsonに、そのレンダリングをkubectl apply --dry-run=serverに入れた出力を/root/hc-scope/out/quote-error.txtに保存してください。そのあと、2つの値にquoteを掛けて直し、もう一度サーバー検証を通過した出力を/root/hc-scope/out/quote-ok.txtに保存してください。
helm template <릴리스> <차트> -s templates/release.yaml(プレースホルダーは順にrelease名とチャートです)で、1つのテンプレートだけをレンダリングできます。引用符がないと、1.10は数値になって末尾の0が消え、noはYAML 1.1の規則で偽になります。ConfigMapのdataは文字列しか受け付けないので、APIサーバーが型変換エラーで拒否します。値が人の読む文字列なら、quoteを付けるのが基本だと考えてください。
誤った値ならレンダリングの段階で止める
/root/hc-scope/frontier/templates/guard.yamlを作成してください。ポートが1024未満ならfailで止め、data.teamはrequiredで値がないときに止めます。正常な値では、名前が<app.name>-guardのConfigMapが出力される必要があります。--set app.port=80でレンダリングした出力を/root/hc-scope/out/guard-fail.txtに、--set app.team=nullでレンダリングした出力を/root/hc-scope/out/guard-required.txtに保存し、オプションなしでレンダリングした全体の結果を/root/hc-scope/out/guard-ok.yamlに保存してください。
failは条件を直接書けるので、「値はあるが意味をなさない値」を止めるのに使い、requiredは「値そのものがないとき」を止めます。どちらのメッセージも、人が読んですぐに直せるように書きます。--set app.team=nullは、そのキーを消します。空の文字列とは違います。数値の比較には、lt (int .Values.app.port) 1024のように、intを1回通してください。
警告をエラーとして扱わせる
/root/hc-scope/lintbadチャート(名前lintbad)を作成し、templates/cm.yamlのConfigMapの名前をBad_Nameにしてください。helm lintの出力と終了コードを/root/hc-scope/out/lint-plain.txtに、helm lint --strictの出力と終了コードを/root/hc-scope/out/lint-strict.txtに保存してください。2つのファイルの最後の行に、exit=<종료 코드>(プレースホルダーは終了コードです)を追記します。同じチャートなのに、通過するかどうかが分かれることが、このステップの答えです。
Kubernetesのオブジェクト名は、小文字のRFC 1123の規則に従う必要があるので、大文字とアンダースコアは警告になります。デフォルトのリントは、警告を出しても0で終わりますが、strictモードは警告を失敗として数えます。CIでstrictモードを使えば、こうした名前がデプロイまで行きません。終了コードは、コマンドの直後に$?で読みます。