概要 #
Terraformのwrite-only argumentでSecret Managerへ値を登録するでは、シークレットを保管するところまでを扱いました。保管した値をPodからどう取り出すかは残ったままです。
環境変数で渡す方法は手軽ですが、kubectl describe podやプロセス一覧から値が見えてしまいます。GKEのSecret Manager add-onを使うと、Podのファイルシステムにマウントする形で渡せます。
ただ「マウントできた」だけでは運用に足りません。シークレットを更新したら自動で反映されるのか。権限を剥がしたら動いているPodはどうなるのか。ログに何が残るのか。運用に入ってから調べ直すのでは遅いので、この3点を先に確かめておく必要があります。
対象読者は、GKEでPodを動かしたことがあり、Secret Managerに値を入れたことがある人です。それぞれの入門は扱いません。
環境変数への同期(secretObjects)は扱いません。シークレットの自動ローテーションも扱いません。
検証すること #
- Podの中でマウントしたファイルが読めること。Pod specに値が現れないこと
- シークレットを更新したとき、稼働中のPodに自動で反映されるか
- IAM権限を剥がしたとき、稼働中のPodと新しいPodがそれぞれどうなるか
- Cloud Loggingに何が残るか
前提環境 #
- Google Cloud CLI、Terraform、Docker
- Billingが有効な検証用Google Cloud Project
- GKE、Secret Manager、Compute Engineを操作できるGoogleアカウント
- 検証時のバージョン:Terraform 1.14.3、google 7.46.0
使用するTerraformコード #
今回の構成 #
flowchart TB
subgraph GCP["Google Cloud"]
SM["Secret Manager
(値はTerraform管理外)"]
subgraph Project["Project"]
subgraph VPC["VPC"]
subgraph GkeSubnet["GKE Subnet(外部IPなし)"]
CSI["CSIドライバ
DaemonSet"]
Pod["Pod
/var/secrets/db-password.txt"]
end
Bastion["踏み台"]
end
end
end
CSI -->|"Private Google Access"| SM
CSI -->|"ファイルとして配置"| Pod
ノードには外部IPがありません。Secret Manager APIへはPrivate Google Access経由で到達します。
設計上の2つの選択 #
値をTerraformで管理しない #
google_secret_manager_secret_versionのsecret_dataを使うと、平文がterraform.tfstateに残ります。器だけをTerraformで作り、値は別で投入します。
resource "google_secret_manager_secret" "demo" {
secret_id = var.secret_id
replication {
auto {}
}
}
printf '%s' 'super-secret-value' | gcloud secrets versions add \
"$(terraform output -raw secret_id)" --data-file=- \
--project="$(terraform output -raw project_id)"
--data-file=-で標準入力から読ませると、シェル履歴にもファイルにも値が残りません。
Google Service Accountを作らない #
Workload Identity Federation for GKEを使います。KubernetesのServiceAccountを、そのままIAMのprincipalとして書けます。
locals {
ksa_principal = "principal://iam.googleapis.com/projects/${data.google_project.current.number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/${var.k8s_namespace}/sa/${var.k8s_service_account_name}"
}
resource "google_secret_manager_secret_iam_member" "ksa_accessor" {
secret_id = google_secret_manager_secret.demo.secret_id
role = "roles/secretmanager.secretAccessor"
member = local.ksa_principal
}
GSAの作成も、iam.gke.io/gcp-service-accountアノテーションも、鍵JSONも要りません。KSA側のマニフェストはこれだけです。
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-ksa
namespace: default
principalの文字列には名前空間とKSA名が埋め込まれます。マニフェストとTerraform変数がずれると権限が付きません。
ノードは e2-medium 以上が必要 #
最初e2-smallで作ったところ、Podが起動しませんでした。
0/1 nodes are available: 1 Insufficient cpu.
ノードの割り当て状況を見ると理由が分かります。
kubectl describe node | grep -A 4 'Allocated resources'
Resource Requests Limits
-------- -------- ------
cpu 935m (99%) 8 (851%)
CSIドライバのDaemonSetとGKE標準コンポーネントだけで、割り当て可能CPUの99%が埋まっていました。e2-mediumに上げて解決しています。
マウントできることを確認する #
SecretProviderClassでどのシークレットをどのファイル名で出すかを指定します。
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: gcp-sm-demo
spec:
provider: gke
parameters:
secrets: |
- resourceName: "projects/PROJECT_ID/secrets/SECRET_ID/versions/latest"
path: "db-password.txt"
Deployment側はcsiボリュームとしてマウントします。
volumes:
- name: secret-vol
csi:
driver: secrets-store-gke.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: gcp-sm-demo
kubectl exec deploy/app-secret-file -- ls -l /var/secrets/
kubectl exec deploy/app-secret-file -- cat /var/secrets/db-password.txt
lrwxrwxrwx 1 root root 22 Sep 3 23:18 db-password.txt -> ..data/db-password.txt
tf-adv-demo-secret-value-2026
投入した値が読めました。Pod specに値が現れないことも確認します。
kubectl get pod -l app=app-secret-file -o yaml | grep -c 'tf-adv-demo-secret-value'
0
specに出るのはSecretProviderClassの名前だけです。
シークレットを更新しても自動では反映されない #
versions/latestを指定しているので、新しいバージョンを追加すれば反映されそうに見えます。実際はどうか試します。
printf '%s' 'UPDATED-value-v2' | gcloud secrets versions add \
"$(terraform output -raw secret_id)" --data-file=- \
--project="$(terraform output -raw project_id)"
Created version [2] of the secret [tf-adv-smcsi-demo].
30秒、90秒、180秒と間隔を空けて確認しました。
経過30s後: tf-adv-demo-secret-value-2026
経過90s後: tf-adv-demo-secret-value-2026
経過180s後: tf-adv-demo-secret-value-2026
5分近く待っても更新されません。 反映にはPodの作り直しが必要です。
kubectl rollout restart deploy/app-secret-file
kubectl rollout status deploy/app-secret-file --timeout=240s
kubectl exec deploy/app-secret-file -- cat /var/secrets/db-password.txt
UPDATED-value-v2
ローテーションを組む場合、シークレットを更新するだけでは足りず、Podの再起動までを手順に入れる必要があります。
権限を剥がしたときの挙動 #
IAMバインディングが本当に効いているかは、付いている状態で成功することだけでは確かめられません。外して試します。
terraform apply -var="grant_ksa_secret_access=false"
稼働中のPodは影響を受けない #
kubectl exec deploy/app-secret-file -- cat /var/secrets/db-password.txt
UPDATED-value-v2
権限を失った後も読めます。マウント済みのボリュームはノード上に保持されているためです。
影響が出るのはPodを作り直したとき #
kubectl rollout restart deploy/app-secret-file
kubectl get pods -l app=app-secret-file
NAME READY STATUS RESTARTS AGE
app-secret-file-7599cff88f-9qcnk 1/1 Running 0 3m36s
app-secret-file-79b4454f4c-rnftx 0/1 ContainerCreating 0 90s
古いPodは動いたまま、新しいPodがContainerCreatingで止まっています。
kubectl describe pod -l app=app-secret-file | grep -A 5 Events
Warning FailedMount kubelet MountVolume.SetUp failed for volume "secret-vol" :
rpc error: code = PermissionDenied desc = Permission 'secretmanager.versions.access'
denied on resource (or it may not exist).
... reason = IAM_PERMISSION_DENIED domain = iam.googleapis.com
この挙動は運用上おさえておく価値があります。権限を誤って剥がしても即座には気づけず、次のデプロイやノード入れ替えで初めて表面化するということです。
権限を戻すと、詰まっていたPodは自動的に起動します。
terraform apply
Cloud Loggingに何が残るか #
CSIドライバのログ #
成功・失敗の両方が残ります。
gcloud logging read \
'resource.type="k8s_container" AND resource.labels.pod_name=~"csi-secrets-store"' \
--limit=5 --freshness=30m --format="value(timestamp,jsonPayload.message)"
"node publish volume complete" targetPath="/var/lib/kubelet/pods/.../mount" pod="default/app-secret-file-79b4454f4c-rnftx" time="277.0441ms"
Pod名と所要時間まで出るので、マウント遅延の切り分けにも使えます。
Secret Managerの監査ログには読み取りが残らない #
gcloud logging read \
'protoPayload.serviceName="secretmanager.googleapis.com"' \
--limit=20 --freshness=30m --format="value(protoPayload.methodName)" | sort -u
google.cloud.secretmanager.v1.SecretManagerService.AddSecretVersion
google.cloud.secretmanager.v1.SecretManagerService.CreateSecret
google.cloud.secretmanager.v1.SecretManagerService.DeleteSecret
google.cloud.secretmanager.v1.SecretManagerService.SetIamPolicy
シークレットの作成、値の追加、権限の変更、削除は残っています。一方でAccessSecretVersion、つまり値の読み取りは記録されていません。
管理操作は追える一方、「誰がいつシークレットの値を読んだか」は既定では分かりません。追跡が必要ならデータアクセス監査ログを明示的に有効化します(既定で無効、かつ課金対象)。
マウントのバリエーション #
複数のシークレットを1つのボリュームに #
SecretProviderClassにresourceNameを並べると、同じボリューム内に別ファイルとして配置されます。
secrets: |
- resourceName: "projects/PROJECT_ID/secrets/SECRET_ID/versions/latest"
path: "db-password.txt"
- resourceName: "projects/PROJECT_ID/secrets/SECRET_ID_2/versions/latest"
path: "api-key.txt"
kubectl exec deploy/app-multi-secret -- ls /var/secrets/
api-key.txt
db-password.txt
版を固定する #
versions/latestではなくversions/1のように明示すると、その版を保持し続けます。
kubectl exec deploy/app-pinned-secret -- cat /var/secrets/db-password-v1.txt
kubectl exec deploy/app-secret-file -- cat /var/secrets/db-password.txt
tf-adv-demo-secret-value-2026
UPDATED-value-v2
版を固定したPodは、latest側が更新されても影響を受けません。ローテーション時に意図せず新しい値を拾わせたくない場合に使えます。
プライベートクラスタからの到達 #
ノードに外部IPがない構成ですが、Secret Manager APIへは問題なく到達できています。
gcloud compute instances list --filter="name~gke-tf-adv-gke-smcsi" \
--format="table(name,networkInterfaces[0].accessConfigs[0].natIP)"
NAME NAT_IP
gke-tf-adv-gke-smcsi-tf-adv-gke-smcsi-a20e7a19-anrj
NAT_IPは空です。サブネットのprivateIpGoogleAccessが有効なため、Google APIへはこの経路で到達します。追加のVPCエンドポイントは要りません。
後片付け #
terraform destroy
Destroy complete! Resources: 27 destroyed.
GKE・踏み台VM・Cloud NATは利用中に料金が発生します。Secret Managerのシークレットとその全バージョンも削除されます。
まとめ #
- ファイルマウントならPod specにシークレットの値が現れない。specに出るのは
SecretProviderClassの名前だけ versions/latestを指定しても自動では更新されない。 ローテーションにはPodの再起動が必要- 権限を剥がしても稼働中のPodは読み続ける。 影響が出るのは次にPodを作り直したときで、そこで初めて
FailedMountになる - 監査ログに値の読み取りは残らない。 管理操作は追えるが、
AccessSecretVersionはデータアクセス監査ログを有効化しないと記録されない - CSIドライバのDaemonSetはCPUを消費する。
e2-smallではアプリのPodが載らない - Google Service Accountを作らず、KSAを直接IAM principalにできる。鍵JSONもアノテーションも不要