🤖

Webhook を使って Cloud Build トリガーのビルド完了後に次のトリガーを自動実行する

2024/05/23に公開

はじめに

ある Cloud Build がおわったら、別の Cloud Build トリガーがキックされる構成をつくりたいです。例えば、build がおわったら deploy が走るといった構成です。今回は Pub/Sub と Cloud Build の Webhook トリガーで実現します。


構成のイメージ

なぜ Webhook トリガーを使うのか?

Cloud Build ではビルドの状態が変化したときに cloud-buildsという Pub/Sub トピックにメッセージを送ります[1]。Cloud Build には PubSub トリガーがあるのでこれと紐付けたいところです。しかし、これを直接cloud-buildsトピックと紐づけることができません。

例えば以下のように Pub/Sub トリガーを作成しようとすると、エラーが出ます 😫

gcloud builds triggers create pubsub \
  --name=my-pubsub-trigger \
  --topic=projects/${PROJECT_ID}/topics/cloud-builds \
  --build-config="cloudbuild-webhook/cloudbuild.yaml" \
  --repo=https://www.github.com/sikeda107/tech-blog \
  --repo-type=GITHUB \
  --branch="main"

# エラーメッセージ
ERROR: (gcloud.builds.triggers.create.pubsub) INVALID_ARGUMENT: trigger should not listen on cloud-builds topic

Stack Overflow の回答[2]をみるに、メッセージを別の Pub/Sub に送信するという解決策が取れるようです。しかし、あらたにリソースをつくるとそれらも管理しないといけないので、別の方法が取れないか考えてみましょう。

Webhook トリガーをつかった連携方法

以下のリソースを作成します。

  1. 手動の Cloud Build トリガー
  2. Webhook の Cloud Build トリガー
  3. Cloud Pub/Sub のトピック
  4. Cloud Pub/Sub の Push 型サブスクリプション
  5. Secret Manager
  6. API Key

1. 起点となる Cloud Build トリガーを作成する

1 秒ごとに、echo するだけの手動トリガーを作成します。前提として、GitHub リポジトリはすでに接続済みとします。作成したら、動くかどうかテストしてみましょう。

# 1. トリガーを作成する
TRIGGER=publisher-trigger
gcloud builds triggers create manual \
  --name=$TRIGGER \
  --build-config="cloudbuild-webhook/cloudbuild.yaml" \
  --repo=https://www.github.com/sikeda107/tech-blog \
  --repo-type=GITHUB \
  --branch="main"

# 2. 動作確認
gcloud builds triggers run $TRIGGER \
  --branch="main"

2. 呼び出される側の Cloud Build トリガーを作成する

次に Pub/Sub からよびだされる トリガーを作成します。Webhook イベントを認証するためのシークレットが必要なので、先にシークレットを作成します。その後、作成したシークレットをつかって Webhook トリガーを作成します。
また、ペイロードバインディング[3]を使って Webhook の リクエストボディを置換変数に設定してます。こうすることで、呼び出し元のトリガーと同じコミットハッシュなどの値を参照することができます。さらに、この置換変数をつかってビルドを実行するか決めるフィルターを構成することができます。今回は「起動元のトリガー名」とそのトリガーの「成功」を条件にトリガーされるよう構成します。

# 1. シークレットを作成
SECRET_ID=cloudbuild-webhook-secret
gcloud secrets create $SECRET_ID \
  --replication-policy="automatic"

# 2. シークレットの値を設定
SECRET_VALUE=<任意のランダムな値>
echo -n $SECRET_VALUE | \
  gcloud secrets versions add $SECRET_ID --data-file=-

# 3. Webhookトリガーを作成
## ※不要な実行を防ぐため、承認を必要としています
SECRET=$(gcloud secrets describe $SECRET_ID --format="value(name)")
TRIGGER_SUB=subscriber-trigger
gcloud builds triggers create webhook \
  --name=$TRIGGER_SUB \
  --build-config="cloudbuild-webhook/cloudbuild.yaml" \
  --repo=https://www.github.com/sikeda107/tech-blog \
  --repo-type=GITHUB \
  --branch="main" \
  --secret="$SECRET/versions/1" \
  --substitutions=_BUILD_TRIGGER_ID='$(body.buildTriggerId)',_COMMIT_SHA='$(body.substitutions.COMMIT_SHA)',_SHORT_SHA='$(body.substitutions.SHORT_SHA)',_STATUS='$(body.status)',_TRIGGER_NAME='$(body.substitutions.TRIGGER_NAME)' \
  --subscription-filter='_TRIGGER_NAME.matches("publisher-trigger") && _STATUS.matches("SUCCESS")' \
  --require-approval

トリガー作成時に以下のエラーがでることがあります。

ERROR: (gcloud.builds.triggers.create.webhook) PERMISSION_DENIED:
service account service-000000000@gcp-sa-cloudbuild.iam.gserviceaccount.com
failed to access secret version for webhook trigger: generic::permission_denied:
Permission 'secretmanager.versions.access' denied for resource
'projects/000000000/secrets/cloudbuild-webhook-secret/versions/1'
(or it may not exist).

そのときは、「Cloud Build Service Agent」のサービスアカウントに対して、シークレットへのアクセス権を付与します。

PROJECT_ID=<YOUR_PROJECT_ID>
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(project_number)")
gcloud secrets add-iam-policy-binding $SECRET_ID \
  --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com" \
  --role='roles/secretmanager.secretAccessor'

3. cloud-builds Topic と Push Subscription を作成する

受信イベントを認証するために API キーを取得します。キーの ID は自動生成します。

# 1. APIキーの作成
gcloud services api-keys create \
  --display-name="Cloud Build Trigger API Key" \
  --api-target=service=cloudbuild.googleapis.com

# 2. APIキーの値を取得
KEY_NAME=$(gcloud services api-keys list --filter="displayName='Cloud Build Trigger API Key'" --format="value(name)")
gcloud services api-keys get-key-string $KEY_NAME

トリガーを連携するため、いよいよ Pub/Sub を構成します。トピックの名前はかならず cloud-buildsにしてください。サブスクリプションは push 型として作成して、ペイロードラップ解除を有効化[5]します。これを使わないと base64 でデコードする必要があり、今回それを自動でおこなうことはできないのでこの機能を利用します。

# 1. トピックの作成
TOPIC=cloud-builds
gcloud pubsub topics create $TOPIC

# 2. pushサブスクリプションの作成
SUBSCRIPTION=push-cloudbuild-trigger
API_KEY=<YOUR_API_KEY>
PUSH_ENDPOINT="https://cloudbuild.googleapis.com/v1/projects/$PROJECT_ID/triggers/${TRIGGER_SUB}:webhook?key=$API_KEY&secret=$SECRET_VALUE"
gcloud pubsub subscriptions create $SUBSCRIPTION \
  --topic $TOPIC \
  --push-endpoint=$PUSH_ENDPOINT \
  --push-no-wrapper


Webhook に push するサブスクリプション

4. 動かしてみる

成功する場合

まず正常に動くかどうか確かめます。publisher-trigger が成功したあと、subscriber-trigger が正常に実行されました。User substitutions に値がはいっていることも確認できます。


publisher-trigger の実行結果

subscriber-trigger の実行結果

失敗する場合

YAML ファイルのスクリプトで、強制的にエラー終了してみます。

cloudbuild-webhook/cloudbuild.yaml
steps:
  - id: 'wait-10-seconds'
    name: 'bash'
    script: |
      #!/usr/bin/env bash
+     exit 1
      for i in {10..1}; do
        echo "待機中... 残り${i}秒"
        sleep 1
      done
      echo "完了しました!"

実行したあと、yaml ファイルを元に戻して再度実行します。結果をみてみると、d297359の失敗のあと subscriber-trigger が実行されていないことが確認できます。その次に72aee70が実行され、ふたたび subscriber-trigger が同じコミットハッシュで実行されています。想定通りに動いていることが確認できました 🎉


それぞれの実行一覧

subscriber-trigger の実行結果

【プラス α】cloud-builds Topic にはどんなデータがくるのか

Webhook に必要な情報がメッセージとしてわたってきていることは確認できました。他にはどんな情報がトピックには送信されてくるのでしょうか?🧐
2 つの方法で確認してみます。

1. pull 型サブスクリプション を利用する

pull 型サブスクリプションを作成して、実際にどんなデータが入ってくるのかを確認してみます。

# 1. pull型サブスクリプションを作成
gcloud pubsub subscriptions create subscription-pull \
  --topic=$TOPIC

# 2. publisher-trigger を実行する
# 3. メッセージを pull する
## コマンド1
gcloud pubsub subscriptions pull subscription-pull --format="json"
## コマンド2
gcloud pubsub subscriptions pull subscription-pull --format="value(message.data)"

# 4. 最後に削除する
gcloud pubsub subscriptions delete subscription-pull
コマンド 1 - 全体の結果

attributes にステータスが入っています。ステータスはQUEUEDWORKINGSUCCESSなどの値があります[6]

[
  {
    "ackId": "dQNNHlAbEGEIBERNK0EPKVgUWQYyODM2LwgRHFEZDDsLRk1SK...",
    "message": {
      "attributes": {
        "buildId": "91e9b513-3aba-470a-ae10-9763fd9119cd",
        "status": "WORKING"
      },
      "data": "SGVsbG8gQ2xvdWQgUHViL1N1YiEgSGVyZSBpcyBteSBtZXNzYWdlIQ==",
      "messageId": "9573243245050455",
      "publishTime": "2024-05-17T13:47:19.812Z"
    }
  }
]
コマンド 2 - message.data の結果

gcloud で取得した data は自動でデコード処理をしてくれます。結果は一例です。

{
  "id": "8c9b326d-693a-471f-bd9f-20397d47a2e3",
  "status": "SUCCESS",
  "source": {
    "gitSource": {
      "url": "https://github.com/sikeda107/tech-blog.git",
      "revision": "9ba09500f55f3c4018bda1f778e953f46e9b69bc"
    }
  },
  "createTime": "2024-05-18T02:12:14.233561Z",
  "startTime": "2024-05-18T02:12:14.836262061Z",
  "finishTime": "2024-05-18T02:12:34.147932Z",
  "results": {
    "buildStepImages": [
      "sha256:890897682a8025c1e178b5ec6126b3b532ad8535f1e81dbf60bc2b7300b1bcf8"
    ],
    "buildStepOutputs": [""]
  },
  "steps": [
    {
      "name": "bash",
      "id": "wait-10-seconds",
      "timing": {
        "startTime": "2024-05-18T02:12:19.766123486Z",
        "endTime": "2024-05-18T02:12:33.435333494Z"
      },
      "status": "SUCCESS",
      "pullTiming": {
        "startTime": "2024-05-18T02:12:19.766123486Z",
        "endTime": "2024-05-18T02:12:22.719876637Z"
      },
      "script": "#!/usr/bin/env bash\nfor i in {10..1}; do\n  echo \"待機中... 残り${i}秒\"\n  sleep 1\ndone\necho \"完了しました!\"\n"
    }
  ],
  "timeout": "3600s",
  "projectId": "xxxxxxxxxxxxxxxxxxxx",
  "logsBucket": "gs://000000000.cloudbuild-logs.googleusercontent.com",
  "sourceProvenance": {
    "resolvedGitSource": {
      "url": "https://github.com/sikeda107/tech-blog.git",
      "revision": "9ba09500f55f3c4018bda1f778e953f46e9b69bc"
    }
  },
  "buildTriggerId": "16cb5965-8f82-4c6e-bb61-faa5e7ccf4a5",
  "options": {
    "substitutionOption": "ALLOW_LOOSE",
    "logging": "LEGACY",
    "dynamicSubstitutions": true,
    "pool": {}
  },
  "logUrl": "https://console.cloud.google.com/cloud-build/builds/8c9b326d-693a-471f-bd9f-20397d47a2e3?project=000000000000",
  "substitutions": {
    "REPO_NAME": "tech-blog",
    "BRANCH_NAME": "main",
    "REPO_FULL_NAME": "sikeda107/tech-blog",
    "REVISION_ID": "9ba09500f55f3c4018bda1f778e953f46e9b69bc",
    "SHORT_SHA": "9ba0950",
    "COMMIT_SHA": "9ba09500f55f3c4018bda1f778e953f46e9b69bc",
    "TRIGGER_NAME": "publisher-trigger",
    "TRIGGER_BUILD_CONFIG_PATH": "cloudbuild-webhook/cloudbuild.yaml",
    "REF_NAME": "main"
  },
  "tags": ["trigger-16cb5965-8f82-4c6e-bb61-faa5e7ccf4a5"],
  "timing": {
    "SETUPBUILD": {
      "startTime": "2024-05-18T02:12:18.469735093Z",
      "endTime": "2024-05-18T02:12:19.125541195Z"
    },
    "FETCHSOURCE": {
      "startTime": "2024-05-18T02:12:15.880615508Z",
      "endTime": "2024-05-18T02:12:18.469599018Z"
    },
    "BUILD": {
      "startTime": "2024-05-18T02:12:19.125626212Z",
      "endTime": "2024-05-18T02:12:33.435434338Z"
    }
  },
  "queueTtl": "3600s",
  "name": "projects/000000000/locations/global/builds/8c9b326d-693a-471f-bd9f-20397d47a2e3"
}

2. 検証用の Cloud Run を利用する

以下のコードを Cloud Run にデプロイします。受信したリクエストヘッダーとボディを 1 行で出力するだけのプログラムです。

https://github.com/sikeda107/tech-blog/blob/d60061041f6823aa41e3b6ec168d4f3507609b9c/cloudbuild-webhook/app/index.js#L4-L23
https://github.com/sikeda107/tech-blog/blob/d60061041f6823aa41e3b6ec168d4f3507609b9c/cloudbuild-webhook/app/Dockerfile#L1-L6

PROJECT_ID=<YOUR_PROJECT_ID>
SERVICE=server-node
REGION=asia-northeast1
gcloud run deploy $SERVICE --source . \
     --project $PROJECT_ID \
     --region $REGION \
     --ingress=all \
     --allow-unauthenticated

動作確認をしてみます。

URL=$(gcloud run services describe $SERVICE --region $REGION --format 'value(status.url)')
curl -H "X-MyHeader: 123" \
-H "Content-Type: application/json" \
-XPOST "$URL" \
-d '{ "msg": "Hello World !!"}'


Cloud Run 動作確認の結果

Cloud Run に対して push するサブスクリプションを作成します。作成したあと、publisher-trigger を実行します。

gcloud pubsub subscriptions create push-cloud-run \
  --topic $TOPIC \
  --push-endpoint=$URL \
  --push-no-wrapper

実行後どういったヘッダーとボディでリクエストされているかが確認できました。

Cloud Run のヘッダーログ

Cloud Run のボディログ

さいごに

Webhook トリガーと Pub/Sub のペイロードラップ解除をもちいることで、あらたにリソースを作ることなく Cloud Build トリガーを実行することができました。ただし、今回の方法では Pub/Sub のコンソール画面や gcloud コマンドによって、API キーやシークレットの値がみれてしまうというデメリットがあります。運用によってはこれは望ましくない場合もあるかと思いますので、注意が必要です。いつになるかわかりませんが、cloud-buildsから直接トリガーできるようになることを願ってます 🙏
以上、この記事がなにかの役に立てば幸いです 😊

参考にした資料

脚注
  1. ビルド通知へのサブスクリプション Cloud Build のドキュメント Google Cloud ↩︎

  2. Why can Google Cloud Build triggers not subscribed to Pub/Sub "cloud-builds" topic anymore? - Stack Overflow ↩︎

  3. ペイロード バインディングと bash パラメータの拡張を置換で使用する    Cloud Build のドキュメント Google Cloud ↩︎

  4. フィルタされていないトリガーに関連するリスク ↩︎

  5. Pub/Sub push サブスクリプションのペイロード ラップ解除 Pub/Sub ドキュメント Google Cloud ↩︎

  6. ステップとビルドのステータス ↩︎

コミューン株式会社

Discussion