Google Apps ScriptでCloud Run Functionsを操作する【GAS】
2024年8月22日、これまでGoogle Cloud FunctionsとしていたサービスがCloud Runに統合されて、Cloud Run Functionsとしてリリースされました。第2世代という形で利用するようになった為、結構内部が変わっているのではないか?ということで今回、いくつかのテーマで調査してみることにしました。
目次
今回利用するサービス
今回この取り組みをするテーマが
- Cloud Run Function第2世代でもGASから叩けるのだろうか?
- Node.js 18で標準装備されたFetch APIは利用可能なのだろうか?
- UrlfetchAppで設定出来ない接続タイムアウトを実現する
ここから、さらにCloud Run FunctionsでPuppeteerを動かして、クラウド上で全ての稼働を完結できるか?に取り組む必要がでてきた為です。まずは、素のCloud Run Functions第2世代で単純にHTTPリクエストを実行して、値を取り、GAS側に返却出来るか?にチャレンジしています。
※今回は特にCloud Run Functions側に接続制限は加えていません。
事前準備
以下の作業はCloud Consoleの既存のプロジェクトに対してログインしてから作業を行います。
IAMのロール付与について
これまでCloud Runのコンテナに関する読み書きのロールは暗黙的にアクセス権限が付与されていました。しかし、2024/11/25のGoogle Cloudからのメールに「2025/1/25以降は、対象のユーザにロールとしてroles/artifactregistryが最低でも必要」といったアナウンスがありました。詳細についてはこちらのブログが参考になります。
対象ユーザのロールがIAM上でオーナーや編集者という基本ロールの場合影響を受けないのですが、同一テナント内でアクセス許可した他のユーザの場合、これら基本ロールがついていないと影響を受けることになります。
影響を受けるのはCloud Runコンテナに対する読み取りや作成・デプロイなどが出来なくなるので、他のユーザでこれらの作業を行っていた場合、突然コンテナが動かなくなったであったり、変更できなくなったと焦ることになります。今のうちに明示的に権限を付与しておくと良いでしょう。
対象はIAMにてCloud Run デベロッパー等の権限がついてるアカウントで、
- IAMを開いて対象ユーザアカウントの編集を開く(Cloud Run デベロッパーを今回あらかじめ付けてあります)
- プリンシパルを編集を開く
- 権限編集画面になるので、別のロールを追加をクリックする
- Cloud Runコンテナに対する編集権限であれば、ロールを選択のフィルタに「artifactregistry.write」を入れる(artifactregistryだけだと出てこないので注意)
- Artifact Registry書き込みという権限が出てくるので選択する
- 保存をクリックする
これで、対処は完了になります。
図:権限を明示的に追加する必要がある
環境の準備
Cloud Runと統合されたことで多少手順がこれまでと異なっています。第一世代Cloud Functionsは引き続き使えるようになっているので、新規に作ることも可能です。事前にサービスアカウントの作成が必要になります。
- 右上のハンバーガーメニュー(≡)をクリックし、サーバーレス項目にあるCloud Runをクリック
- 上部にある「関数を作成」をクリックする
- インライン エディタで関数を作成するの選択状態のままにする
- サービス名には適当な名前を入れます(今回はpuppetmanとして入力しました)
- エンドポイントURLが出ているのでコピーする(例:https://puppetman-xxxxx.us-central1.run.app)
- ランタイムはNode.js20をとりあえず今回は選んでいます。
- 未認証の呼び出しを許可に今回はチェックを入れる(認証を要する場合には別途認証を用意する必要があります)。デフォルトではHTTPトリガーは認証を要するので、第一世代パターンでの未認証の場合にはこちらの手順も見ておきましょう。
- トリガーは省略します
- 今回はテストなのでコンテナの設定に於いて、リソースでは、メモリ512MB, CPUは1で設定します。
- 続けて実行環境に於いては第2世代を選択します。
- セキュリティタブでは作成しておいたサービスアカウントを選択します。
- 作成をクリックする
- Cloud Build APIを有効にしろと出てくるので、有効にするをクリックする
- するとpackage.jsonやindex.jsの記述画面が表示されるようになる。
最初に実行する関数名を指定が無くなっている・・・・
package.jsonの記述
通常はpackage.jsonに利用するパッケージを含めておく必要性があります。左サイドバーのpackage.jsonを開いて以下のような記述を追加します。しかし今回は特に追加のパッケージ無しのプレーンな状態で利用します。
これまでHTTPリクエストを実行するにはnode-fetchを使っていたのですが、Node.js18で標準でFetch APIが装備された為、このモジュールを使わずにHTTPリクエストが出来るようになりました。逆にnode-fetchの最新版を加えてrequireするとESM用なので、デプロイ時にエラーとなってしまいますので要注意です。
1 2 3 4 5 6 7 |
{ "name": "driveman", "version": "0.0.1", "dependencies": { "@google-cloud/functions-framework": "^3.0.0", } } |
これでとりあえず、準備は完了。とりあえず保存して再デプロイを押します。但しこのデプロイは緑色のチェックマークがついたら成功なのですが、かなり時間が掛かります。
図:とりあえずの環境を用意する
ローカルの開発環境
確かにCloud Run FunctionsでNode.jsを動かすことは出来るのですが、この環境は動かすのには向いていても開発する土台としては酷く使いにくいです。
よって、ローカルマシン上にNode.jsを用意して、実際にindex.jsやpackage.jsonを用意してテストしたほうが手っ取り早い。そして無事に動いたらそのコードだけをCloud Run Functions上にコードとしてマージしてデプロイするといった手順が通常の開発手順になると思います。
図:ローカル環境では無事に動いた
HTTPリクエスト先
今回、GAS側からCloud Run Functionsを叩いて、Cloud Run FunctionsからHTTPリクエストをFetch APIにて投げます。この時のリクエスト先として、AppSheetでも使ったP2P地震情報APIを叩いてみようと思います。
特に認証も無いのでそのままURLを叩けば実行されるようにしています。
ソースコード
GCF側コード
GCF側にコードを記述してデプロイすると、GCF上部にURLが表示されます(例:https://xxx.us-central1.run.app)。このURLをもってGAS側で叩いて、答えを返してあげる為のコードです。index.jsに記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
const functions = require('@google-cloud/functions-framework'); //APIエンドポイント const endpoint = "https://api.p2pquake.net/v2/jma/quake?limit=20"; functions.http('helloHttp', (req, res) => { //リクエストヘッダ var headers = { 'Content-Type': 'application/json' } //送信オプション var options = { method: 'get', headers: headers } //ステータス格納用 let status; //HTTPリクエスト fetch(endpoint, options).then((response) => { let json = response.json(); return json; }).then((jsonData) => { console.log(jsonData); res.status(200).send(jsonData); }); }); |
- P2P地震情報APIをendpointとして変数に入れておきます。
- exportではなくfunctions.httpで実行する関数を指定する点が第一世代と異なる点になります。
- この時関数のエントリポイントと実行する実際の関数の名前は一致してる必要があります。
- Node.jsのFetch APIは通常のJavaScriptのFetch APIとほぼ同じ仕様です。
- 取得したデータは非同期で処理が流れるので、thenを使って同期的に取得したものを返さないと、返却値が空になるので要注意。
- response.jsonでリクエスト結果の内容を取得し、thenの次の項目でres.status(200).send(jsonData)とすることで、ステータスコード200を返し、取得した内容をsendでGAS側に送り返しています。
GAS側コード
GAS側ではGCF側で取得したURLを入れて、通常通りUrlfetchAppにてリクエストを投げます。特に変わった点はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function run_gcffunction() { let url = "ここにGCF側の生成されたURLを入力する" //POSTで関数を実行する let response = UrlFetchApp.fetch(url, { method: 'GET', contentType: "application/json", muteHttpExceptions: true }); //サーバーレスポンスコードを取得する let resCode = response.getResponseCode(); //リターンされて来たJSONデータを取得する if (resCode === 200) { console.log(response.getContentText()) }else{ //エラーメッセージ } } |
ポイント
実は、UrlfetchAppには「接続タイムアウトの秒数指定」ができません。よって、相手側サーバ側でタイムアウトの時間設定が無い場合、下手すると6分丸々リクエストを待った挙げ句に6分の実行時間の壁にぶつかってエラーとなってしまいます(Exception: Address unavailableというエラーが記録されます)。今回のP2P地震情報APIはまさにこのパターンで、貴重な実行時間枠を無駄に消費してしまいます。
よって、GCF側のfetchに対してsetTimeoutを用意して一定時間後に強制的にエラーとして返すようにしてみたり、AbortSignal.timeout()を使ってタイムアウト時間を設定してみたり(Node.jsのFetch APIも装備してる)すると、UrlfetchAppで無駄に時間を消費せずにタイムアウトさせてリトライさせるようにすることが可能です。
ちなみにCloud Run Functionsのタイムアウトはデフォルトで60秒ですが(GASよりも短い)、第2世代は最大60分まで延長が可能です。
図:タイムアウト指定しないとエラーになってしまうケース
関連リンク
- Google Cloud、Cloud FunctionsをCloud Runに統合
- [Action Required] Ensure read access on container images deployed to Cloud Run
- Google Cloud でそこそこ大きな無駄な課金が発生したので想定外課金対策を導入した話
- UrlFetchApp.fetchの応答3種 タイムアウトがつらい
- Extend or allow configurable timeout for UrlFetchApp.fetch
- Cloud billing API to get the projects cost
- Cloud FunctionsがCloud Run functionsとしてリブランディング。影響を解説
- Cloud Functions から Cloud Run functions に移行してみたら学びが深かった話
- Use Node-Fetch in TypeScript with CloudFunctions
- package.jsonの中身を理解する
- Google Chromeデベロッパーツールの日本語化
- Node.jsでFetch APIを使ってのHTTPリクエスト
- JavaScriptのFetch APIにtimeoutとretryの機能を追加する方法
- AbortSignal.timeout()を使用してfetchAPIにタイムアウトを設定する
- ts-nodeとnode-fetchの相性が悪い
- UrlFetchApp.fetchの応答3種 タイムアウトがつらい
- Throw custom timeout exception
- Set Timeout in Google Apps Scripts