🧑‍💻

Cloud Deploy でデプロイ後に統合テストを実行する

2025/02/03に公開

概要

Cloud Deploy にはデプロイした後に検証する機能(VERIFY)があり、これは統合テストを実施するのに役立ちます。
本記事では認証付き Cloud Run に対して、APIテストを実行できるような Cloud Deploy の設定を紹介します。

前提

Cloud Deployの基本的な使い方についてはこちらを参照ください。
https://zenn.dev/koga_atsu/articles/b781ff860a01e9

検証機能の使い方

3ステップで説明します。

  1. 検証機能を有効化する
  2. 検証内容を記述する
  3. 必要な権限を設定する

1.検証機能を有効化する

デリバリーパイプラインとターゲット定義を clouddeploy.yaml に記述している場合、次の内容を記載します。

  • serialPipeline.stages.strategy.standardverify: true を設定
  • executionConfigs.usagesVERIFY フェーズを追加
clouddeploy.yaml
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: run-verify-pipeline
description: main application pipeline
serialPipeline:
  stages:
    - targetId: run-verify-target
      profiles: [prod]
      # 検証機能を有効化する
      strategy:
        standard:
          verify: true
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: run-verify-target
description: Cloud Run production service
executionConfigs:
  - usages:
      - RENDER
      - DEPLOY
      - VERIFY  # VERIFYフェーズを追加
    serviceAccount: my-cloud-deploy@sample-project.iam.gserviceaccount.com
    artifactStorage: gs://clouddeploy_bucket
    executionTimeout: 3600s
run:
  location: projects/sample-project/locations/asia-northeast1

参考:構成スキーマリファレンス

次のコマンドで上記内容を反映します。

gcloud deploy apply --file=clouddeploy.yaml \
      --project=$(PROJECT) \
      --region=$(REGION)

Terraform の場合

デリバリーパイプラインとターゲット定義をyamlではなく、Terraform で管理している場合は次のように設定します。

resource "google_clouddeploy_delivery_pipeline" "run-verify-pipeline" {
  location    = "asia-northeast1"
  name        = "run-verify-pipeline"
  description = "verify検証用のデリバリーパイプライン"
  project     = "sample-project"

  serial_pipeline {
    stages {
      # 検証機能を有効化する
      strategy {
        standard {
          verify = true
        }
      }
      profiles = ["prod"]
      target_id = google_clouddeploy_target.verify_target.target_id
    }
  }
}

resource "google_clouddeploy_target" "verify_target" {
  location    = "asia-northeast1"
  name        = "run-verify-target"
  description = "verify検証用のターゲット"
  execution_configs {
    artifact_storage = google_storage_bucket.cloud_deploy.url
    service_account  = google_service_account.cloud_deploy.email
    usages = [
      "RENDER",
      "DEPLOY",
      "VERIFY"  # VERIFYフェーズを追加
    ]
  }
  run {
    location = "projects/sample-project/locations/asia-northeast1"
  }
}

2.検証内容を記述する

skaffold.yamlverify フェーズを追加して、実行するコンテナやコマンドを記述します。
ここでは Cloud Run API を検証するために2つの検証ステップ(curl-test, integration-test)を実行させます。

skaffold.yaml
apiVersion: skaffold/v4beta11
kind: Config
metadata:
  name: deploy-run-quickstart
profiles:
  - name: prod
    manifests:
      rawYaml:
        - run.yaml
deploy:
  cloudrun: {}
verify:
  - name: curl-test
    container:
      name: verification-test-contaier2
      image: gcr.io/cloud-builders/gcloud
      command: ["/bin/sh"]
      args:
        - '-c'
        - |
          IDENTITY_TOKEN=$(gcloud auth print-identity-token)
          STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $IDENTITY_TOKEN" $CLOUD_RUN_SERVICE_URLS/healthcheck)
          if [ "$STATUS_CODE" -eq 200 ]; then
            echo "Verification successful: Status code is 200"
            exit 0
          else
            echo "Verification failed: Status code is $STATUS_CODE"
            exit 1
          fi

  - name: integration-test
    container:
      name: integration-test-container
      image: asia-northeast1-docker.pkg.dev/sample-project/sample-repo/hello-world:latest
      command: ["/bin/sh"]
      args:
        - '-c'
        - |
          APP_URL=$CLOUD_RUN_SERVICE_URLS go test -v app1/tests

curl-test

curl-test は Cloud Run に用意したヘルスチェックエンドポイントを直接叩いて、ステータスコードをチェックする簡易的なテストです。

認証付き Cloud Run にリクエストを投げるには Authorization ヘッダーにIDトークンを含める必要があり、そのIDトークンを gcloud auth print-identity-token で取得しています。

$CLOUD_RUN_SERVICE_URLS は Cloud Deploy のターゲットタイプが Run の場合に Cloud Run サービスのURLを参照できる環境変数です。VERIFY 実行環境で使用可能な環境変数は他にもいくつかあります。

https://cloud.google.com/deploy/docs/verify-deployment?hl=ja#available_environment_variables

integration-test

integration-test_test.go で用意したテストコードを、go testコマンドで実行するものです。テストコードのサンプルとして、次のようなものを用意しました。

package tests

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"google.golang.org/api/idtoken"
	"io"
	"net/http"
	"os"
	"testing"
)

func Test_HealthCheck(t *testing.T) {
	tests := []struct {
		name         string
		StatusCode   int
		responseBody string
	}{
		{
			name:         "/healthcheck を叩いたら、ステータスコード 200 で ok が返されること",
			StatusCode:   http.StatusOK,
			responseBody: "ok",
		},
	}
	for _, tt := range tests {
		res, err := newRequest("/healthcheck")
		if err != nil {
			t.Fatalf("Failed to post request: %v", err)
		}
		if res.StatusCode != tt.StatusCode {
			t.Fatalf("expect = %v, actual = %v", tt.StatusCode, res.StatusCode)
		}
		body, err := io.ReadAll(res.Body)
		if err != nil {
			t.Fatalf("Failed to read response body: %v", err)
		}
		if string(body) != tt.responseBody {
			t.Fatalf("expect = %#v, actual = %#v", tt.responseBody, string(body))
		}
	}
}

func newRequest(path string) (*http.Response, error) {
	client, err := idtoken.NewClient(context.Background(), os.Getenv("APP_URL"))
	if err != nil {
		return nil, errors.New(fmt.Sprintf("idtoken.NewClient: %v", err))
	}
	return client.Get(os.Getenv("APP_URL") + path)
}

Goコードから Cloud Run API を叩き、ステータスコードとレスポンスボディをチェックしています。テストコードをGoコードとして記述することできめ細かいチェックが行い易くなります。
また、idtoken を使うとIDトークンを取得するところからAuthorization ヘッダーに含めるところまでを内部的にやってくれるため、開発者はその辺を意識せずにコーディングできるのがありがたいです。

3.検証に必要な権限を設定する

検証機能は Cloud Build 上で実行されます。
上記でいうところの curl-testintegration-test のコンテナは Cloud Build 上で構築され、そこから検証対象である Cloud Run にAPIリクエストを投げます。

この時、Cloud Build 実行アカウントは executionConfigs.serviceAccount で指定したものであり、このアカウントから Cloud Run にアクセスできる必要があります。

そのためにはまず roles/run.invoker ロールが必要です。
加えて、先述の検証コードでIDトークンを取得するには roles/iam.serviceAccountTokenCreator ロールを自身に付与します。

gcloudコマンドの場合
gcloud iam service-accounts add-iam-policy-binding my-cloud-deploy@${PROJECT_ID}.iam.gserviceaccount.com \
  --member='serviceAccount:my-cloud-deploy@${PROJECT_ID}.iam.gserviceaccount.com' \
  --role='roles/iam.serviceAccountTokenCreator'
terraformの場合
resource "google_service_account_iam_binding" "create_my_token" {
  service_account_id = google_service_account.my_cloud_deploy.id
  role               = "roles/iam.serviceAccountTokenCreator"
  members = [
    "serviceAccount:${google_service_account.my_cloud_deploy.email}",
  ]
}

最初は roles/iam.serviceAccountTokenCreator ロールが無くても idtoken パッケージを使えばトークンを取得できると思っていましたが、Cloud Build 環境ではダメでした。
また、このロールを付与しなくても Cloud Run にアクセスする方法があるのではと調べましたが、自分のニーズに合う方法は見つかりませんした(備考欄を参照)。

検証機能を動かす

gcloud deploy release create を実行してリリースを作成すると、Deploy フェーズが完了した後に Verify フェーズが実行されます。

gcloud deploy releases create sample-release-001 \
      --delivery-pipeline=run-verify-pipeline \
      --skaffold-file=skaffold.yaml \
      --gcs-source-staging-dir=gs://clouddeploy_bucket/source \
      --images my-app-image=$(IMAGE_PATH) \
      --project=$(PROJECT) \
      --region=$(REGION)

備考

IDトークンを取得する方法はいくつかあります。(参考1, 参考2

  • 認証ライブラリ(idtoken)を使用する
    • 実行アカウントがサービスアカウントならば使用可能だが、ユーザアカウントは使用不可。
  • メタデータサーバーを使用する
  • Google Cloud の外部から Workload Identity 連携を使用する
  • Google Cloud の外部からダウンロードしたサービスアカウントキーを使用する
  • 接続サービスを使用して ID トークンを生成する
    • Cloud Scheduler や Workflows を介してアクセスする方法
  • サービスアカウントの権限を借用して ID トークンを生成する
  • gcloud auth print-identity-token コマンドを使用する

また、メタデータサーバにアクセスする際の注意点として次のものがありました。

メンテナンス イベントにより、メタデータ サーバーの利用可能状態が最大 1 秒中断されることがあります。このとき、メタデータ サーバーは Error 503 HTTP サーバー レスポンスを返すことがあります。アプリケーションをメンテナンス イベントに耐えられるようにするには、メタデータ サーバーをクエリするアプリケーションに再試行ロジックを実装することをおすすめします。

https://cloud.google.com/compute/docs/metadata/querying-metadata?hl=ja

参考

Discussion