新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第10回です。第9回で PostgreSQL を追加して 3 層アプリを完成させました。ただし DB 接続情報(ホスト名・ユーザー・パスワード)は Pod のマニフェストに直接(inline)書いたままです。今回は ConfigMap と Secret で設定値と機密情報をコードから切り離し、ServiceAccount の基礎も学びます。設定を外部化すると、同じイメージのまま環境ごとに値を変えられ、パスワードをマニフェストや Git に残さずに済みます。
動作確認バージョン:kind v0.31.0(K8s v1.35.0)/ kubectl v1.35.6 / fanclub-backend:0.3.0 / fanclub-frontend:0.1.0 / postgres:18(2026-06-27 実機検証時点)
今ここマップ(全 19 回中の現在地)
現在地は第 3 部「アプリリソース」の第 10 回です。完成した 3 層アプリの設定を外部化して整えます。
- 第 1 部 コンテナと Docker(第 1〜4 回)
- 第 2 部 Kubernetes 基礎(第 5〜6 回)
- 第 3 部 アプリリソース(第 7〜11 回)← 今ここ
- 第 4 部 ワークロード戦略(第 12〜14 回)
- 第 5 部 セキュリティ基礎(第 15〜16 回)
- 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)
この回のゴール
- ConfigMap(非機密設定)と Secret(機密情報)を使い分け、外部化できる。
envFrom/valueFrom/ Volume マウントで設定を Pod に注入できる。- ServiceAccount の役割と Pod への紐付け、
automountServiceAccountToken: falseの意味を説明できる。
設定をコードから切り離す理由
接続情報やパラメータをイメージや Pod 定義に埋め込むと、次の問題が起きます。
- 環境差分に弱い:dev と prod で DB ホストが違うたびにマニフェストを書き換える羽目になります。
- 機密が漏れる:パスワードを Pod 定義に平文で書くと、マニフェストや Git に残ります。
- 再ビルドが要る:設定をイメージに焼き込むと、値を変えるだけでイメージを作り直すことになります。
設定値を ConfigMap、機密情報を Secret として外部化すれば、同じイメージのまま値だけを差し替えられます。第9回の Backend イメージ(0.3.0)は接続情報を環境変数から読む作りなので、今回イメージは作り直しません。設定の置き場所を Pod 直書きから ConfigMap / Secret に移すだけです。「設定変更にイメージの再ビルドは要らない」——これが外部化の最大の利点です。
ConfigMap(非機密設定)
作成方法
ConfigMap はキーと値の集合です。作り方は 3 通りあります。
--from-literal=KEY=VALUE:コマンドで直接キー・値を指定。--from-file=ファイル:ファイル内容を値にする(設定ファイルの取り込み)。- YAML マニフェスト:
data:に key: value を列挙してkubectl apply。
本回は再現性と版管理のため YAML で作ります。--from-literal の例も示します。実行コマンド(リテラル指定の例・後で YAML 版に統一するのでここでは作成しません):
$ kubectl create configmap sample --from-literal=DB_HOST=fanclub-db --dry-run=client -o yaml
--dry-run=client -o yaml は、実際には作らずに生成される YAML を表示します。マニフェストの雛形づくりに便利です。
注入方法(envFrom と valueFrom)
envFrom:ConfigMap / Secret の全キーを環境変数として一括注入します。valueFrom:特定の 1 キーだけを選んで 1 つの環境変数にします(configMapKeyRef/secretKeyRef)。- Volume マウント:ConfigMap / Secret をファイルとしてマウントします(設定ファイルや証明書向け)。
本回の fanclub-backend は全キーをまとめて使うので envFrom を使います。一方、特定の 1 キーだけを別名の環境変数に入れたいときは valueFrom を使います(書き方の例)。
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: fanclub-config
key: DB_HOST

Secret(機密情報)
base64 は暗号化ではない
Secret の値は etcd 上で base64 エンコードされて保存されますが、base64 は暗号化ではありません。誰でも元に戻せるので、「Secret に入れたから安全」とは言えません。Secret の本当の価値は、ConfigMap と分けて扱える(RBAC で参照を絞る・ログや describe に出にくい・専用の注入経路を持つ)点にあります。保存時の本格的な暗号化(etcd encryption at rest)は本シリーズ第3巻 CKS で扱います。
YAML で Secret を書くときは data:(base64 済みの値)ではなく stringData:(平文で書くと Kubernetes が base64 化)が便利です。kubectl create secret generic --from-literal=... でも作れます。
secretRef と Volume マウント
Secret も ConfigMap と同様、envFrom(secretRef)で環境変数に一括注入するか、valueFrom.secretKeyRef で 1 キーずつ注入できます。証明書のようなファイル形式の機密は Volume マウントを使います。本回の DB ユーザー・パスワードは環境変数として渡します。
ServiceAccount 基礎
ServiceAccount とは
ServiceAccount(SA)は、Pod が Kubernetes API と通信するときに使うクラスタ内のアイデンティティ(認証情報)です。人間が使うユーザーアカウントに対し、SA は Pod(プロセス)用です。Pod に SA を指定しないと、その Namespace の default SA が自動で割り当てられます。
トークンの自動マウントを止める
既定では SA のトークンが Pod 内の /var/run/secrets/kubernetes.io/serviceaccount/ に自動でマウントされます。しかし、API を呼ばないアプリにトークンを置くのは不要なリスクです(漏れれば API 操作に悪用されうる)。fanclub-backend は DB と通信するだけで Kubernetes API を呼ばないので、automountServiceAccountToken: false でトークンのマウントを止めます。これは最小権限の基本です。
SA と API Server 認証の関係
マウントされたトークンは JWT で、API Server はこれを検証して「どの SA からの要求か」を識別します。その SA が何をできるかは RBAC(Role / RoleBinding)で決まります。SA は「誰であるか(認証)」、RBAC は「何を許すか(認可)」の担当です。RBAC の本格実装は第15回で扱います。本回は SA の作成と紐付け、トークン抑止までを押さえます。

fanclub-api への適用
ConfigMap・Secret・ServiceAccount を作る
次のマニフェストを developer のホームに fanclub-config.yaml として作成します(一般ユーザー所有のファイルで、kubectl apply はクラスタへ送るだけなので root 権限は不要です)。非機密の設定は ConfigMap、DB のユーザー・パスワードは Secret、Backend 用の専用 SA をまとめて定義します。
apiVersion: v1
kind: ConfigMap
metadata:
name: fanclub-config
data:
DB_HOST: fanclub-db
DB_PORT: "5432"
DB_NAME: fanclubdb
API_PORT: "8080"
API_BASE_URL: /api
JAVA_OPTS: "-XX:MaxRAMPercentage=75.0 -Djava.io.tmpdir=/opt/payara/tmp"
---
apiVersion: v1
kind: Secret
metadata:
name: fanclub-db-secret
type: Opaque
stringData:
DB_USER: appuser
DB_PASSWORD: apppassword
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fanclub-backend
- 数値も環境変数では文字列なので
DB_PORT: "5432"とクオートします。 API_PORT/API_BASE_URLは設定一覧に合わせた予約キーです。現状の Backend は待受 8080・ベースパス/apiがコード側で固定のため参照しません(Frontend のベース URL は後述の別 ConfigMapconfig.jsで渡します)。値を変えても挙動は変わらない点に注意してください。- Secret は
stringDataに平文で書くと Kubernetes が base64 化して保存します。 JAVA_OPTSは-Djava.io.tmpdir=/opt/payara/tmpを必ず含めます。envFromで渡す値は、イメージ側(Dockerfile のENV JAVA_OPTS)の値を上書きします。ここで省くと Payara の書き込み先(tmpdir)指定が失われ、読み取り専用ルートでは起動に失敗します。
実行コマンド:
$ kubectl apply -f fanclub-config.yaml
$ kubectl get configmap fanclub-config
$ kubectl get secret fanclub-db-secret
$ kubectl get serviceaccount fanclub-backend
実行結果(例):
configmap/fanclub-config created
secret/fanclub-db-secret created
serviceaccount/fanclub-backend created
NAME DATA AGE
fanclub-config 6 2s
NAME TYPE DATA AGE
fanclub-db-secret Opaque 2 2s
NAME AGE
fanclub-backend 2s
ここで「base64 は暗号化ではない」を実際に確かめます。Secret の data の値は base64 なので、参照権限があれば誰でも平文に戻せます(だからこそ Secret は RBAC で参照を絞ることが重要です)。
実行コマンド:
$ kubectl get secret fanclub-db-secret -o jsonpath='{.data.DB_PASSWORD}'; echo
$ kubectl get secret fanclub-db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d; echo
実行結果(例):1 行目が保存されている base64、2 行目がそれを復号した平文です。これがあるため「Secret に入れたら暗号化されて安全」ではありません。
YXBwcGFzc3dvcmQ=
apppassword
Backend Pod を envFrom 版に作り直す
第9回の Backend Pod は接続情報を env に直接書いていました。これを envFrom(ConfigMap + Secret)に置き換え、専用 SA を紐付け、トークンのマウントを止めます。イメージは 0.3.0 のままです。次のマニフェストを developer のホームに fanclub-backend-pod.yaml として作成します(一般ユーザー所有・root 不要)。
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend
labels:
app: fanclub-backend
spec:
serviceAccountName: fanclub-backend
automountServiceAccountToken: false
initContainers:
- name: wait-for-db
image: busybox:1.37
command:
- sh
- -c
- 'until nslookup fanclub-db.default.svc.cluster.local >/dev/null 2>&1; do echo "waiting for fanclub-db..."; sleep 2; done; echo "fanclub-db resolved"'
- name: log-shipper
image: busybox:1.37
restartPolicy: Always
command:
- sh
- -c
- 'echo "sidecar started"; tail -F /var/log/app/app.log 2>/dev/null || sleep infinity'
volumeMounts:
- name: applog
mountPath: /var/log/app
containers:
- name: backend
image: k8s-registry:5000/fanclub-backend:0.3.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: fanclub-config
- secretRef:
name: fanclub-db-secret
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "768Mi"
cpu: "1000m"
volumeMounts:
- name: applog
mountPath: /var/log/app
volumes:
- name: applog
emptyDir: {}
envFromで ConfigMap と Secret の全キーが環境変数として注入されます(DB_HOST等は第9回と同じ名前なので、アプリ側は変更不要)。serviceAccountNameで専用 SA を紐付け、automountServiceAccountToken: falseでトークンを置きません。envFromが参照するfanclub-config/fanclub-db-secretは先に作成しておきます(本記事では前項でapply済み)。未作成のまま Pod を作るとCreateContainerConfigErrorで起動しません。
Pod は作り直しが必要:稼働中の Pod に envFrom や SA を後から足そうと kubectl apply しても、Pod の大半のフィールドは変更不可で拒否されます。Pod は一度 delete してから apply で作り直します(第12回の Deployment を使うと、この入れ替えが自動になります)。
実行コマンド:
$ kubectl delete pod fanclub-backend
$ kubectl apply -f fanclub-backend-pod.yaml
$ kubectl get pod fanclub-backend
実行結果(例):本体 + ネイティブ Sidecar で 2/2 になります。
NAME READY STATUS RESTARTS AGE
fanclub-backend 2/2 Running 0 25s
外部化した設定で DB につながるか、第9回と同じく /health/ready と一覧で確認します。実行コマンド:
$ kubectl run c1 --rm -i --restart=Never --image=curlimages/curl:latest -- \
curl -s http://fanclub-backend:8080/health/ready
$ kubectl run c2 --rm -i --restart=Never --image=curlimages/curl:latest -- \
curl -s http://fanclub-backend:8080/api/members
実行結果(例):UP になり、第9回で登録した会員が引き続き取得できます(DB のデータは PVC に残っています)。
{"status":"UP","checks":[{"name":"fanclub-api-ready","status":"UP","data":{}}]}
[{"createdAt":"2026-06-27T00:04:07.560548","email":"taro@example.com","id":1,"name":"山田太郎","plan":"premium"}]
環境変数が ConfigMap / Secret 由来で入っているか、kubectl exec で確認します。
実行コマンド:
$ kubectl exec fanclub-backend -c backend -- \
sh -c 'echo "$DB_HOST / $DB_NAME / $DB_USER"'
実行結果(例):ConfigMap の DB_HOST / DB_NAME と Secret の DB_USER が注入されています。
fanclub-db / fanclubdb / appuser
SA トークンがマウントされていないことも確認します。実行コマンド:
$ kubectl exec fanclub-backend -c backend -- \
ls /var/run/secrets/kubernetes.io/serviceaccount/
実行結果(例):ディレクトリが存在せずエラーになります。これがトークン抑止の効果です(API を呼ぶ必要がある Pod では、必要な SA だけにトークンを許可します)。
ls: cannot access '/var/run/secrets/kubernetes.io/serviceaccount/': No such file or directory
command terminated with exit code 2
Frontend の API_BASE_URL を外部化する
Frontend(Nginx)が配信する静的 JavaScript には環境変数が直接届きません。そこで API のベース URL を config.js という小さな設定ファイルにまとめ、これを ConfigMap として Volume マウントして差し替えます。次を developer のホームに fanclub-frontend-config.yaml として作成します(一般ユーザー所有・root 不要)。
apiVersion: v1
kind: ConfigMap
metadata:
name: fanclub-frontend-config
data:
config.js: |
window.API_BASE_URL = "/api";
---
apiVersion: v1
kind: Pod
metadata:
name: fanclub-frontend
labels:
app: fanclub-frontend
spec:
containers:
- name: frontend
image: k8s-registry:5000/fanclub-frontend:0.1.0
ports:
- containerPort: 80
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.js
subPath: config.js
volumes:
- name: config
configMap:
name: fanclub-frontend-config
subPath: config.jsが要点です。subPathなしでディレクトリにマウントすると、/usr/share/nginx/html/内のindex.htmlなどが隠れてしまいます。subPathでconfig.jsという1 ファイルだけを差し替えます。- Frontend イメージも
0.1.0のまま(再ビルド不要)です。
Frontend Pod も作り直します。実行コマンド:
$ kubectl delete pod fanclub-frontend
$ kubectl apply -f fanclub-frontend-config.yaml
$ kubectl run cj --rm -i --restart=Never --image=curlimages/curl:latest -- \
curl -s http://fanclub-frontend/config.js
実行結果(例):配信される config.js が ConfigMap の内容になります(他の静的ページも従来どおり配信され、/api 経由の CRUD も動きます)。
window.API_BASE_URL = "/api";
本番の Secret 管理(概念)
本回の Secret は学習用に平文の値を YAML に書きました。実運用では、パスワードを Git に置かないために 外部のシークレットストア(HashiCorp Vault など)や、それを Kubernetes に同期する External Secrets Operator を使うのが一般的です。これらは本巻の範囲外として概念紹介にとどめます(保存時暗号化やシークレット運用は第3巻 CKS で詳しく扱います)。
やってみよう
fanclub-config.yaml(ConfigMap + Secret + ServiceAccount)を作成してapplyし、getで 3 つが作られたことを確認する。- Backend Pod を
delete→envFrom版でapplyし直し、2/2 Runningにする。 /health/readyがUP、/api/membersで会員が取得できることを確認する(外部化後も DB 接続が成立)。kubectl execでDB_HOST/DB_USERが注入され、SA トークンのディレクトリが無いことを確認する。fanclub-frontend-config.yamlでconfig.jsを ConfigMap マウントし、/config.jsが ConfigMap の内容になることを確認する。
理解度チェック
次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。
- ConfigMap は非機密の設定値、Secret は機密情報を扱うために使い分ける。
- Secret の値は base64 されているので暗号化されており、それだけで安全である。
envFromは ConfigMap / Secret の全キーを環境変数として一括注入する。valueFromは特定の 1 キーだけを選んで 1 つの環境変数にできる。automountServiceAccountToken: falseで SA トークンの自動マウントを止められる。- ConfigMap の値を変えたら、必ずアプリのイメージを再ビルドしなければならない。
- 静的 JS に設定を渡すには、ConfigMap を
subPathでファイルとしてマウントする方法がある。
解答
- 1. ○:非機密は ConfigMap、機密は Secret。
- 2. ×:base64 はエンコードであり暗号化ではない。誰でも復号できる。
- 3. ○:
envFromは全キーを一括注入する。 - 4. ○:
configMapKeyRef/secretKeyRefで 1 キーずつ注入する。 - 5. ○:API を呼ばない Pod ではトークンを置かないのが安全。
- 6. ×:外部化していれば値の変更に再ビルドは不要。これが設定外部化の利点。
- 7. ○:
config.jsをsubPathでマウントして差し替える。
まとめ
本記事では、設定をコードから切り離す理由を起点に、ConfigMap(非機密)と Secret(機密・base64 は暗号化ではない)の使い分け、envFrom / valueFrom / Volume マウントの注入経路、ServiceAccount の役割と automountServiceAccountToken: false を学びました。fanclub-api では DB 接続情報を ConfigMap / Secret に外部化し、専用 SA を紐付け、Frontend の config.js も ConfigMap で差し替えました。イメージは作り直さず、設定の置き場所を変えるだけで環境差分や機密に強い構成になりました。
次回予告
次回(第11回)は Job / CronJob / DaemonSet を学びます。一度きりの処理を実行する Job で DB マイグレーションを実装し、定期実行の CronJob、全ノードに 1 つずつ配置する DaemonSet を扱います。
シリーズ一覧
第1部:コンテナと Docker
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージ + JDK 25 / Payara Micro イメージビルド
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン
- 第8回 Service とネットワーキング
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎 ← 今ここ
- 第11回 Job + CronJob + DaemonSet
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
