🐶

CloudRunでFastAPIサーバーを構築しつつ、ジョブやイベントトリガーを試してみる

2024/05/15に公開

はじめに

CloudRunを技術検証を兼ねて触ってみます。
合わせてCloudRunジョブやEventarcトリガーによる処理も検証していきます。

アプリケーションコード

全コードはこちらのリポジトリに公開しています。

% tree .
.
├── Dockerfile
├── README.md
├── app
│   ├── __pycache__
│   │   └── main.cpython-311.pyc
│   └── main.py
└── requirements.txt
main.py
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回実行したものではないようです。

差分がある箇所だけを抜粋します。

1
"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"
}
2
"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などが異なっていました。
QUERYJOBという異なる理由で分かれているようです。
イベントトリガー1回につき、確実に1回だけ処理を行いたい場合、bodyの内容を検証したうえで処理を行うか、別の方法で重複処理の排除を行う必要がありそうです。

おわりに

コンテナイメージ単位で簡単にAPIサーバーを構築することができました。
また、CloudRunジョブやイベントトリガーを駆使することで、GoogleCloudの各サービスとの連携を実現できることがわかりました。

Discussion