くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Cloud Buildの結果をCloud FunctionでSlackに通知する

最近、Cloud RunのデプロイをCloud Buildでやっているけど、
ビルドの完了とか失敗をSlackで通知したいなと思って、いろいろ調べたときの備忘録。

ソースコードは、GitHubで公開してます。
https://github.com/memory-lovers/cloudbuild-slack

Cloud Buildの結果を通知する方法

公式ドキュメントにもSlack通知に関して書かれているけど、
通知用のCroud Runを別途用意しないといけないので、すこしハードルが高い。。

Cloud Build Notifier は Cloud Run でコンテナとして実行される Docker イメージです。

Slack 通知を構成する  |  Cloud Build のドキュメント  |  Google Cloud

いろいろ見てみると、

Cloud Buildは、すべてのビルドイベントの更新とビルドメタデータcloud-buildsトピックのPub/Subに送信します。

とのことなので、Cloud FunctionsのPub/Subトリガーを使って、Slack通知ができるっぽい。

Cloud Functionsも、

  • GCPのCloud Functionsと
  • Cloud Functions for Firebase

の2つがあって、Firebaseを使ってるなら、
Cloud Functions for Firebaseに寄せてしまうのが良い感じ。

Firebaseの場合

シンプルな形はこんな感じ。
slackの通知部分は@slack/webhookを利用。

import * as functions from "firebase-functions";
import { IncomingWebhook } from "@slack/webhook";

const WEBHOOK_URL = functions.config().slack.webhook_url;
const webhook = new IncomingWebhook(WEBHOOK_URL);

export const notifyBuild = functions
  .region("asia-northeast1")
  .pubsub.topic("cloud-builds")
  .onPublish(async (message: functions.pubsub.Message) => {
    // messageを取得
    const body = message.data
      ? Buffer.from(message.data, "base64").toString()
      : null;

    if (!body) return;
    const data = JSON.parse(body);
    
    // messageの中身から通知する情報を取得
    const status = data.status;
    const repoName = data.substitutions?.REPO_NAME;
    const logUrl = data.logUrl;
    
    // Cloud Function for Firebaseのデプロイなどは
    // repoNameがないので、スキップ
    if (!repoName) return;
    
    // ステータスがWORKINGのときはスキップ
    if (status == "WORKING") return;

    // Slackへ通知
    await webhook.send({
      text: `${status}, repo: ${repoName}, ${logUrl}`,
    });
  });

Webhook URLは環境変数から取得しているので、デプロイ前に設定が必要。

$ firebase functions:config:set slack.webhook_url="YOUR_WEBHOOK_URL"

Environment configuration  |  Firebase

設定したらデプロイすればOK

$ firebase deploy --only functions:notifyBuild

通知する内容もカスタマイズできる

await webhook.send({
  text: "通知するテキスト",
  username: "通知するアカウントの名前";
  icon_emoji: "通知するアカウントのアイコン絵文字";
  icon_url: "通知するアカウントのアイコンのURL";
  channel: "通知するチャネル";
  link_names: true; // "ユーザ名やチャネルをリンクにするフラグ";
  attachments: []; // message attachments
  blocks: []; // Block Kit UI components
  unfurl_links: true; // "テキストURLをリンクにするフラグ";
  unfurl_media: true; // "メディアURLを展開するかどうかのフラグ";
});

Firebaseだと、デプロイも簡単(´ω`)

GCPのCloud Functionsの場合

こんな感じ。ほとんどFirebase verと変わらない。

import { IncomingWebhook } from "@slack/webhook";

const WEBHOOK_URL = process.env.WEBHOOK_URL || "";
const webhook = new IncomingWebhook(WEBHOOK_URL);

export const notifyBuild = async (message: any) => {
  // messageを取得
  const body = message.data
    ? Buffer.from(message.data, "base64").toString()
    : null;

  if (!body) return;
  const data = JSON.parse(body);

  // messageの中身から通知する情報を取得
  const status = data.status;
  const repoName = data.substitutions?.REPO_NAME;
  const logUrl = data.logUrl;

  // Cloud Function for Firebaseのデプロイなどは
  // repoNameがないので、スキップ
  if (!repoName) return;
  
  // ステータスがWORKINGのときはスキップ
  if (status == "WORKING") return;

  // Slackへ通知
  await webhook.send({
    text: `${status}, repo: ${repoName}, ${logUrl}`,
  });
};

Webhook URLは環境変数から取得しているので、.env.yamlを用意しておく。

# .env.yaml
WEBHOOK_URL: "YOUR_WEBHOOK_URL"

デプロイはちょっとめんどう。。

$ gcloud functions deploy notifySlackBuild \
    --runtime=nodejs14 \
    --region=asia-northeast1 \
    --trigger-topic=cloud-builds \
    --project=<YOUR_PROJECT_ID> \
    --env-vars-file=./.env.yaml \
    --entry-point=notifyBuild
  • notifySlackBuild: 関数名
  • --runtime: ランタイムの設定。
  • --region: リージョン
  • --trigger-topic: Pub/Subトリガーで購読するトピック名
  • --project: プロジェクトのID
  • --env-vars-file: 環境変数のファイル。上記で作成したもの
  • --entry-point: 関数名とモジュール名が違う場合、モジュール名を指定

これでOK!!

ハマったところ

Cloud Buildの結果は自分が設定したもの以外もある

この部分。

// Cloud Function for Firebaseのデプロイなどは
// repoNameがないので、スキップ
if (!repoName) return;

なにもせずにそのままにしていたら、
Cloud Functions for Firebaseをデプロイしたときにも通知が。。

GitHub連携している場合は、リポジトリ名が取得できるので、
それを見て判断するようにしている。

関数名はGCPとFirebaseで共通

両方の名前を同じにしたら、上書きされてしまった。。

GCPの方を関数名だけ変えようとしたけど、うまく行かず。。
--entry-pointが必要だったよう。

--entry-pointを指定しない場合は、同じモジュール名を探してくるらしい。

参考にしたサイトさま