😭

Kubernetesで重複定義した環境変数を片方削除するとResource側の環境変数が消える

2022/03/16に公開

はじめに

Kubernetesの環境変数周りで予期しない挙動に遭遇して事故ったので挙動・対処法を共有したいと思います。

TL;DR

  1. Manifest(今回はDeployment)に何らかの環境変数を重複定義(nameが同じものが複数存在)してkubectl applyする
  2. 重複している環境変数の内、片方を削除してkubectl applyする
  3. Podから環境変数が消える
  4. もう一度kubectl applyすれば直る

再現手順

環境構築

恐らくどのKubernetesでも再現するかと思いますが、今回はローカルで簡単に構築できるkindを使います。

cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: envvar-cluster
nodes:
- role: control-plane
- role: worker
$ kind create cluster --config cluster.yaml
(略)
 Have a nice day!
$ 
$ 
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:56066
CoreDNS is running at https://127.0.0.1:56066/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
$ 
$ 
$ kubectl get nodes -o wide
NAME                           STATUS   ROLES                  AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION     CONTAINER-RUNTIME
envvar-cluster-control-plane   Ready    control-plane,master   72m   v1.23.4   172.19.0.3    <none>        Ubuntu 21.10   5.10.76-linuxkit   containerd://1.5.10
envvar-cluster-worker          Ready    <none>                 72m   v1.23.4   172.19.0.2    <none>        Ubuntu 21.10   5.10.76-linuxkit   containerd://1.5.10

動作確認

下記なんの変哲もないDeploymentをベースに確認を進めます。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        env:
        - name: SAMPLE_ENV_VAR1
          value: sample1
        - name: SAMPLE_ENV_VAR2
          value: sample2

Step1: Deploymentを作成

$ kubectl apply -f deployment.yaml
deployment.apps/nginx-deployment created
$ 
$ 
$ kubectl get deployment -o wide
NAME               READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES         SELECTOR
nginx-deployment   1/1     1            1           16s   nginx        nginx:latest   app=nginx
$ 
$ 
$ kubectl get replicasets -o wide
NAME                        DESIRED   CURRENT   READY   AGE   CONTAINERS   IMAGES         SELECTOR
nginx-deployment-dfc74d86   1         1         1       60s   nginx        nginx:latest   app=nginx,pod-template-hash=dfc74d86
$ 
$ 
$ kubectl get pods -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE                    NOMINATED NODE   READINESS GATES
nginx-deployment-dfc74d86-qmck4   1/1     Running   0          79s   10.244.1.14   envvar-cluster-worker   <none>           <none>

以下のように環境変数が想定通りセットされています。

$ kubectl get deployments -o json | jq '.items[].spec.template.spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2"
  }
]
$ kubectl get replicasets -o json | jq '.items[].spec.template.spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2"
  }
]
$ kubectl get pods -o json | jq '.items[].spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2"
  }
]

Step2: 環境変数を重複させる

先程のManifestを以下のように編集しapplyします。

        env:
        - name: SAMPLE_ENV_VAR1
          value: sample1
        - name: SAMPLE_ENV_VAR2
          value: sample2
+       - name: SAMPLE_ENV_VAR2
+         value: sample2

エラーが出るかと思いきや特に問題無く成功します。
diffも出ないのでこの段階で気が付くことは難しいでしょう。

$ kubectl diff -f deployment.yaml
$ 
$ kubectl apply -f deployment.yaml
deployment.apps/nginx-deployment configured

diffが無いので当然Resourceの環境変数にも変化はありません。
(Deployment, ReplicaSetも同様ですが冗長なのでログは省略)

$ kubectl get pods -o json | jq '.items[].spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2"
  }
]

Step3: 重複を片方削除する

重複している SAMPLE_ENV_VAR2 を削除しようとすると追加時には出なかったdiffが発生します。

        env:
        - name: SAMPLE_ENV_VAR1
          value: sample1
        - name: SAMPLE_ENV_VAR2
          value: sample2
-       - name: SAMPLE_ENV_VAR2
-         value: sample2
$ kubectl diff -f deployment.yaml
diff -u -N /var/folders/wm/yhjlm5nd3vg1g_x433204z7c0000gp/T/LIVE-948535760/apps.v1.Deployment.default.nginx-deployment /var/folders/wm/yhjlm5nd3vg1g_x433204z7c0000gp/T/MERGED-3665042967/apps.v1.Deployment.default.nginx-deployment
--- /var/folders/wm/yhjlm5nd3vg1g_x433204z7c0000gp/T/LIVE-948535760/apps.v1.Deployment.default.nginx-deployment 2022-03-16 00:25:43.000000000 +0900
+++ /var/folders/wm/yhjlm5nd3vg1g_x433204z7c0000gp/T/MERGED-3665042967/apps.v1.Deployment.default.nginx-deployment      2022-03-16 00:25:43.000000000 +0900
@@ -6,7 +6,7 @@
     kubectl.kubernetes.io/last-applied-configuration: |
       {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"nginx-deployment","namespace":"default"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"env":[{"name":"SAMPLE_ENV_VAR1","value":"sample1"},{"name":"SAMPLE_ENV_VAR2","value":"sample2"},{"name":"SAMPLE_ENV_VAR2","value":"sample2"}],"image":"nginx:latest","name":"nginx","ports":[{"containerPort":80}]}]}}}}
   creationTimestamp: "2022-03-15T15:18:24Z"
-  generation: 2
+  generation: 3
   labels:
     app: nginx
   managedFields:
@@ -46,10 +46,6 @@
                     .: {}
                     f:name: {}
                     f:value: {}
-                  k:{"name":"SAMPLE_ENV_VAR2"}:
-                    .: {}
-                    f:name: {}
-                    f:value: {}
                 f:image: {}
                 f:imagePullPolicy: {}
                 f:name: {}
@@ -130,8 +126,6 @@
       - env:
         - name: SAMPLE_ENV_VAR1
           value: sample1
-        - name: SAMPLE_ENV_VAR2
-          value: sample2
         image: nginx:latest
         imagePullPolicy: Always
         name: nginx

「多分動くと思うからリリースしようぜ」とばかりにapplyすると……?

$ kubectl apply -f deployment.yaml
deployment.apps/nginx-deployment configured

diffの通り無慈悲にSAMPLE_ENV_VAR2が消滅してしまいます。
ちなみに value の値は既存と重複させてもさせなくても同様の挙動になります。

$ kubectl get pods -o json | jq '.items[].spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  }
]

Extra: 新規作成時に重複がある場合の挙動

このように重複させた値で新規作成すると警告は出ますがそのままapplyされ、特段エラーが出ることもなく正常にPodが立ち上がります。

        env:
        - name: SAMPLE_ENV_VAR1
          value: sample1
        - name: SAMPLE_ENV_VAR2
          value: sample2
        - name: SAMPLE_ENV_VAR2
          value: sample2-duplicated
$ kubectl apply -f deployment.yaml
Warning: spec.template.spec.containers[0].env[2].name: duplicate name "SAMPLE_ENV_VAR2"
deployment.apps/nginx-deployment created
$ 
$ 
$ kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-66759f486b-4qrrr   1/1     Running   0          42s

そしてPodの設定としてはconfiguredで重複させた時と違って同じ値が2つ存在しますが、実際にPod内部から見える値はどうもManifestで後ろに書いた値となるようです。

$ kubectl get pods -o json | jq '.items[].spec.containers[].env'
[
  {
    "name": "SAMPLE_ENV_VAR1",
    "value": "sample1"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2"
  },
  {
    "name": "SAMPLE_ENV_VAR2",
    "value": "sample2-duplicated"
  }
]
$ 
$ 
$ kubectl exec -it nginx-deployment-66759f486b-4qrrr -- env | grep SAMPLE
SAMPLE_ENV_VAR2=sample2-duplicated
SAMPLE_ENV_VAR1=sample1

Manifestで SAMPLE_ENV_VAR2 の順序を入れ替えると以下のようになります。

$ kubectl exec -it nginx-deployment-74f47d648d-nb87h -- env | grep SAMPLE
SAMPLE_ENV_VAR1=sample1
SAMPLE_ENV_VAR2=sample2

肝心の重複箇所を片方削除した場合の挙動ですが、やはり先程と同様に当該環境変数が消えてしまいます。

$ kubectl exec -it nginx-deployment-55569b7c8b-vdcvc -- env | grep SAMPLE
SAMPLE_ENV_VAR1=sample1

対処法

こういう事態に陥らないために (予防)

  • 何らかのLinterを導入する
    • 但し既存では存在しないかも。軽くググッて出てきたkube-linter, kubevalでは警告が出なかった
  • CI/CDフローのどこかでdiffをチェックする
    • 環境変数の消失は予防できるが重複の発生は予防できない
    • そもそも人力なのであまりアテにできない
    • リリースに要する時間が伸びる
  • ConfigMapやkustomize等を活用してなるべく直接編集する環境変数を減らす

うっかりapplyして環境変数を消してしまった場合 (火消し)

もう一度同じManifestでapplyすれば現状との差分が埋まるため意図した状態に戻ります。
慌てず騒がずkubectl applyを実行するなりCI/CDフローをもう一度回すなりして再デプロイしましょう。

重複定義を発見した場合 (地雷処理)

前述した通り迂闊に削除すると環境変数が足りないPodがデプロイされ事故が発生するので何らかの回避策が必要です。
パッと思いつく方法としてはDeploymentを別名で作成しServiceのLabelSelectorを一時的に切り替えてトラフィックを逃してやりその間に修復することでしょうか。

(今回は検証していませんがkubectl editでどうにかできるかもしれません)

1. 重複発見

2. 退避用Deployment作成

3. トラフィック切り替え

$ kubectl diff -f service.yaml
(略)
@@ -47,7 +47,7 @@
     protocol: TCP
     targetPort: 80
   selector:
-    app: nginx
+    app: nginx-temp
   sessionAffinity: None
   type: ClusterIP
 status:

4. 修復

4. トラフィック切り戻し

$ kubectl diff -f service.yaml
(略)
@@ -47,7 +47,7 @@
     protocol: TCP
     targetPort: 80
   selector:
+    app: nginx
-    app: nginx-temp
   sessionAffinity: None
   type: ClusterIP
 status:

5. 退避用Deployment削除

考察

  • env: はKeyValueではなく配列なのでデータ構造的に重複定義できてしまうのは仕方ない
    • .items[].spec.template.spec.containers[].env[]
  • Kubernetes側でバリデーションし重複を禁止することは可能に思える
  • なぜやっていないか(やらないことによるメリット)は思いつかない
    • IssueやSlackも漁ってみたが類似の議論は見当たらない?

まとめ

  • 迂闊に環境変数重複の解消を試みると事故るので気を付けよう
  • もし重複してしまっても(valueにミスが無ければ)それだけでは悪影響無いはずなので慎重に修復しよう

Discussion