スクエニ ITエンジニア ブログ

Helmで0とdefaultを区別する

Helm

Kubernetes (k8s)のパッケージ管理を行うアプリケーションとしてHelmがあります。 Helmではテンプレートによって使うimageのタグやDeploymentのPodの数といった設定値をデプロイ時に注入することが可能になっています。

Helmのこのテンプレートファイル群の単位をchartと呼びますが、私のチームでchartを管理する中でoptionalなパラメータを表現しようとして躓いたのでまとめます。
chartの設計上、optionalなパラメータをhelm templateで表現する必要がない場合が多いと思われますが、要件の都合や運用の都合で明確に区別したいという場合に参考になれば幸いです。

やりたいこと

Helmで注入する設定値 values.yaml が以下のような構造になっているとします。

setting:
  foo: 100
  bar: "test string"

このとき、この値を注入したJSONファイルをConfigMapとして作成したいです。

{ "foo": "100", "bar": "test string" }

そのためには、以下のようなテンプレートを用意すればいいです。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |- 
    { "foo": {{ .Values.setting.foo | quote }}, "bar": {{ .Values.setting.bar | quote }} }

実際にテンプレート処理を試してみましょう。

> helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "100", "bar": "test string" }

問題なさそうです。 ところで、我々のアプリケーションではfooは多くの場合で数値ですが、いわゆるnullの意味を持たせて "foo": "" と設定したい場合もありました。

このテンプレートをそのまま適用すると、以下のように不正なJSONになってしまうため、工夫が必要でした。

$ cat ./sample-chart/values.yaml
setting:
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": , "bar": "test string" }

ところがこれが一筋縄ではいかなかったのです。

初期実装案

以下のvalues.yaml のようにfooを省略した場合にはデフォルト値として "" を描画するようにすることを考えます。

setting:
  bar: "test string"

具体的には、この values.yaml で以下のようなマニフェストになってほしいということです。

apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "", "bar": "test string" }

※ もちろん、 foo: "100" のように省略しない場合はこれまで通り { "foo": "100", "bar": "test string" } のように描画したいです。

初めに思い浮かんだのは、Using the default function(Helmドキュメント)で触れられている default 関数を使うことでした。

This function allows you to specify a default value inside of the template, in case the value is omitted.

とのことなので今回のケースにぴったりに見えます。試してみましょう。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |- 
    { "foo": {{ .Values.setting.foo | default "" | quote }}, "bar": {{ .Values.setting.bar | quote }} }
$ cat ./sample-chart/values.yaml
setting:
  foo: 100
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "100", "bar": "test string" }
$ cat ./sample-chart/values.yaml # fooを省略してみる
setting:
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "", "bar": "test string" }

一見これで問題なさそうなのですが、例外があります。それは、foo: 0 の場合です。

$ cat ./sample-chart/values.yaml # fooを省略してみる
setting:
  foo: 0
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "", "bar": "test string" }

我々は { "foo": "0", "bar": "test string" } を期待していたのですが、 { "foo": "", "bar": "test string" } になってしまいました。

原因(推測)

Helmのif-else文では、

A pipeline is evaluated as false if the value is:

  • a boolean false
  • a numeric zero
  • an empty string
  • a nil (empty or null)
  • an empty collection (map, slice, tuple, dict, array)

にあるように0や空文字列とnilがまとめて扱われるのですが、これが影響しているものと思われます。実際、以下のようなテンプレートでも同様の挙動をします。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |- 
    { "foo": {{ if .Values.setting.foo }}{{ .Values.setting.foo | quote }}{{ else }}""{{ end }}, "bar": {{ .Values.setting.bar | quote }} }
$ cat ./sample-chart/values.yaml # fooを省略してみる
setting:
  foo: 0
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "", "bar": "test string" }

古いものですが、 https://github.com/helm/helm/issues/3164 でも同様の報告がなされていました。

対応策

テンプレートが少し複雑になってしまいますが、上記のissueを参考に明示的に分岐処理を入れることとしました。

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |- 
    { "foo": {{ if ( kindIs "invalid" .Values.setting.foo ) }}""{{ else }}{{ .Values.setting.foo | quote }}{{ end }}, "bar": {{ .Values.setting.bar | quote }} }

試してみましょう。

$ cat ./sample-chart/values.yaml # foo: 100 (0以外)
setting:
  foo: 100
  bar: "test string"
$ helm template ./sample-chart
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "100", "bar": "test string" }
$ cat ./sample-chart/values.yaml # foo: 0 
setting:
  foo: 0
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "0", "bar": "test string" }
$ cat ./sample-chart/values.yaml # fooの省略
setting:
  bar: "test string"
$ helm template ./sample-chart
---
# Source: sample-chart/templates/sample.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  sample.json: |-
    { "foo": "", "bar": "test string" }

問題なさそうです!

ちなみに ( kindIs "invalid" .Values.setting.foo ) の部分は ( eq .Values.setting.foo nil ) でも同様でした。後者の方がより分かりやすいかもしれません。

設定ファイルの設計を見直す

ところで今回の場合、アプリケーションのJSONパーサの設定でfooを省略可能にして、例えば以下の2つのJSON構造のどちらも許容するような作りにするという選択肢がありました。

  • { "foo": 100, "bar": "test string" }
  • { "bar": "another string" }

実際こちらの方がfooを省略することが分かりやすく、もしこのような構造になっていればtoJsonという関数でYAMLの構造をそのままJSON出力可能なので、テンプレートが単純になってよさそうです。

しかし、今回は

  • アプリケーションをk8s以外でも動作させる場合がある
  • 設定ファイルの影響範囲が広い

という背景からアプリケーション改修を行わないこととしたので、複雑になることを許容してHelmのテンプレートで表現することにしました。

まとめ

Helmで values.yaml 内のフィールドを省略することを想定して default 関数を使おうとしましたが、0と区別することを考えるとうまくいきませんでした。

今回は個別にif-elseで処理することとしましたが、代わりにテンプレートが複雑になるので、今回はテンプレートで表現するようにしましたが、常にこの選択がよいわけではないところに注意が必要があると思います。

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!