Vercel + GCP Workload Identity Federation 完全ガイド:キーレス認証でCloud Runを呼び出す
テクノロジー

Vercel + GCP Workload Identity Federation 完全ガイド:キーレス認証でCloud Runを呼び出す

カジュアルモードは準備中です

Vercel + GCP Workload Identity Federation 完全ガイド

Vercel(Next.js)からGCP Cloud Runのバックエンドを呼び出したい。シンプルな要件ですが、GCPの組織ポリシーによっては思わぬ壁にぶつかります。

⚠️ 重要:AIアシスタント利用時の注意 CLAUDE Code (Claude Opus 4.5等) をはじめとする最新のAIモデルでも、Vercel OIDC連携の選択肢を把握していないケースが多く見られます。 AIが提案する設定をそのまま適用するとセキュリティ脆弱性に繋がるため、必ず本記事だけでなく公式のドキュメント(参考リソース参照)の手順に従って設定を確認してください。

本記事では、サービスアカウントキーを使わず、セキュアにVercelからCloud Runを呼び出す方法を解説します。

背景:なぜキーレス認証が必要か

組織ポリシーによる制約

企業のGCP環境では、セキュリティ強化のため以下の組織ポリシーが設定されていることがあります:

# gcloud org-policies describe constraints/iam.allowedPolicyMemberDomains --effective
spec:
  rules:
  - values:
      allowedValues:
      - C0xxxxxxxx  # Google Workspace カスタマーID

この設定がある場合、allUsers(誰でもアクセス可)や allAuthenticatedUsers は許可されません。つまり、Cloud Runを公開APIとしてデプロイできないのです。

一般的なPaaSからGCPへの移行で直面する課題

HerokuやRenderなどのPaaSでは、多くの場合バックエンドを公開APIとしてデプロイし、クライアントから直接アクセスすることが容易です。しかし、セキュリティ要件の厳しいGCP環境へ移行すると、以下の課題が発生します:

  1. Cloud Run自体は認証必須(組織ポリシーによる)
  2. Vercelは外部サービスなので、GCP内部の認証が使えない
  3. サービスアカウントキーをVercelに置く? → 漏洩リスク、ローテーション運用が必要

ここで登場するのが Workload Identity Federation です。

認証方式の比較

VercelからCloud Runを呼び出す方法を4つ比較します。

選択肢一覧

# 方式 概要
1 サービスアカウントキー Vercel環境変数にJSONキーを保存
2 Cloud Functionsプロキシ 公開CFがJWT検証後、Cloud Runに転送
3 組織ポリシー変更 プロジェクトでallUsersを許可
4 OIDC連携 VercelのOIDCトークンをGCPが信頼

詳細比較

観点 SAキー CF Proxy Org Policy OIDC連携
キー管理 90日ローテーション 不要 不要 不要
セキュリティ キー漏洩リスク 二重認証 公開API 短期トークン
組織ポリシー変更 不要 必要 必要 不要
GCP推奨
設定の複雑さ シンプル 中程度 シンプル やや複雑
運用コスト

結論:OIDC連携が最適

  • キー管理不要 = 運用負荷の低減
  • 組織ポリシー変更不要 = 既存のセキュリティ維持
  • GCPデシジョンツリー推奨 = ベストプラクティス
  • 短期トークン = 漏洩しても短時間で失効(デフォルト1時間、最短15分)

Vercel OIDC + Workload Identity Federation の仕組み

sequenceDiagram
    participant V as Vercel Functions
    participant OIDC as Vercel OIDC Provider
    participant WIF as GCP Workload Identity
    participant CR as Cloud Run

    V->>OIDC: 1. OIDCトークン取得(自動)
    Note over V,OIDC: x-vercel-oidc-token ヘッダー
    V->>WIF: 2. トークン交換リクエスト
    WIF->>WIF: 3. トークン検証
    WIF-->>V: 4. GCPアクセストークン(短期)
    V->>CR: 5. Authorization: Bearer {token}
    CR-->>V: 6. レスポンス

ポイント:

  • VercelがOIDCトークンを自動生成
  • GCPがそのトークンを信頼し、短期アクセストークンを発行
  • サービスアカウントキーは一切不要

設定手順

前提条件:Vercel アカウントの種類を確認

Vercel OIDC の Issuer URL はアカウント種別で異なります:

アカウント種別 Issuer URL Audience
個人(Hobby) https://oidc.vercel.com なし or プロジェクト名
チーム https://oidc.vercel.com/チームスラッグ https://vercel.com/チームスラッグ

確認方法: Vercel Dashboard 左上でアカウント名を確認。「Personal Account」なら個人、それ以外はチームです。

ステップ1: GCP API の有効化

Workload Identity Federation に必要なAPIを有効化します:

# IAM Credentials API(トークン発行用)
gcloud services enable iamcredentials.googleapis.com --project=your-project-id

# Security Token Service(トークン交換用)
gcloud services enable sts.googleapis.com --project=your-project-id

確認コマンド:

gcloud services list --enabled --project=your-project-id | grep -E "(iam|sts)"
# 期待する出力:
# iamcredentials.googleapis.com
# sts.googleapis.com

ステップ2: Workload Identity Pool 作成

gcloud iam workload-identity-pools create vercel-pool \
  --location="global" \
  --display-name="Vercel Identity Pool" \
  --project=your-project-id

成功時の出力:

Created workload identity pool [vercel-pool].

ステップ3: Vercel OIDC プロバイダ追加

個人アカウント(Hobby)の場合:

gcloud iam workload-identity-pools providers create-oidc vercel-provider \
  --location="global" \
  --workload-identity-pool=vercel-pool \
  --issuer-uri="https://oidc.vercel.com" \
  --attribute-mapping="google.subject=assertion.sub" \
  --project=your-project-id

チームアカウントの場合:

# YOUR_TEAM_SLUG を実際のチームスラッグに置き換え
gcloud iam workload-identity-pools providers create-oidc vercel-provider \
  --location="global" \
  --workload-identity-pool=vercel-pool \
  --issuer-uri="https://oidc.vercel.com/YOUR_TEAM_SLUG" \
  --attribute-mapping="google.subject=assertion.sub,attribute.project_id=assertion.project_id" \
  --allowed-audiences="https://vercel.com/YOUR_TEAM_SLUG" \
  --project=your-project-id

既存プロバイダーの更新(作成済みの場合):

# プロバイダーを誤って削除した場合は先に復元
gcloud iam workload-identity-pools providers undelete vercel-provider \
  --location="global" \
  --workload-identity-pool=vercel-pool \
  --project=your-project-id

# Issuer URL を更新
gcloud iam workload-identity-pools providers update-oidc vercel-provider \
  --location="global" \
  --workload-identity-pool=vercel-pool \
  --issuer-uri="https://oidc.vercel.com/YOUR_TEAM_SLUG" \
  --allowed-audiences="https://vercel.com/YOUR_TEAM_SLUG" \
  --project=your-project-id

注意: GCPのプロバイダーは削除後30日間ソフトデリート状態になります。同じ名前で再作成するには undelete で復元してから更新してください。

ステップ4: サービスアカウント作成(キーは作らない)

# サービスアカウント作成
gcloud iam service-accounts create vercel-backend-invoker \
  --display-name="Vercel Backend Invoker" \
  --project=your-project-id

# Cloud Run 呼び出し権限付与
gcloud run services add-iam-policy-binding your-service-name \
  --region=asia-northeast1 \
  --member="serviceAccount:vercel-backend-invoker@your-project-id.iam.gserviceaccount.com" \
  --role="roles/run.invoker" \
  --project=your-project-id

重要: your-service-name は実際のCloud Runサービス名に置き換えてください。

ステップ5: Workload Identity と SA を紐付け

まず、プロジェクト番号を確認します(プロジェクトIDではなく番号が必要):

# プロジェクト番号を確認
gcloud projects describe your-project-id --format="value(projectNumber)"
# 出力例: 123456789012

プロジェクト番号を使って紐付けを実行:

# PROJECT_NUMBER を上記で取得した番号に置き換え
gcloud iam service-accounts add-iam-policy-binding \
  vercel-backend-invoker@your-project-id.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/vercel-pool/*" \
  --project=your-project-id

重要: --member のパスには プロジェクト番号(数字)を使用します。プロジェクトID(your-project-id)を使うと以下のエラーが発生します:

ERROR: INVALID_ARGUMENT: Identity Pool does not exist

ステップ6: Vercel プロジェクト設定

Vercel Dashboard で OIDC を有効化:

  1. Project SettingsSecurity
  2. Secure backend access with OIDC federation
  3. Issuer Mode:を選択
    • 個人アカウント → Global
    • チームアカウント → Team

確認: 有効化後、Vercel Functions のリクエストヘッダーに x-vercel-oidc-token が自動付与されます。

ステップ7: Vercel Functions でトークン交換

// lib/gcp-auth.ts
import { ExternalAccountClient } from 'google-auth-library';

export async function getGCPAccessToken(oidcToken: string): Promise<string> {
  // 重要: PROJECT_ID ではなく PROJECT_NUMBER を使用
  const projectNumber = process.env.GCP_PROJECT_NUMBER!;
  const poolId = process.env.GCP_WORKLOAD_IDENTITY_POOL_ID!;
  const providerId = process.env.GCP_WORKLOAD_IDENTITY_PROVIDER_ID!;
  const serviceAccountEmail = process.env.GCP_SERVICE_ACCOUNT_EMAIL!;

  const client = ExternalAccountClient.fromJSON({
    type: 'external_account',
    // audience にはプロジェクト番号(数字)を使用
    audience: `//iam.googleapis.com/projects/${projectNumber}/locations/global/workloadIdentityPools/${poolId}/providers/${providerId}`,
    subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
    token_url: 'https://sts.googleapis.com/v1/token',
    service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountEmail}:generateAccessToken`,
    subject_token_supplier: {
      getSubjectToken: async () => oidcToken,
    },
  });

  if (!client) {
    throw new Error('Failed to create ExternalAccountClient');
  }

  const accessToken = await client.getAccessToken();
  return accessToken.token!;
}

重要: audience にはプロジェクトID(your-project-id)ではなく、プロジェクト番号123456789012等の数字)を使用します。プロジェクトIDを使うと認証に失敗します。

ステップ8: API Route での使用

// app/api/admin/articles/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getGCPAccessToken } from '@/lib/gcp-auth';

export async function GET(request: NextRequest) {
  // Vercel が自動で付与する OIDC トークン
  const oidcToken = request.headers.get('x-vercel-oidc-token');

  if (!oidcToken) {
    return NextResponse.json({ error: 'OIDC token missing' }, { status: 401 });
  }

  // GCP アクセストークンに交換
  const gcpToken = await getGCPAccessToken(oidcToken);

  // Cloud Run API 呼び出し
  const response = await fetch(`${process.env.CLOUD_RUN_URL}/api/v1/articles`, {
    headers: {
      'Authorization': `Bearer ${gcpToken}`,
    },
  });

  const data = await response.json();
  return NextResponse.json(data);
}

Next.js SSG/ISR との注意点

cookies() 問題

Next.js の cookies() を使用すると、そのルートは Dynamic になります。ビルド時にSupabaseから公開データを取得する場合、これが問題になります。

// NG: cookies() を使うと Dynamic になる
import { createClient } from '@/lib/supabase/server';

export async function generateStaticParams() {
  const supabase = createClient(); // 内部で cookies() を使用
  // ビルド時にエラー
}

解決策: Static Supabase Client

ビルド時(SSG)用に、Cookie を使わない Supabase クライアントを用意:

// lib/supabase/static.ts
import { createClient as createSupabaseClient } from '@supabase/supabase-js';

export function createStaticClient() {
  return createSupabaseClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

使い分け:

用途 クライアント
SSG(sitemap, feed, 静的ページ) createStaticClient()
SSR(認証が必要なページ) createClient() (server)
CSR(ブラウザ) createClient() (client)

Vercel 環境変数

# GCP設定(Workload Identity Federation)
GCP_PROJECT_NUMBER=123456789012  # 重要: IDではなく番号を使用
GCP_WORKLOAD_IDENTITY_POOL_ID=vercel-pool
GCP_WORKLOAD_IDENTITY_PROVIDER_ID=vercel-provider
GCP_SERVICE_ACCOUNT_EMAIL=vercel-backend-invoker@your-project-id.iam.gserviceaccount.com

# Cloud Run URL
CLOUD_RUN_URL=https://media-backend-xxxxx-an.a.run.app

プロジェクト番号の確認方法:

gcloud projects describe your-project-id --format="value(projectNumber)"

セキュリティ上の利点

項目 サービスアカウントキー OIDC連携
有効期限 無期限(ローテーション必要) 45分で自動失効
漏洩リスク JSONファイルの管理が必要 トークンは短期、自動生成
最小権限 △(キーがあれば何でも可) ◎(プロジェクト単位で制限可)
監査 キーの使用追跡が困難 Cloud Audit Logs で追跡可

まとめ

観点 SAキー OIDC連携
キー管理 90日ごとにローテーション 不要
セキュリティ キー漏洩リスク 短期トークンで安全
組織ポリシー 回避策が必要 そのまま準拠
GCP推奨 ◎ デシジョンツリー推奨

Vercel + GCP の構成で組織ポリシーに阻まれた場合、Workload Identity Federation が最適解です。初期設定はやや複雑ですが、一度設定すれば運用負荷はほぼゼロになります。


参考リソース


更新履歴

更新日 内容
2025-12-26 初版公開
この記事をシェア