目次
6 章

ConfigMap と Secret

ConfigMap と Secret で設定とパスワードをマニフェスト本体から分離します。12-factor の「設定は環境に置く」を K8s で解く形、env・envFrom・volume の3つの注入方式、Secret の base64 が暗号化ではないという一行、そして設定変更時に Pod 再起動が必要な理由までを扱います。

第5章 Service まで作ったマニフェストには1点だけ不自然に残っています — イメージタグ・ポート・ドメインといった値がマニフェストに 直接書かれたまま という点です。本章ではその値をマニフェスト本体から分離する2つのオブジェクトである ConfigMapSecret を扱います。設定はどう注入するのか、秘密の値はどう違うのか、そして値が変わったとき Pod へどう再反映するのかまで整理します。

本章の終わりには 同じ Deployment マニフェスト1枚を dev / staging / prod のどこにでもそのまま適用できる形 が手に入ります。環境ごとに変わる値は ConfigMap・Secret 側にだけ分かれていればよく、ワークロード定義は一揃いで十分です。

12-factor の一行 — 設定は環境に置く #

ConfigMap・Secret が解こうとする問題は K8s が最初に作り出したものではありません。コンテナが普及するずっと前から Web アプリ運用の定説として固まったパターンです。最もよく引用される出典が 12-factor app の III 番の項目で、一行で表現するとこうです。

Store config in the environment.

設定は環境に置く。

ここで「設定 (config)」は環境ごとに変わる値たちを指します — DB ホスト、外部 API キー、ログレベル、パスワードといった値です。コンテナ時代以前にはこれを環境変数、/etc/ の設定ファイル、.env ファイルといった形で解いていました。K8s 時代に来てはその役割を ConfigMap (普通の設定値) と Secret (秘密の値) が担います。

なぜわざわざ分離しなければならないのかを一行ずつ押さえておくと次の3つです。

  • イメージ・コード変更なしに設定だけ変える — 同じコンテナイメージをそのまま置いて環境変数だけ違えて動作を変えられます。新しいビルドも、新しいイメージタグも必要ありません。
  • 環境別の多重デプロイ — dev / staging / prod のマニフェストがほぼ同じで、違う部分は ConfigMap・Secret に分かれています。ワークロード定義を環境ごとにまるごと複製しなくてよいです。
  • 秘密の値を git に上げない — DB パスワード、API トークンといった値がマニフェスト本体に平文で書かれていると、それがそのまま git リポジトリに上がってしまいます。Secret オブジェクトに分離すればマニフェストは「その Secret を参照する」という一行だけ書き、実際の値は別の経路でクラスタに入ります。

この3つが運用のほぼすべての決定を左右します。ではまず ConfigMap から見ましょう。

ConfigMap — 設定のキー・バリューのまとまり #

ConfigMap は名前の通り 設定値のキー・バリューの集まり を持っている K8s オブジェクトです。マニフェスト1枚で作ります。

web-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
data:
  LOG_LEVEL: "info"
  APP_GREETING: "hello from k8s"
  app.conf: |
    server {
      listen 80;
      location / {
        return 200 "ok\n";
      }
    }

apiVersion は Service と同様にコアグループの v1 です。ConfigMap はワークロードコントローラではないので apps/v1 ではありません。

核心のフィールドは data フィールドです。その中の形が2つに分かれます。

  • 短いキー・バリュー (スカラ)LOG_LEVEL: "info"APP_GREETING: "hello from k8s" のような一行の値です。環境変数にそのまま注ぎ込んで使うのによい形です。
  • マルチラインのファイル — YAML のブロックスカラ (|) で書いた長いテキストです。上の例の app.conf のように nginx 設定ファイル、アプリの config.yaml、小さな SQL スクリプトといったものをそのまま入れられます。

命令型生成も一度押さえておくと #

マニフェストで書く道のほかに命令型で作る道もあります。

命令型で ConfigMap を作る
kubectl create configmap web-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=APP_GREETING="hello from k8s" \
  --from-file=app.conf

--from-literal はインラインのキー・バリューを、--from-file はディスクのファイルをそのまま取り込んでキーとして登録します。速いですが明らかな短所があります — 作られた ConfigMap がマニフェストとして残りません。 次の人がクラスタの ConfigMap を見ても、この値がどこから来たのかを git リポジトリから追跡する道がありません。運用ではほぼ常に kubectl apply -f の流れで行き、命令型はデバッグ・実験中だけ少し使います。

サイズ上限 — 1 MiB #

ConfigMap が持っている値の総和は 1 MiB (メビバイト) を超えられません。これは K8s の任意の決定ではなく、その下の etcd がオブジェクト1個あたり持てるサイズの上限です。小さな設定ファイル・環境変数のまとまりはこの上限の中に十分入りますが、大きな静的アセット (例: モデルの重み、大きな SQL スキーマ、ブラウザ用のバンドルファイル) を ConfigMap に詰め込むのはパターンに合いません。そうしたアセットは別途のストレージ (S3・GCS、PV) に置いてコンテナから取得してくる形が正攻法です。

作っておいて一度見ましょう。

ConfigMap 適用
kubectl apply -f web-config.yaml
出力例
configmap/web-config created
ConfigMap 一覧
kubectl get cm
出力例
NAME               DATA   AGE
kube-root-ca.crt   1      2d
web-config         3      10s

カラム名を一行で押さえておくと — NAME / DATA / AGE です。DATA カラムの数字は data の下のキーの個数です。上の例で3つのキー (LOG_LEVELAPP_GREETINGapp.conf) を入れたので3と出ます。kube-root-ca.crt は K8s が自前で持っている ConfigMap なので気にしなくてよいです。

ConfigMap を Pod に注入する3つの方法 #

ConfigMap を作るだけでは Pod がその値に気づきません。Pod がその値をどう受け取るかをマニフェストに書いてあげなければなりません。方法が3つあり、この3つの違いをつかんでおくのが本章の最も実用的な部分です。

1. 単一キー → 環境変数 (env.valueFrom.configMapKeyRef) #

ConfigMap のキー1個をコンテナの環境変数1つにマッピングする最も明示的な形です。

env.valueFrom.configMapKeyRef
spec:
  containers:
    - name: web
      image: nginx:1.27
      env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: web-config
              key: LOG_LEVEL

env第3章 kubectl と最初の Pod のマニフェストで短く見たことがあるあのフィールドです。普通はインラインで value: "info" のように書きますが、valueFrom を使うと 別のオブジェクトから値を引っ張ってくる という意味になります。configMapKeyRefname が ConfigMap 名、key がその中のキー名です。

長所は明示性です — どの環境変数がどの ConfigMap のどのキーから来たかがマニフェストにそのまま現れます。短所は長さです。環境変数が複数あればその分だけ行が長くなります。

2. 全キー → 環境変数を一度に (envFrom.configMapRef) #

ConfigMap の中のキー全部を一度に環境変数として注ぎ込みたいときに使う形です。

envFrom.configMapRef
spec:
  containers:
    - name: web
      image: nginx:1.27
      envFrom:
        - configMapRef:
            name: web-config

こう書くと web-config のすべてのキーがコンテナの環境変数として自動注入されます。LOG_LEVELAPP_GREETINGapp.conf の3つがすべて同じ名前の環境変数になります。短くて楽ですが1つ注意する点があります — ConfigMap のキー名がそのまま環境変数名になります。 ですから ConfigMap を envFrom で使うつもりならキー名を UPPER_SNAKE_CASE に置くのが無難です。app.conf のようにドットが入ったキーは環境変数として使うのに適しません (シェルでドットが入った環境変数は扱いにくいです)。そうしたキーは次の節のボリュームマウントへ行くのが正解です。

3. ファイルとしてマウント (volumes.configMap + volumeMounts) #

app.conf のように それ自体がファイルであってこそ意味のある値 はコンテナの中にファイルとして入れてあげなければなりません。ConfigMap をボリュームのようにマウントするとキーごとに1つのファイルが作られます。

volumes + volumeMounts
spec:
  containers:
    - name: web
      image: nginx:1.27
      volumeMounts:
        - name: app-conf
          mountPath: /etc/myapp
  volumes:
    - name: app-conf
      configMap:
        name: web-config
        items:
          - key: app.conf
            path: app.conf

読み方は2段階です。

  • volumes — Pod 単位で定義するボリュームです。上では app-conf という名前のボリュームを作り、その中身が web-config ConfigMap の app.conf キーで決まっています。items を書けば ConfigMap の中から一部のキーだけ選んでマウントできます。items を省略すると ConfigMap のすべてのキーがそれぞれファイルとしてマウントされます。
  • volumeMounts — コンテナ単位で、上のボリュームをコンテナファイルシステムのどのパスに差し込むかを決めます。mountPath: /etc/myapp ならコンテナの中で /etc/myapp/app.conf というパスでその内容が見えます。

いつどれを使うか #

3つの用途を一表に整理しておくと決定が一段と速くなります。

注入の形適した場合説明
env.valueFrom.configMapKeyRef環境変数が1〜2個明示的ですが長さが長いです
envFrom.configMapRef環境変数のまとまり全体短いですがキー名の規則が必要です
volumes.configMap設定ファイル全体をファイルとしてアプリがファイルから読むように作られているときに適します

頭の中のシンプルな決定ルールは — 値が1〜2個なら env、キー・バリューのまとまりがまるごと環境変数になるべきなら envFrom、ファイルであってこそ意味があるなら volume です。

全体を合わせてみる — Deployment + ConfigMap 一連の流れ #

上の3つの形を1つのマニフェストにまとめてみます。第4章 の web Deployment を持ってきて、環境変数1個は env で、残りは envFrom で、そして app.conf はボリュームとしてマウントする構成です。

web.yaml — ConfigMap を引っ張って使う Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
          env:
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: web-config
                  key: LOG_LEVEL
          envFrom:
            - configMapRef:
                name: web-config
          volumeMounts:
            - name: app-conf
              mountPath: /etc/myapp
      volumes:
        - name: app-conf
          configMap:
            name: web-config
            items:
              - key: app.conf
                path: app.conf

(例として envenvFrom を両方書きました — 実務ではどちらか一方だけ使うのが普通です。同じ ConfigMap から同一のキーを2回引っ張ると、最後に定義された側が優先します。)

適用して結果を確認します。

apply
kubectl apply -f web-config.yaml -f web.yaml
出力例
configmap/web-config unchanged
deployment.apps/web created

Pod の中に入って環境変数とファイルが実際に入ったか見ます。

環境変数の確認
kubectl exec -it deploy/web -- env | grep -E "LOG_LEVEL|APP_GREETING"
出力例
LOG_LEVEL=info
APP_GREETING=hello from k8s
ファイルマウントの確認
kubectl exec -it deploy/web -- cat /etc/myapp/app.conf
出力例
server {
  listen 80;
  location / {
    return 200 "ok\n";
  }
}

ConfigMap に書いたそのままがコンテナの中で環境変数とファイルとして見えるのが確認できます。これが一連の流れの終わりです。マニフェストの本体には値そのものは書かれておらず、「ConfigMap から持ってくる」という参照だけが書かれています。

Secret — 秘密の値の分離 #

ConfigMap が普通の設定値のためのオブジェクトなら、Secret はパスワード・トークン・証明書のようにマニフェスト本体に平文で置いてはいけない値のためのオブジェクトです。マニフェストの形は ConfigMap とほぼ同じです。

db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  DB_USER: "myapp"
  DB_PASSWORD: "s3cret-do-not-commit"

apiVersion は同様に v1 です。ConfigMap と違う部分が2つあります。

  • type — Secret は種類がいくつもあるので type フィールドがあります。普通のキー・バリューのまとまりなら Opaque (デフォルト値) です。
  • stringData vs data — Secret 本体で値を書く2つの道です。

data vs stringData — そして base64 という一行 #

本章の最も重要な一行がここです。

Secret という名前が付いていますが、デフォルトの動作は base64 エンコードにすぎません。暗号化ではありません。

kubectl get secret db-secret -o yaml で見るとマニフェストの stringData が消えて data の下に base64 エンコードされた文字列だけが見えます。

apply と確認
kubectl apply -f db-secret.yaml
kubectl get secret db-secret -o yaml
出力例 — 抜粋
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  DB_USER: bXlhcHA=
  DB_PASSWORD: czNjcmV0LWRvLW5vdC1jb21taXQ=

bXlhcHA=czNjcmV0LWRvLW5vdC1jb21taXQ= は難しく見えますが、base64 はセキュリティの仕組みではなく バイナリをテキストに移すためのエンコード です。一行で復号できます。

base64 デコード
kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
出力例
s3cret-do-not-commit

元の値がそのまま出ます。ですから Secret オブジェクトを扱う作業は本質的に 平文の秘密の値を扱う作業 と同じだと見なすべきです。本当の保護は別の層から来なければなりません — あとで短く押さえておきます。

datastringData の違いは人が書きやすいかどうかだけです。

  • data — base64 で事前にエンコードした値を書きます。人が直接書くのは煩わしいです。
  • stringData — 平文を書けば K8s が受け取って base64 で勝手にエンコードします。人がマニフェストで Secret を作るときはほぼ常にこちらを使います。

命令型生成も ConfigMap と同じ方向です。

命令型で Secret を作る
kubectl create secret generic db-secret \
  --from-literal=DB_USER=myapp \
  --from-literal=DB_PASSWORD=s3cret-do-not-commit

Secret type を一行ずつ #

Secret は用途によっていくつかの決まった type があります。よく出会う4つを一行ずつ押さえておきます。

type用途
Opaqueデフォルト。任意のキー・バリューのまとまり
kubernetes.io/dockerconfigjsonプライベートコンテナレジストリの資格情報。imagePullSecrets で参照
kubernetes.io/tlsTLS 証明書・キーのペア。Ingress の HTTPS 終端で参照
kubernetes.io/service-account-tokenServiceAccount トークン。RBAC と一緒に登場

このうち人が直接マニフェストで触るのは普通 Opaquekubernetes.io/tls 程度です。dockerconfigjsonkubectl create secret docker-registry という専用コマンドで作るのが一般的で、service-account-token は K8s がほぼ勝手に扱います。RBAC と ServiceAccount の関係は 第14章 RBAC / NetworkPolicy / ResourceQuota第16章 RBAC / ServiceAccount の深層 で本格的に扱います。

get secret 出力カラム #

Secret 一覧
kubectl get secret
出力例
NAME        TYPE     DATA   AGE
db-secret   Opaque   2      1m

ConfigMap と違う点は TYPE カラムが追加されたことだけです — NAME / TYPE / DATA / AGEDATA の数字はキーの個数です。

Secret を Pod に注入 #

Secret を Pod に注入する形は ConfigMap と同じです — キー名が少し違うだけです。3つを一度に整理します。

1. 単一キー → 環境変数 (env.valueFrom.secretKeyRef) #

env.valueFrom.secretKeyRef
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: DB_PASSWORD

ConfigMap の configMapKeyRefsecretKeyRef に変わるのがすべてです。

2. 全キー → 環境変数を一度に (envFrom.secretRef) #

envFrom.secretRef
envFrom:
  - secretRef:
      name: db-secret

configMapRefsecretRef に変わります。同じ心構えでキー名は UPPER_SNAKE_CASE に置くのが無難です。

3. ファイルとしてマウント (volume.secret) #

volumes + volumeMounts (Secret)
volumes:
  - name: db-creds
    secret:
      secretName: db-secret
volumeMounts:
  - name: db-creds
    mountPath: /etc/db
    readOnly: true

ConfigMap ボリュームは configMap キーの下に name を書きましたが、Secret ボリュームは secret キーの下に secretName で名前が少し違います。もう1つの違いはディスク上の位置です — ボリュームマウントで解かれた Secret ファイルはノードのディスクに平文で落ちません。K8s が tmpfs (メモリベースのファイルシステム) に置いて、ノードを再起動すると消えるようにしてあります。ConfigMap にはこうした保護がありません。

本物の秘密の値を安全に扱うには #

Secret が base64 にすぎないという一行を押さえたので、実際の運用で秘密の値をどう扱うのかも短く整理しておきます。深いインストールは本章の範囲の外で、名前だけ知っておくのが目的 です。

  • etcd 段階の暗号化 — apiserver の EncryptionConfiguration を設定して KMS (AWS KMS、GCP KMS など) と連携すると、etcd に入る Secret 値が暗号化されたまま保存されます。クラスタを自分で運用するとき最初にオンにする項目です。
  • Sealed Secrets (Bitnami) — Secret マニフェストをクラスタの公開鍵で暗号化した SealedSecret というオブジェクトに変換しておきます。この暗号化されたマニフェストは git に安全に上げてよく、クラスタに入るとコントローラが解いて一般の Secret に変えてくれます。
  • External Secrets Operator — Vault、AWS Secrets Manager、GCP Secret Manager、Azure Key Vault といった 外部の秘密ストア が本物の秘密の値を持っていて、このオペレーターがその値を K8s Secret に同期してくれます。運用の一般的な答えです。

運用クラスタでは上の3つのうち1つ以上をほぼ常に併せて使います。本章ではマニフェストの形と注入方式までだけ扱い、秘密の値運用の深い部分 (sealed-secrets vs external-secrets vs SOPS の比較、IRSA と連携したパスワード0運用) は 第29章 シークレット運用 で本格的に整理します。

設定が変わるとどう適用されるか #

ConfigMap・Secret を作ったので自然に次の問いが浮かびます — 値を変えたときその変更が Pod に自動で反映されるのか? 答えは注入方式によって分かれます。運用でよく混乱する部分なので一度整理しておく価値があります。

環境変数で注入した場合 — 起動時点に一度 #

envenvFrom で環境変数に注ぎ込んだ値は Pod が起動するとき一度だけ埋められて終わり です。その後に ConfigMap を修正してもすでに起動している Pod の環境変数は変わりません。プロセスの環境ブロックは起動時点に一度作られると OS の次元でその形のまま固まるからです — K8s だけの限界ではなくプロセスモデルの本性です。

新しい値で環境変数を再び埋めるには Pod が新しく起動しなければなりません。 意図された段階的置き換えを強制する標準コマンドが次の一行です。

設定反映のための Pod 段階的置き換え
kubectl rollout restart deployment/web

このコマンドは 第4章 のローリングアップデートと同じメカニズムで動作します — ただし spec はそのまま置いて Pod だけ段階的に置き換えます。新しく起動してきた Pod が ConfigMap の最新の値を環境変数として持って起動します。

(kubectl rollout restart は 1.15+ から導入された標準コマンドです。それ以前は Pod ラベルに任意のアノテーションを追加して spec を少し変える迂回方法を使っていましたが、今ではほとんど触りません。)

ボリュームでマウントした場合 — 自動更新 #

ボリュームでマウントした ConfigMap・Secret は K8s が周期的に同期してファイルが自動で更新 されます。ConfigMap を修正すると普通は分単位の遅延の中でコンテナの中のファイル内容が変わっています。この遅延は kubelet の同期周期 (デフォルト1分) と噛み合っているので、即時ではありません。

ただし1つ条件が付きます — アプリがそのファイルを再び読むコードを持っていてこそ変更が意味を持ちます。 nginx のように SIGHUP を受けると設定を再び読むプロセスなら動作しますが、起動するとき一度だけ設定を読んで終わりのアプリはファイルが変わっても振る舞いが変わりません。設定ファイルの変更を検知して自動 reload をかけるサイドカー (例: configmap-reload) を置くパターンもよく見られます。

一表で整理 #

注入の形変更反映強制更新
env / envFrom自動反映されない (起動時1回)kubectl rollout restart
volume自動反映 (分単位の遅延)アプリ自体の reload または kubectl rollout restart

運用のシンプルな基本は — 設定を変えたらまず kubectl rollout restart で意図された段階的置き換えを一度回す です。環境変数でもボリュームでもその時点では確実に反映されます。

後片付け・クリーンアップ #

今日作ったオブジェクトを整理します。

すべてクリーンアップ
kubectl delete -f web.yaml
kubectl delete -f web-config.yaml
kubectl delete -f db-secret.yaml
出力例
deployment.apps "web" deleted
configmap "web-config" deleted
secret "db-secret" deleted

kubectl get deploy,cm,secret で空になっているか確認すれば出発点に戻ります。kube-root-ca.crt ConfigMap と default-token-... Secret は K8s が自前で持っているオブジェクトなので一行ずつ残っていても正常です。

練習問題 #

  1. 上の本文どおり web-config.yamlLOG_LEVEL 値を "info" から "debug" に変えたあと kubectl apply してみてください。すでに起動している Pod の LOG_LEVEL 環境変数がどう見えるか (kubectl exec deploy/web -- env | grep LOG_LEVEL) と、同じ時点の /etc/myapp/app.conf ファイルがどう見えるかを別々に記録します。そのあと kubectl rollout restart deployment/web を回して2つの出力がどう変わるかを §「設定が変わるとどう適用されるか」の表と合わせてメモします。
  2. db-secret.yamlstringData で書いた平文の値が kubectl get secret db-secret -o yaml 出力でどう表記されるかを確認し、kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' \| base64 -d で元の値を復元してみてください。「base64 は暗号化ではない」の意味を自分の言葉で一段落で整理し、運用で本当の保護のためにどんなオプションがあるかを §「本物の秘密の値を安全に扱うには」の3つのオプションを短く比較します。
  3. web-config.yamlapp.conf キー名を app_conf のようにドットなしの名前に変えたあと、Deployment で envFrom: [configMapRef: ...] の一行だけ書いてすべてのキーが環境変数になるようにしてみてください。kubectl exec deploy/web -- env 出力に app_conf がどう入ってきたか (入ってきたが値がマルチラインのテキストなのでどう見えるか) を確認し、§「いつどれを使うか」の決定ルールを自分の手に合わせて書き直しておきます。

一行まとめ: ConfigMap と Secret は 12-factor の「設定は環境に置く」を K8s で解く2つのオブジェクトであり、マニフェスト本体から環境別の値と秘密の値を分離する標準の形である。Pod に注入する道は env (単一キー)・envFrom (全体)・volume (ファイル) の3つで、それぞれの変更反映モデルが異なる。Secret のデフォルトの動作は base64 エンコードにすぎず暗号化ではない — 本当の保護は etcd 暗号化、Sealed Secrets、External Secrets Operator といった別の層から来る。

次の章 #

ここまで来ても1つが依然として不自然に残っています — これまで作ったすべてのオブジェクト (Pod、Deployment、Service、ConfigMap、Secret) が全部 default ネームスペース に入ったという点です。1つのクラスタの中に複数の環境 (開発 / ステージング) や複数のチームのワークロードが一緒に起動していなければならないなら、この単一の空間がすぐに狭くなります。そして 第4章 の selector からずっと出会ってきた ラベル も、このあたりで一度整理しておくに足る量がたまりました。

第7章 Namespace とラベル では、ネームスペースがクラスタをどう論理的に分けてくれるのか、ラベル・セレクタの文法とよく使うラベルのコンベンション、kubectl をネームスペース単位で扱う運用のヒントまでたどりながら、1部で扱ったマニフェスト7種を1つのクラスタの中できれいに分けておく形で1部を締めくくります。

X