TT Lab
はじめる
学ぶ 学習パス コース

Helmチャートの作成とデプロイ

ドットが変わり行がくっつく — テンプレート事故の二つの根

TT Labで続きを見る

一言でいうと

テンプレート事故の大部分は、文法ではなく2つから来ます。with・rangeがドットの指す対象を変えること、そして空白のトリムが改行を消してしまうことです。

なぜこれが問題になるのか

Helmのテンプレートで出るエラーは、たいていこんな形です。

Error: YAML parse error on frontier/templates/portal.yaml:
error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context

このメッセージは、YAMLパーサーが言ったものです。テンプレートをすべてレンダリングしたあとで、その結果をYAMLとして読もうとして失敗したので、メッセージに書かれた行番号は、レンダリング結果の行番号であって、テンプレートファイルの行番号ではありません。そのため、テンプレートの5行目をいくら見ても、何も問題がありません。犯人は、3行上のタグの末尾に付いた-}}の2文字ですが、その文字は、エラーのどこにも出てきません。

ここから抜け出す道は1つです。壊れた結果を直接見ることです。helm template --debugは、YAMLとしてパースできない結果も、そのまま出力します。出力を見ると、annotations:のあとにコメントのブロックが同じ行にくっついていて、さらにlabels:まで前の行の末尾にくっついているのが、一目でわかります。原因を推測する作業が、観察する作業に変わります。

ドットはブロックごとに変わる

withとrangeは、便利な構文ではなく、コンテキストを差し替えるブロックです。

{{- with .Values.app }}
data:
  team: {{ .team }}
  release: {{ .Release.Name }}
{{- end }}

ブロックの中で.は、もはやルートではなく.Values.appです。そのため、.teamは見つかり、.Releaseはありません。Helmは次のように言います。

nil pointer evaluating interface {}.Name

このメッセージも、原因を指していません。.Releaseがnilだという意味ですが、人の目には.Releaseは常にあるものなので、疑いません。解決策は、ルートを指す変数$です。$は、テンプレートが始まったときのコンテキストに結び付けられていて、どのブロックの中でも変わりません。{{ $.Release.Name }}と書けば済みます。

rangeも同じです。range $i, $e := .Values.envsのように変数を受け取っておけば、インデックスと要素を安全に使えて、ルートの値は$.Values...で取り出します。変数を受け取らず.だけを使っていて、その中で再びルートの値が必要になった瞬間に、行き詰まります。

indentとnindent、そして空白の除去記号

{{-はタグの前の空白と改行を消し、-}}はタグのあとの空白と改行を消します。事故の大部分は、後ろ側で起きます。次の行がまるごと前の行にくっつくからです。

indentとnindentの違いも、同じ軸にあります。

関数 やること 使う場所
indent 4 各行の前に4スペースを入れる すでに行が変わっている場所
nindent 4 改行を先に入れて、4スペースを入れる キーのすぐ下にブロックを差し込むとき

annotations:の下にマップ1つを差し込む場所は、ほとんどいつもnindentです。ここでindentを使うと、ブロックの最初の行がannotations:と同じ行にくっついて、annotations: owner: sreのようなものが作られ、YAMLパーサーは、これを「マッピングの値が来る場所ではない」と拒否します。

引用符を外すと値の型が変わる

3つ目の落とし穴は、レンダリングが成功したあとで爆発します。

data:
  tag: {{ .Values.release.tag }}       # values 에는 "1.10"
  enabled: {{ .Values.release.enabled }}  # values 에는 "no"

レンダリング結果はtag: 1.10とenabled: noです。引用符がなくなったので、この値たちはもう文字列ではありません。YAML 1.1の規則を使うパーサーは、noを偽として読み、数値として読まれた1.10は末尾の0を失って1.1になります。ConfigMapのdataは文字列しか受け付けないので、APIサーバーが拒否します。

cannot unmarshal bool into Go struct field ConfigMap.data of type string

イメージタグ、バージョン、電話番号、国コードのように、人が文字列として扱う値は、テンプレートでquoteを通すのが基本です。逆に、数値でなければならない場所(replicas、port)にquoteを付けると、そちらで同じ種類の拒否が起きます。

現場での姿

この3つは、たいてい、デプロイパイプラインの異なる地点で捕まります。スコープと空白の事故は、レンダリングですぐに捕まりますが、引用符の事故は、クラスターが受け取って確認するまで生き残ります。そのため、helm templateだけを実行して「レンダリングできた」と済ませると、最も遅い地点で爆発します。CIにhelm template | kubectl apply --dry-run=serverを1行入れているチームが多い理由が、これです。スキーマと型を、本物のAPIサーバーが見てくれます。

デプロイの前に値そのものを止める仕組みも、一緒に使います。requiredは値がないとき、failは値が意味をなさないときに、レンダリングを止めます。どちらもメッセージを人が書くので、ここに「何をどう直すべきか」を書いておくと、障害の時間に読む人がすぐに動けます。そして、helm lint --strictは、デフォルトのリントが警告として知らせるだけのもの(大文字が入ったオブジェクト名など)を、失敗に変えてくれます。同じチャートが、helm lintでは0で終わり、--strictでは1で終わるのを一度見れば、CIにどちらを掛けるべきかがはっきりします。

次のラボですること

withの中で.Releaseが見つからないエラーを自分で起こして、$で直します。rangeで変数を受け取って、インデックスとルートの値を一緒に使います。空白のトリムをわざと間違えてレンダリングを壊したあと、--debugで壊れた結果を読んで、nindentで直します。引用符のない値がAPIサーバーに拒否されることを確認し、最後に、required・fail・helm lint --strictで、同じ事故がデプロイまで行かないように止めます。