第14章: エッジからGCPサービスをセキュアに呼ぶ(実践チュートリアル)
Cloudflare Workersで受けたリクエストのうち、「重い機械学習の推論」「ヘッドレスブラウザ(Puppeteer)でのスクレイピング」「長時間のバッチ処理」などを Google Cloud Run に委譲するハイブリッド構成を構築します。
この際、Cloud Runのエンドポイントを全世界に公開(未認証許可)にしてしまうと、第三者に勝手にリクエストを送られてGCPの利用料が高騰する危険があります。 本ハンズオンでは、Cloud Runを「認証が必要(未認証不可)」に設定し、Cloudflare WorkersからのみOIDCトークンで安全に呼び出す実装をマスターします。
本チュートリアルのゴール
- GCP側の準備:
- サービスアカウント作成とCloud Run起動元権限(
roles/run.invoker)の付与
- サービスアカウント作成とCloud Run起動元権限(
- Workers側の実装:
- Google認証ライブラリ不要で、軽量・高速にGoogle OIDC IDトークンをエッジで生成
Authorization: Bearer <ID_TOKEN>ヘッダーを付けてCloud RunへPOST
- ローカルおよび本番での検証:
- 未認証リクエスト(403 Forbidden)と、Workers経由の正常リクエスト(200 OK)の検証
【認証フロー】[ ユーザー ] ↓ (HTTP Request)[ Cloudflare Workers ] (Hono) ↓ 1. Google サービスアカウント情報からOIDC IDトークン生成 ↓ 2. Authorization: Bearer <ID_TOKEN>[ GCP Cloud Run ] (未認証不可・非公開) ↓ (処理結果)[ Cloudflare Workers ] ↓ (高速レスポンス・キャッシュ)[ ユーザー ]Step 1: GCP(Google Cloud)側の設定
1. サービスアカウントの作成
GCPコンソール(または gcloud CLI)で、Workers用のサービスアカウントを作成します。
# サービスアカウント作成gcloud iam service-accounts create cf-workers-invoker \ --description="Cloudflare WorkersからCloud Runを呼び出す用" \ --display-name="cf-workers-invoker"
# Cloud Run 起動元ロールを付与gcloud run services add-iam-policy-binding my-cloud-run-service \ --region=asia-northeast1 \ --member="serviceAccount:cf-workers-invoker@YOUR_PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/run.invoker"
# サービスアカウントキー(JSON)を発行gcloud iam service-accounts keys create sa-key.json \ --iam-account=cf-workers-invoker@YOUR_PROJECT_ID.iam.gserviceaccount.com発行された sa-key.json から以下の2つの値を取得します。
client_email:cf-workers-invoker@YOUR_PROJECT_ID.iam.gserviceaccount.comprivate_key:-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----
Step 2: Workersへのシークレット登録
取得したサービスアカウントの認証情報を、Wranglerのシークレットとして安全に登録します。
# Cloud RunのURLnpx wrangler secret put CLOUD_RUN_URL# (プロンプトで https://my-cloud-run-service-xxxx-an.a.run.app を入力)
# サービスアカウントのメールアドレスnpx wrangler secret put GCP_CLIENT_EMAIL
# サービスアカウントの秘密鍵npx wrangler secret put GCP_PRIVATE_KEYローカル開発用には .dev.vars に記述しておきます。
CLOUD_RUN_URL="https://my-cloud-run-service-xxxx-an.a.run.app"GCP_CLIENT_EMAIL="cf-workers-invoker@YOUR_PROJECT_ID.iam.gserviceaccount.com"GCP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvg...-----END PRIVATE KEY-----\n"Step 3: Workers 側でのOIDCトークン生成と呼び出しコード
Cloudflare WorkersのV8エッジランタイムで動作するよう、Web標準の crypto.subtle を使ってGoogle OIDC IDトークン(JWT)を生成し、Cloud Runへ安全に通信する完全コードです。
import { Hono } from 'hono';
type Bindings = { CLOUD_RUN_URL: string; GCP_CLIENT_EMAIL: string; GCP_PRIVATE_KEY: string;};
const app = new Hono<{ Bindings: Bindings }>();
// Google OIDC IDトークンを生成するヘルパー関数async function getGoogleIdToken(clientEmail: string, privateKeyPem: string, targetAudience: string): Promise<string> { const now = Math.floor(Date.now() / 1000); const header = { alg: 'RS256', typ: 'JWT' };
// Google OAuth2 トークンエンドポイント用のアサーション const payload = { iss: clientEmail, sub: clientEmail, aud: 'https://oauth2.googleapis.com/token', target_audience: targetAudience, // Cloud RunのURL iat: now, exp: now + 3600, // 1時間有効 };
const encodeBase64Url = (obj: any) => btoa(JSON.stringify(obj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const unsignedToken = `${encodeBase64Url(header)}.${encodeBase64Url(payload)}`;
// PEM形式の秘密鍵をバイナリDER形式に変換してインポート const cleanPem = privateKeyPem .replace(/-----BEGIN PRIVATE KEY-----/, '') .replace(/-----END PRIVATE KEY-----/, '') .replace(/\s/g, ''); const binaryKey = Uint8Array.from(atob(cleanPem), (c) => c.charCodeAt(0));
const cryptoKey = await crypto.subtle.importKey( 'pkcs8', binaryKey, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign'] );
// JWTの署名 const signature = await crypto.subtle.sign( 'RSASSA-PKCS1-v1_5', cryptoKey, new TextEncoder().encode(unsignedToken) );
const signedJwt = `${unsignedToken}.${btoa(String.fromCharCode(...new Uint8Array(signature))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '')}`;
// Google OAuth2エンドポイントへ署名付きJWTを投げてIDトークンを取得 const tokenRes = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: signedJwt, }), });
const tokenData: any = await tokenRes.json(); if (!tokenData.id_token) { throw new Error(`Failed to fetch ID token: ${JSON.stringify(tokenData)}`); }
return tokenData.id_token;}
// エンドポイント実装app.post('/api/heavy-job', async (c) => { const body = await c.req.json();
try { // 1. Google OIDC IDトークンを取得 const idToken = await getGoogleIdToken( c.env.GCP_CLIENT_EMAIL, c.env.GCP_PRIVATE_KEY, c.env.CLOUD_RUN_URL );
// 2. 認証付きでCloud Runへリクエスト転送 const gcpResponse = await fetch(`${c.env.CLOUD_RUN_URL}/process`, { method: 'POST', headers: { 'Authorization': `Bearer ${idToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), });
const result = await gcpResponse.json(); return c.json({ success: true, fromGcp: result }); } catch (error: any) { console.error('GCP呼び出しエラー:', error); return c.json({ error: error.message }, 500); }});
export default app;Step 4: 動作検証
1. Cloud Run URLを直接ブラウザやcurlで叩く
curl -i https://my-cloud-run-service-xxxx-an.a.run.app/process結果: HTTP/1.1 403 Forbidden(未認証アクセスのため拒絶される)
2. Cloudflare Workers 経由で叩く
curl -X POST http://localhost:8787/api/heavy-job \ -H "Content-Type: application/json" \ -d '{"task": "generate_pdf", "data": [1, 2, 3]}'結果: HTTP/1.1 200 OK
WorkersがOIDCトークンを自動発行してCloud Runを叩き、安全に処理結果が返ってきます。
まとめ
このアーキテクチャにより、個人開発者は以下の恩恵を最大化できます。
- セキュリティ: Cloud Runのエンドポイントは一切公開せず、Google IAMにより厳密に保護。
- コスト最小化: Cloud Runはアクセス時だけ秒単位課金(待機中は0円)。Cloudflare Workersの無料枠と組み合わせることで、月数百円以下でエンタープライズ級の拡張性を手に入れられます。
💡 用語解説コラム
[!NOTE] OIDC (OpenID Connect)
OAuth 2.0を拡張した認証規格。安全に暗号署名された「IDトークン(JWT)」を発行し、サービス間で「私は正当な呼び出し元である」ことを証明します。
[!NOTE] Web Crypto API (
crypto.subtle)
ブラウザおよびCloudflare Workers等のWeb標準ランタイムに組み込まれた高速かつセキュアな暗号化API。外部ライブラリなしでJWTのRSA署名や検証が可能です。