【Google Cloud】Cloud Buildのビルド結果をSlackに通知する
はじめに
Rehab for JAPANでCTOを務めている@rkyuragiです。
Rehabではデータ分析基盤やMLOps基盤でGoogle Cloudを活用しており、
各基盤をデプロイするため、Cloud Buildを利用してCI/CDを実現しています。
弊社はコミュケーションツールとしてSlackを利用しているため、
ビルド結果はSlackに通知してビルドのステータスを楽に把握できるようにしたいと考えました。
今回は、Cloud Buildのビルド結果をSlackに通知する方法(Pythonベース)をご紹介します。
ちなみに、今回の方法で構築した場合、特定のCloud Buildだけではなく全てのビルド結果をSlackに通知することができるので、プロジェクトで一度設定しておけばビルドが増えても都度設定する必要がありません。
公式ドキュメントとしては、この辺りが参考になります。
(ただ、初見だとよくわからない。。。)
本記事のターゲット
- Cloud Buildを利用している方
- Cloud Buildのビルド結果をSlackに通知する方法を知りたい方
要約
Cloud Buildのビルド結果をSlackに通知するために、
Cloud Buildのビルド結果をPub/Subに通知し、Pub/SubからCloud Run関数を呼び出してSlackに通知する方法を紹介します。
Cloud Buildのビルド結果をSlackに通知する方法
以下の手順でCloud Buildのビルド結果をSlackに通知する方法を説明します。
0. 事前準備
- Google Cloudのプロジェクトを作成
- Google CloudのAPIを有効化(抜けがあったらスミマセン)
- Cloud Build API
- Cloud Pub/Sub API
- Cloud Run API
- Cloud Functions API
- Secret Manager API
- IAM API
- SlackのWebhook URLを取得
- ローカル環境にgcloudコマンドをインストール(デプロイするため)
1. 各処理の実装
ディレクトリ構成は以下の通りです。
.
├── app
│ ├── main.py
│ └── requirements.txt
├── .env
└── deploy.sh
1-1. メイン処理
./app
ディレクトリを作成し、
./app/main.py
に以下を記述する。
import base64
import json
import logging
import os
from urllib.parse import urlparse
import requests
from flask import Request
# ロガーの設定
logging.basicConfig(level=logging.INFO)
# SlackのIncoming Webhook URL
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
CLOUD_BUILD_STATUS_STATUS_UNKNOWN = "STATUS_UNKNOWN"
CLOUD_BUILD_STATUS_QUEUED = "QUEUED"
CLOUD_BUILD_STATUS_WORKING = "WORKING"
CLOUD_BUILD_STATUS_SUCCESS = "SUCCESS"
CLOUD_BUILD_STATUS_FAILURE = "FAILURE"
CLOUD_BUILD_STATUS_INTERNAL_ERROR = "INTERNAL_ERROR"
CLOUD_BUILD_STATUS_TIMEOUT = "TIMEOUT"
CLOUD_BUILD_STATUS_CANCELLED = "CANCELLED"
STATUS_COLOR_MAP = {
CLOUD_BUILD_STATUS_STATUS_UNKNOWN: "#FFA500",
CLOUD_BUILD_STATUS_QUEUED: "#0072EF",
CLOUD_BUILD_STATUS_WORKING: "#0063A7",
CLOUD_BUILD_STATUS_SUCCESS: "#00EF7D",
CLOUD_BUILD_STATUS_FAILURE: "#FF0000",
CLOUD_BUILD_STATUS_INTERNAL_ERROR: "#FF0000",
CLOUD_BUILD_STATUS_TIMEOUT: "#FF0000",
CLOUD_BUILD_STATUS_CANCELLED: "#FF0000",
}
SLACK_MESSAGE_TEMPLATE = """
*Project ID*: {project_id}\n
*Build ID*: {build_id}\n
*TRIGGER Name*: {trigger_name}\n
*GitHub Repository*: <{git_url}|{git_repo_name}>\n
*Logs*: <{log_url}|View Build Logs>
"""
def get_repo_name_from_url(url: str) -> str:
"""GitリポジトリのURLからリポジトリ名を取得する"""
# URLを解析
parsed_url = urlparse(url)
# パスをスラッシュで分割し、最後の要素を取得
repo_name = parsed_url.path.split("/")[-1]
# '.git'を削除してリポジトリ名のみ取得
if repo_name.endswith(".git"):
repo_name = repo_name[:-4]
return repo_name
def send_slack_notification(title: str, message: str, color: str = "#FF0000") -> None:
"""Slackに通知を送信する"""
webhook_url = SLACK_WEBHOOK_URL
payload = {
"attachments": [
{
"title": title,
"text": f"{message}",
"mrkdwn_in": ["text"],
"color": color,
}
]
}
headers = {"Content-Type": "application/json"}
response = requests.post(webhook_url, data=json.dumps(payload), headers=headers)
if response.status_code != 200:
raise ValueError(
f"Request to slack returned an error {response.status_code}, the response is:\n{response.text}"
)
def app(request: Request) -> str:
"""Pub/Subメッセージを受け取り、Slackに通知するCloud Function"""
try:
# Pub/Subメッセージを取得
envelope = request.get_json()
if not envelope:
logging.error("No Pub/Sub message received")
return ("Bad Request: No Pub/Sub message", 400)
pubsub_message = envelope.get("message", {}).get("data", "")
if not pubsub_message:
logging.error("No data in Pub/Sub message")
return ("Bad Request: No message data", 400)
# Base64デコードしてメタデータを取得
message_data = base64.b64decode(pubsub_message).decode("utf-8")
build_metadata = json.loads(message_data)
# Cloud Buildのビルド情報を取得
status = build_metadata.get("status", "UNKNOWN")
build_id = build_metadata.get("id", "N/A")
git_url = (
build_metadata.get("source", {}).get("gitSource", {}).get("url", "N/A")
)
git_repo_name = get_repo_name_from_url(git_url)
trigger_name = build_metadata.get("substitutions", {}).get(
"TRIGGER_NAME", "N/A"
)
project_id = build_metadata.get("projectId", "N/A")
log_url = build_metadata.get("logUrl", "N/A")
if trigger_name == "N/A":
logging.warning("Trigger name is not available")
return ("not target.", 200)
# Slackメッセージを組み立てる
color = STATUS_COLOR_MAP.get(status, "#FF0000")
slack_message = SLACK_MESSAGE_TEMPLATE.format(
project_id=project_id,
build_id=build_id,
git_repo_name=git_repo_name,
git_url=git_url,
trigger_name=trigger_name,
log_url=log_url,
)
if status == CLOUD_BUILD_STATUS_SUCCESS:
status = f"✅{status}"
elif status in (
CLOUD_BUILD_STATUS_FAILURE,
CLOUD_BUILD_STATUS_INTERNAL_ERROR,
CLOUD_BUILD_STATUS_TIMEOUT,
CLOUD_BUILD_STATUS_CANCELLED,
):
status = f":no_entry:{status}"
# ビルドが失敗した場合は@hereで通知
slack_message = "<!here>\n" + slack_message
title = f"Cloud Build Notification: {status}"
send_slack_notification(title, slack_message, color)
logging.info(f"Notification sent for build: {build_id}")
return ("Success", 200)
except Exception as e:
logging.error(f"Error: {str(e)}")
return ("Internal Server Error", 500)
1-2. 必要ライブラリの設定
./app
ディレクトリに
./app/requirements.txt
に以下を記述する。
Flask==2.3.3
requests==2.25.1
google-cloud-secret-manager==2.7.0
1-3. 環境変数の設定
./.env
に以下を記述する。
SLACK_WEBHOOK_URL="https://hooks.slack.com/xxxxxxx
1-4. デプロイスクリプトの作成
./deploy.sh
に以下を記述する。
source .env
FIRST_DEPLOYMENT=true # 初回デプロイの場合はtrueにする
PROJECT_ID="project_id" # Google CloudプロジェクトID
REGION="us-central1" # デプロイするリージョン
# 関数の設定
FUNCTION_NAME="cloudbuild-notifier"
ENTRY_POINT="app"
RUNTIME="python311" # 使用するランタイムを指定
SOURCE_PATH="./app" # ソースコードのディレクトリ
SERVICE_ACCOUNT_NAME="pubsub-cloud-run-invoker"
SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud config set project $PROJECT_ID
gcloud config set run/region $REGION
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format="value(projectNumber)")
echo $PROJECT_NUMBER
if [ $FIRST_DEPLOYMENT = true ]; then
gcloud pubsub topics create cloud-builds
printf ${SLACK_WEBHOOK_URL} | gcloud secrets create CLOUD_BUILD_NOTIFIERS_SLACK_WEBHOOK_URL --data-file=-
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
--display-name "Pub/Sub Cloud Run Invoker"
gcloud secrets add-iam-policy-binding CLOUD_BUILD_NOTIFIERS_SLACK_WEBHOOK_URL \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
fi
gcloud functions deploy $FUNCTION_NAME \
--gen2 \
--runtime=$RUNTIME \
--region=$REGION \
--entry-point=$ENTRY_POINT \
--source=$SOURCE_PATH \
--trigger-http \
--timeout=30s \
--max-instances 1 \
--service-account=${SERVICE_ACCOUNT} \
--set-secrets SLACK_WEBHOOK_URL=projects/${PROJECT_NUMBER}/secrets/CLOUD_BUILD_NOTIFIERS_SLACK_WEBHOOK_URL/versions/latest
if [ $FIRST_DEPLOYMENT = true ]; then
gcloud pubsub subscriptions create cloud-builds-to-cloud-run \
--topic=cloud-builds \
--push-endpoint=https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME} \
--push-auth-service-account="${SERVICE_ACCOUNT}"
fi
2. デプロイ
以下のコマンドを実行してデプロイします。
sh deploy.sh
デプロイに成功すると、以下の画像のようにCloud Run関数が作成されます。
通知の確認
そして、Cloud Buildのビルドが実行されると、以下の画像のようにSlackにビルド進捗が通知されます。
(通知アイコンはSlack Webhookの設定画面でCloud Buildの画像を登録してます。)
ビルドが開始した場合の通知
ビルドが成功した場合の通知
まとめ
今回は、Cloud Buildのビルド結果をSlackに通知する方法をご紹介しました。
Cloud Buildのビルド結果をPub/Subに通知し、Pub/SubからCloud Run関数を呼び出してSlackに通知することで、
ビルドのステータスをリアルタイムで把握できるようになります。
参考になれば幸いです!
Discussion