Tutorial: depurar eventos de enrutamiento en Cloud Run

En este tutorial se explica cómo solucionar los errores de tiempo de ejecución que se producen al usar Eventarc para enrutar eventos de Cloud Storage a un servicio de Cloud Run sin autenticar mediante registros de auditoría de Cloud.

Crear un repositorio estándar de Artifact Registry

Crea un repositorio estándar de Artifact Registry para almacenar tu imagen de contenedor:

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

Sustituye REPOSITORY por un nombre único para el repositorio.

Crea un segmento de Cloud Storage

Crea un segmento de Cloud Storage en cada una de las dos regiones como origen de eventos del servicio de Cloud Run:

  1. Crea un segmento en us-east1:

    export BUCKET1="troubleshoot-bucket1-PROJECT_ID"
    gcloud storage buckets create gs://${BUCKET1} --location=us-east1
  2. Crea un segmento en us-west1:

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

Una vez que se haya creado el origen del evento, despliega el servicio de receptor de eventos en Cloud Run.

Implementar el receptor de eventos

Despliega un servicio de Cloud Run que recibe y registra eventos.

  1. Para obtener el código de muestra, clona el repositorio de GitHub:

    Go

    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. Revisa el código de este tutorial, que consta de lo siguiente:

    • Un controlador de eventos que recibe el evento entrante como un CloudEvent en la solicitud HTTP POST:

      Go

      
      // 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)
      
      
    • Un servidor que usa el controlador de eventos:

      Go

      
      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)))
    • Un archivo Dockerfile que define el entorno operativo del servicio. El contenido del Dockerfile varía según el idioma:

      Go

      
      # 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. Crea tu imagen de contenedor con Cloud Build y súbela a 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. Despliega la imagen de contenedor en Cloud Run:

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

    Si la implementación se realiza correctamente, la línea de comandos muestra la URL del servicio.

Crear activador

Después de implementar un servicio de Cloud Run, configura un activador para que detecte eventos de Cloud Storage a través de los registros de auditoría.

  1. Crea un activador de Eventarc para detectar eventos de Cloud Storage que se enrutan mediante Registros de auditoría de 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
    

    De esta forma, se crea un activador llamado troubleshoot-trigger.

  2. Para confirmar que se ha creado troubleshoot-trigger, ejecuta el siguiente comando:

    gcloud eventarc triggers list
    

    La salida debería ser similar a la siguiente:

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

Generar y ver un evento

Confirma que has desplegado el servicio correctamente y que puedes recibir eventos de Cloud Storage.

  1. Crea y sube un archivo al BUCKET1 segmento de almacenamiento:

     echo "Hello World" > random.txt
     gcloud storage cp random.txt gs://${BUCKET1}/random.txt
    
  2. Monitoriza los registros para comprobar si el servicio ha recibido un evento. Para ver la entrada del registro, sigue estos pasos:

    1. Filtra las entradas de registro y devuelve el resultado en formato JSON:

      gcloud logging read "resource.labels.service_name=troubleshoot-service \
          AND textPayload:random.txt" \
          --format=json
    2. Busca una entrada de registro similar a la siguiente:

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

Ten en cuenta que, al principio, no se devuelve ninguna entrada de registro. Esto indica que hay un problema en la configuración que debes investigar.

Investigar el problema

Sigue el proceso para investigar por qué el servicio no recibe eventos.

Tiempo de inicialización

Aunque el activador se crea inmediatamente, puede tardar hasta dos minutos en propagarse y filtrar eventos. Ejecuta el siguiente comando para confirmar que un activador está activo:

gcloud eventarc triggers list

El resultado indica el estado del activador. En el siguiente ejemplo, troubleshoot-trigger se activará a las 14:16:56:

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

Una vez que el activador esté activo, vuelve a subir un archivo al bucket de almacenamiento. Los eventos se escriben en los registros de servicio de Cloud Run. Si el servicio no recibe eventos, puede que se deba al tamaño de los eventos.

Registros de auditoría

En este tutorial, los eventos de Cloud Storage se enrutan mediante registros de auditoría de Cloud y se envían a Cloud Run. Confirma que los registros de auditoría estén habilitados en Cloud Storage.

  1. En la Google Cloud consola, ve a la página Registros de auditoría.

    Ir a los registros de auditoría

  2. Seleccione la casilla Google Cloud Storage.
  3. Asegúrate de que estén seleccionados los tipos de registro Actividad de administración, Lectura de datos y Escritura de datos.

Una vez que hayas habilitado los registros de auditoría de Cloud, vuelve a subir el archivo al segmento de almacenamiento y consulta los registros. Si el servicio sigue sin recibir eventos, puede que se deba a la ubicación del activador.

Ubicación de activación

Puede haber varios recursos en diferentes ubicaciones y debe filtrar los eventos de las fuentes que se encuentren en la misma región que el destino de Cloud Run. Para obtener más información, consulta las ubicaciones compatibles con Eventarc y el artículo ¿Qué son las ubicaciones de Eventarc?

En este tutorial, has desplegado el servicio de Cloud Run en us-central1. Como has definido eventarc/location en us-central1, también has creado un activador en la misma ubicación.

Sin embargo, has creado dos segmentos de Cloud Storage en las ubicaciones us-east1 y us-west1. Para recibir eventos de esas ubicaciones, debe crear activadores de Eventarc en ellas.

Crea un activador de Eventarc ubicado en us-east1:

  1. Confirma la ubicación del activador:

    gcloud eventarc triggers describe troubleshoot-trigger
    
  2. Define la ubicación y la región en us-east1:

    gcloud config set eventarc/location us-east1
    gcloud config set run/region us-east1
    
  3. Vuelve a desplegar el receptor de eventos creando y desplegando la imagen de contenedor en Cloud Run.

  4. Crea un activador en 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. Comprueba que se haya creado el activador:

    gcloud eventarc triggers list
    

    Un activador puede tardar hasta dos minutos en inicializarse antes de empezar a enrutar eventos.

  6. Para confirmar que el activador se ha implementado correctamente, genera y consulta un evento.

Otros problemas que pueden surgir

Puede que tengas otros problemas al usar Eventarc.

Tamaño del evento

Los eventos que envíe no deben superar los límites de tamaño de evento.

Un activador que antes enviaba eventos ha dejado de funcionar

  1. Verifica que la fuente esté generando eventos. Consulta los registros de auditoría de Cloud y comprueba que el servicio monitorizado emite registros. Si se registran los registros, pero no se envían los eventos, ponte en contacto con el equipo de Asistencia.

  2. Verifica que exista un tema de Pub/Sub con el mismo nombre de activador. Eventarc usa Pub/Sub como capa de transporte y utilizará un tema de Pub/Sub que ya exista o creará uno automáticamente y lo gestionará por ti.

    1. Para ver una lista de los activadores, consulta gcloud eventarc triggers list.
    2. Para enumerar los temas de Pub/Sub, ejecuta el siguiente comando:

      gcloud pubsub topics list
      
    3. Comprueba que el nombre del tema de Pub/Sub incluya el nombre del activador creado. Por ejemplo:

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

    Si falta el tema de Pub/Sub, vuelve a crear el activador para un proveedor, un tipo de evento y un destino de Cloud Run específicos.

  3. Comprueba que el activador se haya configurado para el servicio.

    1. En la Google Cloud consola, ve a la página Servicios.

      Ir a Servicios

    2. Haz clic en el nombre del servicio para abrir su página Detalles del servicio.

    3. Haz clic en la pestaña Activadores.

      Debería aparecer el activador de Eventarc asociado al servicio.

  4. Verifica el estado del tema y la suscripción de Pub/Sub mediante los tipos de métricas de Pub/Sub.

    Para obtener más información, consulta Crear políticas de alertas de umbral de métricas.