コンテンツにスキップ

第14章: エッジからGCPサービスをセキュアに呼ぶ(実践チュートリアル)

Cloudflare Workersで受けたリクエストのうち、「重い機械学習の推論」「ヘッドレスブラウザ(Puppeteer)でのスクレイピング」「長時間のバッチ処理」などを Google Cloud Run に委譲するハイブリッド構成を構築します。

この際、Cloud Runのエンドポイントを全世界に公開(未認証許可)にしてしまうと、第三者に勝手にリクエストを送られてGCPの利用料が高騰する危険があります。 本ハンズオンでは、Cloud Runを「認証が必要(未認証不可)」に設定し、Cloudflare WorkersからのみOIDCトークンで安全に呼び出す実装をマスターします。


本チュートリアルのゴール

  1. GCP側の準備:
    • サービスアカウント作成とCloud Run起動元権限(roles/run.invoker)の付与
  2. Workers側の実装:
    • Google認証ライブラリ不要で、軽量・高速にGoogle OIDC IDトークンをエッジで生成
    • Authorization: Bearer <ID_TOKEN> ヘッダーを付けてCloud RunへPOST
  3. ローカルおよび本番での検証:
    • 未認証リクエスト(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用のサービスアカウントを作成します。

Terminal window
# サービスアカウント作成
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.com
  • private_key: -----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----

Step 2: Workersへのシークレット登録

取得したサービスアカウントの認証情報を、Wranglerのシークレットとして安全に登録します。

Terminal window
# Cloud RunのURL
npx 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 に記述しておきます。

.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へ安全に通信する完全コードです。

src/index.ts
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で叩く

Terminal window
curl -i https://my-cloud-run-service-xxxx-an.a.run.app/process

結果: HTTP/1.1 403 Forbidden(未認証アクセスのため拒絶される)

2. Cloudflare Workers 経由で叩く

Terminal window
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署名や検証が可能です。