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

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

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

前提知識

Cloud Storage と Cloud Functions

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

blog.g-gen.co.jp

blog.g-gen.co.jp

Cloud Storage トリガの Cloud Functions とは

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

Cloud Storage トリガの関数

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

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

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

検証

やること

Cloud Storage トリガの Cloud Function (第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 Functions の実行環境にはデフォルトで含まれますのでライブラリをデプロイパッケージに含ませる必要はありません。まずはおまじないと思っても問題ありません。

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

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

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

実行結果

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

このバケットに animal_panda.png という名称のファイルをアップロードすると Functions が動き出し、以下のような出力結果となりました。なお Cloud 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 ではフォルダ階層があるかのように表示にされ、オブジェクトを整理しやすくすることができます。

デプロイの手順

参考 : Cloud Storage から直接イベントを受信する(gcloud 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 Functions
  • Cloud Run
  • Cloud Logging
  • Cloud Build
  • Cloud Storage
  • Pub/Sub
  • Eventarc

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

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

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

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

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

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

Cloud Functions 関数のデプロイ

先程のサンプルコードを main.py という名称でローカルに配置し、同じディレクトリで以下のコマンドを実行してください。

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

PROJECT="プロジェクト ID に置き換えてください"
bucket="バケット名に置き換えてください"
function="gcs-trigger-test"
 
gcloud functions deploy ${function} \
--gen2 \
--project=${PROJECT} \
--region=asia-northeast1 \
--runtime=python39 \
--memory=128Mi \
--entry-point main \
--trigger-bucket=${bucket}

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

実行結果確認

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

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

Cloud Logging 画面

他のログが多くて確認しづらい場合、以下のクエリでフィルタすれば、 Cloud Run (Cloud 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 Functions (Cloud Run) が実行されなかったことを意味しています。

Comute Engine のデフォルトサービスアカウントは 編集者 (editor) 権限をデフォルトで持っていますが、もし過去にこの権限を外したことがある場合、権限が足りず、当エラーが発生します。

なぜ Comute Engine のデフォルトサービスアカウントが関係するのでしょうか? Cloud Storage トリガで Cloud Functions 第2世代 (実体は Cloud Run) を呼び出す際、裏で Eventarc と Pub/Sub が動いています。当記事の方法でデプロイした場合、 Eventarc と Pub/Sub は Comute Engine のデフォルトサービスアカウント を使って Cloud Functions (第2世代) を呼び出すように設定されるのです。

このエラーは、以下のコマンドで Comute Engine のデフォルトサービスアカウントに Cloud Run 起動元 のロールを付与することで解決します ( Cloud Functions 起動元 ではないことに注意) 。

PROJECT="プロジェクト ID に置き換えてください"
PROJECT_NUM=`gcloud projects describe ${PROJECT} --format="value(projectNumber)"`
SERVICE_ACCOUNT="${PROJECT_NUM}-compute@developer.gserviceaccount.com"
  
gcloud projects add-iam-policy-binding ${PROJECT} \
    --member="serviceAccount:${SERVICE_ACCOUNT}" \
    --role='roles/run.invoker'

ローカルでのテスト

以下の記事に functions-framework を使ってローカル環境で Cloud 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 のアップデート情報をつぶやいています。