Tutorial: depure eventos de encaminhamento para o Cloud Run

Este tutorial ensina como resolver problemas de erros de tempo de execução encontrados quando usa o Eventarc para encaminhar eventos do Cloud Storage para um serviço do Cloud Run não autenticado através dos registos de auditoria do Google Cloud.

Crie um repositório padrão do Artifact Registry

Crie um repositório padrão do Artifact Registry para armazenar a sua imagem de contentor:

gcloud artifacts repositories create REPOSITORY \
    --repository-format=docker \
    --location=$REGION

Substitua REPOSITORY por um nome exclusivo para o repositório.

Crie um contentor do Cloud Storage

Crie um contentor do Cloud Storage em cada uma das duas regiões como origem do evento para o serviço do Cloud Run:

  1. Crie um contentor em us-east1:

    export BUCKET1="troubleshoot-bucket1-PROJECT_ID"
    gcloud storage buckets create gs://${BUCKET1} --location=us-east1
  2. Crie um contentor em us-west1:

    export BUCKET2="troubleshoot-bucket2-PROJECT_ID"
    gcloud storage buckets create gs://${BUCKET2} --location=us-west1

Depois de criar a origem do evento, implemente o serviço de receção de eventos no Cloud Run.

Implemente o recetor de eventos

Implemente um serviço do Cloud Run que receba e registe eventos.

  1. Obtenha o exemplo de código clonando o repositório do GitHub:

    Ir

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
    cd golang-samples/eventarc/audit_storage
    

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git
    cd java-docs-samples/eventarc/audit-storage
    

    .NET

    git clone https://github.com/GoogleCloudPlatform/dotnet-docs-samples.git
    cd dotnet-docs-samples/eventarc/audit-storage
    

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
    cd nodejs-docs-samples/eventarc/audit-storage
    

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
    cd python-docs-samples/eventarc/audit-storage
    
  2. Reveja o código deste tutorial, que consiste no seguinte:

    • Um controlador de eventos que recebe o evento recebido como um CloudEvent no pedido HTTP POST:

      Ir

      
      // Processes CloudEvents containing Cloud Audit Logs for Cloud Storage
      package main
      
      import (
      	"fmt"
      	"log"
      	"net/http"
      	"os"
      
      	cloudevent "github.com/cloudevents/sdk-go/v2"
      )
      
      // HelloEventsStorage receives and processes a Cloud Audit Log event with Cloud Storage data.
      func HelloEventsStorage(w http.ResponseWriter, r *http.Request) {
      	if r.Method != http.MethodPost {
      		http.Error(w, "Expected HTTP POST request with CloudEvent payload", http.StatusMethodNotAllowed)
      		return
      	}
      
      	event, err := cloudevent.NewEventFromHTTPRequest(r)
      	if err != nil {
      		log.Printf("cloudevent.NewEventFromHTTPRequest: %v", err)
      		http.Error(w, "Failed to create CloudEvent from request.", http.StatusBadRequest)
      		return
      	}
      	s := fmt.Sprintf("Detected change in Cloud Storage bucket: %s", event.Subject())
      	fmt.Fprintln(w, s)
      }
      

      Java

      import io.cloudevents.CloudEvent;
      import io.cloudevents.rw.CloudEventRWException;
      import io.cloudevents.spring.http.CloudEventHttpUtils;
      import org.springframework.http.HttpHeaders;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.ResponseEntity;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestHeader;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class EventController {
      
        @RequestMapping(value = "/", method = RequestMethod.POST, consumes = "application/json")
        public ResponseEntity<String> receiveMessage(
            @RequestBody String body, @RequestHeader HttpHeaders headers) {
          CloudEvent event;
          try {
            event =
                CloudEventHttpUtils.fromHttp(headers)
                    .withData(headers.getContentType().toString(), body.getBytes())
                    .build();
          } catch (CloudEventRWException e) {
            return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
          }
      
          String ceSubject = event.getSubject();
          String msg = "Detected change in Cloud Storage bucket: " + ceSubject;
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.OK);
        }
      }

      .NET

      
      using Microsoft.AspNetCore.Builder;
      using Microsoft.AspNetCore.Hosting;
      using Microsoft.AspNetCore.Http;
      using Microsoft.Extensions.DependencyInjection;
      using Microsoft.Extensions.Hosting;
      using Microsoft.Extensions.Logging;
      
      public class Startup
      {
          public void ConfigureServices(IServiceCollection services)
          {
          }
      
          public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
          {
              if (env.IsDevelopment())
              {
                  app.UseDeveloperExceptionPage();
              }
      
              logger.LogInformation("Service is starting...");
      
              app.UseRouting();
      
              app.UseEndpoints(endpoints =>
              {
                  endpoints.MapPost("/", async context =>
                  {
                      logger.LogInformation("Handling HTTP POST");
      
                      var ceSubject = context.Request.Headers["ce-subject"];
                      logger.LogInformation($"ce-subject: {ceSubject}");
      
                      if (string.IsNullOrEmpty(ceSubject))
                      {
                          context.Response.StatusCode = 400;
                          await context.Response.WriteAsync("Bad Request: expected header Ce-Subject");
                          return;
                      }
      
                      await context.Response.WriteAsync($"GCS CloudEvent type: {ceSubject}");
                  });
              });
          }
      }
      

      Node.js

      const express = require('express');
      const app = express();
      
      app.use(express.json());
      app.post('/', (req, res) => {
        if (!req.header('ce-subject')) {
          return res
            .status(400)
            .send('Bad Request: missing required header: ce-subject');
        }
      
        console.log(
          `Detected change in Cloud Storage bucket: ${req.header('ce-subject')}`
        );
        return res
          .status(200)
          .send(
            `Detected change in Cloud Storage bucket: ${req.header('ce-subject')}`
          );
      });
      
      module.exports = app;

      Python

      @app.route("/", methods=["POST"])
      def index():
          # Create a CloudEvent object from the incoming request
          event = from_http(request.headers, request.data)
          # Gets the GCS bucket name from the CloudEvent
          # Example: "storage.googleapis.com/projects/_/buckets/my-bucket"
          bucket = event.get("subject")
      
          print(f"Detected change in Cloud Storage bucket: {bucket}")
          return (f"Detected change in Cloud Storage bucket: {bucket}", 200)
      
      
    • Um servidor que usa o controlador de eventos:

      Ir

      
      func main() {
      	http.HandleFunc("/", HelloEventsStorage)
      	// Determine port for HTTP service.
      	port := os.Getenv("PORT")
      	if port == "" {
      		port = "8080"
      	}
      	// Start HTTP server.
      	log.Printf("Listening on port %s", port)
      	if err := http.ListenAndServe(":"+port, nil); err != nil {
      		log.Fatal(err)
      	}
      }
      

      Java

      
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      
      @SpringBootApplication
      public class Application {
        public static void main(String[] args) {
          SpringApplication.run(Application.class, args);
        }
      }

      .NET

          public static void Main(string[] args)
          {
              CreateHostBuilder(args).Build().Run();
          }
          public static IHostBuilder CreateHostBuilder(string[] args)
          {
              var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
              var url = $"http://0.0.0.0:{port}";
      
              return Host.CreateDefaultBuilder(args)
                  .ConfigureWebHostDefaults(webBuilder =>
                  {
                      webBuilder.UseStartup<Startup>().UseUrls(url);
                  });
          }
      

      Node.js

      const app = require('./app.js');
      const PORT = parseInt(process.env.PORT) || 8080;
      
      app.listen(PORT, () =>
        console.log(`nodejs-events-storage listening on port ${PORT}`)
      );

      Python

      import os
      
      from cloudevents.http import from_http
      
      from flask import Flask, request
      
      app = Flask(__name__)
      if __name__ == "__main__":
          app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
    • Um Dockerfile que define o ambiente operacional do serviço. O conteúdo do Dockerfile varia consoante o idioma:

      Ir

      
      # Use the official Go image to create a binary.
      # This is based on Debian and sets the GOPATH to /go.
      # https://hub.docker.com/_/golang
      FROM golang:1.24-bookworm as builder
      
      # Create and change to the app directory.
      WORKDIR /app
      
      # Retrieve application dependencies.
      # This allows the container build to reuse cached dependencies.
      # Expecting to copy go.mod and if present go.sum.
      COPY go.* ./
      RUN go mod download
      
      # Copy local code to the container image.
      COPY . ./
      
      # Build the binary.
      RUN go build -v -o server
      
      # Use the official Debian slim image for a lean production container.
      # https://hub.docker.com/_/debian
      # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
      FROM debian:bookworm-slim
      RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
          ca-certificates && \
          rm -rf /var/lib/apt/lists/*
      
      # Copy the binary to the production image from the builder stage.
      COPY --from=builder /app/server /server
      
      # Run the web service on container startup.
      CMD ["/server"]
      

      Java

      
      # Use the official maven image to create a build artifact.
      # https://hub.docker.com/_/maven
      FROM maven:3-eclipse-temurin-17-alpine as builder
      
      # Copy local code to the container image.
      WORKDIR /app
      COPY pom.xml .
      COPY src ./src
      
      # Build a release artifact.
      RUN mvn package -DskipTests
      
      # Use Eclipse Temurin for base image.
      # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
      FROM eclipse-temurin:17.0.16_8-jre-alpine
      
      # Copy the jar to the production image from the builder stage.
      COPY --from=builder /app/target/audit-storage-*.jar /audit-storage.jar
      
      # Run the web service on container startup.
      CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/audit-storage.jar"]
      

      .NET

      
      # Use Microsoft's official build .NET image.
      # https://hub.docker.com/_/microsoft-dotnet-core-sdk/
      FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
      WORKDIR /app
      
      # Install production dependencies.
      # Copy csproj and restore as distinct layers.
      COPY *.csproj ./
      RUN dotnet restore
      
      # Copy local code to the container image.
      COPY . ./
      WORKDIR /app
      
      # Build a release artifact.
      RUN dotnet publish -c Release -o out
      
      
      # Use Microsoft's official runtime .NET image.
      # https://hub.docker.com/_/microsoft-dotnet-core-aspnet/
      FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
      WORKDIR /app
      COPY --from=build /app/out ./
      
      # Run the web service on container startup.
      ENTRYPOINT ["dotnet", "AuditStorage.dll"]

      Node.js

      
      # Use the official lightweight Node.js image.
      # https://hub.docker.com/_/node
      FROM node:20-slim
      # Create and change to the app directory.
      WORKDIR /usr/src/app
      
      # Copy application dependency manifests to the container image.
      # A wildcard is used to ensure both package.json AND package-lock.json are copied.
      # Copying this separately prevents re-running npm install on every code change.
      COPY package*.json ./
      
      # Install dependencies.
      # if you need a deterministic and repeatable build create a 
      # package-lock.json file and use npm ci:
      # RUN npm ci --omit=dev
      # if you need to include development dependencies during development
      # of your application, use:
      # RUN npm install --dev
      
      RUN npm install --omit=dev
      
      # Copy local code to the container image.
      COPY . .
      
      # Run the web service on container startup.
      CMD [ "npm", "start" ]
      

      Python

      
      # Use the official Python image.
      # https://hub.docker.com/_/python
      FROM python:3.11-slim
      
      # Allow statements and log messages to immediately appear in the Cloud Run logs
      ENV PYTHONUNBUFFERED True
      
      # Copy application dependency manifests to the container image.
      # Copying this separately prevents re-running pip install on every code change.
      COPY requirements.txt ./
      
      # Install production dependencies.
      RUN pip install -r requirements.txt
      
      # Copy local code to the container image.
      ENV APP_HOME /app
      WORKDIR $APP_HOME
      COPY . ./
      
      # Run the web service on container startup. 
      # Use gunicorn webserver with one worker process and 8 threads.
      # For environments with multiple CPU cores, increase the number of workers
      # to be equal to the cores available.
      CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

  3. Crie a imagem de contentor com o Cloud Build e carregue a imagem para o Artifact Registry:

    export PROJECT_ID=$(gcloud config get-value project)
    export SERVICE_NAME=troubleshoot-service
    gcloud builds submit --tag $REGION-docker.pkg.dev/${PROJECT_ID}/REPOSITORY/${SERVICE_NAME}:v1
  4. Implemente a imagem do contentor no Cloud Run:

    gcloud run deploy ${SERVICE_NAME} \
        --image $REGION-docker.pkg.dev/${PROJECT_ID}/REPOSITORY/${SERVICE_NAME}:v1 \
        --allow-unauthenticated

    Quando a implementação é bem-sucedida, a linha de comandos apresenta o URL do serviço.

Crie um acionador

Depois de implementar um serviço do Cloud Run, configure um acionador para ouvir eventos do Cloud Storage através de registos de auditoria.

  1. Crie um acionador do Eventarc para ouvir eventos do Cloud Storage encaminhados através dos registos de auditoria do Google Cloud:

    gcloud eventarc triggers create troubleshoot-trigger \
        --destination-run-service=troubleshoot-service \
        --event-filters="type=google.cloud.audit.log.v1.written" \
        --event-filters="serviceName=storage.googleapis.com" \
        --event-filters="methodName=storage.objects.create" \
        --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
    

    Esta ação cria um acionador denominado troubleshoot-trigger.

  2. Para confirmar que troubleshoot-trigger foi criado, execute o seguinte comando:

    gcloud eventarc triggers list
    

    O resultado deve ser semelhante ao seguinte:

    NAME: troubleshoot-trigger
    TYPE: google.cloud.audit.log.v1.written
    DESTINATION: Cloud Run service: troubleshoot-service
    ACTIVE: By 20:03:37
    LOCATION: us-central1
    

Gere e veja um evento

Confirme que implementou o serviço com êxito e que pode receber eventos do Cloud Storage.

  1. Crie e carregue um ficheiro para o contentor de armazenamento BUCKET1:

     echo "Hello World" > random.txt
     gcloud storage cp random.txt gs://${BUCKET1}/random.txt
    
  2. Monitorize os registos para verificar se o serviço recebeu um evento. Para ver a entrada do registo, conclua os seguintes passos:

    1. Filtre as entradas do registo e devolva o resultado no formato JSON:

      gcloud logging read "resource.labels.service_name=troubleshoot-service \
          AND textPayload:random.txt" \
          --format=json
    2. Procure uma entrada de registo semelhante a:

      "textPayload": "Detected change in Cloud Storage bucket: ..."
      

Tenha em atenção que, inicialmente, não é devolvida nenhuma entrada de registo. Isto indica que existe um problema na configuração que tem de investigar.

Investigue o problema

Siga o processo de investigação para saber por que motivo o serviço não está a receber eventos.

Tempo de inicialização

Embora o acionador seja criado imediatamente, pode demorar até dois minutos para que um acionador propague e filtre eventos. Execute o seguinte comando para confirmar que um acionador está ativo:

gcloud eventarc triggers list

O resultado indica o estado do acionador. No exemplo seguinte, troubleshoot-trigger vai estar ativo às 14:16:56:

NAME                  TYPE                               DESTINATION_RUN_SERVICE  ACTIVE
troubleshoot-trigger  google.cloud.audit.log.v1.written  troubleshoot-service     By 14:16:56

Assim que o acionador estiver ativo, carregue novamente um ficheiro para o contentor de armazenamento. Os eventos são escritos nos registos do serviço do Cloud Run. Se o serviço não receber eventos, pode estar relacionado com o tamanho dos eventos.

Registos de auditoria

Neste tutorial, os eventos do Cloud Storage são encaminhados através dos registos de auditoria do Cloud e enviados para o Cloud Run. Confirme que os registos de auditoria estão ativados para o Cloud Storage.

  1. Na Google Cloud consola, aceda à página Registos de auditoria.

    Aceder aos registos de auditoria

  2. Selecione a caixa de verificação Google Cloud Storage.
  3. Certifique-se de que os tipos de registos Admin Read, Data Read e Data Write estão selecionados.

Depois de ativar os registos de auditoria do Cloud, carregue novamente o ficheiro para o contentor de armazenamento e verifique os registos. Se o serviço continuar a não receber eventos, isto pode estar relacionado com a localização do acionador.

Localização do acionador

Podem existir vários recursos em localizações diferentes e tem de filtrar os eventos de origens que se encontram na mesma região que o destino do Cloud Run. Para mais informações, consulte as localizações suportadas pelo Eventarc e compreenda as localizações do Eventarc.

Neste tutorial, implementou o serviço do Cloud Run em us-central1. Como definiu eventarc/location como us-central1, também criou um acionador na mesma localização.

No entanto, criou dois contentores do Cloud Storage nas localizações us-east1 e us-west1. Para receber eventos dessas localizações, tem de criar acionadores do Eventarc nessas localizações.

Crie um acionador do Eventarc localizado em us-east1:

  1. Confirme a localização do acionador existente:

    gcloud eventarc triggers describe troubleshoot-trigger
    
  2. Defina a localização e a região para us-east1:

    gcloud config set eventarc/location us-east1
    gcloud config set run/region us-east1
    
  3. Implemente novamente o recetor de eventos criando e implementando a imagem do contentor no Cloud Run.

  4. Crie um novo acionador localizado em us-east1:

    gcloud eventarc triggers create troubleshoot-trigger-new \
      --destination-run-service=troubleshoot-service \
      --event-filters="type=google.cloud.audit.log.v1.written" \
      --event-filters="serviceName=storage.googleapis.com" \
      --event-filters="methodName=storage.objects.create" \
      --service-account=${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
    
  5. Verifique se o acionador foi criado:

    gcloud eventarc triggers list
    

    Um acionador pode demorar até dois minutos a inicializar antes de começar a encaminhar eventos.

  6. Para confirmar que o acionador está agora implementado corretamente, gere e veja um evento.

Outros problemas que pode encontrar

Pode encontrar outros problemas ao usar o Eventarc.

Tamanho do evento

Os eventos que envia não podem exceder os limites de tamanho dos eventos.

Um acionador que enviou eventos anteriormente deixou de funcionar

  1. Verifique se a origem está a gerar eventos. Verifique os registos de auditoria da nuvem e certifique-se de que o serviço monitorizado está a emitir registos. Se os registos forem gravados, mas os eventos não forem enviados, contacte o apoio técnico.

  2. Verifique se existe um tópico do Pub/Sub com o mesmo nome do acionador. O Eventarc usa o Pub/Sub como camada de transporte e usa um tópico Pub/Sub existente ou cria automaticamente um tópico e gere-o por si.

    1. Para ver uma lista de acionadores, consulte gcloud eventarc triggers list.
    2. Para listar os tópicos do Pub/Sub, execute o seguinte comando:

      gcloud pubsub topics list
      
    3. Verifique se o nome do tópico Pub/Sub inclui o nome do acionador criado. Por exemplo:

      name: projects/PROJECT_ID/topics/eventarc-us-east1-troubleshoot-trigger-new-123

    Se o tópico do Pub/Sub estiver em falta, crie novamente o acionador para um fornecedor, um tipo de evento e um destino do Cloud Run específicos.

  3. Confirme que o acionador foi configurado para o serviço.

    1. Na Google Cloud consola, aceda à página Serviços.

      Aceder a Serviços

    2. Clique no nome do serviço para abrir a respetiva página Detalhes do serviço.

    3. Clique no separador Acionadores.

      O acionador do Eventarc associado ao serviço deve ser indicado.

  4. Valide o estado do tópico e da subscrição do Pub/Sub através dos tipos de métricas do Pub/Sub.

    • Pode monitorizar mensagens não entregues encaminhadas através da métrica subscription/dead_letter_message_count. Esta métrica mostra o número de mensagens não entregues que o Pub/Sub encaminha a partir de uma subscrição.

      Se as mensagens não forem publicadas no tópico, verifique os registos de auditoria da nuvem e certifique-se de que o serviço monitorizado está a emitir registos. Se os registos forem gravados, mas os eventos não forem enviados, contacte o apoio técnico.

    • Pode monitorizar as subscrições push através da métrica subscription/push_request_count e agrupando a métrica por response_code e subcription_id.

      Se forem comunicados erros de envio, verifique os registos do serviço do Cloud Run. Se o ponto final de receção devolver um código de estado não OK, indica que o código do Cloud Run não está a funcionar como esperado e tem de contactar o apoio técnico.

    Para mais informações, consulte o artigo Crie políticas de alertas de limite métrico.