チュートリアル: Cloud Run へのイベント転送をデバッグする

このチュートリアルでは、未認証の Cloud Run サービスへ Cloud Audit Logs を使用して Cloud Storage から Eventarc を使用してイベントを転送するときに発生するランタイム エラーのトラブルシューティング方法について説明します。

Artifact Registry 標準リポジトリを作成する

コンテナ イメージを保存する Artifact Registry 標準リポジトリを作成します。

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

REPOSITORY は、リポジトリの一意の名前に置き換えます。

Cloud Storage バケットを作成する

Cloud Run サービスのイベントソースとして、2 つのリージョンに Cloud Storage バケットを作成します。

  1. us-east1 にバケットを作成します。

    export BUCKET1="troubleshoot-bucket1-PROJECT_ID"
    gcloud storage buckets create gs://${BUCKET1} --location=us-east1
  2. us-west1 にバケットを作成します。

    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

    デプロイに成功すると、コマンドラインにサービスの URL が表示されます。

トリガーを作成する

Cloud Run サービスをデプロイした後は、監査ログを使用して Cloud Storage からのイベントをリッスンするトリガーを設定します。

  1. Cloud Audit Logs を使用して転送された Cloud Storage イベントをリッスンする Eventarc トリガーを作成します。

    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 ストレージ バケットにアップロードします。

     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: ..."
      

最初はログエントリが返されません。これは、セットアップに問題があり、調査する必要があることを意味します。

問題を調査する

サービスがイベントを受信しない原因を調査するプロセスを行います。

初期化時間

トリガーはすぐに作成されますが、トリガーが伝播されてイベントがフィルタされるまでに最大で 2 分かかることがあります。次のコマンドを実行して、トリガーが有効であることを確認します。

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

トリガーが有効になったら、ファイルをストレージ バケットに再度アップロードします。イベントは Cloud Run サービスログに書き込まれます。サービスがイベントを受信しない場合は、イベントサイズに関連している可能性があります。

監査ログ

このチュートリアルでは、Cloud Audit Logs を使用して Cloud Storage イベントが転送され、Cloud Run に送信されます。Cloud Storage の監査ログが有効になっていることを確認します。

  1. Google Cloud コンソールで、[監査ログ] ページに移動します。

    監査ログに移動

  2. [Google Cloud Storage] チェックボックスをオンにします。
  3. [管理読み取り]、[データ読み取り]、[データ書き込み] のログタイプが選択されていることを確認します。

Cloud Audit Logs を有効にしたら、ファイルをストレージ バケットに再度アップロードして、ログを確認します。サービスがまだイベントを受信していない場合は、トリガーのロケーションに関連している可能性があります。

トリガーのロケーション

ロケーションが異なる複数のリソースが存在している可能性があります。Cloud Run ターゲットと同じリージョン内のソースからのイベントをフィルタする必要があります。詳細については、Eventarc でサポートされているロケーションEventarc のロケーションについてをご覧ください。

このチュートリアルでは、Cloud Run サービスを us-central1 にデプロイしました。eventarc/locationus-central1 に設定したため、同じロケーションにトリガーも作成しました。

しかし、2 つの Cloud Storage バケットは、us-east1us-west1 のロケーションに作成しました。これらのロケーションからイベントを受信するには、各ロケーションで 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
    

    トリガーが初期化され、イベントの転送が開始するまでに 2 分ほどかかることがあります。

  6. トリガーが正しくデプロイされたことを確認するには、イベントを生成し表示します。

発生する可能性がある他の問題

Eventarc の使用時に、他の問題が発生することがあります。

イベントサイズ

送信するイベントは、イベントサイズの上限を超えないようにする必要があります。

以前にイベントを配信したトリガーが停止している

  1. ソースがイベントを生成していることを確認します。Cloud Audit Logs で、モニタリング対象サービスがログを出力していることを確認します。ログが記録されていてもイベントが配信されない場合は、サポートにお問い合わせください。

  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 コンソールで、[サービス] ページに移動します。

      [サービス] に移動

    2. サービスの名前をクリックして、[サービスの詳細] ページを開きます。

    3. [トリガー] タブをクリックします。

      サービスに関連付けられた Eventarc トリガーが表示されます。

  4. Pub/Sub 指標タイプを使用して、Pub/Sub トピックとサブスクリプションの健全性を確認します。

    • subscription/dead_letter_message_count 指標を使用して、転送済みの配信不能メッセージをモニタリングできます。この指標は、Pub/Sub がサブスクリプションから転送する配信不能メッセージの数を示します。

      トピックにメッセージが公開されていない場合は、Cloud Audit Logs で、モニタリング対象サービスがログを出力していることを確認します。ログが記録されていてもイベントが配信されない場合は、サポートにお問い合わせください。

    • subscription/push_request_count 指標を使用してプッシュ サブスクリプションをモニタリングし、指標を response_codesubcription_id でグループ化できます。

      push エラーが報告された場合は、Cloud Run サービスログを確認します。受信エンドポイントが OK 以外のステータス コードを返した場合、Cloud Run コードが期待どおりに機能していません。この場合、サポートにお問い合わせいただく必要があります。

    詳細については、指標しきい値のアラート ポリシーを作成するをご覧ください。