K8s 基礎 #6 ConfigMap と Secret — 設定の分離

読了 16分

#5 Service でマニフェストの中に設定値や秘密値が直接埋まっていると運用が不便である、という点を確認しました。この記事ではその値をマニフェスト本体から分離する方法、すなわち ConfigMapSecret を扱います。設定はどう注入するか、秘密値はどう違うか、そして値が変わったとき Pod にどう反映するかまで整理します。

このシリーズは K8s 基礎 7 編です。

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

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

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

Store config in the environment. 設定は環境に置く。

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

なぜわざわざ分離するのか、1 行ずつ押さえると次の 3 つです。

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

この 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 1 か所です。その中の形が 2 通りに分かれます。

  • 短いキー-値 (スカラー)LOG_LEVEL: "info"APP_GREETING: "hello from k8s" のような 1 行の値。環境変数にそのまま流し込みやすい形です。
  • 複数行ファイル — 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

列名を 1 行で押さえると — NAME / DATA / AGEDATA 列の数字は 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 で短く触れたフィールドです。普通はインラインで 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 つの用途を 1 つの表に整理しておくと判断が早くなります。

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

頭の中の単純な判断ルールは — 値が 1〜2 個なら env、キー-値の束を丸ごと環境変数にすべきなら envFrom、ファイルでなければ意味がないなら volume です。

全部合わせる — Deployment + ConfigMap の 1 サイクル #

上の 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 に書いた内容そのままがコンテナ内で環境変数とファイルとして見えるのが確認できます。これが 1 サイクルの終わりです。マニフェスト本体には値そのものが書かれておらず、「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 という 1 行 #

この記事の最も重要な 1 行がここです。

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 はセキュリティ装置ではなく バイナリをテキストに移すためのエンコーディング です。1 行で解けます。

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 を 1 行ずつ #

Secret は用途に応じていくつか定まった type があります。よく出会う 4 つを 1 行ずつ押さえます。

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 がほぼ自動で扱います。

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 だけだという 1 行を押さえたので、実際の運用で秘密値をどう扱うかも短く整理しておきます。詳しい説明はこのシリーズの範囲外で、名前を知っておくのが目的 です。

  • 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 つ以上をほぼ常に併用します。このシリーズではマニフェストの形と注入方式までを扱い、秘密値運用の深い部分は K8s 中級で 1 編にまとめて扱います。

設定が変わったらどう反映されるか #

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

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

envenvFrom で環境変数に流し込んだ値は Pod が起動するときに 1 度だけ満たされておしまい です。その後 ConfigMap を修正しても、既に立っている Pod の環境変数は変わりません。プロセスの環境ブロックは起動時に 1 度作られると OS のレベルでその形のまま固まるからです — K8s だけの限界ではなく、プロセスモデルの本性です。

新しい値で環境変数を再び満たすには Pod が新しく立つ必要があります。 意図された段階的入れ替えを強制する標準コマンドが次の 1 行です。

設定反映のための Pod 段階的入れ替え
kubectl rollout restart deployment/web

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

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

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

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

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

1 つの表に整理 #

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

運用の単純な基本は — 設定を変えたらまず kubectl rollout restart で意図された段階的入れ替えを 1 度回す です。環境変数でもボリュームでもその時点では確実に反映されます。

片付け #

今日作ったオブジェクトを片付けます。

すべて片付け
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 行ずつ残っていても正常です。

まとめ #

この記事で押さえた流れ:

  • 環境ごとに変わる値と秘密値をマニフェスト本体から切り出すパターンが 12-factor の 「設定は環境に置く」。K8s でこの役割を担うオブジェクトが ConfigMapSecret
  • ConfigMap マニフェストの背骨は apiVersion: v1 / kind: ConfigMap / datadata の下は短いキー-値 (スカラー) と複数行ファイル (|) の両方を書ける。サイズ上限は 1 MiB
  • Pod に注入する道は 3 つ — env.valueFrom.configMapKeyRef (単一キー)、envFrom.configMapRef (全体)、volumes.configMap (ファイル)。単純な判断ルールは 1〜2 個なら env、束全体なら envFrom、ファイルでなければなら volume
  • Secret は ConfigMap とほぼ同じ形に type フィールドが加わるオブジェクト。名前は Secret だが既定の動作は base64 エンコーディングだけで暗号化ではありません。 本当の保護は etcd 暗号化・Sealed Secrets・External Secrets Operator のような別の層から来る。
  • Secret 注入は ConfigMap と同じ 3 つ — secretKeyRef / envFrom.secretRef / volume.secret。平文はマニフェストに stringData で書くのが人が扱いやすい。
  • 環境変数は Pod 起動時に 1 度満たされる — 値変更後反映するには kubectl rollout restartボリュームマウントは分単位で自動更新 されるが、アプリがファイルを読み直すように作られていなければ意味がありません。

次 — Namespace とラベル #

ここまで来ても 1 つ依然として違和感が残っています — 今まで作った全てのオブジェクト (Pod、Deployment、Service、ConfigMap、Secret) が全て default namespace に入ったという点。1 つのクラスタの中に複数の環境 (開発/ステージング) や複数のチームのワークロードが一緒に立たねばならないなら、この単一空間はすぐに狭くなります。そして #4 の selector からずっと出会ってきた ラベル も、このあたりで一度整理しておくに値する量が積もりました。

#7 Namespace とラベル では (1) namespace がクラスタを論理的にどう分けるか(2) ラベル・セレクタの文法とよく使うラベル規約(3) kubectl を namespace 単位で扱う運用 Tips までを追いながら、このシリーズで扱ったマニフェスト 7 種を 1 つのクラスタ内できれいに分けておく形で締めくくります。

X