システム設計/インフラ — Workload Identity × Secret Manager

2026-04-07 (Day 3) B: システム設計/インフラ ★★★☆☆ GKE / Workload Identity / Terraform / Secret Manager

概要

🔑

Workload Identity

GKE Pod内のコードがGCP APIを呼ぶとき、キーファイル不要・有効期限1時間・自動ローテーションでKSA→GSAのトークン交換が自動で行われる。

🔐

Secret Manager

外部SaaSのOAuth2クレデンシャルをGCPで一元管理。Workload Identity経由で取得することでK8s Secretの平文保存リスクを排除。

🏗️

Terraform管理

GSA・IAMバインディング・Secret Manager定義をコード化。手動作業によるローテーション漏れや設定ミスをなくす。

🛡️

最小権限原則

GSAに付与する権限は secretmanager.secretAccessor(特定のシークレットのみ)に絞る。BigQuery等は別途分離して管理。

問題

セキュリティレビューで指摘: 「サービスアカウントキーのローテーションが手動で行われており、漏洩リスクがある。Workload Identity Federationへの移行を検討すること」

現在の構成

項目内容
実行環境GKE Autopilot上のArgo Workflows(毎日9:00キャンペーンメール配信)
コンテナPythonコンテナがBigQueryからユーザー取得 → 外部SaaS APIへメール配信
現在の認証サービスアカウントキー(JSON)をKubernetes Secretに格納
外部SaaS認証OAuth2.0 Client Credentials Grant(client_id / client_secret)

制約・前提条件

  • GKEクラスタはGKE Autopilot(Workload Identity有効化済み)
  • Terraformでインフラ管理(既存のTFコードに追記)
  • Pythonコンテナ内では google-auth ライブラリを使用中
  • コンテナのコード変更は最小限にしたい
期待する回答形式: 文章(問題点の特定)+ Terraformコード + Kubernetes Manifestコード + Pythonコード変更箇所

ヒント(段階的開示)

ヒント1 — 方向性
「サービスアカウントキーJSON」の何が問題か整理しよう。キーが漏洩したらどうなるか、ローテーションが手動だと何が困るかを考えると、解決方向が見えてくる。
ヒント2 — アプローチ
GKE Workload Identityを使うと、KubernetesのServiceAccountとGCPのServiceAccountを紐付けられる。 Pod内のコードは GOOGLE_APPLICATION_CREDENTIALS を設定しなくてもGCPリソースにアクセスできる(メタデータサーバー経由)。 ただし今回の外部SaaS APIはGCPのリソースではないため、直接Workload Identityで認証できない点に注意。
ヒント3 — 典型設計パターン
外部SaaS APIの client_secret はSecret Manager(GCP)に格納し、Workload IdentityでGSAに secretmanager.secretAccessor 権限を付与してコンテナから取得する設計が典型パターン。
# Terraform骨格
resource "google_service_account_iam_member" "workload_identity" {
  service_account_id = google_service_account.campaign_job.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${var.ksa_name}]"
}

問題点の特定

#問題点リスク対策
1 サービスアカウントキーJSON(長期認証情報)のリスク キーが漏洩すると失効まで悪用され続ける。手動ローテーションは失念・遅延リスク Workload Identityで短命トークン(1時間・自動更新)に移行
2 K8s Secretの暗号化問題 デフォルトのK8s SecretはetcdにBase64エンコードで保存(暗号化なし) Secret Managerで一元管理し、K8s Secretから機密情報を排除
3 外部SaaS API認証情報の管理 client_secret をK8s Secretに入れているため上記リスクが直撃する Secret Managerで管理 + Workload Identity経由で取得

アーキテクチャ図 — 改善後の認証フロー

Pod (GKE) KSA: campaign-job-ksa namespace: mops Python + google-auth Workload Identity GSA (GCP IAM) campaign-job@project .iam.gserviceaccount.com secretmanager.secretAccessor secret access Secret Manager saas-client-secret client_secret: xxxxxx 自動レプリケーション 外部 SaaS OAuth2 Client Credentials (access_token 取得後にAPI呼び出し)

模範解答

# GSA(GCP Service Account)の作成
resource "google_service_account" "campaign_job" {
  account_id   = "campaign-job"
  display_name = "Campaign Job Service Account"
  project      = var.project_id
}

# Secret Manager にシークレットを定義
resource "google_secret_manager_secret" "saas_client_secret" {
  project   = var.project_id
  secret_id = "saas-client-secret"

  replication {
    auto {}
  }
}

# Secret Managerへのアクセス権限付与
resource "google_secret_manager_secret_iam_member" "campaign_job_secret_access" {
  project   = var.project_id
  secret_id = google_secret_manager_secret.saas_client_secret.secret_id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.campaign_job.email}"
}

# Workload Identity バインディング
# KSA: campaign-job-ksa(namespace: mops)← GSA に紐付け
resource "google_service_account_iam_member" "workload_identity" {
  service_account_id = google_service_account.campaign_job.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project_id}.svc.id.goog[mops/campaign-job-ksa]"
}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: campaign-job-ksa
  namespace: mops
  annotations:
    # GKE Workload Identity: GSAと紐付け
    iam.gke.io/gcp-service-account: campaign-job@${PROJECT_ID}.iam.gserviceaccount.com
---
# Argo Workflow テンプレート側でServiceAccountを指定
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: campaign-mailer
  namespace: mops
spec:
  serviceAccountName: campaign-job-ksa   # KSA を指定
  templates:
    - name: send-campaign
      container:
        image: asia-northeast1-docker.pkg.dev/${PROJECT_ID}/mops/campaign-job:latest
        # GOOGLE_APPLICATION_CREDENTIALS の設定不要(Workload Identity経由)
        env:
          - name: GCP_PROJECT_ID
            value: "${PROJECT_ID}"
          - name: SAAS_CLIENT_ID
            value: "your-client-id"   # client_idは非機密なのでenvでOK
          # client_secret はコード内でSecret Managerから取得
# before: K8s SecretからJSONキーを読む(廃止)
# import json, os
# creds = json.loads(os.environ["SAAS_CLIENT_SECRET_JSON"])

# after: Secret ManagerからOAuth2クレデンシャルを取得
from google.cloud import secretmanager
import requests


def get_saas_client_secret(project_id: str, secret_name: str = "saas-client-secret") -> str:
    """Secret ManagerからSaaS APIのclient_secretを取得する。

    Workload Identityにより、サービスアカウントキーなしで認証される。

    Args:
        project_id: GCPプロジェクトID
        secret_name: Secret Manager上のシークレット名

    Returns:
        client_secretの文字列
    """
    client = secretmanager.SecretManagerServiceClient()
    name = f"projects/{project_id}/secrets/{secret_name}/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("utf-8")


def get_saas_access_token(client_id: str, client_secret: str, token_url: str) -> str:
    """OAuth2.0 Client Credentials GrantでSaaS APIのアクセストークンを取得する。"""
    resp = requests.post(
        token_url,
        data={
            "grant_type": "client_credentials",
            "client_id": client_id,
            "client_secret": client_secret,
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]

ポイント解説

1 Workload Identity の仕組み
GKE Pod内のコードがGCP APIを呼ぶとき、メタデータサーバー(169.254.169.254)経由でKSA→GSAのトークン交換が自動で行われる。キーファイル不要・有効期限1時間・自動ローテーション。
2 外部APIはWorkload Identityで直接認証できない
Workload IdentityはGCPのIAMと統合されたサービス専用。外部SaaSのOAuth2認証には使えないため、「Secret Managerで秘密情報を管理し、Workload Identity経由で取得する」という2段階設計が定石。
3 最小権限原則
GSAに付与する権限は secretmanager.secretAccessor(特定のシークレットのみ)に絞る。BigQueryアクセスが必要なら別途 bigquery.dataViewer を付与し、権限を分離して管理する。

実務への応用

MOpsチームのArgo Workflowsジョブ

複数の外部SaaS(メール配信・プッシュ通知・SMS)と連携することが多い。それぞれの client_secret をSecret Managerで一元管理し、Workload Identityで取得する設計にすると、キーローテーション時もSecret Managerの値を更新するだけでよく、K8s Secretの手動更新やPod再起動が不要になる。

次のステップ

発展問題: Secret Managerのシークレットに自動ローテーション(Cloud Functions trigger)を設定し、SaaS側のOAuth2クレデンシャルを定期更新する仕組みを設計する
  • 参考: GKE公式「Workload Identity Federation for GKE」
  • 参考: Secret Manager公式「シークレットのローテーション」

今日のまとめ

GKEでのサービスアカウントキーはWorkload Identityで廃止し、外部SaaS認証情報はSecret Managerで管理・Workload Identity経由で取得するのが2026年時点のベストプラクティス。

Workload Identity(短命トークン)+ Secret Manager(集中管理)+ Terraform(IaC)の組み合わせで、手動ローテーションリスクを完全に排除できる。

自己評価

自分の回答

気づき・メモ