チュートリアル: Eventarc を使用した Cloud Run サービスのデバッグ

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

目標

  • イベントソースとなる Cloud Storage バケットを作成する。
  • コンテナ イメージをビルドしてアップロードし、Cloud Run にデプロイする。
  • Eventarc トリガーを作成する。
  • ファイルを Cloud Storage バケットにアップロードする。
  • ランタイム エラーのトラブルシューティングと修正を行う。

費用

このチュートリアルでは、課金対象である次の Google Cloud コンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。新しい Google Cloud ユーザーは無料トライアルをご利用いただけます。

このチュートリアルを終了した後、作成したリソースを削除すると、それ以上の請求は発生しません。詳しくは、クリーンアップをご覧ください。

新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

Cloud Audit Logs イベントを受信するための前提条件を満たします。

Cloud Storage バケットを作成する

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

us-east1

export BUCKET1="troubleshoot-bucket1-PROJECT_ID"
gsutil mb -l us-east1 gs://${BUCKET1}

us-west1

export BUCKET2="troubleshoot-bucket2-$PROJECT_ID"
gsutil mb -l us-west1 gs://${BUCKET2}

イベントソースの作成後、イベント レシーバ サービスを Cloud Run にデプロイします。

コードサンプルを取得する

リポジトリのクローンを作成します。

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

コードを確認する

このチュートリアルのコードは、次のものから構成されます。

  • HTTP POST リクエスト内の CloudEvent にラップされた受信メッセージを処理するサーバー。

    Go

    
    // Sample audit_storage is a Cloud Run service which handles Cloud Audit Log events with Cloud Storage data.
    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    // HelloEventsStorage receives and processes a Cloud Audit Log event with Cloud Storage data.
    func HelloEventsStorage(w http.ResponseWriter, r *http.Request) {
    	s := fmt.Sprintf("Detected change in Cloud Storage bucket: %s", string(r.Header.Get("Ce-Subject")))
    	log.Printf(s)
    	fmt.Fprintln(w, s)
    }
    

    Java

    import java.util.Arrays;
    import java.util.List;
    import java.util.Map;
    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 {
    
      private static final List<String> requiredFields =
          Arrays.asList("ce-id", "ce-source", "ce-type", "ce-specversion");
    
      @RequestMapping(value = "/", method = RequestMethod.POST)
      public ResponseEntity<String> receiveMessage(
          @RequestBody Map<String, Object> body, @RequestHeader Map<String, String> headers) {
        for (String field : requiredFields) {
          if (headers.get(field) == null) {
            String msg = String.format("Missing expected header: %s.", field);
            System.out.println(msg);
            return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
          }
        }
    
        if (headers.get("ce-subject") == null) {
          String msg = "Missing expected header: ce-subject.";
          System.out.println(msg);
          return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
        }
    
        String ceSubject = headers.get("ce-subject");
        String msg = "Detected change in Cloud Storage bucket: " + ceSubject;
        System.out.println(msg);
        return new ResponseEntity<String>(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

    import os
    
    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 offical golang image to create a binary.
    # This is based on Debian and sets the GOPATH to /go.
    # https://hub.docker.com/_/golang
    FROM golang:1.16-buster 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:buster-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/Java 8 image to create a build artifact.
    # https://hub.docker.com/_/maven
    FROM maven:3.8-jdk-11 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 AdoptOpenJDK for base image.
    # It's important to use OpenJDK 8u191 or above that has container support enabled.
    # https://hub.docker.com/r/adoptopenjdk/openjdk8
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM adoptopenjdk/openjdk11:alpine-slim
    
    # 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/core/sdk:3.1-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/core/aspnet:3.1-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 10 image.
    # https://hub.docker.com/_/node
    FROM node:12-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 add a package-lock.json speed your build by switching to 'npm ci'.
    # RUN npm ci --only=production
    RUN npm install --production
    
    # 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.9-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

コードを配布する

コードを配布するには:

  1. Cloud Build でコンテナ イメージをビルドし、Container Registry にアップロードします。

    Go

     gcloud builds submit --tag gcr.io/PROJECT_ID/audit-storage

    PROJECT_ID は、実際の Google Cloud プロジェクト ID に置き換えます。

    ビルドが成功すると、ID、作成時間、イメージ名を含む成功メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Java

    1. Docker から Container Registry に push するには、gcloud 認証ヘルパーを使用します。
      gcloud auth configure-docker
    2. Jib Maven プラグインを使用して、コンテナをビルドして Container Registry に push します。

      mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/audit-storage

      PROJECT_ID は、実際の Google Cloud プロジェクト ID に置き換えます。

      audit-storage はコンテナ名です。成功すると、成功メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    .NET

     gcloud builds submit --tag gcr.io/PROJECT_ID/audit-storage

    PROJECT_ID は、実際の Google Cloud プロジェクト ID に置き換えます。

    ビルドが成功すると、ID、作成時間、イメージ名を含む成功メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Node.js

     gcloud builds submit --tag gcr.io/PROJECT_ID/audit-storage

    PROJECT_ID は、実際の Google Cloud プロジェクト ID に置き換えます。

    ビルドが成功すると、ID、作成時間、イメージ名を含む成功メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Python

     gcloud builds submit --tag gcr.io/PROJECT_ID/audit-storage

    PROJECT_ID は、実際の Google Cloud プロジェクト ID に置き換えます。

    ビルドが成功すると、ID、作成時間、イメージ名を含む成功メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

  2. コンテナ イメージを Cloud Run にデプロイする。

    gcloud run deploy troubleshoot-service --image gcr.io/PROJECT_ID/audit-storage \
    --allow-unauthenticated
    

    PROJECT_ID は実際の GCP プロジェクト ID に置き換えます。
    audit-storage はコンテナ名、troubleshoot-service は Cloud Run サービスの名前です。

    コンテナ イメージは、gcloud の設定時に構成したサービスとリージョンにデプロイされることに注意してください。

  3. Cloud Run にデプロイする場合、未認証の許可を求めるプロンプトで y(はい)と応答します。IAM ベースの認証の詳細については、Eventarc のロールと権限をご覧ください。

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

トリガーを作成する

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

  1. 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
    

    troubleshoot-triggertroubleshoot-service のターゲットとともに表示されます。

イベントを生成して表示する

サービスが正常にデプロイされ、Cloud Storage からイベントを受信できることを確認するには、次の操作を行います。

  1. ファイルを作成して、最初のストレージ バケットにアップロードします。

     echo "Hello World" > random.txt
     gsutil cp random.txt gs://${BUCKET1}/random.txt
    
  2. ログをモニタリングして、サービスがイベントを受信したかどうかを確認します。サービスログには、サービスがイベントをリッスンしていることと、設定に問題があることが示されます。

    Now listening on: http://0.0.0.0:8080

問題を調査する

次に、サービスがイベントを受信しない理由を調査するプロセスに進みます。

監査ログ

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

  1. [IAM と管理] > [監査ログ] に移動し、[Google Cloud Storage] チェックボックスをオンにします。Cloud Audit Logs のコンソールに移動
  2. Cloud Audit Logs の管理読み取りデータ読み取りデータ書き込みのログタイプが選択されていることを確認します。

Cloud Audit Logs を有効にしたら、ファイルをもう一度アップロードして、ログを確認します。サービスはまだイベントを受信していません。トリガーのロケーションに問題がある可能性があります。

トリガーのロケーション

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

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

トリガーのロケーションを一覧表示するには、次のようにトリガーを記述します。

gcloud eventarc triggers describe troubleshoot-trigger

ここでは、us-east1us-west1 に 2 つのバケットを作成しています。これらのロケーションからイベントを受信するには、それらのロケーションでトリガーを作成する必要があります。では、us-east1 にトリガーを作成してみましょう。

  1. us-central1 ロケーションの既存のトリガーを削除します。

       gcloud eventarc triggers delete troubleshoot-trigger
    
  2. ロケーションとリージョンを us-east1 に設定します。

      gcloud config set eventarc/location us-east1
      gcloud config set run/region us-east1
    
  3. もう一度コードをリリースします。

  4. ロケーション us-east1 で新しいトリガーを作成します。

     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
    
  5. トリガーが作成されたことを確認します。

       gcloud eventarc triggers list
    

トリガーが初期化され、イベントの配信が開始するまでに最大で 10 分ほどかかることがあります。

初期化時間

トリガーが作成されてからイベントの送信を開始するまでに最大で 10 分ほどかかります。次のコマンドを実行して、トリガーが有効になっていることを確認します。

   gcloud eventarc triggers list

トリガーのステータスを示す次のような出力が表示されます。

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

10 分後、各バケットにファイルを再度アップロードします。各ファイルのイベントが Cloud Run サービスログに書き込まれます。サービスがイベントを受信しない場合は、イベントサイズに問題がある可能性があります。

イベントサイズ

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

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

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

  2. 同じトリガー名の Pub/Sub トピックが存在することを確認します。

    1. トリガーを一覧表示する方法については、gcloud eventarc triggers list をご覧ください。
    2. Pub/Sub トピックを一覧表示するには、次のコマンドを実行します。

        gcloud pubsub topics list
      

    Pub/Sub トピック名に、作成されたトリガーの名前が含まれていることを確認します。Pub/Sub トピックがない場合は、トリガーの作成時にトピックを作成します。

  3. Pub/Sub トピックの正常性を確認します。

    1. [トリガー] タブにチェックマーク があることを確認します。Cloud Console で [Cloud Run] に移動し、作成したサービスを選択して、[トリガー] タブに移動します。

    2. [Pub/Sub] > [トピック] に移動し、Pub/Sub トピックをクリックします。

      Pub/Sub トピックに移動

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

      トピックの指標

    4. response_code で、指標 subscription/push_request_count を含むメッセージが Cloud Run に正常に push されているかどうかをモニタリングします。

      1. Cloud Console で Cloud Monitoring に移動します。

        [Monitoring] に移動

      2. プロジェクトを新しいワークスペースに追加します。

      3. [ダッシュボード] をクリックし、[Cloud Pub/Sub] ダッシュボードを選択します。

      4. [Cloud Pub/Sub] ダッシュボードで、作成した Pub/Sub トピックをクリックします。

      5. [Incidents] セクションで、[CREATE POLICY] をクリックします。

      6. [通知ポリシーを作成] ページで、[ADD CONDITION] をクリックします。

      7. [METRIC] タブで、次の条件を指定します。

        • リソースタイプとして Cloud Pub/Sub subscription
        • 指標として Push requests
        • グループ化として response_code
        • 構成しきい値として 0サブスクリプションの指標 Pub/Sub の使用状況の指標の詳細については、push サブスクリプションのモニタリングをご覧ください。
      8. [ADD] をクリックして、[Create alerting policy] ページに移動します。

      9. [What's the steps to fixing the issue?] で、アラート名(例: samplealert)を指定して [Save] をクリックします。

      10. アラートを表示するには、[Monitoring] > [ダッシュボード] > [Cloud Pub/Sub] に移動します。Pub/Sub トピックをクリックして、[Subscription] タブをクリックします。

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

クリーンアップ

このチュートリアル用に新規プロジェクトを作成した場合は、そのプロジェクトを削除します。既存のプロジェクトを使用し、このチュートリアルで変更を加えずに残す場合は、チュートリアル用に作成したリソースを削除します。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

チュートリアル リソースの削除

  1. このチュートリアルでデプロイした Cloud Run サービスを削除します。

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME は、選択したサービス名です。

    Cloud Run サービスは Google Cloud Console から削除することもできます。

  2. チュートリアルを設定したときに追加した gcloud のデフォルト構成を削除します。

    リージョン設定を削除します。

     gcloud config unset run/region
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

    • トリガーを削除します。
      gcloud eventarc triggers delete TRIGGER_NAME
      
      TRIGGER_NAME は実際のトリガー名に置き換えます。

次のステップ