CloudRunでFastAPIサーバーを構築しつつ、ジョブやイベントトリガーを試してみる
はじめに
CloudRunを技術検証を兼ねて触ってみます。
合わせてCloudRunジョブやEventarcトリガーによる処理も検証していきます。
アプリケーションコード
全コードはこちらのリポジトリに公開しています。
% tree .
.
├── Dockerfile
├── README.md
├── app
│ ├── __pycache__
│ │ └── main.cpython-311.pyc
│ └── main.py
└── requirements.txt
import logging
import os
from fastapi import FastAPI, Request
app = FastAPI()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/test")
def read_test():
return {"Hello": "Test"}
@app.post("/test_post")
async def post_test(request: Request):
json_body = await request.json()
logger.info(f"Received JSON body: {json_body}")
return {"message": "Body logged"}
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)
GET /
, GET /test
, POST /test_post
だけのAPIサーバーです。
ArtifactRegistry へのpush
CloudRunは単一のコンテナイメージだけで起動することができます。
ArtifactRegistryへpushすれば、そのイメージを簡単に起動できます。
Apple Mシリーズ シリコンの場合、docker build
時にはアーキテクチャを意識する必要があります。
docker build --platform linux/amd64 -t app .
docker tag app {HOST-NAME}/{PROJECT-ID}/{REPOSITORY}/{IMAGE}:{タグ}
docker push {HOST-NAME}/{PROJECT-ID}/{REPOSITORY}/{IMAGE}:{タグ}
Appleシリコン端末でアーキテクチャを指定しないでbuildをした場合、CloudRunで起動したときに以下のようなエラーが発生し、起動に失敗します。
docker build . -t app
terminated: Application failed to start: failed to load /usr/local/bin/uvicorn: exec format error
Default STARTUP TCP probe failed 1 time consecutively for container "app-1" on port 8080. The instance was not started.
CloudRun サービスの作成
CloudRunのサービスを作成します。
サービスの作成画面で、先程pushしたDockerimageを指定して起動してみます。
コンテナイメージのURL: ArtifactRegistryにpushしたイメージ
サービス名: 適当な名前
リージョン: asia-northeast1
認証: 未認証の呼び出しを許可
CPU の割り当てと料金: リクエストの処理中にのみ CPU を割り当てる
サービスの自動スケーリング: 1
上り(内向き)の制御: すべて
で起動してみます。
緑のチェックマークがついて、無事起動できました。
黒塗りの部分のURLでアクセスができ、GETリクエストを処理できていることが確認できました。
このままでは、プロダクト商用環境として使うには貧弱なため、
実際にはLoadBalancerやCloudNATなどを組み合わせて構成する必要があります。
CloudRun ジョブ
CloudRunにはバッチ実行を目的とした機能でCloudRun ジョブというものが提供されています。
指定したDockerimageを起動し、タスクを実行するだけで完了するとコンテナは終了します。ジョブは先程のサービスとは異なり、タイムアウト設定が設けられており、リクエストをリッスンするなどの用途には向きません。
ジョブのタブから確認することができ、ジョブを作成
から作成します。
コンテナイメージのURLをGoogleCloud側が用意しているデモコンテナ>helloを指定してみます。
特になにもしないコンテナですが、正常完了が確認できました。
これをスケジューラーを用いて定期実行をしてみます。
トリガー
タブからCloud Scheduler ジョブを作成します。
unix-cron
形式で実行間隔を指定します。
cronの評価結果を表示してくれるので地味に便利です。
0分を起点として10分ごとに実行する設定をしてみました。
開始時刻が16:10:07
となり、実際に処理が行われるまで少々ロスがありました。Dockerimageからコンテナへの立ち上げに時間がかかっているのでしょうか。
CloudRun イベントによるトリガー
CloudRunには、GoogleCloudリソースのイベントをトリガーにしてCloudRunサービスにHTTPリクエストをするという機能があります。
例えば、BigQueryのテーブルにインサートイベントが走ったときに、CloudRunで何かしら処理を行うケースを想定します。
CloudRunサービスのトリガータブからEventarc トリガーを追加
から作成します。
イベントプロバイダ: BigQuery
イベントトリガー: google.cloud.bigquery.v2.JobService.InsertJob
リソース パスパターン: /projects/{project_id}/datasets/{dataset_id}/tables/*
サービスURLパス: test_post
を設定します。
上記のパスパターンを設定することによって、特定のプロジェクトのBigQueryデータセット配下のすべてのテーブルを対象に、Insert処理が発生したときにイベントが発火します。そしてサービスURLパスに設定したパスに対してPOSTメソッドが叩かれます。
このパスはあらかじめアプリケーション側にAPIを用意しておきます。
試しに以下のようなINSERT SQLをBigQueryコンソールから実行してみます。
INSERT
`todo-xxxxx.test_dateset.customers` (id, name)
VALUES
(1,"START")
CloudRun側のログには、test_post
が叩かれていることが確認できます。
ただし、よく見ると2回実行されています。厳密に1回ではないようです。
POST時のbodyを検証してみました。
insertIdは異なっており、リトライなどで2回実行したものではないようです。
差分がある箇所だけを抜粋します。
"authorizationInfo": [
{
"resource": "projects/todo-xxxxx/datasets/test_dateset/tables/customers",
"permission": "bigquery.tables.updateData",
"granted": "True",
"resourceAttributes": {}
}
],
"metadata": {
"tableDataChange": {
"jobName": "projects/todo-xxxxx/jobs/bquxjob_1595e94d_18f7b686251",
"insertedRowsCount": "1",
"reason": "QUERY"
},
"@type": "type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata"
}
"authorizationInfo": [
{
"resource": "projects/todo-xxxxx/datasets/test_dateset/tables/customers",
"permission": "bigquery.tables.getData",
"granted": "True",
"resourceAttributes": {}
}
],
"metadata": {
"tableDataRead": {
"jobName": "projects/todo-xxxxx/jobs/bquxjob_1595e94d_18f7b686251",
"reason": "JOB"
},
"@type": "type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata",
}
metadata
の中身や、authorizationInfo.permission
などが異なっていました。
QUERY
とJOB
という異なる理由で分かれているようです。
イベントトリガー1回につき、確実に1回だけ処理を行いたい場合、bodyの内容を検証したうえで処理を行うか、別の方法で重複処理の排除を行う必要がありそうです。
おわりに
コンテナイメージ単位で簡単にAPIサーバーを構築することができました。
また、CloudRunジョブやイベントトリガーを駆使することで、GoogleCloudの各サービスとの連携を実現できることがわかりました。
Discussion