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

普通にループで処理するなら、TargetGroupBindingyamlはこんな感じ。

# 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 にループ内のコンテキスト . を渡しているので、テンプレート内では ルートコンテキスト = ループ内のコンテキスト になってしまっているため。

参考