Cloud StorageトリガでCloud Run functions(2nd gen)を動かしてみた

記事タイトルとURLをコピーする

G-gen の杉村です。Google Cloud(旧称 GCP)の Cloud Run functions(第2世代)を使い、Cloud Storage へファイルが配置されたことを起点に起動するプログラムを作ってみました。

前提知識

Cloud Storage と Cloud Run functions

Cloud Storage と Cloud Run functions の基礎知識については以下の記事をご参照ください。

blog.g-gen.co.jp

blog.g-gen.co.jp

Cloud Storage トリガの Cloud Run functions とは

Cloud Storage にオブジェクトがアップロードされたことをトリガーにして Cloud Run functions を起動させることができます。この呼び出し方を Cloud Storage トリガーと呼びます。

Cloud Storage トリガの関数

ユースケースとしては、例えば以下のようなものが挙げられます。

  • csv ファイルがアップロードされると中身を自動的に BigQuery のテーブルに投入する
  • 画像ファイルがアップロードされるど自動的に切り抜き & 画像サイズを調整してサムネイルを作る
  • Zip ファイルがアップロードされると自動的に展開して適切なパス (フォルダ) に振り分ける

なお Cloud Run functions の第1世代と第2世代で少し実装方法が異なります。詳細は以下の公式ドキュメントをご確認ください。

検証

やること

Cloud Storage トリガの Cloud Run functions(第2世代)では、トリガの情報(Cloud Storage にアップロードされたオブジェクトのパスやファイル名、サイズ等)が CloudEvent形式で渡されてきます。

今回は、関数に渡されるイベントの内容を確かめる検証のため、特にファイルに対して処理をせず、イベントの中身をテキストとして Cloud Logging に出力するだけのプログラムを開発しました。

今回の検証

ソースコード

import functions_framework
@functions_framework.cloud_event
def main(cloud_event):
# printing all the event data
print(cloud_event)
# Name of the bucket and the object
bucket = cloud_event.data['bucket']
object = cloud_event.data['name']
size = cloud_event.data['size']
print(f"bucket : {bucket}")
print(f"object : {object}")
print(f"size : {size}")

冒頭の import functions_framework は CloudEvent 関数を使うときに必須のライブラリです。Cloud Run functions の実行環境にはデフォルトで含まれますのでライブラリをデプロイパッケージに含ませる必要はありません。まずはおまじないと思っても問題ありません。

main 関数の前の @functions_framework.cloud_event はデコレータです。デコレータとは、ある関数の実行前後に別の処理を加える際などに用いる Python の機能です。こちらもおまじないだと思っても構いません。

def main(cloud_event): 以降が本来の処理となります。 Cloud Storage にファイル(オブジェクト)がアップロードされると、そのファイルの情報が cloud_event として渡され、 main 関数が実行されます。この関数の処理は cloud_event の中身や cloud_event から取り出したバケット名、ファイル名、ファイルサイズを print する簡単なものです。

Cloud Run functions では標準出力が Cloud Logging に自動的に送信されるため、 print コマンドで簡易的にロギングしています。

実行結果

手順は後述しますが、上記のソースを Cloud Run functions(第2世代)にデプロイして、gcs-function-test という Cloud Storage バケットと紐づけました。

このバケットに animal_panda.png という名称のファイルをアップロードすると関数が起動し、以下のような出力結果となりました。なお Cloud Run functions から print すると Cloud Logging 側では様々なメタ情報を付加しますが、以下に標準出力の中身だけを掲載します。

print(cloud_event) の結果

{
'attributes': {
'specversion': '1.0',
'id': '5635814697830791',
'source': ' //storage.googleapis.com/projects/_/buckets/gcs-function-test',
'type': 'google.cloud.storage.object.v1.finalized',
'datacontenttype': 'application/json',
'subject': 'objects/animal_panda.png',
'time': '2022-09-17T05:59:11.036457Z',
'bucket': 'gcs-function-test'
},
'data': {
'kind': 'storage#object',
'id': 'gcs-function-test/animal_panda.png/1663394351028903',
'selfLink': 'https://www.googleapis.com/storage/v1/b/gcs-function-test/o/animal_panda.png',
'name': 'animal_panda.png',
'bucket': 'gcs-function-test',
'generation': '1663394351028903',
'metageneration': '1',
'contentType': 'image/png',
'timeCreated': '2022-09-17T05:59:11.036Z',
'updated': '2022-09-17T05:59:11.036Z',
'storageClass': 'STANDARD',
'timeStorageClassUpdated': '2022-09-17T05:59:11.036Z',
'size': '214319',
'md5Hash': '8ui/28TJi+Qi/kJrJHWYuA==',
'mediaLink': 'https://storage.googleapis.com/download/storage/v1/b/gcs-function-test/o/animal_panda.png?generation=1663394351028903&alt=media',
'crc32c': 'cYHNvQ==',
'etag': 'CKe1qOuSm/oCEAE='
}
}

これが Cloud Storage トリガで得られる情報の全量となります。渡されるデータは StorageObjectData タイプであり、フォーマットは以下のように決まっています。

attributes にはイベントの性質が入っています。今回のイベントがオブジェクトの finalize (新規オブジェクト作成か既存オブジェクト上書きの完了) がきっかけであることや、イベントの時刻 (UTC) が入っています。

data にはオブジェクト名(name)、バケット名(bucket)、ストレージクラス(storageClass)、バイト数(size)などが含まれていることが分かります。

print(f"bucket : {bucket}") の結果

bucket : gcs-function-test

print(f"object : {object}") の結果

object : animal_panda.png

print(f"size : {size}") の結果

size : 214319

先程の cloud_event から情報を読み出して利用できることが分かります。

今回は行っていませんが、続くプログラム内で Cloud Storage API を呼び出してアップロードされたファイルに対して処理をすること等ができます。

なお cloud_event.data['name'] にはオブジェクト名が入りますが、フォルダの中に入っている場合は myfolder/myfile.txt のようにフルパスが入ります。

余談ですが、Cloud Storage にはフォルダという概念は実体としては存在しません。

Cloud Storage はあくまでキー・バリューストアであり、フラットな空間にオブジェクトが配置されます。 myfolder/myfile.txtmyfolder はフォルダという実体があるわけではなく、オブジェクト名の一部にすぎません。ただしコンソール画面や CLI ではフォルダ階層があるかのように表示にされ、オブジェクトを整理しやすくすることができます。

デプロイの手順

必要な API の有効化

以下のコマンドで、必要な API を有効化します。

gcloud services enable \
artifactregistry.googleapis.com
cloudfunctions.googleapis.com \
run.googleapis.com \
logging.googleapis.com \
cloudbuild.googleapis.com \
storage.googleapis.com \
pubsub.googleapis.com \
eventarc.googleapis.com \

このコマンドで有効化されるのは以下のサービスです。既に有効化されているものがあっても悪影響はありませんのでそのまま実行して構いません。

  • Artifact Registry
  • Cloud Run functions
  • Cloud Run
  • Cloud Logging
  • Cloud Build
  • Cloud Storage
  • Pub/Sub
  • Eventarc

Cloud Storage サービスエージェントに権限付与

以下のコマンドを実行して Cloud Storage のサービスエージェントに対し、 Pub/Sub へパブリッシュするための IAM 権限を付与します。

プロジェクト名に置き換えてください の部分は、ご自身のプロジェクト ID に置き換えてください。

PROJECT_ID="プロジェクト ID に置き換えてください"
SERVICE_AGENT="$(gcloud storage service-agent --project=${PROJECT_ID})"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SERVICE_AGENT}" \
--role='roles/pubsub.publisher'

gcloud storage service-agent --project=${PROJECT_ID} により Cloud Storage のサービスエージェント名を取得しています。サービスエージェントとは、Google Cloud サービスが他のサービスを呼び出すときに利用する特別なサービスアカウントです。Cloud Storage のサービスエージェントはプロジェクトに1つだけ存在します。

このサービスアカウントに Pub/Sub へパブリッシュ(メッセージを発行)する権限を与えているのです。Cloud Storage トリガの関数起動時には、内部的には Pub/Sub が利用されています(正確に言うと、裏で使われている Eventarc が Cloud Run functions や Cloud Run を呼び出す際に、Pub/Sub を使います)。

トリガー用サービスアカウントの作成

Eventarc が関数を起動するために使うサービスアカウントを作成します。サービスアカウントには、Eventarc イベント受信者(roles/eventarc.eventReceiver)ロールと、Cloud Run サービス起動元(roles/run.servicesInvoker)ロールをプロジェクトレベルで付与します。前者は Eventarc が Cloud Storage からのイベントを受信するために、後者は Eventarc が関数を起動するために必要です。

PROJECT_ID="プロジェクト ID に置き換えてください"
SA_NAME="gce-trigger-test"
gcloud iam service-accounts create ${SA_NAME} --project=${PROJECT_ID}
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role='roles/eventarc.eventReceiver'
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role='roles/run.servicesInvoker'

ソースコードの配置

新しいディレクトリを作成し、先程のサンプルコードを main.py という名称で配置します。

また、requirements.txt という空のテキストファイルを同じディレクトリに配置してください。requirements.txt は Python ライブラリの依存関係を記述するファイルであり、Cloud Run functions をデプロイすると、このファイルに基づいて実行環境が自動的に用意されます。今回のサンプルソースコードでは、何も記述しない状態で問題ありません。

Cloud Run functions 関数のデプロイ

ソースコードと同じディレクトリに移動して、以下のコマンドを実行してください。

プロジェクト名に置き換えてください」の部分は、ご自身のプロジェクト ID に置き換えてください。「バケット名に置き換えてください」の部分は、ご自身のバケット名に置き換えてください。function= 以降は Cloud Run functions の関数名であり、任意の名称にしてください。

PROJECT_ID="プロジェクト ID に置き換えてください"
BUCKET="バケット名に置き換えてください"
SA_NAME="gce-trigger-test"
FUNCTION_NAME="gcs-trigger-test"
gcloud functions deploy ${FUNCTION_NAME} \
--gen2 \
--project=${PROJECT_ID} \
--region=asia-northeast1 \
--runtime=python39 \
--memory=128Mi \
--entry-point main \
--trigger-bucket=${BUCKET} \
--trigger-service-account="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

上記のコマンドではリージョン、ランタイム、メモリ数などを指定しています。このコマンドを実行すると、ビルドとデプロイにおよそ 2 分程度かかります。デプロイが完了すると関数が利用可能になり、バケットにファイルを配置したり上書きしたりすると関数が起動するようになります。

実行結果確認

指定した Cloud Storage バケットにファイルをアップロードしてみてください。

うまくいけば Cloud Logging のログエクスプローラで print した内容が確認できます。

Cloud Logging 画面

他のログが多くて確認しづらい場合、以下のクエリでフィルタすれば、 Cloud Run (Cloud Run functions 第2世代) のログだけに絞ることができます。

resource.type="cloud_run_revision"

トラブルシューティング

Please verify that the bucket exists

gcloud functions deploy コマンドを実行後、以下のようなエラーメッセージが出力されることがあります。

ERROR: (gcloud.functions.deploy) PERMISSION_DENIED: Cannot create trigger projects/my-project-id/locations/asia-northeast1/triggers/gcs-trigger-test-489977: Permission "storage.buckets.get" denied on "Bucket \"my-test-bucket\" could not be validated. Please verify that the bucket exists and that the Eventarc service account has permission."

素直に読むと「バケット名が正しいか」「Eventarc サービスアカウントが正しい権限を持っているか」などを確かめる必要があるように思えます。

しかしこれは、当記事の手順を始めて実施した直後に起こることがあり、時間をおいて再実行すると発生しなくなることがあります。

これは、 API の有効化や IAM 権限の付与が Google Cloud 内で伝搬するのに時間がかかる場合があるからです。数分〜十数分程度、間を開けて再実行してください。

Build failed with status: FAILURE and message: An unexpected error occurred

同じく gcloud functions deploy コマンドを実行後、以下のようなエラーメッセージが出力されることがあります。

ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Build failed with status: FAILURE and message: An unexpected error occurred. Refer to build logs: https://console.cloud.google.com/cloud-build/builds;region=asia-northeast1/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?project=0000000000000. For more details see the logs at https://console.cloud.google.com/cloud-build/builds;region=asia-northeast1/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?project=0000000000000.

ビルド失敗を意味するメッセージです。エラーメッセージ内に Cloud Logging へのリンクがあるのでそちらへ移動してさらにログを精査すると、以下のようなメッセージが見つかることがあります。

Artifact Registry API has not been used in project 0000000000000 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/artifactregistry.googleapis.com/overview?project=0000000000000 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

メッセージ通り、 API の有効化が Google Cloud 内で伝搬するのに時間がかかっているために出るエラーです。数分〜十数分程度、間を開けて再実行してください。

この原因以外でも、ソースコードの誤り等でこのエラーが発生することもありえますので、ビルドのログを確認することが解決への近道です。

The request was not authenticated.

ビルド・デプロイが成功し、ファイルをバケットにアップロードしても print 内容が Cloud Logging に現れず、以下のようなエラーが出ていることもあります。

The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header. Read more at https://cloud.google.com/run/docs/securing/authenticating Additional troubleshooting documentation can be found at: https://cloud.google.com/run/docs/troubleshooting#unauthorized-client

The request was not authenticated.

これは、認証がうまくいかず関数が実行されなかったことを意味しています。当記事に示したデプロイコマンドでデプロイした場合、指定したサービスアカウントの認証情報を使って関数が起動されます。その際に権限が足りず、エラーが起きていることが考えられます。

Cloud Storage トリガで Cloud Run functions 第2世代(実体は Cloud Run)を呼び出す際に、背後では Eventarc と Pub/Sub が動いています。このときサービスアカウントの認証情報を使って関数が呼び出されますが、サービスアカウントには Cloud Run サービス起動元(roles/run.servicesInvoker)ロールもしくは Cloud Run 起動元(roles/run.invoker)ロールが必要です。なお前者のロールのほうが権限が小さく、最小権限の原則に従っています。

権限不足は、以下のコマンドで対象のサービスアカウントに Cloud Run サービス起動元(roles/run.servicesInvoker)ロールを付与することで解決します。

PROJECT_ID="プロジェクト ID に置き換えてください"
SA_NAME="サービスアカウント名に置き換えてください"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role='roles/run.servicesInvoker'

なお、付与すべきロールは Cloud Functions 起動元(roles/cloudfunctions.invoker)ロールではないことに注意してください。このロールは、第1世代の古い Cloud Run functions 関数を起動するためのロールです。

もし関数のデプロイ時に --service-account オプションで関数自体にサービスアカウントをアタッチしており、かつ --trigger-service-account オプションを指定しなかった場合は、--service-account オプションで指定したサービスアカウントが Eventarc に設定されます。またいずれのオプションも指定しなかった場合は、Eventarc と関数の両方に Comute Engine のデフォルトサービスアカウントが設定されます。Eventarc にどのサービスアカウントが設定されているかは、Google Cloud コンソールで Eventarc トリガーの一覧画面に遷移し、関数の名前を含むトリガーの設定を閲覧することで確認することができます。そのサービスアカウントに、Cloud Run サービス起動元(roles/run.servicesInvoker)ロールを付与してください。

ローカルでのテスト

以下の記事に functions-framework を使ってローカル環境で Cloud Run functions 関数の単体テストを行う方法が書いてあります。

blog.g-gen.co.jp

functions-framework を使える環境が整ったら、以下のように仮想 function を起動します。

functions-framework --debug --target main

以下のような curl コマンドで CloudEvents を再現して単体テストを実行できます。

curl localhost:8080 \
-X POST \
-H "Content-Type: application/json" \
-H "ce-id: 123451234512345" \
-H "ce-specversion: 1.0" \
-H "ce-time: 2022-09-17T05:59:11.036Z" \
-H "ce-type: google.cloud.storage.object.v1.finalized" \
-H "ce-source: //storage.googleapis.com/projects/_/buckets/gcs-function-test" \
-H "ce-subject: objects/animal_panda.png" \
-d '{
"bucket": "gcs-function-test",
"contentType": "image/png",
"kind": "storage#object",
"md5Hash": "...",
"metageneration": "1",
"name": "animal_panda.png",
"size": "214319",
"storageClass": "STANDARD",
"timeCreated": "2022-09-17T05:59:11.036Z",
"timeStorageClassUpdated": "2022-09-17T05:59:11.036Z",
"updated": "2022-09-17T05:59:11.036Z"
}'

この curl リクエストは gcs-function-test バケットに animal_panda.png というファイルが置かれたときのイベントを再現しています。

参考 : ローカル関数の呼び出し

杉村 勇馬 (記事一覧)

執行役員 CTO / クラウドソリューション部 部長

元警察官という経歴を持つ現 IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 12資格、Google Cloud認定資格11資格。X (旧 Twitter) では Google Cloud や AWS のアップデート情報をつぶやいています。