Helmのtemplateでループ内から変数を参照する
環境
- helm v3.8.0
背景
こんな感じで、ほぼ同じ内容のマニフェストを複数作りたいとする。
当然 values.yaml
で変数を管理したいし、2つ分コピペするのではなくループでうまいこと処理したい。
# targetgroupbinding.yaml apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: name: my-target-group-binding-1 spec: serviceRef: name: awesome-service port: 8080 targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz" --- apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: name: my-target-group-binding-2 spec: serviceRef: name: awesome-service port: 8081 targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz"
values.yaml
がこんな感じだとする。
# values.yaml appName: awesome-service service: foo: id: 1 port: 8080 targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz" bar: id: 2 port: 8081 targetGroupARN: "arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz"
パターン1:ループ中にyaml本体を直接書く場合
サンプル全体: https://github.com/ser1zw/helm-loop-demo/tree/main/demo1
普通にループで処理するなら、TargetGroupBinding
のyamlはこんな感じ。
# targetgroupbinding.yaml {{- range .Values.service }} apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: name: my-target-group-binding-{{ .id }} spec: serviceRef: name: {{ $.Values.appName }} port: {{ .port }} targetGroupARN: {{ quote .targetGroupARN }} --- {{- end -}}
{{- range .Values.service }} ~ {{- end -}}
の間ではコンテキストが .Values.service
のそれぞれの値になるので(後述の「参考:ループ内のコンテキストの値」参照)、.Values.service.*
配下の値は .id
, port
, .targetGroupARN
のように直接参照できる。
逆に values.yaml
で定義した appName
は .Values.appName
では参照できない。この場合は、ルートコンテキストを指すグローバル変数 $
を使い、 $.Values.appName
のように参照する。
参考:ループ内のコンテキストの値
{{- range .Values.service }} ~ {{- end -}}
内でのコンテキストはこんな感じのmap
になっている
map[id:1 port:8080 targetGroupARN:arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-1/zzzzzzzzzzzzzzzz] map[id:2 port:8081 targetGroupARN:arn:aws:elasticloadbalancing:ap-northeast-3:999999999999:targetgroup/awesome-service-2/zzzzzzzzzzzzzzzz]
ちなみにこんな感じで表示して確認できる。
{{- range .Values.service }} ctx: {{ . }} {{- end -}}
パターン2:テンプレートを使う場合
サンプル全体: https://github.com/ser1zw/helm-loop-demo/tree/main/demo2
さまざまな事情により、yaml本体をループ中に直接書くのではなく、名前付きテンプレートにしたいということもよくある。
こうする。
# targetgroupbinding.yaml {{- range .Values.service }} {{- $ctx := (merge (dict "rootCtx" $) .) -}} # <-- ポイント(1) {{ template "target-group-binding-template" $ctx }} {{- end -}}
# _helpers.tpl {{- define "target-group-binding-template" }} apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: name: my-target-group-binding-{{ .id }} spec: serviceRef: name: {{ .rootCtx.Values.appName }} # <-- ポイント(2) port: {{ .port }} targetGroupARN: {{ quote .targetGroupARN }} --- {{- end }}
テンプレートは {{ template テンプレート名 コンテキスト }}
で呼び出せるが、コンテキストは1つしか渡せない。
そこで ポイント(1)
のように、ループ時点でのルートコンテキスト $
とループ内のコンテキスト .
をマージした上で渡してやる。
ここではルートコンテキストのキーを rootCtx
にしているが、これは何でもよい。
こうすることで、テンプレート内ではループ内のコンテキストの値は普通に .id
のように参照できるし、元のルートコンテキストの値は ポイント(2)
のように rootCtx
を使って .rootCtx.Values.appName
のように参照できる。
ちなみに今回は template
を使っているが、include
でも同じ。
ダメな例
公式ドキュメントだと template
の呼び出し時のコンテキストに .
を渡す例しかないので、そのまま .
を渡しがち。
# targetgroupbinding.yaml {{- range .Values.service }} {{ template "target-group-binding-template" . }} # <-- これ {{- end -}}
で、テンプレート内でルートコンテキストを参照するために $
を使おうとすると…
# _helpers.tpl {{- define "target-group-binding-template" }} apiVersion: elbv2.k8s.aws/v1beta1 kind: TargetGroupBinding metadata: name: my-target-group-binding-{{ .id }} spec: serviceRef: name: {{ $.Values.appName }} # <-- これ port: {{ .port }} targetGroupARN: {{ quote .targetGroupARN }} --- {{- end }}
こんな感じでエラーになる。
Error: template: demo/templates/_helpers.tpl:8:14: executing "target-group-binding-template" at <$.Values.appName>: nil pointer evaluating interface {}.appName Use --debug flag to render out invalid YAML
なんでかというと、template
にループ内のコンテキスト .
を渡しているので、テンプレート内では ルートコンテキスト = ループ内のコンテキスト
になってしまっているため。