教學課程:對 Cloud Run 的事件路由進行偵錯

本教學課程說明如何排解使用 Eventarc 將 Cloud Storage 事件透過 Cloud 稽核記錄轉送至未經驗證的 Cloud Run 服務時,所發生的執行階段錯誤。

建立 Artifact Registry 標準存放區

建立 Artifact Registry 標準存放區,用於儲存容器映像檔:

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

REPOSITORY 替換成存放區的專屬名稱。

建立 Cloud Storage 值區

在兩個區域中各建立一個 Cloud Storage bucket,做為 Cloud Run 服務的事件來源:

  1. us-east1 中建立 bucket:

    export BUCKET1="troubleshoot-bucket1-PROJECT_ID"
    gcloud storage buckets create gs://${BUCKET1} --location=us-east1
  2. us-west1 中建立 bucket:

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

建立事件來源後,請在 Cloud Run 上部署事件接收器服務。

部署事件接收器

部署可接收及記錄事件的 Cloud Run 服務。

  1. 複製 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. 請檢查本教學課程的程式碼,其中包含下列項目:

    • 事件處理常式,會在 HTTP POST 要求中,以 CloudEvent 形式接收傳入的事件:

      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)
      
      
    • 使用事件處理常式的伺服器:

      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)))
    • 定義服務作業環境的 Dockerfile。Dockerfile 的內容依程式語言而有所不同:

      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. 使用 Cloud Build 建構容器映像檔,並將映像檔上傳至 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. 將容器映像檔部署至 Cloud Run:

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

    部署成功後,指令列會顯示服務網址。

建立觸發條件

部署 Cloud Run 服務後,請設定觸發條件,透過稽核記錄監聽 Cloud Storage 的事件。

  1. 建立 Eventarc 觸發條件,監聽透過 Cloud 稽核記錄轉送的 Cloud Storage 事件:

    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
    

    這項操作會建立名為 troubleshoot-trigger 的觸發條件。

  2. 如要確認 troubleshoot-trigger 是否已建立,請執行:

    gcloud eventarc triggers list
    

    畫面會顯示如下的輸出內容:

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

產生及查看活動

確認您已成功部署服務,且可以接收 Cloud Storage 傳送的事件。

  1. 建立檔案並上傳至 BUCKET1 storage bucket:

     echo "Hello World" > random.txt
     gcloud storage cp random.txt gs://${BUCKET1}/random.txt
    
  2. 監控記錄,確認服務是否收到事件。如要查看記錄項目,請完成下列步驟:

    1. 篩選記錄檔項目,並以 JSON 格式傳回輸出內容:

      gcloud logging read "resource.labels.service_name=troubleshoot-service \
          AND textPayload:random.txt" \
          --format=json
    2. 尋找類似下列內容的記錄項目:

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

請注意,一開始不會傳回任何記錄項目。這表示設定有問題,您必須進行調查。

調查問題

請逐步調查服務未收到事件的原因。

初始化時間

雖然觸發條件會立即建立,但最多可能需要兩分鐘才能傳播並篩選事件。執行下列指令,確認觸發條件是否處於啟用狀態:

gcloud eventarc triggers list

輸出內容會顯示觸發條件的狀態。在以下範例中,troubleshoot-trigger 會在 14:16:56 啟用:

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

觸發條件啟用後,請再次將檔案上傳至儲存空間 bucket。事件會寫入 Cloud Run 服務記錄。如果服務未收到事件,可能與事件大小有關。

稽核記錄

在本教學課程中,Cloud Storage 事件會透過 Cloud 稽核記錄傳送至 Cloud Run。確認 Cloud Storage 已啟用稽核記錄。

  1. 前往 Google Cloud 控制台的「稽核記錄」頁面。

    前往「稽核記錄」

  2. 選取「Google Cloud Storage」核取方塊。
  3. 確認已選取「管理員讀取」、「資料讀取」和「資料寫入」記錄類型。

啟用 Cloud 稽核記錄後,請再次將檔案上傳至儲存空間 bucket,並檢查記錄。如果服務仍未收到事件,這可能與觸發條件位置有關。

觸發地點

不同位置可能有多個資源,您必須篩選來自與 Cloud Run 目標位於相同區域的來源事件。詳情請參閱「Eventarc 支援的位置」和「瞭解 Eventarc 位置」。

在本教學課程中,您已將 Cloud Run 服務部署至 us-central1。由於您將 eventarc/location 設為 us-central1,因此您也在相同位置建立了觸發條件。

不過,您在 us-east1us-west1 位置建立了兩個 Cloud Storage bucket。如要接收這些位置的事件,您必須在這些位置建立 Eventarc 觸發條件。

us-east1 中建立 Eventarc 觸發條件:

  1. 確認現有觸發條件的位置:

    gcloud eventarc triggers describe troubleshoot-trigger
    
  2. 將位置和區域設為 us-east1

    gcloud config set eventarc/location us-east1
    gcloud config set run/region us-east1
    
  3. 建構容器映像檔並部署至 Cloud Run,再次部署事件接收器

  4. 在「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. 確認觸發條件已建立:

    gcloud eventarc triggers list
    

    觸發條件最多可能需要兩分鐘才能完成初始化,然後開始轉送事件。

  6. 如要確認觸發條件已正確部署,請產生並查看事件

你可能會遇到的其他問題

使用 Eventarc 時,您可能會遇到其他問題。

事件大小

傳送的事件不得超過事件大小限制。

先前傳送事件的觸發條件已停止運作

  1. 確認來源是否正在產生事件。檢查 Cloud 稽核記錄,確認受監控的服務是否發出記錄。如果系統記錄了記錄檔,但未傳送事件,請與支援團隊聯絡

  2. 確認是否有相同觸發條件名稱的 Pub/Sub 主題。Eventarc 會使用 Pub/Sub 做為傳輸層,並使用現有的 Pub/Sub 主題,或自動建立及管理主題。

    1. 如要列出觸發條件,請參閱 gcloud eventarc triggers list
    2. 如要列出 Pub/Sub 主題,請執行下列指令:

      gcloud pubsub topics list
      
    3. 確認 Pub/Sub 主題名稱包含所建立的觸發條件名稱。例如:

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

    如果 Pub/Sub 主題遺失,請針對特定供應商、事件類型和 Cloud Run 目的地重新建立觸發程序

  3. 確認服務已設定觸發條件。

    1. 前往 Google Cloud 控制台的「Services」頁面。

      前往「服務」

    2. 按一下服務名稱,開啟「Service details」(服務詳細資料) 頁面。

    3. 按一下「觸發條件」分頁標籤。

      與服務相關聯的 Eventarc 觸發條件應會列出。

  4. 使用 Pub/Sub 指標類型,驗證 Pub/Sub 主題和訂閱項目的健康狀態。

    • 您可以使用 subscription/dead_letter_message_count 指標監控轉寄的無法傳送郵件。這項指標會顯示 Pub/Sub 從訂閱項目轉送的無法遞送訊息數量。

      如果訊息未發布至主題,請檢查 Cloud 稽核記錄,並確認受監控的服務是否發出記錄。如果系統已記錄記錄檔,但未傳送事件,請與支援團隊聯絡

    • 您可以使用 subscription/push_request_count 指標,並依 response_codesubcription_id 分組指標,監控推送訂閱項目

      如果系統回報推送錯誤,請檢查 Cloud Run 服務記錄檔。如果接收端點傳回非「OK」的狀態碼,表示 Cloud Run 程式碼無法正常運作,請與支援團隊聯絡

    詳情請參閱建立指標閾值快訊政策