機密情報 (secret) の管理
UZU の機密値 (DB password, API token, GitHub App PEM, OAuth secret 等) は GCP Secret Manager を単一の Source of Truth として管理する。ローカル開発・K8s 本番・Cloudflare Workers 本番の 3 経路から同じ Secret Manager を参照する形で統一する。
設計判断の根拠は ADR 0001 を参照。
SoT モデル
K8s 経路は External Secrets Operator (ESO) が Secret Manager → K8s Secret の同期を自動化する (検証フェーズで dev クラスタに先行導入済み、stg/prd は UZU-5262 本実装フェーズで展開)。CF Workers 経路は現状手動同期、将来的に WIF + GHA で自動化する余地あり。
2 階層レイアウト
env 境界を GCP プロジェクト単位で物理的に切る ため、Secret Manager を 2 階層で使う。
| 種別 | 配置先プロジェクト | 命名規則 | ClusterSecretStore | 例 |
|---|---|---|---|---|
| env 固有 | uzu-{env} | {service}-{key-kebab} (env suffix なし、project が env を表現) | gsm-uzu-{env} | uzu-dev/uzutokyo-database-url |
| env 共通 | uzu-shared | {prefix}-{key-kebab} | gsm-uzu-shared | uzu-shared/cloudflare-api-token, uzu-shared/translator-github-app-private-key |
env 共通に置いて良いもの
- 1 アカウントに 1 個しか発行されない token (Cloudflare API token 等)
- dev/stg/prd で意図的に同じ値を共有したいもの (社外 SaaS の共通鍵)
- ローカル開発専用で本番には流れないもの (translator GitHub App credential 等)
「env で値が違うもの」は必ず env 固有側 (uzu-{env}) に置く。共通側に {key}-dev / {key}-prd を並べて IAM Condition で振り分ける案は不採用 (Condition の書き間違いで漏洩する事故リスクがあるため、ADR 0001 案 S 参照)。
key 名の付け方
{service}-{key-kebab}— kebab-case 固定 (env suffix は付けない)- 値は 1 secret = 1 用途に分け、JSON で複数値を束ねない (rotation 単位を細かく保つ)
- 複数行 / バイナリ (PEM 等) はそのまま投入し、利用側で改行処理を行う
ローカル開発経路
各開発者は gcloud auth application-default login 済み + sally-inc.jp domain の roles/secretmanager.secretAccessor を保持していれば追加設定不要 (= 新メンバーは ADC ログインだけで動く)。
構成
.dev.vars (もしくは .env.local) は task <service> 起動の度に再生成する。リポジトリにも開発者の Home にも長期保管しない。
参考実装
scripts/translator/gen-dev-vars.sh— gen script の参考taskfiles/translator.yamlの_apitask — cmds 前置の参考
新規 secret の追加手順 (ローカル開発)
gcloud secrets create {service}-{key-kebab} --project=uzu-shared --replication-policy=automatic(env 共通の場合)gcloud secrets add-iam-policy-binding ... --member=domain:sally-inc.jp --role=roles/secretmanager.secretAccessorecho -n "<value>" | gcloud secrets versions add {service}-{key-kebab} --project=... --data-file=-scripts/<service>/gen-dev-vars.shの出力に 1 行追加
複数行値 (PEM 等) を dotenv (.dev.vars) で扱う場合は 1 行化が必須。改行を \n リテラルにエスケープし、コード側で replace(/\\n/g, '\n') で戻す:
printf 'PRIVATE_KEY="'
fetch <service>-private-key | awk 'BEGIN{ORS="\\n"} {print}'
printf '"\n'.gitignore への .dev.vars 追加を忘れない (commit されると Secret Manager に置いた意味が消える)。
K8s 本番経路 (ESO)
ESO 基盤は dev クラスタに先行導入済み、stg/prd は本実装フェーズで展開予定。
構成
新規 secret の追加手順 (K8s)
Secret Manager に投入 (上記「ローカル開発」と同じ手順)
<service>/k8s/<env>/external-secret.yamlを新規 or 追記。例:yamlapiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: <service>-secret spec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: gsm-uzu-{env} # env 固有なら gsm-uzu-{env}、共通なら gsm-uzu-shared target: name: <service>-secret # 生成される K8s Secret 名 creationPolicy: Owner data: - secretKey: DATABASE_URL # Pod envFrom が参照する key remoteRef: key: <service>-database-url # Secret Manager 上の secret 名<service>/k8s/<env>/kustomization.template.yamlのresourcesにexternal-secret.yamlを追加kustomization.template.yamlのsecretGenerator.literalsから該当行を削除Pod の
envFrom.secretRef.nameが<service>-secretを指していれば変更不要
参考実装
backend/k8s/deploy/dev/external-secret.yaml— env 共通 (uzu-shared) パターンjavascript/uzutokyo/k8s/dev/external-secret.yaml— env 固有 (uzu-dev) パターンinfra/k8s/external-secrets/dev/cluster-secret-store.yaml—ClusterSecretStore定義infra/terraform/dev/eso.tf— GSA + WI binding + 自プロジェクト IAMinfra/terraform/shared/eso.tf— 越境 IAM
ESO 基盤自体のデプロイ
infra/k8s/external-secrets/** の差分検知で infra_k8s_external_secrets.yml workflow が dev/stg/prd へ自動 apply する (terraform_apply.yml と同じパターン: dev/stg は main push、prd は tag push)。手元から kubectl apply -k を直接打つのは検証時の例外のみ。
Cloudflare Workers 本番経路
ESO は K8s 内でしか動かないので、CF Workers の secret は別経路で投入する。
cd javascript/apps/<service>/worker-api
wrangler secret put SOME_API_KEY # 値は Secret Manager から取って貼るSecret Manager 上の値が SoT である点は K8s 経路と同じ。CF Workers への同期は現状手動。将来的に GitHub Actions の WIF 経由で Secret Manager → wrangler secret を自動同期する形にすると一元化できる (v1 では手動で OK)。
Rotation 運用
- GCP Secret Manager 上で
versions add(新 version を作るだけ。旧 version は disable → 数日後 destroy) - ローカル開発: 次回
task <service>起動で.dev.vars再生成 → 自動反映 - K8s 本番: ExternalSecret の
refreshInterval(1h) で自動反映。即時反映したい場合は ExternalSecret に annotation を打って sync を強制 - CF Workers 本番: 手動で
wrangler secret putを再実行
env 共通 secret (uzu-shared) は 1 箇所更新するだけで全 env (dev/stg/prd) に反映される。
トラブルシュート
| 症状 | 確認 |
|---|---|
ローカルで gen-dev-vars.sh が permission denied | gcloud auth application-default login 済みか / sally-inc.jp domain かを確認 |
kubectl get clustersecretstore が Ready にならない | external-secrets@uzu-{env} GSA に自プロジェクト + uzu-shared の secretAccessor が付いているか / WI annotation が KSA に付いているか |
kubectl get externalsecret <name> が SecretSyncedError | Secret Manager 上の key 名が remoteRef.key と一致しているか / 値が空でないか |
| ESO controller が動いていない | kubectl get pods -n external-secrets で external-secrets Pod の状態 / infra_k8s_external_secrets.yml workflow の最新 run |