CloudRunの認証付きエンドポイントをGASから実行する
背景
Firebase Functions V2でonScheduleを使って定期実行するバッチ処理を実装していました。Firebase Functions V2は内部的にCloud Runにデプロイされ、Cloud Scheduler経由で呼び出される仕組みになっています。
以外と記事が出てこなかったのでまとめます。
運用上の課題
定期実行以外に任意のタイミングで実行したいケースが頻繁に発生していました。その度に以下のような非効率な運用フローが発生していました:
- 運用担当者が開発者に再実行を依頼
- 開発者がCloud Schedulerのコンソールを開く
- 手動で強制実行を行う
この課題を解決するため、運用担当者が管理しているスプレッドシートから直接実行できる仕組みを構築することにしました。
技術的な課題
onScheduleでデプロイされた関数は認証が必須なAPIとしてCloud Runにデプロイされます。そのため、単純にHTTPリクエストを送るだけでは401 Unauthorizedエラーが返ってきます。
Cloud Shellでの実行例
Cloud Runのコンソールに表示されるテスト実行コマンド:
curl -X POST "https://xxxxxxx.asia-northeast1.run.app" \
-H "Authorization: bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d '{
"name": "Developer"
}'
このコマンドから、IDトークンがあれば実行できることが分かりました。
解決方法
スプレッドシートでCloud Runを活用:Apps ScriptからCloud Runへの認証方法を参考に、以下の5つのステップで実装しました。
1. OAuth同意画面の設定
Google Cloud Consoleで「APIとサービス」→「OAuth同意画面」から必要な情報を設定します。これを行わないとプロジェクトの紐付けでエラーが発生します。
2. GASプロジェクトとFirebaseプロジェクトの紐付け
GASエディタの「プロジェクトの設定」から、FirebaseプロジェクトのプロジェクトIDを入力して紐付けます。
3. 実行ユーザーへのIAM権限付与
GASを実行するユーザーにCloud Runの起動権限を付与:
gcloud run services add-iam-policy-binding [SERVICE_NAME] \
--region=asia-northeast1 \
--member="user:[USER_EMAIL]" \
--role="roles/run.invoker"
4. Cloud Runサービスの更新
OAuth情報を反映させるためにサービスを更新:
gcloud run services update [SERVICE_NAME] \
--region=asia-northeast1 \
--update-env-vars=UPDATED_AT="$(date)"
注意:これを行わないと認証が通らなかった
5. GASコードの実装
appsscript.json
必要なOAuthスコープを設定:
{
"timeZone": "Asia/Tokyo",
"dependencies": {},
"oauthScopes": [
"openid",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets"
],
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
実行コード
// メニューを作成
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('バッチ実行')
.addItem('手動実行', 'executeCloudRun')
.addToUi();
}
// Cloud Runの実行
function executeCloudRun() {
const ui = SpreadsheetApp.getUi();
// 確認ダイアログ
const result = ui.alert(
'確認',
'バッチ処理を実行しますか?',
ui.ButtonSet.YES_NO
);
if (result !== ui.Button.YES) {
return;
}
try {
const url = "https://[REGION]-[PROJECT_ID].cloudfunctions.net/[FUNCTION_NAME]";
const token = ScriptApp.getIdentityToken();
const response = UrlFetchApp.fetch(url, {
method: 'POST',
contentType: "application/json",
muteHttpExceptions: true,
headers: {
'Authorization': 'Bearer ' + token
},
payload: JSON.stringify({
timestamp: new Date().toISOString()
})
});
const responseCode = response.getResponseCode();
if (responseCode === 200 || responseCode === 202) {
ui.alert('成功', 'バッチ処理を開始しました', ui.ButtonSet.OK);
} else {
throw new Error(`HTTPエラー: ${responseCode}`);
}
} catch (error) {
console.error('Error:', error);
ui.alert('エラー', 'バッチ処理の実行に失敗しました', ui.ButtonSet.OK);
}
}
実装時のポイント
1. Error: Unauthorized
最初は以下のような401エラーが多発しました:
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL</h2>
</body></html>
この問題は、Cloud Runサービスの更新(ステップ4)を行うことで解決しました。
2. Firebase Functions Gen2の特徴
Firebase Functions Gen2は内部的にCloud Runを使用しているため:
- Cloud Functions URLとCloud Run URLの両方でアクセス可能
- IAM設定はCloud Run側で管理される
他の実装方法
今回はGASとGoogle Cloudプロジェクトが同じ組織内にあることを前提とした方法ですが、他にも以下のような方法があります:
- サービスアカウントキーを使用する方法
- Cloud Functions を公開設定にして独自認証を実装する方法
- Pub/Sub経由で間接的に実行する方法
詳細はGoogleスプレッドシート(GAS)からGoogle Cloudへアクセスする3つの方法などを参照してください。
まとめ
GASからCloud Runの認証付きエンドポイントを実行する仕組みを構築することで、運用担当者が開発者に依存せずに任意のタイミングでバッチ処理を実行できるようになりました。
Discussion