Cloud Run functions を使用してイベント トリガーで Cloud Run を拡張する

Cloud Run functions を使用してコードをデプロイすることで、Cloud Run データベースの変更によってトリガーされるイベントを処理できます。これにより、独自のサーバーを実行しなくても、サーバー側の機能を追加できます。

このガイドでは、Firestore イベントから Cloud Run 関数のトリガーを作成する方法について説明します。

Firestore データベース内のイベントから Cloud Run functions をトリガーできます。トリガーされると、関数は Firestore API とクライアント ライブラリを介して、これらのイベントに応答して Firestore データベースを読み取り、更新します。

Firestore イベントが Cloud Run functions をトリガーするプロセスは、次の手順で構成されます。

  1. サービスは、特定のドキュメントに変更が加えられるのを待ちます。

  2. 変更が発生すると、サービスがトリガーされ、タスクを実行します。

  3. サービスは、影響を受けるドキュメントのスナップショットを含むデータ オブジェクトを受信します。write または update イベントの場合、トリガー イベントの前後のドキュメントの状態を表すスナップショットがデータ オブジェクトに含まれます。

始める前に

  1. 設定ページの説明に従って、Cloud Run に新しいプロジェクトを設定したことを確認してください。
  2. Artifact Registry、Cloud Build、Cloud Run Admin API、Eventarc、Firestore Cloud Logging、Pub/Sub API を有効にします。

    API を有効にする

必要なロール

ユーザーまたは管理者が、デプロイ担当者アカウントとトリガー ID を付与する必要があります。必要に応じて、Pub/Sub サービス エージェントに次の IAM ロールを付与します。

デプロイ担当者のアカウントに必要なロール

Firestore イベントからトリガーするために必要な権限を取得するには、プロジェクトに対する次の IAM ロールを付与するよう管理者に依頼してください。

ロールの付与については、プロジェクト、フォルダ、組織に対するアクセス権の管理をご覧ください。

必要な権限は、カスタムロールや他の事前定義ロールから取得することもできます。

デフォルトでは、Cloud Build の権限には、Artifact Registry アーティファクトをアップロードおよびダウンロードするための権限が含まれています

トリガー ID に必要なロール

  1. Compute Engine のデフォルトのサービス アカウントをメモしておいてください。テスト目的で、Eventarc トリガーに関連付けて、トリガーの ID を示すためです。このサービス アカウントは、Compute Engine を使用する Google Cloud サービスを有効にするか使用すると自動的に作成されます。メールアドレスの形式は次のとおりです。

    PROJECT_NUMBER-compute@developer.gserviceaccount.com

    PROJECT_NUMBER は、使用する Google Cloudプロジェクト番号に置き換えます。プロジェクト番号は、 Google Cloud コンソールの [ようこそ] ページで確認できます。また、次のコマンドでも確認できます。

    gcloud projects describe PROJECT_ID --format='value(projectNumber)'

    本番環境では、新しいサービス アカウントを作成して、必要最小限の権限を含む、最小権限の原則に従った 1 つ以上の IAM ロールを付与することを強くおすすめします。

  2. デフォルトでは、Cloud Run サービスを呼び出すことができるのは、プロジェクト オーナー、プロジェクト編集者、Cloud Run 管理者、起動元のみです。サービスごとにアクセスを制御できます。ただし、テスト目的の場合は、 Google Cloud プロジェクトの Cloud Run 起動元ロールrun.invoker)を Compute Engine サービス アカウントに付与してください。これにより、プロジェクト内のすべての Cloud Run サービスとジョブにロールが付与されます。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/run.invoker

    Cloud Run 起動元ロールを付与せずに認証済みの Cloud Run サービスのトリガーを作成すると、トリガーは正常に作成され、アクティブになります。ただし、トリガーが期待どおりに機能せず、次のようなメッセージがログに記録されます。

    The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
  3. プロジェクトの Eventarc イベント レシーバーのロールroles/eventarc.eventReceiver)を Compute Engine のデフォルト サービス アカウントに付与して、Eventarc トリガーがイベント プロバイダからイベントを受信できるようにします。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/eventarc.eventReceiver

Pub/Sub サービス エージェントのオプションのロール

  • 2021 年 4 月 8 日以前に、認証済みの Pub/Sub push リクエストをサポートするために Cloud Pub/Sub サービス エージェントを有効にした場合は、サービス アカウント トークン作成者のロールroles/iam.serviceAccountTokenCreator)をサービス エージェントに付与します。それ以外の場合、このロールはデフォルトで付与されます。
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
        --role=roles/iam.serviceAccountTokenCreator

Firestore データベースを設定する

サービスをデプロイする前に、Firestore データベースを作成する必要があります。

  1. Firestore ページに移動します。

  2. [Firestore データベースを作成] を選択します。

  3. [データベースに名前を付ける] フィールドに、データベース IDfirestore-db など)を入力します。

  4. [構成オプション] セクションで、該当するセキュリティ ルールとともに [Firestore ネイティブ] がデフォルトで選択されています。

  5. [ロケーション タイプ] で [リージョン] を選択し、データベースを配置するリージョンを選択します。この選択は永続的に適用されます。

  6. [データベースを作成] をクリックします。

Firestore データモデルは、ドキュメントを含むコレクションで構成されます。ドキュメントには、一連の Key-Value ペアが含まれています。

Firestore でトリガーされる関数を作成する

Firestore イベントに応答する関数を作成するには、デプロイ中に次の内容を指定する準備を行います。

イベントタイプ

Firestore は、createupdatedeletewrite イベントをサポートしています。write イベントには、ドキュメントに対するすべての変更が含まれます。

イベントタイプ トリガー
google.cloud.firestore.document.v1.created(デフォルト) ドキュメントが最初に書き込まれたときにトリガーされます。
google.cloud.firestore.document.v1.updated すでに存在するドキュメントの値が変更されたときにトリガーされます。
google.cloud.firestore.document.v1.deleted データを含むドキュメントが削除されたときにトリガーされます。
google.cloud.firestore.document.v1.written ドキュメントが作成、更新、削除されたときにトリガーされます。
google.cloud.firestore.document.v1.created.withAuthContext created と同じですが、認証情報を追加します。
google.cloud.firestore.document.v1.updated.withAuthContext updated と同じですが、認証情報を追加します。
google.cloud.firestore.document.v1.deleted.withAuthContext deleted と同じですが、認証情報を追加します。
google.cloud.firestore.document.v1.written.withAuthContext written と同じですが、認証情報を追加します。

ワイルドカードは、トリガー内で中括弧を使用して記述します。例: projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

トリガー イベント フィルタ

サービスをトリガーするには、リッスンするドキュメント パスを指定します。ドキュメント パスは、サービスと同じ Google Cloud プロジェクトに存在する必要があります。

有効なドキュメント パスの例を次に示します。

  • users/marie: 1 つのドキュメント(/users/marie)をモニタリングします。

  • users/{username}: すべてのユーザー ドキュメントをモニタリングします。コレクション内のすべてのドキュメントをモニタリングする場合は、ワイルドカードを使用します。

  • users/{username}/addresses/home: すべてのユーザーの自宅住所のドキュメントをモニタリングします。

  • users/{username}/addresses/{addressId}: すべての住所ドキュメントをモニタリングします。

  • users/{user=**}: すべてのユーザー ドキュメントと、各ユーザー ドキュメントのサブコレクション(/users/userID/address/home/users/userID/phone/work など)内のドキュメントをモニタリングします。

  • users/{username}/addresses: 無効なアドレスパス。ドキュメントではなく、サブコレクション addresses を参照します。

ワイルドカードとパラメータ

モニタリングするドキュメントが不明な場合は、ドキュメント ID の代わりに {wildcard} を使用します。

  • users/{username} は、すべてのユーザー ドキュメントに対する変更をリッスンします。

この例では、users にあるドキュメントの任意のフィールドが変更されると、{username} というワイルドカードと照合されます。

users に含まれるドキュメントにサブコレクションがある場合、サブコレクションのいずれかに含まれるドキュメントのフィールドが変更されても、{username} ワイルドカードはトリガーされません。サブコレクション内のイベントにも応答することが目標なら、マルチセグメント ワイルドカード {username=**} を使用します。

ワイルドカードに一致した部分が、ドキュメント パスから抽出されます。明示的なコレクションまたはドキュメント ID に置き換えるワイルドカードは、必要な数だけ定義できます。マルチセグメント ワイルドカード({username=**} など)は 1 つまで使用できます。

関数コード

ネイティブ モードの Firestore イベントを使用して Cloud Run 関数をトリガーする方法については、をご覧ください。

proto 依存関係をソースに含める

関数のソース ディレクトリに Cloud Run data.proto ファイルを含める必要があります。このファイルは、ソース ディレクトリにも含める必要がある次の proto をインポートします。

依存関係に同じディレクトリ構造を使用します。たとえば、struct.protogoogle/protobuf 内に配置します。

これらのファイルは、イベントデータをデコードするのに必要です。関数のソースにこれらのファイルが含まれていない場合は、実行時にエラーが返されます。

イベント属性

各イベントには、イベントがトリガーされた時間など、イベントに関する情報を含むデータ属性が含まれます。Cloud Run は、イベントに関連するデータベースとドキュメントに関するデータを追加します。これらの属性には、次のようにアクセスできます。

Java
logger.info("Function triggered by event on: " + event.getSource());
logger.info("Event type: " + event.getType());
logger.info("Event time " + event.getTime());
logger.info("Event project: " + event.getExtension("project"));
logger.info("Event location: " + event.getExtension("location"));
logger.info("Database name: " + event.getExtension("database"));
logger.info("Database document: " + event.getExtension("document"));
// For withAuthContext events
logger.info("Auth information: " + event.getExtension("authid"));
logger.info("Auth information: " + event.getExtension("authtype"));
Node.js
console.log(`Function triggered by event on: ${cloudEvent.source}`);
console.log(`Event type: ${cloudEvent.type}`);
console.log(`Event time: ${cloudEvent.time}`);
console.log(`Event project: ${cloudEvent.project}`);
console.log(`Event location: ${cloudEvent.location}`);
console.log(`Database name: ${cloudEvent.database}`);
console.log(`Document name: ${cloudEvent.document}`);
// For withAuthContext events
console.log(`Auth information: ${cloudEvent.authid}`);
console.log(`Auth information: ${cloudEvent.authtype}`);
Python
print(f"Function triggered by change to: {cloud_event['source']}")
print(f"Event type: {cloud_event['type']}")
print(f"Event time: {cloud_event['time']}")
print(f"Event project: {cloud_event['project']}")
print(f"Location: {cloud_event['location']}")
print(f"Database name: {cloud_event['database']}")
print(f"Document: {cloud_event['document']}")
// For withAuthContext events
print(f"Auth information: {cloud_event['authid']}")
print(f"Auth information: {cloud_event['authtype']}")

イベントの構造

このトリガーは、次のようなイベントでサービスを呼び出します。

{
    "oldValue": { // Update and Delete operations only
        A Document object containing a pre-operation document snapshot
    },
    "updateMask": { // Update operations only
        A DocumentMask object that lists changed fields.
    },
    "value": {
        // A Document object containing a post-operation document snapshot
    }
}

それぞれの Document オブジェクトに 1 つまたは複数の Value オブジェクトが含まれます。型の詳細については、Value ドキュメントをご覧ください。

関数のトリガーを作成する

タブをクリックして、使用するツールでの手順を確認してください。

コンソール

Google Cloud コンソールを使用して関数を作成する場合、関数の作成時にトリガーを追加することもできます。関数のトリガーを作成する手順は次のとおりです。

  1. Google Cloud コンソールで Cloud Run に移動します。

    Cloud Run に移動

  2. [関数を作成] をクリックし、関数の詳細を入力します。デプロイ時に関数を構成する方法については、関数をデプロイするをご覧ください。

  3. [トリガー] セクションで [トリガーを追加] をクリックします。

  4. [Firestore トリガー] を選択します。

  5. [Eventarc トリガー] ペインで、トリガーの詳細を次のように変更します。

    1. [トリガー名] フィールドにトリガーの名前を入力するか、デフォルトの名前を使用します。

    2. リストからトリガーのタイプを選択します。

      • Google のソース: Pub/Sub、Cloud Storage、Firestore などの Google イベント プロバイダのトリガーを指定できます。

      • サードパーティ: Eventarc ソースを提供する Google 以外のプロバイダと統合できます。詳細については、Eventarc のサードパーティ イベントをご覧ください。

    3. [イベント プロバイダ] リストから [Firestore] を選択し、関数をトリガーするイベントの種類を提供するプロダクトを選択します。イベント プロバイダのリストについては、イベント プロバイダと宛先をご覧ください。

    4. [イベントの種類] リストから [type=google.cloud.firestore.document.v1.created] を選択します。トリガーの構成は、サポートされているイベントタイプによって異なります。詳細については、イベントタイプをご覧ください。

    5. [フィルタ] セクションで、データベース、オペレーション、属性の値を選択するか、デフォルト値を使用します。

    6. [リージョン] フィールドが有効になっている場合は、Eventarc トリガーのロケーションを選択します。一般に、Eventarc トリガーのロケーションは、イベントをモニタリングする Google Cloud リソースのロケーションと一致している必要があります。ほとんどの場合に、関数を同じリージョンにデプロイすることも必要です。Eventarc トリガーのロケーションの詳細については、Eventarc のロケーションについてをご覧ください。

    7. [サービス アカウント] フィールドで、サービス アカウントを選択します。Eventarc トリガーはサービス アカウントにリンクされ、関数を呼び出すときに ID として使用します。Eventarc トリガーのサービス アカウントには、関数を呼び出す権限が必要です。デフォルトでは、Cloud Run は Compute Engine のデフォルトのサービス アカウントを使用します。

    8. 受信リクエストの送信先であるサービスの URL パスを指定することもできます。これは、トリガーのイベントの送信先である宛先サービスの相対パスです。例: //routerouteroute/subroute.

  6. 必須フィールドに値を入力したら、[トリガーを保存] をクリックします。

gcloud

gcloud CLI を使用して関数を作成する場合は、まず関数をデプロイしてからトリガーを作成する必要があります。関数のトリガーを作成する手順は次のとおりです。

  1. サンプルコードのあるディレクトリで次のコマンドを実行して、関数をデプロイします。

    gcloud run deploy FUNCTION \
            --source . \
            --function FUNCTION_ENTRYPOINT \
            --base-image BASE_IMAGE_ID \
            --region REGION
    

    次のように置き換えます。

    • FUNCTION: デプロイする関数の名前。このパラメータは省略できますが、省略すると名前の入力を求められます。

    • FUNCTION_ENTRYPOINT: ソースコード内の関数のエントリ ポイント。これは、関数の実行時に Cloud Run が実行するコードです。このフラグには、ソースコード内に存在する関数名または完全修飾クラス名を指定する必要があります。

    • BASE_IMAGE_ID: 関数のベースイメージ環境。ベースイメージと各イメージに含まれるパッケージの詳細については、ランタイム ベースイメージをご覧ください。

    • REGION: 関数をデプロイする Google Cloudリージョン。例: europe-west1

  2. 次のコマンドを実行して、イベントをフィルタするトリガーを作成します。

    gcloud eventarc triggers create TRIGGER_NAME  \
        --location=EVENTARC_TRIGGER_LOCATION \
        --destination-run-service=FUNCTION  \
        --destination-run-region=REGION \
        --event-filters="type=google.cloud.firestore.document.v1.created" \
        --service-account=PROJECT_NUMBER-compute@developer.gserviceaccount.com
    

    次のように置き換えます。

    • TRIGGER_NAME は、トリガーの名前に置き換えます。

    • EVENTARC_TRIGGER_LOCATION は、Eventarc トリガーのロケーションに置き換えます。一般に、Eventarc トリガーのロケーションは、イベントをモニタリングする Google Cloud リソースのロケーションと一致している必要があります。ほとんどの場合に、関数を同じリージョンにデプロイすることも必要です。詳細については、Eventarc のロケーションをご覧ください。

    • FUNCTION: デプロイする関数の名前。

    • REGION: 関数の Cloud Run リージョン

    • PROJECT_NUMBER: Google Cloud プロジェクト番号。Eventarc トリガーはサービス アカウントにリンクされ、関数を呼び出すときに ID として使用します。Eventarc トリガーのサービス アカウントには、関数を呼び出す権限が必要です。デフォルトでは、Cloud Run はデフォルトのコンピューティング サービス アカウントを使用します。

    event-filters フラグはイベントのタイプを指定します。関数は、イベントが event-filters フラグで指定されたすべての条件を満たす場合にのみトリガーされます。各トリガーには、Firestore に書き込まれた新しいドキュメントや Cloud Storage にアップロードされたファイルなど、サポートされているイベントタイプを指定する event-filters フラグが必要です。作成後にイベント フィルタのタイプを変更することはできません。イベント フィルタの種類を変更するには、新しいトリガーを作成して古いトリガーを削除する必要があります。(省略可)さらにフィルタを追加するには、--event-filters フラグを繰り返し、サポートされているフィルタを ATTRIBUTE=VALUE の形式で指定します。

Terraform

Cloud Run 関数の Eventarc トリガーを作成するには、Terraform を使用してトリガーを作成するをご覧ください。

次の例では、Firestore ネイティブ モードのイベントを使用して Cloud Run 関数をトリガーする方法について説明します。

例 1: Hello Firestore 関数

次のサンプルは、トリガーされた Firestore イベントのフィールドを出力します。

Node.js

/**
 * Cloud Event Function triggered by a change to a Firestore document.
 */
const functions = require('@google-cloud/functions-framework');
const protobuf = require('protobufjs');

functions.cloudEvent('helloFirestore', async cloudEvent => {
  console.log(`Function triggered by event on: ${cloudEvent.source}`);
  console.log(`Event type: ${cloudEvent.type}`);

  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  console.log('\nOld value:');
  console.log(JSON.stringify(firestoreReceived.oldValue, null, 2));

  console.log('\nNew value:');
  console.log(JSON.stringify(firestoreReceived.value, null, 2));
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.events.cloud import firestore


@functions_framework.cloud_event
def hello_firestore(cloud_event: CloudEvent) -> None:
    """Triggers by a change to a Firestore document.

    Args:
        cloud_event: cloud event with information on the firestore event trigger
    """
    firestore_payload = firestore.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    print(f"Function triggered by change to: {cloud_event['source']}")

    print("\nOld value:")
    print(firestore_payload.old_value)

    print("\nNew value:")
    print(firestore_payload.value)

Go


// Package hellofirestore contains a Cloud Event Function triggered by a Cloud Firestore event.
package hellofirestore

import (
	"context"
	"fmt"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

func init() {
	functions.CloudEvent("helloFirestore", HelloFirestore)
}

// HelloFirestore is triggered by a change to a Firestore document.
func HelloFirestore(ctx context.Context, event event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(event.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	fmt.Printf("Function triggered by change to: %v\n", event.Source())
	fmt.Printf("Old value: %+v\n", data.GetOldValue())
	fmt.Printf("New value: %+v\n", data.GetValue())
	return nil
}

Java

import com.google.cloud.functions.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.logging.Logger;

public class FirebaseFirestore implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestore.class.getName());

  @Override
  public void accept(CloudEvent event) throws InvalidProtocolBufferException {
    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    logger.info("Function triggered by event on: " + event.getSource());
    logger.info("Event type: " + event.getType());

    logger.info("Old value:");
    logger.info(firestoreEventData.getOldValue().toString());

    logger.info("New value:");
    logger.info(firestoreEventData.getValue().toString());
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FirebaseFirestore;

public class Function : ICloudEventFunction<DocumentEventData>
{
    private readonly ILogger _logger;

    public Function(ILogger<Function> logger) =>
        _logger = logger;

    public Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Function triggered by event on {subject}", cloudEvent.Subject);
        _logger.LogInformation("Event type: {type}", cloudEvent.Type);
        MaybeLogDocument("Old value", data.OldValue);
        MaybeLogDocument("New value", data.Value);

        // In this example, we don't need to perform any asynchronous operations, so the
        // method doesn't need to be declared async.
        return Task.CompletedTask;
    }

    /// <summary>
    /// Logs the names and values of the fields in a document in a very simplistic way.
    /// </summary>
    private void MaybeLogDocument(string message, Document document)
    {
        if (document is null)
        {
            return;
        }

        // ConvertFields converts the Firestore representation into a .NET-friendly
        // representation.
        IReadOnlyDictionary<string, object> fields = document.ConvertFields();
        var fieldNamesAndTypes = fields
            .OrderBy(pair => pair.Key)
            .Select(pair => $"{pair.Key}: {pair.Value}");
        _logger.LogInformation(message + ": {fields}", string.Join(", ", fieldNamesAndTypes));
    }
}

関数をデプロイする

Hello Firestore 関数をデプロイするには、次のコマンドを実行します。

Firestore データベースをまだ設定していない場合は、設定します。

関数をデプロイするには、関数のトリガーを作成するをご覧ください。

関数をテストする

Hello Firestore 関数をテストするには、Firestore データベースusers というコレクションを設定します。

  1. Google Cloud コンソールで、[Firestore データベース] ページに移動します。

    Firestore に移動

  2. [コレクションを開始] をクリックします。

  3. コレクション ID として users を指定します。

  4. コレクションの最初のドキュメントの追加を開始するには、[最初のドキュメントの追加] で、自動生成されたドキュメント ID を使用します。

  5. ドキュメントに少なくとも 1 つのフィールドを追加し、名前と値を指定します。たとえば、[フィールド名] に「username」、[フィールド値] に「rowan」と入力します。

  6. 完了したら [保存] をクリックします。

    この操作により新しいドキュメントが作成され、関数がトリガーされます。

  7. 関数がトリガーされたことを確認するには、 Google Cloud コンソールの Cloud Run の概要ページで関数のリンク名をクリックして、[サービスの詳細] ページを開きます。

  8. [ログ] タブを選択して、次の文字列を探します。

Function triggered by change to: //firestore.googleapis.com/projects/your-project-id/databases/(default)'

例 2: Convert to Uppercase 関数

次の例では、ユーザーが追加した値を取得し、その場所にある文字列を大文字に変換して、値を大文字の文字列に置き換えています。

Node.js

protobufjs を使用して、イベントデータをデコードします。ソースに google.events.cloud.firestore.v1 data.proto を配置します。

const functions = require('@google-cloud/functions-framework');
const Firestore = require('@google-cloud/firestore');
const protobuf = require('protobufjs');

const firestore = new Firestore({
  projectId: process.env.GOOGLE_CLOUD_PROJECT,
});

// Converts strings added to /messages/{pushId}/original to uppercase
functions.cloudEvent('makeUpperCase', async cloudEvent => {
  console.log('Loading protos...');
  const root = await protobuf.load('data.proto');
  const DocumentEventData = root.lookupType(
    'google.events.cloud.firestore.v1.DocumentEventData'
  );

  console.log('Decoding data...');
  const firestoreReceived = DocumentEventData.decode(cloudEvent.data);

  const resource = firestoreReceived.value.name;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

  const curValue = firestoreReceived.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();

  if (curValue === newValue) {
    // Value is already upper-case
    // Don't perform a(nother) write to avoid infinite loops
    console.log('Value is already upper-case.');
    return;
  }

  console.log(`Replacing value: ${curValue} --> ${newValue}`);
  affectedDoc.set({
    original: newValue,
  });
});

Python

from cloudevents.http import CloudEvent
import functions_framework
from google.cloud import firestore
from google.events.cloud import firestore as firestoredata

client = firestore.Client()


# Converts strings added to /messages/{pushId}/original to uppercase
@functions_framework.cloud_event
def make_upper_case(cloud_event: CloudEvent) -> None:
    firestore_payload = firestoredata.DocumentEventData()
    firestore_payload._pb.ParseFromString(cloud_event.data)

    path_parts = firestore_payload.value.name.split("/")
    separator_idx = path_parts.index("documents")
    collection_path = path_parts[separator_idx + 1]
    document_path = "/".join(path_parts[(separator_idx + 2) :])

    print(f"Collection path: {collection_path}")
    print(f"Document path: {document_path}")

    affected_doc = client.collection(collection_path).document(document_path)

    cur_value = firestore_payload.value.fields["original"].string_value
    new_value = cur_value.upper()

    if cur_value != new_value:
        print(f"Replacing value: {cur_value} --> {new_value}")
        affected_doc.set({"original": new_value})
    else:
        # Value is already upper-case
        # Don't perform a second write (which can trigger an infinite loop)
        print("Value is already upper-case.")

Go


// Package upper contains a Firestore Cloud Function.
package upper

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go/v4"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
	"github.com/cloudevents/sdk-go/v2/event"
	"github.com/googleapis/google-cloudevents-go/cloud/firestoredata"
	"google.golang.org/protobuf/proto"
)

// set the GOOGLE_CLOUD_PROJECT environment variable when deploying.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a Firestore client, reused between function invocations.
var client *firestore.Client

func init() {
	// Use the application default credentials.
	conf := &firebase.Config{ProjectID: projectID}

	// Use context.Background() because the app/client should persist across
	// invocations.
	ctx := context.Background()

	app, err := firebase.NewApp(ctx, conf)
	if err != nil {
		log.Fatalf("firebase.NewApp: %v", err)
	}

	client, err = app.Firestore(ctx)
	if err != nil {
		log.Fatalf("app.Firestore: %v", err)
	}

	// Register cloud event function
	functions.CloudEvent("MakeUpperCase", MakeUpperCase)
}

// MakeUpperCase is triggered by a change to a Firestore document. It updates
// the `original` value of the document to upper case.
func MakeUpperCase(ctx context.Context, e event.Event) error {
	var data firestoredata.DocumentEventData

	// If you omit `DiscardUnknown`, protojson.Unmarshal returns an error
	// when encountering a new or unknown field.
	options := proto.UnmarshalOptions{
		DiscardUnknown: true,
	}
	err := options.Unmarshal(e.Data(), &data)

	if err != nil {
		return fmt.Errorf("proto.Unmarshal: %w", err)
	}

	if data.GetValue() == nil {
		return errors.New("Invalid message: 'Value' not present")
	}

	fullPath := strings.Split(data.GetValue().GetName(), "/documents/")[1]
	pathParts := strings.Split(fullPath, "/")
	collection := pathParts[0]
	doc := strings.Join(pathParts[1:], "/")

	var originalStringValue string
	if v, ok := data.GetValue().GetFields()["original"]; ok {
		originalStringValue = v.GetStringValue()
	} else {
		return errors.New("Document did not contain field \"original\"")
	}

	newValue := strings.ToUpper(originalStringValue)
	if originalStringValue == newValue {
		log.Printf("%q is already upper case: skipping", originalStringValue)
		return nil
	}
	log.Printf("Replacing value: %q -> %q", originalStringValue, newValue)

	newDocumentEntry := map[string]string{"original": newValue}
	_, err = client.Collection(collection).Doc(doc).Set(ctx, newDocumentEntry)
	if err != nil {
		return fmt.Errorf("Set: %w", err)
	}
	return nil
}

Java

import com.google.cloud.firestore.Firestore;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.SetOptions;
import com.google.cloud.functions.CloudEventsFunction;
import com.google.events.cloud.firestore.v1.DocumentEventData;
import com.google.events.cloud.firestore.v1.Value;
import com.google.protobuf.InvalidProtocolBufferException;
import io.cloudevents.CloudEvent;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.logging.Logger;

public class FirebaseFirestoreReactive implements CloudEventsFunction {
  private static final Logger logger = Logger.getLogger(FirebaseFirestoreReactive.class.getName());
  private final Firestore firestore;

  private static final String FIELD_KEY = "original";
  private static final String APPLICATION_PROTOBUF = "application/protobuf";

  public FirebaseFirestoreReactive() {
    this(FirestoreOptions.getDefaultInstance().getService());
  }

  public FirebaseFirestoreReactive(Firestore firestore) {
    this.firestore = firestore;
  }

  @Override
  public void accept(CloudEvent event)
      throws InvalidProtocolBufferException, InterruptedException, ExecutionException {
    if (event.getData() == null) {
      logger.warning("No data found in event!");
      return;
    }

    if (!event.getDataContentType().equals(APPLICATION_PROTOBUF)) {
      logger.warning(String.format("Found unexpected content type %s, expected %s",
          event.getDataContentType(),
          APPLICATION_PROTOBUF));
      return;
    }

    DocumentEventData firestoreEventData = DocumentEventData
        .parseFrom(event.getData().toBytes());

    // Get the fields from the post-operation document snapshot
    // https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents#Document
    Map<String, Value> fields = firestoreEventData.getValue().getFieldsMap();
    if (!fields.containsKey(FIELD_KEY)) {
      logger.warning("Document does not contain original field");
      return;
    }
    String currValue = fields.get(FIELD_KEY).getStringValue();
    String newValue = currValue.toUpperCase();

    if (currValue.equals(newValue)) {
      logger.info("Value is already upper-case");
      return;
    }

    // Retrieve the document name from the resource path:
    // projects/{project_id}/databases/{database_id}/documents/{document_path}
    String affectedDoc = firestoreEventData.getValue()
        .getName()
        .split("/documents/")[1]
        .replace("\"", "");

    logger.info(String.format("Replacing values: %s --> %s", currValue, newValue));

    // Wait for the async call to complete
    this.firestore
        .document(affectedDoc)
        .set(Map.of(FIELD_KEY, newValue), SetOptions.merge())
        .get();
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Firestore;
using Google.Cloud.Functions.Framework;
using Google.Cloud.Functions.Hosting;
using Google.Events.Protobuf.Cloud.Firestore.V1;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace FirestoreReactive;

public class Startup : FunctionsStartup
{
    public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) =>
        services.AddSingleton(FirestoreDb.Create());
}

// Register the startup class to provide the Firestore dependency.
[FunctionsStartup(typeof(Startup))]
public class Function : ICloudEventFunction<DocumentEventData>
{
    private readonly ILogger _logger;
    private readonly FirestoreDb _firestoreDb;

    public Function(ILogger<Function> logger, FirestoreDb firestoreDb) =>
        (_logger, _firestoreDb) = (logger, firestoreDb);

    public async Task HandleAsync(CloudEvent cloudEvent, DocumentEventData data, CancellationToken cancellationToken)
    {
        // Get the recently-written value. This expression will result in a null value
        // if any of the following is true:
        // - The event doesn't contain a "new" document
        // - The value doesn't contain a field called "original"
        // - The "original" field isn't a string
        string currentValue = data.Value?.ConvertFields().GetValueOrDefault("original") as string;
        if (currentValue is null)
        {
            _logger.LogWarning($"Event did not contain a suitable document");
            return;
        }

        string newValue = currentValue.ToUpperInvariant();
        if (newValue == currentValue)
        {
            _logger.LogInformation("Value is already upper-cased; no replacement necessary");
            return;
        }

        // The CloudEvent subject is "documents/x/y/...".
        // The Firestore SDK FirestoreDb.Document method expects a reference relative to
        // "documents" (so just the "x/y/..." part). This may be simplified over time.
        if (cloudEvent.Subject is null || !cloudEvent.Subject.StartsWith("documents/"))
        {
            _logger.LogWarning("CloudEvent subject is not a document reference.");
            return;
        }
        string documentPath = cloudEvent.Subject.Substring("documents/".Length);

        _logger.LogInformation("Replacing '{current}' with '{new}' in '{path}'", currentValue, newValue, documentPath);
        await _firestoreDb.Document(documentPath).UpdateAsync("original", newValue, cancellationToken: cancellationToken);
    }
}

関数をデプロイする

Convert to Uppercase 関数をデプロイするには、次のコマンドを実行します。

Firestore データベースをまだ設定していない場合は、設定します。

関数をデプロイするには、関数のトリガーを作成するをご覧ください。

関数をテストする

デプロイした Convert to Uppercase 関数をテストするには、Firestore データベースmessages というコレクションを設定します。

  1. Google Cloud コンソールで、[Firestore データベース] ページに移動します。

    Firestore に移動

  2. [コレクションを開始] をクリックします。

  3. コレクション ID として messages を指定します。

  4. コレクションの最初のドキュメントの追加を開始するには、[最初のドキュメントの追加] で、自動生成されたドキュメント ID を使用します。

  5. デプロイされた関数をトリガーするには、フィールド名original で、フィールド値minka のドキュメントを追加します。

  6. ドキュメントを保存すると、値フィールドの小文字の単語が大文字に変換されます。

    その後、フィールド値を編集して小文字を含めると、関数が再度トリガーされ、すべての小文字が大文字に変換されます。

関数の制限事項

  • 順序は保証されません。短時間に複数の変更を行うと、予期しない順序で関数の呼び出しがトリガーされることがあります。
  • イベントは必ず 1 回以上処理されますが、1 つのイベントで関数が複数回呼び出される場合があります。「正確に 1 回」のメカニズムに依存することは避け、べき等になるように関数を記述してください。
  • トリガーは、単一のデータベースに関連付けられます。複数のデータベースに一致するトリガーは作成できません。
  • データベースを削除しても、そのデータベースのトリガーは自動的に削除されません。トリガーはイベントの配信を停止しますが、トリガーを削除するまで存在し続けます。