ConfigMap を書き換えたのに、なぜ Pod の環境変数は変わらないのか?
あなたも設定値を ConfigMap に外出しし、kubectl apply で更新したことがあるはずだ。マニフェストは確かに新しい値に変わっている。ところが Pod の中に入って env を叩くと、古い値のままだったという経験はないだろうか。API 上は更新できているのに、なぜ Pod の中身は変わらないのか。
configMapKeyRef/secretKeyRefで注入した環境変数は Pod 起動時に一度だけ解決される値であり、ConfigMap/Secret を更新しても実行中のプロセスの環境変数は書き換わらない。反映には Pod の再起動が要る。- ボリュームマウントなら kubelet が変更を検知してファイルを更新するが、即座ではなく同期のタイミング依存の遅延がある。
subPathでマウントした場合はその更新の対象からも外れ、二重に反映されない。 Secretは名前に反して暗号化ではなく Base64 エンコードにすぎず、etcd には既定で平文相当のまま保存される。暗号化するにはEncryptionConfigurationを明示的に設定する必要がある。
なぜ設定をコンテナイメージの外に出すのか
コンテナイメージは1つビルドしたら、開発・ステージング・本番のどこでも同じものを動かしたい。しかし DB の接続先や外部 API のエンドポイントは環境ごとに違う。これらの値をコードやイメージに焼き込んでしまうと、環境が変わるたびにイメージを作り直す羽目になり、「同じイメージをどこでも動かす」という前提が崩れる。
ConfigMap と Secret は、この設定値をイメージから切り離して Kubernetes の API オブジェクトとして管理するための仕組みだ。ConfigMap は接続先ホスト名のような機密でない値を、Secret はパスワードやトークンのような機密情報を保持する用途に分かれている。Pod のマニフェストは、これらを環境変数として注入するか、ファイルとしてボリュームマウントするかを選んで参照する。
環境変数への注入が「起動時一回きり」になる理由
環境変数はプロセスが起動する瞬間に OS からそのプロセスへ渡される値であり、後から外部が書き換えられるものではない。configMapKeyRef で ConfigMap の値を環境変数に注入する設定も同じで、kubelet がコンテナを起動する時点で ConfigMap の中身を読みに行き、その時点の値をコンテナランタイムに渡して環境変数として確定させる。
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db_host
この DB_HOST は、Pod が起動した瞬間の app-config の値で固定される。その後 kubectl apply で ConfigMap の db_host を書き換えても、Kubernetes 公式ドキュメントが明記する通り、すでに起動しているコンテナの環境変数は自動更新されない。新しい値を反映させるには、Pod を再起動して環境変数の解決をもう一度やり直す以外に方法がない。
環境変数への注入は、朝の朝礼で係員が紙のメモを1枚ずつ手渡すようなものだ。メモを受け取った人は自分の机に戻り、その紙だけを見て一日中作業する。昼に廊下の掲示板の張り紙が新しい内容に差し替わっても、すでにメモを持って席に着いた人はそれに気づかない。気づくのは、次にまた廊下へ出て掲示板を見に行った人だけだ。ボリュームマウントは「都度掲示板を見に行く」動作に近く、環境変数は「渡された紙を後生大事に持ち続ける」動作に近い。
ボリュームマウントは更新されるが、即座ではない
同じ ConfigMap をボリュームとしてマウントした場合は事情が違う。kubelet は参照先の ConfigMap / Secret の変更を継続的に見張っており、変更を検知するとマウント先のファイルの中身を書き換える。
┌─ ConfigMap 更新 ───────────┐
│ kubectl apply │
└─────────────┬────────────────┘
▼
kubelet の同期ループ
(タイミング依存の遅延あり)
│
┌───────┴────────┐
▼ ▼
volume mount env var
ファイルが更新 変化なし
(subPathは対象外) (再起動が必要)
ただしこの更新は kubelet の同期タイミングに依存しており、kubectl apply の直後に即座に反映されるわけではない。さらに、ボリュームの中の1ファイルだけを subPath で指定してマウントしている場合は、公式ドキュメントが明記する通りこの自動更新の対象から外れ、コンテナが動いている限り最初にコピーされた内容のまま固定される。加えてファイルが更新されても、多くのアプリケーションは起動時にしか設定ファイルを読まないため、ファイルの変更に気づいて再読込する処理をアプリ側に実装していない限り、結局は実質的な反映にはならない。
Secret は「隠して」いるだけで「守って」いない
ConfigMap と同じ更新の癖を Secret も引き継ぐが、Secret にはもう1つ見落とされがちな性質がある。Secret の値は Base64 でエンコードされているが、これは暗号化ではなく単なる文字コード変換であり、誰でも base64 --decode で元の値に戻せる。Kubernetes 公式ドキュメントも、Secret は既定では etcd に暗号化されずに保存され、etcd に直接アクセスできる者やその権限を持つ者は中身を読めると明言している。実際に機密性を確保するには、API サーバーに EncryptionConfiguration を設定して aescbc や aesgcm などのプロバイダで暗号化を有効にする必要があり、これは既定で有効になっている機能ではない。
更新の扱いにくさへの対策として使えるのが immutable フィールドだ。immutable: true を設定した ConfigMap / Secret は、作成後に中身を書き換えられなくなる。
apiVersion: v1 kind: ConfigMap metadata: name: app-config-v2 immutable: true data: db_host: db.example.internal
一見不便に思えるが、値を変えたいときは新しい名前で作り直して Pod の参照先を切り替える運用に統一されるため、「更新したのに一部の Pod にしか反映されていない」という中途半端な状態を構造的に防げる。加えて公式ドキュメントは、immutable な ConfigMap / Secret は kubelet が変更を見張る必要がなくなるため、大規模クラスタで API サーバーへの負荷を下げる効果もあるとしている。ただし一度 true にすると false には戻せない。
DB パスワードを Secret 側だけ更新して「反映されない」と焦る場面や、Kustomize の configMapGenerator が ConfigMap の内容からハッシュ値を計算して名前の末尾に付与し、値が変わるたびに別名の ConfigMap を作って Pod の参照先ごと更新させる(結果として Pod のローリング再起動を誘発する)設計に出会う場面がこれにあたる。External Secrets Operator のような外部シークレット管理ツールも、内部的にはこの「更新には Secret の再作成 or Pod の再起動が要る」という制約を踏まえて設計されている。
同じ ConfigMap を環境変数とボリュームマウントの両方で参照する Pod を作り、更新後にどちらが反映されるかを実際に見比べる(kind または任意のローカル Kubernetes クラスタが必要)。
kind create cluster --name cfgdemo 2>/dev/null
kubectl create configmap demo-cm --from-literal=MSG=v1
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
env:
- name: MSG_ENV
valueFrom:
configMapKeyRef:
name: demo-cm
key: MSG
volumeMounts:
- name: cfg
mountPath: /etc/cfg
volumes:
- name: cfg
configMap:
name: demo-cm
EOF
kubectl wait --for=condition=Ready pod/demo --timeout=60s
kubectl exec demo -- sh -c 'echo ENV=$MSG_ENV; cat /etc/cfg/MSG'
kubectl create configmap demo-cm --from-literal=MSG=v2 \
--dry-run=client -o yaml | kubectl apply -f -
sleep 60
kubectl exec demo -- sh -c 'echo ENV=$MSG_ENV; cat /etc/cfg/MSG'
更新前はどちらも v1 と表示される。更新して1分ほど待った後の2回目の exec では、ENV=v1 のまま変わらない一方で、ボリューム側のファイルは v2 に書き換わっているのが確認できる。同じ ConfigMap から注入していても、経路によって反映のタイミングがまったく違うことが手元で体感できる。
- ConfigMap を kubectl apply で更新すれば、参照している Pod にはすぐ反映される — 環境変数として注入している場合は Pod の再起動まで一切反映されない。ボリュームマウントでも kubelet の同期タイミング依存で即座ではない。
- Secret は Base64 でエンコードされているので中身は読めない — Base64 は暗号化ではなく可逆なエンコードであり、誰でもデコードできる。既定では etcd にも平文相当で保存されるため、機密性が要るなら暗号化を別途有効にする必要がある。
- immutable: true にしても、後から値だけ書き換えれば安全に更新できる — immutable にした ConfigMap / Secret は値の書き換え自体ができない。更新したい場合は別名で作り直し、Pod の参照先を切り替える必要がある。
- ConfigMap
- 機密でない設定値をキーと値の組で保持する Kubernetes の API オブジェクト。
- Secret
- パスワードやトークンなど機密情報を保持する API オブジェクト。値は Base64 エンコードされるが暗号化ではない。
- kubelet の同期ループ
- 各ノードで動く kubelet が、参照先の ConfigMap / Secret の変更を検知してボリューム内のファイルを更新し続ける仕組み。
- subPath
- ボリューム内の特定のファイル/ディレクトリだけをマウントする指定方法。使うと自動更新の対象から外れる。
- immutable(イミュータブル)
- ConfigMap / Secret を作成後に変更不可にするフィールド。一度 true にすると false には戻せない。
- ConfigMaps | Kubernetes — 環境変数とボリュームマウントの反映挙動の違い、immutable フィールドの仕様を定める公式ドキュメント。
- Secrets | Kubernetes — Secret が既定では暗号化されず etcd に保存されること、subPath マウントが自動更新の対象外であることを明記する公式ドキュメント。
- Encrypting Confidential Data at Rest | Kubernetes — EncryptionConfiguration で Secret を etcd 上で暗号化する具体的な設定手順。