Cloud Run mit Ereignistriggern mithilfe von Cloud Run Functions erweitern

Mit Cloud Run-Funktionen können Sie Code bereitstellen, um Ereignisse zu verarbeiten, die durch Änderungen in Ihrer Cloud Run-Datenbank ausgelöst werden. So können Sie serverseitige Funktionen hinzufügen, ohne Ihre eigenen Server zu betreiben.

In diesem Leitfaden wird beschrieben, wie Sie Trigger für Cloud Run Functions aus Firestore-Ereignissen erstellen.

Sie können Ihre Cloud Run-Funktionen durch Ereignisse in einer Firestore-Datenbank auslösen. Nach der Auslösung kann Ihre Funktion Als Reaktion auf diese Ereignisse eine Firestore-Datenbank lesen und aktualisieren und zwar durch die Firestore APIs und client-Bibliotheken.

Der Prozess, bei dem Firestore-Ereignisse eine Cloud Run-Funktion auslösen, besteht aus den folgenden Schritten:

  1. Der Dienst wartet auf Änderungen an einem bestimmten Dokument.

  2. Wenn eine Änderung eintritt, wird der Dienst ausgelöst und führt seine Aufgaben aus.

  3. Der Dienst empfängt ein Datenobjekt mit einem Snapshot des betreffenden Dokuments. Für write- oder update-Ereignisse enthält das Datenobjekt Snapshots, die den Dokumentstatus vor und nach dem auslösenden Ereignis darstellen.

Hinweise

  1. Prüfen Sie, ob Sie ein neues Projekt für Cloud Run eingerichtet haben, wie auf der Seite Einrichtung beschrieben.
  2. Aktivieren Sie die Artifact Registry API, die Cloud Build API, die Cloud Run Admin API, die Eventarc API, die Firestore Cloud Logging API und die Pub/Sub API:

    APIs aktivieren

Erforderliche Rollen

Sie oder Ihr Administrator müssen dem Bereitstellerkonto und der Triggeridentität Berechtigungen zuweisen. Weisen Sie dem Pub/Sub-Dienst-Agent optional die folgenden IAM-Rollen zu.

Erforderliche Rollen für das Bereitstellerkonto

Bitten Sie Ihren Administrator, Ihnen die folgenden IAM-Rollen für Ihr Projekt zuzuweisen, um die Berechtigungen zu erhalten, die Sie zum Auslösen von Firestore-Ereignissen benötigen:

Weitere Informationen zum Zuweisen von Rollen finden Sie unter Zugriff auf Projekte, Ordner und Organisationen verwalten.

Sie können die erforderlichen Berechtigungen auch über benutzerdefinierte Rollen oder andere vordefinierte Rollen erhalten.

Beachten Sie, dass Cloud Build-Berechtigungen standardmäßig Berechtigungen zum Hochladen und Herunterladen von Artifact Registry-Artefakten enthalten.

Erforderliche Rollen für die Triggeridentität

  1. Notieren Sie sich das Compute Engine Standarddienstkonto, das Sie an einen Eventarc-Trigger anhängen, um die Identität des Triggers zu Testzwecken darzustellen. Dieses Dienstkonto wird automatisch nach der Aktivierung oder Verwendung eines Google Cloud -Dienstes, der Compute Engine verwendet, mit dem folgenden E-Mail-Format erstellt:

    PROJECT_NUMBER-compute@developer.gserviceaccount.com

    Ersetzen Sie PROJECT_NUMBER durch die Projektnummer Ihres Google Cloud-Projekts. Sie finden Ihre Projektnummer auf der Willkommensseite der Google Cloud -Konsole oder durch Ausführen des folgenden Befehls:

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

    Für Produktionsumgebungen empfehlen wir dringend, ein neues Dienstkonto zu erstellen und ihm eine oder mehrere IAM-Rollen zuzuweisen, die die erforderlichen Mindestberechtigungen enthalten und dem Grundsatz der geringsten Berechtigung folgen.

  2. Standardmäßig können Cloud Run-Dienste nur von Nutzern mit der Rolle „Project Owner“, „Project Editor“, „Cloud Run Admin“ oder „Cloud Run Invoker“ aufgerufen werden. Sie können den Zugriff für einzelne Dienste steuern. Weisen Sie jedoch zu Testzwecken dem Compute Engine-Dienstkonto die Rolle „Cloud Run-Aufrufer” (run.invoker) für das Projekt Google Cloud zu. Dadurch wird die Rolle für alle Cloud Run-Dienste und -Jobs in einem Projekt zugewiesen.
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/run.invoker

    Wenn Sie einen Trigger für einen authentifizierten Cloud Run-Dienst erstellen, ohne die Rolle "Cloud Run Invoker" zuzuweisen, wird der Trigger erfolgreich erstellt und ist aktiv. Der Trigger funktioniert jedoch nicht wie erwartet und in den Logs wird eine Meldung wie die folgende angezeigt:

    The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
  3. Weisen Sie dem Compute Engine-Standarddienstkonto die Rolle „Eventarc-Ereignisempfänger“ (roles/eventarc.eventReceiver) für das Projekt zu, damit der Eventarc-Trigger Ereignisse vom Ereignisanbieter empfangen kann.
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com \
        --role=roles/eventarc.eventReceiver

Optionale Rolle für den Pub/Sub-Dienst-Agent

  • Wenn Sie den Cloud Pub/Sub-Dienst-Agent am oder vor dem 8. April 2021 aktiviert haben, um authentifizierte Pub/Sub-Push-Anfragen zu unterstützen, weisen Sie dem von Google verwalteten Dienstkonto die Rolle „Ersteller von Dienstkonto-Tokens“ (roles/iam.serviceAccountTokenCreator) zu. Andernfalls wird diese Rolle standardmäßig zugewiesen:
    gcloud projects add-iam-policy-binding PROJECT_ID \
        --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
        --role=roles/iam.serviceAccountTokenCreator

Firestore-Datenbank einrichten

Bevor Sie Ihren Dienst bereitstellen, müssen Sie eine Firestore-Datenbank erstellen:

  1. Zur Seite „Firestore“

  2. Wählen Sie Firestore-Datenbank erstellen aus.

  3. Geben Sie im Feld Datenbank benennen eine Datenbank-ID ein, z. B. firestore-db.

  4. Im Abschnitt Konfigurationsoptionen ist Firestore-nativ standardmäßig zusammen mit den entsprechenden Sicherheitsregeln ausgewählt.

  5. Wählen Sie unter Standorttyp die Option Region und dann die Region aus, in der sich Ihre Datenbank befinden soll. Diese Entscheidung ist endgültig.

  6. Klicken Sie auf Datenbank erstellen.

Das Firestore-Datenmodell besteht aus Sammlungen, die Dokumente enthalten. Jedes Dokument enthält eine Reihe von Schlüssel/Wert-Paaren.

Durch Firestore ausgelöste Funktion schreiben

Wenn Sie eine Funktion schreiben möchten, die auf Firestore-Ereignisse reagiert, müssen Sie bei der Bereitstellung Folgendes angeben:

Ereignistypen

Firestore unterstützt Ereignisse vom Typ create, update, delete und write. Das write-Ereignis umfasst alle Änderungen eines Dokuments.

Ereignistyp Trigger
google.cloud.firestore.document.v1.created (Standard) Wird ausgelöst, wenn ein Dokument zum ersten Mal beschrieben wird.
google.cloud.firestore.document.v1.updated Wird ausgelöst, wenn ein Dokument bereits existiert und sich ein Wert geändert hat.
google.cloud.firestore.document.v1.deleted Wird ausgelöst, wenn ein Dokument mit Daten gelöscht wird.
google.cloud.firestore.document.v1.written Wird ausgelöst, wenn ein Dokument erstellt, aktualisiert oder gelöscht wird.
google.cloud.firestore.document.v1.created.withAuthContext Wie created, aber mit Authentifizierungsinformationen.
google.cloud.firestore.document.v1.updated.withAuthContext Wie updated, aber mit Authentifizierungsinformationen.
google.cloud.firestore.document.v1.deleted.withAuthContext Wie deleted, aber mit Authentifizierungsinformationen.
google.cloud.firestore.document.v1.written.withAuthContext Wie written, aber mit Authentifizierungsinformationen.

Platzhalter werden in Triggern in geschweiften Klammern dargestellt, z. B.: projects/YOUR_PROJECT_ID/databases/(default)/documents/collection/{document_wildcard}

Triggerereignisfilter

Wenn Sie einen Dienst auslösen möchten, müssen Sie den Dokumentpfad angeben, der überwacht werden soll. Der Dokumentpfad muss sich im selben Google Cloud Projekt wie der Dienst befinden.

Hier sehen Sie ein paar Beispiele gültiger Dokumentpfade:

  • users/marie: Überwacht ein einzelnes Dokument, /users/marie.

  • users/{username}: Überwacht alle Nutzerdokumente. Bei Angabe von Platzhaltern werden alle Dokumente in der Sammlung überwacht.

  • users/{username}/addresses/home: Überwacht das Privatadressdokument für alle Nutzer.

  • users/{username}/addresses/{addressId}: Überwacht alle Adressdokumente.

  • users/{user=**}: Überwacht alle Nutzerdokumente und alle Dokumente in Unterkollektionen unter jedem Nutzerdokument, z. B. /users/userID/address/home oder /users/userID/phone/work.

  • users/{username}/addresses: Ungültiger Adresspfad. Bezieht sich auf die untergeordnete Sammlung addresses und nicht auf ein Dokument.

Platzhalter und Parameter

Wenn Sie das Dokument, das überwacht werden soll, nicht kennen, verwenden Sie {wildcard} anstelle der Dokument-ID:

  • users/{username} wartet auf Änderungen für alle Nutzerdokumente.

Wenn in diesem Beispiel ein Feld in einem Dokument im Verzeichnis users geändert wird, entspricht es einem Platzhalter namens {username}.

Wenn ein Dokument in users untergeordnete Sammlungen enthält und ein Feld in einem Dokument dieser Sammlungen geändert wird, wird der Platzhalter {username} nicht ausgelöst. Wenn Sie auf Ereignisse in Untersammlungen reagieren möchten, verwenden Sie den Multi-Segment-Platzhalter {username=**}.

Platzhalterübereinstimmungen werden aus Dokumentpfaden extrahiert. Sie können beliebig viele Platzhalter für explizite Sammlungs- oder Dokument-IDs festlegen. Sie können bis zu einen Platzhalter mit mehreren Segmenten wie {username=**} verwenden.

Funktionscode

Beispiele für die Verwendung von Firestore im nativen Modus-Ereignissen zum Auslösen einer Cloud Run-Funktion

Proto-Abhängigkeiten in die Quelle einfügen

Sie müssen die Datei Cloud Run data.proto im Quellverzeichnis für Ihre Funktion einfügen. In dieser Datei werden die folgenden Protos importiert, die Sie auch in Ihr Quellverzeichnis aufnehmen müssen:

Verwenden Sie für die Abhängigkeiten dieselbe Verzeichnisstruktur. Platzieren Sie beispielsweise struct.proto in google/protobuf.

Diese Dateien sind erforderlich, um Ereignisdaten zu decodieren. Wenn die Funktionsquelle diese Dateien nicht enthält, wird bei der Ausführung ein Fehler zurückgegeben.

Ereignisattribute

Jedes Ereignis enthält Datenattribute mit Informationen zum Ereignis, z. B. zum Zeitpunkt, zu dem es ausgelöst wurde. Cloud Run fügt zusätzliche Daten zur Datenbank und zum Dokument hinzu, die am Ereignis beteiligt sind. So greifen Sie auf diese Attribute zu:

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']}")

Ereignisstrukturen

Dieser Trigger ruft Ihren Dienst mit einem Ereignis wie dem folgenden auf:

{
    "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
    }
}

Jedes Document-Objekt enthält ein oder mehrere Value-Objekte. Informationen zu Typreferenzen finden Sie in der Dokumentation zu Value.

Trigger für Funktionen erstellen

Klicken Sie auf den Tab, um eine Anleitung zum gewünschten Tool zu erhalten.

Console

Wenn Sie die Google Cloud Console zum Erstellen einer Funktion verwenden, können Sie Ihrer Funktion auch einen Trigger hinzufügen. So erstellen Sie einen Trigger für Ihre Funktion:

  1. Rufen Sie in der Google Cloud Console Cloud Run auf:

    Zu Cloud Run

  2. Klicken Sie auf Funktion schreiben und geben Sie die Funktionsdetails ein. Weitere Informationen zum Konfigurieren von Funktionen während der Bereitstellung finden Sie unter Funktionen bereitstellen.

  3. Klicken Sie im Bereich Trigger auf Trigger hinzufügen.

  4. Wählen Sie Firestore-Trigger aus.

  5. Ändern Sie im Bereich Eventarc-Trigger die Triggerdetails so:

    1. Geben Sie im Feld Triggername einen Namen für den Trigger ein oder verwenden Sie den Standardnamen.

    2. Wählen Sie einen Triggertyp aus der Liste aus:

      • Google-Quellen, um Trigger für Pub/Sub, Cloud Storage, Firestore und andere Google-Ereignisanbieter anzugeben.

      • Drittanbieter, um eine Einbindung in Nicht-Google-Anbieter vorzunehmen, die eine Eventarc-Quelle anbieten. Weitere Informationen finden Sie unter Drittanbieterereignisse in Eventarc.

    3. Wählen Sie in der Liste Ereignisanbieter die Option Firestore aus, um ein Produkt auszuwählen, das den Ereignistyp für das Auslösen Ihrer Funktion bereitstellt. Eine Liste der Ereignisanbieter finden Sie unter Ereignisanbieter und -ziele.

    4. Wählen Sie in der Liste Ereignistyp die Option type=google.cloud.firestore.document.v1.created aus. Die Triggerkonfiguration variiert je nach unterstütztem Ereignistyp. Weitere Informationen finden Sie unter Ereignistypen.

    5. Wählen Sie im Bereich „Filter“ eine Datenbank, einen Vorgang und Attributwerte aus oder verwenden Sie die Standardauswahl.

    6. Wenn das Feld Region aktiviert ist, wählen Sie einen Standort für den Eventarc-Trigger aus. Im Allgemeinen sollte der Standort eines Eventarc-Triggers mit dem Standort der Google Cloud -Ressource übereinstimmen, die Sie auf Ereignisse überwachen möchten. In den meisten Szenarien sollten Sie Ihre Funktion auch in derselben Region bereitstellen. Weitere Informationen zu Eventarc-Triggerstandorten finden Sie unter Informationen zu Eventarc-Standorten.

    7. Wählen Sie im Feld Dienstkonto ein Dienstkonto aus. Eventarc-Trigger sind mit Dienstkonten verknüpft, um sie beim Aufrufen Ihrer Funktion als Identität zu verwenden. Das Dienstkonto Ihres Eventarc-Triggers muss die Berechtigung zum Aufrufen Ihrer Funktion haben. Standardmäßig verwendet Cloud Run das Compute Engine-Standarddienstkonto.

    8. Geben Sie optional den Dienst-URL-Pfad an, an den die eingehende Anfrage gesendet werden soll. Dies ist der relative Pfad im Zieldienst, an den die Ereignisse für den Trigger gesendet werden sollen. Beispiel: /, /route, route und route/subroute.

  6. Wenn Sie alle erforderlichen Felder ausgefüllt haben, klicken Sie auf Trigger speichern.

gcloud

Wenn Sie eine Funktion mit der gcloud CLI erstellen, müssen Sie sie zuerst bereitstellen und dann einen Trigger erstellen. So erstellen Sie einen Trigger für Ihre Funktion:

  1. Führen Sie den folgenden Befehl in dem Verzeichnis aus, das den Beispielcode enthält, um die Funktion bereitzustellen:

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

    Ersetzen Sie:

    • FUNCTION durch den Namen der Funktion, die Sie bereitstellen. Sie können diesen Parameter auch weglassen, werden dann jedoch nach dem Namen gefragt.

    • FUNCTION_ENTRYPOINT durch den Einstiegspunkt zur Funktion in Ihrem Quellcode. Dies ist der Code, der von Cloud Run ausgeführt wird, wenn Ihre Funktion ausgeführt wird. Der Wert dieses Flags muss ein Funktionsname oder ein voll qualifizierter Klassenname sein, der in Ihrem Quellcode vorhanden ist.

    • BASE_IMAGE_ID durch die Basis-Image-Umgebung für Ihre Funktion. Weitere Informationen zu Basis-Images und den in den einzelnen Images enthaltenen Paketen finden Sie unter Laufzeit-Basis-Images.

    • REGION durch die Google Cloud Region, in der Sie die Funktion bereitstellen möchten. Beispiel: europe-west1

  2. Führen Sie den folgenden Befehl aus, um einen Trigger zu erstellen, der Ereignisse filtert:

    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
    

    Ersetzen Sie:

    • Ersetzen Sie TRIGGER_NAME durch den Namen des Triggers.

    • EVENTARC_TRIGGER_LOCATION mit dem Standort für den Eventarc-Trigger. Im Allgemeinen sollte der Standort eines Eventarc-Triggers mit dem Standort der Google Cloud -Ressource übereinstimmen, die Sie auf Ereignisse überwachen möchten. In den meisten Szenarien sollten Sie Ihre Funktion auch in derselben Region bereitstellen. Weitere Informationen finden Sie unter Eventarc-Standorte.

    • FUNCTION durch den Namen der Funktion, die Sie bereitstellen.

    • REGION durch die Cloud Run-Region der Funktion.

    • PROJECT_NUMBER durch Ihre Google Cloud Projektnummer. Eventarc-Trigger sind mit Dienstkonten verknüpft, um sie beim Aufrufen Ihrer Funktion als Identität zu verwenden. Das Dienstkonto Ihres Eventarc-Triggers muss die Berechtigung zum Aufrufen Ihrer Funktion haben. Standardmäßig verwendet Cloud Run das Compute-Standarddienstkonto.

    Jedes event-filters-Flag gibt einen Ereignistyp an. Die Funktion wird nur ausgelöst, wenn ein Ereignis alle in den event-filters-Flags angegebenen Kriterien erfüllt. Jeder Trigger muss ein event-filters-Flag haben, das einen unterstützten Ereignistyp angibt, z. B. ein neues Dokument, das in Firestore geschrieben wurde, oder eine Datei, die in Cloud Storage hochgeladen wurde. Nachdem Sie einen Trigger erstellt haben, können Sie den Ereignisfiltertyp nicht mehr ändern. Wenn Sie den Ereignisfiltertyp ändern möchten, müssen Sie einen neuen Trigger erstellen und den alten löschen. Optional können Sie das Flag --event-filters mit einem unterstützten Filter im Format ATTRIBUTE=VALUE wiederholen, um weitere Filter hinzuzufügen.

Terraform

Informationen zum Erstellen eines Eventarc-Triggers für eine Cloud Run-Funktion finden Sie unter Trigger mit Terraform erstellen.

Beispiele

In den folgenden Beispielen wird beschrieben, wie Sie mit Firestore im nativen Modus Ereignisse zum Auslösen einer Cloud Run-Funktion verwenden.

Beispiel 1: Hello Firestore-Funktion

Im folgenden Beispiel werden die Felder eines auslösenden Firestore-Ereignisses ausgegeben:

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));
    }
}

Funktion implementieren

Führen Sie den folgenden Befehl aus, um die Hello Firestore-Funktion bereitzustellen:

Richten Sie Ihre Firestore-Datenbank ein, falls noch nicht geschehen.

Informationen zum Bereitstellen der Funktion finden Sie unter Trigger für Funktionen erstellen.

Funktion testen

Richten Sie eine Sammlung mit dem Namen users in Ihrer Firestore-Datenbank ein, um die Funktion Hello Firestore zu testen:

  1. Rufen Sie in der Google Cloud Console die Seite „Firestore-Datenbanken“ auf:

    Firestore aufrufen

  2. Klicken Sie auf Sammlung starten.

  3. Geben Sie users als Sammlungs-ID an.

  4. Um das erste Dokument der Sammlung hinzuzufügen, akzeptieren Sie unter Erstes Dokument hinzufügen die automatisch generierte Dokument-ID.

  5. Fügen Sie mindestens ein Feld für das Dokument hinzu und geben Sie einen Namen und einen Wert an. Geben Sie beispielsweise in Feldname den Wert username und in Feldwert den Wert rowan ein.

  6. Wenn Sie fertig sind, klicken Sie auf Speichern.

    Dadurch wird ein neues Dokument erstellt und die Funktion wird ausgelöst.

  7. Klicken Sie auf der Cloud Run-Übersichtsseite in der Google Cloud Console auf den verknüpften Namen der Funktion, um die Seite Dienstdetails zu öffnen und zu prüfen, ob die Funktion ausgelöst wurde.

  8. Wählen Sie den Tab Protokolle aus und suchen Sie nach diesem String:

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

Beispiel 2: Funktion "In Großbuchstaben umwandeln"

Im folgenden Beispiel wird der vom Nutzer hinzugefügte Wert abgerufen, der String an dieser Stelle in Großbuchstaben umgewandelt und der Wert durch den String in Großbuchstaben ersetzt:

Node.js

Verwenden Sie protobufjs, um die Ereignisdaten zu decodieren. Fügen Sie google.events.cloud.firestore.v1 data.proto in Ihre Quelle ein.

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);
    }
}

Funktion implementieren

Führen Sie den folgenden Befehl aus, um die Convert to Uppercase-Funktion bereitzustellen:

Richten Sie Ihre Firestore-Datenbank ein, falls noch nicht geschehen.

Informationen zum Bereitstellen der Funktion finden Sie unter Trigger für Funktionen erstellen.

Funktion testen

Richten Sie eine Sammlung mit dem Namen messages in Ihrer Firestore-Datenbank ein, um die gerade bereitgestellte Funktion Convert to Uppercase zu testen:

  1. Rufen Sie in der Google Cloud Console die Seite „Firestore-Datenbanken“ auf:

    Firestore aufrufen

  2. Klicken Sie auf Sammlung starten.

  3. Geben Sie messages als Sammlungs-ID an.

  4. Um das erste Dokument der Sammlung hinzuzufügen, akzeptieren Sie unter Erstes Dokument hinzufügen die automatisch generierte Dokument-ID.

  5. Um die bereitgestellte Funktion auszulösen, fügen Sie ein Dokument mit dem Feldnamen original und dem Feldwert minka hinzu.

  6. Beim Speichern des Dokuments wird das Wort in Kleinbuchstaben im Wertfeld in ein Wort in Großbuchstaben umgewandelt.

    Wenn Sie den Feldwert anschließend so bearbeiten, dass er Kleinbuchstaben enthält, wird die Funktion noch einmal ausgelöst. Dadurch werden alle Kleinbuchstaben in Großbuchstaben umgewandelt.

Einschränkungen für Funktionen

  • Die Reihenfolge ist nicht garantiert. Schnelle Änderungen können Funktionsaufrufe in einer unvorhergesehenen Reihenfolge auslösen.
  • Ereignisse werden mindestens einmal übergeben. Ein einzelnes Ereignis kann aber zu mehreren Funktionsaufrufen führen. Vermeiden Sie die Abhängigkeit von genau einmal vorkommenden Verfahren und schreiben Sie idempotente Funktionen.
  • Ein Trigger ist mit einer einzelnen Datenbank verknüpft. Sie können keinen Trigger erstellen, der mit mehreren Datenbanken übereinstimmt.
  • Wenn Sie eine Datenbank löschen, werden nicht automatisch alle Trigger für diese Datenbank gelöscht. Der Trigger sendet keine Ereignisse mehr, bleibt aber bestehen, bis Sie ihn löschen.