Webhook を使って Cloud Build トリガーのビルド完了後に次のトリガーを自動実行する
はじめに
ある 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 トリガーをつかった連携方法
以下のリソースを作成します。
- 手動の Cloud Build トリガー
- Webhook の Cloud Build トリガー
- Cloud Pub/Sub のトピック
- Cloud Pub/Sub の Push 型サブスクリプション
- Secret Manager
- 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 ファイルのスクリプトで、強制的にエラー終了してみます。
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
にステータスが入っています。ステータスはQUEUED
やWORKING
、SUCCESS
などの値があります[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 行で出力するだけのプログラムです。
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
から直接トリガーできるようになることを願ってます 🙏
以上、この記事がなにかの役に立てば幸いです 😊
参考にした資料
- Cloud Build Webhook トリガーを始めよう(応用編) by Rnrn google-cloud-jp Medium
- Cloud Build Pub/Sub トリガーで別プロジェクトのトピックをサブスクライブする #GoogleCloud - Qiita
Discussion