はじめに
GKEでアプリケーションを運用していると、こんな状況に陥りがちです。
- Kubernetes SecretがマニフェストにベタBookきされていてGitにコミットできない
- 複数のNamespaceやコンポーネントで同じSecretを使い回していて、変更のたびに複数箇所を修正している
この記事では、Google Secret ManagerをSingle Source of Truthとして、Kubernetes Secretを安全に管理・同期するまでの試行錯誤を紹介します。
筆者が辿った道のりはこうです。
- 方法①:SPC + SecretSync を試したが、運用上の課題があった
- 方法②:CDパイプライン(Cloud Deploy)上のshスクリプト で現在運用中
また、他の選択肢として External Secrets Operator(ESO) も簡単に紹介します。
同じ課題を抱えている方の参考になれば幸いです。
対象読者
- KubernetesとGCPの基本的な操作経験がある方
- Kubernetes SecretのGit管理に課題を感じている方
- GKEでのシークレット管理のベストプラクティスを探している方
なぜKubernetes SecretをGitに直書きしてはいけないのか
Kubernetes Secretはbase64エンコードされた状態でマニフェストに記述されます。しかし、base64はエンコードであって暗号化ではありません。以下のコマンドで誰でも即座に復元できます。
$ echo "cGFzc3dvcmQ=" | base64 -d
password
Gitリポジトリにアクセスできる全員が、秘密情報を平文で読める状態になります。さらに、複数のマイクロサービスやNamespaceで同じSecretを利用している場合、以下の問題が生じます。
- 変更時に複数のマニフェストを修正しなければならない
- 管理が属人化し、どこで何が使われているか把握しにくくなる
これらの課題を解決するために、Google Secret Managerで一元管理し、Kubernetes Secretへ同期するアプローチを検討しました。
構成の全体像
どの方法でも、基本的な考え方は同じです。
Google Secret Manager(一元管理)
├── secret/db-password
├── secret/api-key
└── secret/db-host
↓ 何らかの方法で同期
Kubernetes Secret
├── DB_PASSWORD
├── API_KEY
└── DB_HOST
「何らかの方法」の部分が、以降で紹介する3つのアプローチの違いです。
方法①:SecretProviderClass(SPC)+ SecretSyncを使う
SPC + SecretSyncとは
この方法はGKEネイティブの機能を使います。2つのリソースをセットで利用します。
- SecretProviderClass(SPC):Secret Store CSI Driver が提供するカスタムリソース。「どのGoogle SecretをKubernetes Secretに同期するか」を定義します
- SecretSync:GKEが提供するリソース。SPCをベースにKubernetes Secretへの同期を制御します
GKEクラスタへの機能有効化
SecretSyncを利用するには、GKEクラスタレベルで機能を有効化する必要があります。デフォルトではOFFです。
Terraformで設定する場合
resource "google_container_cluster" "main" {
name = "my-cluster"
location = "asia-northeast1"
# Secret Sync機能を有効化
secret_sync_config {
enabled = true
}
}
gcloud CLIで既存クラスタに設定する場合
gcloud container clusters update my-cluster \
--location=asia-northeast1 \
--enable-secret-sync
SPCのYAML定義
Google Secret Managerの各シークレットをどう参照するかを定義します。
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: my-app-secrets
namespace: default
spec:
provider: gcp
parameters:
secrets: |
- resourceName: "projects/my-project/secrets/db-password/versions/latest"
fileName: "db-password"
- resourceName: "projects/my-project/secrets/api-key/versions/latest"
fileName: "api-key"
- resourceName: "projects/my-project/secrets/db-host/versions/latest"
fileName: "db-host"
SecretSyncのYAML定義
SPCをベースに、Kubernetes Secretへの同期を定義します。
apiVersion: secretmanager.cnrm.cloud.google.com/v1beta1
kind: SecretSync
metadata:
name: my-app-secret-sync
namespace: default
spec:
secretProviderClassName: my-app-secrets # 上記SPCの名前
serviceAccountName: my-ksa-name
secretObjects:
- secretName: my-app-secret
type: Opaque
data:
- objectName: db-password
key: DB_PASSWORD
- objectName: api-key
key: API_KEY
- objectName: db-host
key: DB_HOST
実装してみてわかった課題
① Google Secret Managerの1KVP制約
Google Secret Managerは 「1シークレット = 1つの値(文字列)」 という設計です。Kubernetes Secretは複数のKVP(DB_HOST、DB_PASS、API_KEY など)を1リソースに持てますが、Google Secret Manager側はそれができません。
そのため、Kubernetes Secretに複数のKVPを持たせたい場合は、Google Secret Manager側に同じ数だけシークレットを作成し、SPCでまとめて束ねる必要があります。
Google Secret Manager Kubernetes Secret
secret/db-password ──┐
secret/api-key ──┼──→ my-app-secret
secret/db-host ──┘ ├── DB_PASSWORD
├── API_KEY
└── DB_HOST
KVPが増えるほどGoogle Secret Manager側のシークレット数も増え、対応関係の管理が煩雑になります。
② 管理リソースの増加
SPC・SecretSyncというリソースがクラスタに追加されます。シークレットの種類やNamespaceが増えるにつれ、管理するリソースも比例して増えていきます。
③ Workload Identityの設定が必要
GKEのPodがGoogle Secret Managerにアクセスするには、Workload Identityの設定が別途必要です。KubernetesのServiceAccountとGCPのService Accountを紐付け、roles/secretmanager.secretAccessor 権限を付与します。
方法②:CDパイプライン(Cloud Deploy)でshスクリプト同期
アプローチの概要
SPC+SecretSyncの運用コストを考慮した結果、よりシンプルな方法に切り替えました。デプロイ時に、シェルスクリプトでGoogle Secret ManagerからシークレットをPullし、kubectl でKubernetes Secretを作成・更新します。
Cloud Deploy パイプライン
│
├── [事前ステップ] sync-secrets.sh 実行
│ ↓
│ gcloud secrets versions access → kubectl apply
│
└── [本番ステップ] マニフェストのデプロイ(kubectl apply)
常駐リソースを持たず、デプロイのたびに最新のシークレットが適用されるシンプルな方式です。
Google Secret Manager側のシークレット設計
この方法では、Google Secret Managerのシークレット値を**env形式(KEY=VALUE)**で保存します。1つのシークレットに複数のKVPをまとめられるため、SPC+SecretSyncで問題になった1KVP制約を回避できます。
secret/my-app-config の値:
DB_PASSWORD=xxx
API_KEY=yyy
DB_HOST=zzz
シェルスクリプト例
Google Secret Managerからenv形式のシークレットを取得し、パースしてKubernetes Secretを作成するスクリプト例です。
#!/bin/bash
# sync-secrets.sh
# Google Secret ManagerからシークレットをPullしてKubernetes Secretを作成する
set -euo pipefail
PROJECT_ID="${GCP_PROJECT_ID}"
NAMESPACE="${K8S_NAMESPACE:-default}"
SECRET_NAME="${K8S_SECRET_NAME:-my-app-secret}"
echo "Fetching secrets from Google Secret Manager..."
# env形式(KEY=VALUE)で保存されたシークレットを取得
ENV_VARS=$(gcloud secrets versions access latest \
--secret="my-app-config" \
--project="${PROJECT_ID}")
# env形式をパースして --from-literal の引数を組み立てる
FROM_LITERAL_ARGS=""
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
FROM_LITERAL_ARGS="${FROM_LITERAL_ARGS} --from-literal=${line}"
done <<< "${ENV_VARS}"
# --dry-run=client + kubectl apply で冪等に実行
echo "Applying Kubernetes Secret..."
kubectl create secret generic "${SECRET_NAME}" \
--namespace="${NAMESPACE}" \
${FROM_LITERAL_ARGS} \
--dry-run=client -o yaml | kubectl apply -f -
echo "Secret sync completed: ${SECRET_NAME}"
--dry-run=client -o yaml | kubectl apply -f - のパターンを使うことで、SecretがすでにExistしていても冪等に実行できます。
この方法を選んだ理由
- シンプルさ:シェルスクリプト1本で完結し、クラスタへの追加リソースが不要
- CDパイプラインへの自然な統合:Cloud Deployの仕組みを壊さずに組み込める
- 1KVP制約を回避できる:env形式で保存することで、1つのGoogle Secretに複数のKVPをまとめられる
- デプロイサイクルで十分:常時自動同期は不要で、デプロイ時に最新が反映されれば問題なかった
補足:External Secrets Operator(ESO)という選択肢もある
External Secrets Operator はKubernetes上にOperatorとしてデプロイし、ExternalSecretリソースでGoogle Secret ManagerのシークレットをKubernetes Secretへ同期する仕組みです。筆者はまだ試していないので参考程度に。
この方法であれば複数のKVPを1つのシークレットにまとめられ、同期自体もマニフェストで管理できます。
Google Secret Manager
secret/my-app-config
値: {"DB_PASSWORD":"xxx","API_KEY":"yyy","DB_HOST":"zzz"}
↓ ESOが自動展開
Kubernetes Secret
├── DB_PASSWORD: xxx
├── API_KEY: yyy
└── DB_HOST: zzz
また、AWS Secrets ManagerやAzure Key Vaultなど他のバックエンドにも対応しており、マルチクラウド環境への拡張性も高いです。KVP数が多い構成や長期的な運用を見据える場合は、有力な選択肢の1つです。
まとめ
- Kubernetes SecretのGit直書きは、base64エンコードのみで誰でも復元できるため危険
- Google Secret Managerで一元管理し、Kubernetes Secretへ同期することで安全に管理できる
- SPC + SecretSync:GKEネイティブで自動同期できるが、管理リソースが増える。1KVP制約あり
- CDスクリプト方式:シンプルで導入コストが低い。env形式で保存することで1KVP制約も回避できる
- ESO:VP数が多い場合や長期運用に向く。