概要
Cloud Buildでのデプロイ失敗を検知し、Discordに通知する仕組みを構築してみたので備忘録としてまとめます。Cloud Run、Cloud Pub/Sub、Cloud Loggingを組み合わせて、自動通知システムを実現します。
アーキテクチャ
今回やりたいことのイメージ図です
前提条件
- GCPプロジェクトの作成済み
- 必要な権限の設定
- Cloud Run 管理者
- Cloud Build 管理者
- Cloud Logging 管理者
- Cloud Pub/Sub 管理者
- Discord Webhookの設定済み
手順
1. Cloud Pub/Sub トピックの作成
- トピックID:
cloud-run-deploy-errors
- デフォルトのサブスクリプションを追加する
- Googleが管理する暗号鍵
2. 通知サービスの実装
deploy-notify.ts
import express from 'express'
import { fetch } from 'undici'
const app = express()
const port = process.env.PORT || 8080
interface DiscordEmbed {
title?: string
description?: string
color?: number
fields?: Array<{
name: string
value: string
inline?: boolean
}>
timestamp?: string
}
const sendDeploymentNotification = async (
message: { message: { data: string } },
context: { eventId: string; timestamp: string },
): Promise<void> => {
try {
if (!message.message?.data) {
throw new Error('Pub/Sub メッセージデータが存在しません')
}
const data = Buffer.from(message.message.data, 'base64').toString()
const logEntry = JSON.parse(data)
// Cloud Buildのエラー情報を取得
const buildId = logEntry.resource?.labels?.build_id || '不明なビルドID'
const projectId = logEntry.resource?.labels?.project_id || '不明なプロジェクト'
const triggerId = logEntry.resource?.labels?.build_trigger_id || '不明なトリガー'
const errorCode = logEntry.protoPayload?.status?.code
const serviceName = logEntry.protoPayload?.serviceName || '不明なサービス'
const methodName = logEntry.protoPayload?.methodName || '不明なメソッド'
const timestamp = logEntry.timestamp || context.timestamp
// エラーメッセージの生成
const errorMessage = `ビルドエラーが発生しました。\nエラーコード: ${errorCode}\nメソッド: ${methodName}`
const discordWebhookUrl = process.env.DISCORD_WEBHOOK_URL as string
if (!discordWebhookUrl) {
throw new Error('Discord Webhook URL が設定されていません。')
}
const embed: DiscordEmbed = {
title: '⚠️ Cloud Build 失敗',
description: errorMessage,
color: 0xdc3545,
fields: [
{
name: 'プロジェクト',
value: projectId,
inline: true,
},
{
name: 'ビルドID',
value: buildId,
inline: true,
},
{
name: 'トリガーID',
value: triggerId,
inline: true,
},
{
name: 'サービス',
value: serviceName,
inline: true,
},
],
timestamp: timestamp,
}
await fetch(discordWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] }),
})
console.log('Discord 通知送信完了:', JSON.stringify(embed))
} catch (error) {
console.error('通知送信失敗:', error)
throw error
}
}
// JSONパーサーを追加
app.use(express.json())
// ヘルスチェック用エンドポイント
app.get('/', (_req, res) => {
res.status(200).send('OK')
})
// Pub/Subメッセージを受け取るエンドポイント
app.post('/', async (req, res) => {
try {
await sendDeploymentNotification(req.body, {
eventId: req.body.message?.id || '',
timestamp: new Date().toISOString(),
})
res.status(200).send('OK')
} catch (error) {
console.error('Error processing message:', error)
res.status(500).send('Internal Server Error')
}
})
app.listen(port, () => {
console.log(`Server is running on port ${port}`)
})
Dockerfile
FROM node:22.13.1-slim
WORKDIR /app
# pnpmのインストール
RUN npm install -g pnpm@10.2.0
# package.jsonをコピー
COPY package.json ./
COPY deploy-notify.ts ./
COPY tsconfig.json ./
# 依存関係のインストール
RUN pnpm install --no-frozen-lockfile
# ビルド
RUN pnpm run build
ENV NODE_ENV=production
ENV PORT=8080
ENV DISCORD_WEBHOOK_URL=""
EXPOSE 8080
CMD [ "pnpm", "start" ]
cloudbuild.yaml
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/deploy-notify:$COMMIT_SHA'
- '-f'
- 'Dockerfile'
- '.'
dir: 'functions'
# Push the container image to Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/deploy-notify:$COMMIT_SHA'
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'deploy-notify'
- '--image'
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/deploy-notify:$COMMIT_SHA'
- '--region'
- '${_REGION}'
- '--platform'
- 'managed'
- '--allow-unauthenticated'
# Clean up old images
- id: delete-old-images
name: asia-docker.pkg.dev/gcr-cleaner/gcr-cleaner/gcr-cleaner-cli:latest
args:
- --repo=${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/deploy-notify
- --keep=1
- --tag-filter-any=.*
options:
logging: CLOUD_LOGGING_ONLY
images:
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_REPOSITORY}/deploy-notify:$COMMIT_SHA'
3. Cloud Runサービスの作成
Cloud Run > 関数を作成
- Github(monorepoで外出しの関数もリポジトリ内で管理したいので)
- リポジトリを選択
- ブランチを選択
- ビルドタイプ:
Dockerfile
- サービス名:
notify-deploy-error
- リージョン:
asia-northeast1(東京)
ここでEventarc トリガーの設定もまとめてできます
- 認証
- 認証が必要
他の設定項目はデフォルトでサービスを作成します
作成したらAPIサーバーなどのコンテナ同様に環境変数を設定します(今回はDiscordのWebhookURL)
4. Eventarc トリガーの設定
Cloud Runサービスと Pub/Subトピックを接続するために、Eventarcトリガーを設定します。
- Cloud Run > deploy-notify サービスを選択
- トリガー タブを選択
- トリガーを作成をクリック
- 以下の設定を行う:
- トリガー名:
deploy-notify-trigger
- イベントプロバイダー:
Cloud Pub/Sub
- イベント:
google.cloud.pubsub.topic.v1.messagePublished
- トピック:
cloud-run-deploy-errors
- サービスアカウント: デフォルトのCompute Engine サービスアカウント
- トリガー名:
5. Cloud Logging シンクの設定
- Logging > ログルーターに移動
- シンクを作成をクリック
- 以下の設定を行う:
- シンク名:
deploy-notify-sink
- シンクの宛先:
- シンクサービス:
Cloud Pub/Sub
- シンクの宛先:
cloud-run-deploy-errors
- シンクサービス:
- シンクに含めるログの選択:
resource.type="build" resource.labels.project_id="${project_id}" severity>=ERROR
- シンク名:
シンクに含めるログの選択のときにログをプレビュー
からデプロイエラーのログを拾えていることを確認してください
6. 動作確認
Cloud Buildで意図的にエラーを発生させ、Discordに通知が送信されることを確認します
推奨設定
-
Cloud Build トリガー設定
- デプロイエラー用のコンテナトリガーと、APIサーバーなどのトリガーが分かれているため、含まれるファイルフィルタと無視されるファイルフィルタを適切に設定する
- 例えば、Cloud Run関数専用のディレクトリを指定することで、同じリポジトリの他のコード変更によって不要なビルドがトリガーされるのを防ぐ
まとめ
本記事では、Cloud Buildのデプロイ失敗を検知し、Discordに通知する仕組みを構築しました。
Cloud Run関数にリブランディングされてから初めて触れましたが、Gitとの連携や設定の手軽さが大きく向上していると感じました。
今後、機会があれば、より複雑なアーキテクチャの構築にも挑戦してみたいと思います。