Kustomizeのreplacementsを使ってコンテナイメージタグ値で環境変数を作成する

2022.04.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

やりたいこと

Podのコンテナで使われているコンテナイメージのタグ値をつかって、そのPodに環境変数を設定したいです。

以下のように同じ値をそれぞれの設定箇所に書けばシンプルに実現できますが、保守性の観点から同じ値を複数箇所に書くのは避けたいのです。(絶対に環境変数値の更新を忘れる)

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: nginx:1.20
        env:
        - name: HOGE
          value: "1.20"

1箇所書けばイメージタグ値にも環境変数値にも使われる様になればよいので、以下でも構いません。

  • 環境変数を設定して、イメージタグ指定時にその環境変数を参照する
  • どこかで変数的なものを設定して、環境変数もイメージタグもその変数を参照する

解決方法: Kustomizeのreplacementsを使う

replacementsはその名の通り、特定のリソースの特定のフィールド値を、1個以上のターゲットフィールドの値として使う(置換する)機能です。

今回はこの機能を使ってイメージタグ値をもとに環境変数を定義します。

先に完成形をお見せします。

.kustomization.yaml

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- applications/deployment.sample.yaml
replacements:
- source:
    kind: Deployment
    name: sample
    fieldPath: spec.template.spec.containers.[name=sample].image
    options:
      delimiter: ":"
      index: 1
  targets:
  - select:
      kind: Deployment
      name: sample
    fieldPaths: 
    - spec.template.spec.containers.[name=sample].env.[name=HOGE].value

.applications/deployment.sample.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: nginx:1.20
        env:
        - name: HOGE
          value: "(overwrite with replacements)"

解説します。

  • kustomization.yamlreplacementsフィールド以下に置換設定を書き連ねていきます。配列型で記述していきます。
  • 各配列要素はsourcetargetフィールドが必要です。
  • source以下に参照元のフィールドの情報を定義します。
    • fieldPathフィールドで書いているように途中配列型になっている箇所については、どの要素なのか絞り込む必要があります。そういう場合は角括弧([])で囲って要素内のハッシュをキー=バリューの形で指定します。
    • 今回参照したい値は imageフィールドの一部分、タグの部分だけです。 imageフィールドは (イメージ名):(タグ)という構造になっているため、タグ値を取りたい場合は :で区切った場合の2つ目の要素=インデックス値は1の部分を取得しています。
  • targetsに置換先の情報を定義します。複数個置換先に指定できるのでこのフィールド以下も配列になります。

ちなみにreplacementsの記述は別ファイル化もできます。

.kustomization.yaml

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- applications/deployment.sample.yaml
replacements:
- path: replacements/replacement.sample.yaml

.replacements/replacement.sample.yaml

source:
  kind: Deployment
  name: sample
  fieldPath: spec.template.spec.containers.[name=sample].image
  options:
    delimiter: ":"
    index: 1
targets:
- select:
    kind: Deployment
    name: sample
  fieldPaths: 
  - spec.template.spec.containers.[name=sample].env.[name=HOGE].value

エラー発生

検証実装は上記でOKだったのですが、本番のコードに移植したところエラーになりました。本番のコードは複数環境(dev,stg,prod)に対応できるように以下のようなディレクトリ構成にしてoverlayを使っています。

.
├── base
│   ├── applications
│   │   └── deployment.sample.yaml
│   └── kustomization.yaml
└── env
    ├── dev
    │   └── kustomization.yaml
    ├── prod
    │   └── kustomization.yaml
    └── stg
        └── kustomization.yaml

./env/(dev|stg|prod)/以下で環境毎の差分を定義する感じです。例えば以下はdev環境だけnginxイメージのタグをfugaに変えています。

./env/dev/kustomization.yaml

---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: nginx
  newTag: fuga

このコードに対してreplacementsのコードを追加します。replacementsの処理自体は環境差異がないので、./base/kustomization.yamlに追記しました。

./base/kustomization.yaml

  ---
  apiVersion: kustomize.config.k8s.io/v1beta1
  kind: Kustomization
  resources:
  - applications/deployment.sample.yaml
+ replacements:
+ - source:
+     kind: Deployment
+     name: sample
+     fieldPath: spec.template.spec.containers.[name=sample].image
+     options:
+       delimiter: ":"
+       index: 1
+   targets:
+   - select:
+       kind: Deployment
+       name: sample
+     fieldPaths: 
+     - spec.template.spec.containers.[name=sample].env.[name=HOGE].value

しかし、このやり方だと思ったような結果になりませんでした。環境変数HOGEの値は1.20のままです。これはおそらく以下のような順序で処理が走るからだと思われます。

  1. 環境変数HOGEの値はDeploymentのマニフェストファイルに書かれている通り(overwrite with replacements)でスタート
  2. baseのレイヤーでの処理。replacementsによって タグ値=1.20で環境変数HOGEが上書きされる
  3. devのレイヤーでの処理。newTagによってnginxのタグ値がfugaに上書きされる
  4. devレイヤーにreplacementsは無いので、環境変数HOGEの値が再度上書きされることはない

うまく行かなかったので、前述の./base/kustomization.yamlの修正は取りやめ、./env/(dev|stg|prod)/kustomization.yamlを修正することにしました。

./env/dev/kustomization.yaml

  ---
  apiVersion: kustomize.config.k8s.io/v1beta1
  kind: Kustomization
  resources:
  - ../../base
  images:
  - name: nginx
    newTag: fuga
+ replacements:
+ - path: ../../base/replacements/replacement.sample.yaml

そしてbase以下にreplacements用のファイルを置きます。

./base/replacements/replacement.sample.yaml

source:
  kind: Deployment
  name: sample
  fieldPath: spec.template.spec.containers.[name=sample].image
  options:
    delimiter: ":"
    index: 1
targets:
- select:
    kind: Deployment
    name: sample
  fieldPaths: 
  - spec.template.spec.containers.[name=sample].env.[name=HOGE].value

しかし、このコードに対して kustomize build env/dev/ | kubectl apply -f -するとエラーになりました。

Error: trouble configuring builtin ReplacementTransformer with config: `
Replacements:
- path: ../../base/replacements/replacement.sample.yaml
`: security; file './base/replacements/replacement.sample.yaml' is not in or below './env/dev'

エラー解消方法

これは、resourcesなど一部機能を除き、ルートの kustomization.yaml、今回の例だと./env/dev/kustomization.yamlがあるディレクトリ外のファイルを読み込もうとするとエラーになるという判定に引っかかりました。この判定を無効化するには、kustomize buildコマンドにフラグを追加します。私が今回使っているVersion4系だと以下です。

% kustomize build --load-restrictor LoadRestrictionsNone env/dev/

Version3系だと以下だそうです。

% kustomize build --load_restrictor none env/dev/

以上で要件の実現ができました?

参考情報

失敗した方法達

replacementsに至るまでの色々試行錯誤した過程も残しておきます。まだまだ全然マニフェストファイルの書き方わからない…

imageで環境変数を参照

$()で囲めば環境変数を参照できるので、それをimage欄で試してみました。

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: nginx:$(HOGE)
        env:
        - name: HOGE
          value: "1.20"

結果、applyはできましたが変数展開されませんでした。imageでは環境変数使えないということなんでしょうか。

`% kubectl get pod/sample-94fb5856-28qp7 -o json | jq .spec.containers`の実行結果

[
  {
    "env": [
      {
        "name": "HOGE",
        "value": "1.20"
      }
    ],
    "image": "nginx:$(HOGE)",
    "imagePullPolicy": "IfNotPresent",
    "name": "sample",
    "resources": {},
    "terminationMessagePath": "/dev/termination-log",
    "terminationMessagePolicy": "File",
    "volumeMounts": [
      {
        "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
        "name": "default-token-lt8qm",
        "readOnly": true
      }
    ]
  }
]

参考情報

fieldRef

fieldRefを使うとlabelやannotationなどに定義した値を参照できるので、annotationでタグ値を定義して、imageと環境変数欄でそれらを参照してみました。以下の例はタグ値だけでなくてイメージ名までまとめてannotation化してみた例です。

---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: kcr-cs
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
      annotations:
        image: nginx:1.20
    spec:
      containers:
      - name: sample
        image: 
          valueFrom:
            fieldRef:
              fieldPath: metadata.annotations['image']  
        env:
        - name: HOGE
          valueFrom:  
            fieldRef:
              fieldPath: metadata.annotations['image']

これはエラーになりました。環境変数はこの方法でOKなのですがimageはダメなようです。

% kustomize build . | kubectl apply  -f -
Error: wrong Node Kind for  expected: ScalarNode was MappingNode: value: {valueFrom:
  fieldRef:
    fieldPath: metadata.annotations['image']}
error: no objects passed to apply

参考情報

resourceFieldRef

resourceFieldRefを使えばコンテナの情報を参照できる!のですが、参照できる情報が限られており、以下のみです。今回の要件には合いませんでした。

  • limits.cpu
  • limits.memory
  • limits.ephemeral-storage
  • requests.cpu
  • requests.memory
  • requests.ephemeral-storage

参考情報

vars/varReference

ググるとKustomizeのVars/VarReference機能を使って似たような要件を実現している情報が見つかります。が、公式ドキュメントにvarsはdeprecatedにする計画があり、可能な限り早くreplacementsに置き換えることが推奨と書かれていました。そのため深く調査はせず、replacementsの調査に進みました。

参考情報

sed

kustomizeの一つ前のレイヤで動的にマニフェストファイルを作ろうという案です。

例えば以下のようなテンプレートを作っておきます。

./applications/deployment.sample.yaml.tmpl

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: nginx:{{IMAGE_TAG}}
        env:
        - name: HOGE
          value: "{{IMAGE_TAG}}"

そして以下のようなコマンドを実行します。

% sed -e 's/{{IMAGE_TAG}}/fuga/g' ./applications/deployment.sample.yaml.tmpl> ./applications/deployment.sample.yaml

すると{{IMAGE_TAG}}部分が置換されたマニフェストファイルができあがります。

./applications/deployment.sample.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
      - name: sample
        image: nginx:fuga
        env:
        - name: HOGE
          value: "fuga"

これでも要件は実現できますが、もう1レイヤ増えるのがイマイチだなと感じたので今回は採用しませんでした。

私はまだ全然さわったことがないのですが、cdk8sを使うとこういうのが簡単にできるのかもしれません。