GCPの料金アラートをSlackに出してみる|sitateru tech blog

シタテルの技術やエンジニアの取り組みを紹介するテックブログです。

2020年9月4日金曜日

GCPの料金アラートをSlackに出してみる

 AWSやGCPの料金、たまに気になりますよね。

そういえば今月どれくらい料金食ってるんだ?と思ったとき、もちろん各サービスのコンソールで課金管理のページを開けば確認できますね。
が、日常的にはお知らせが届くほうが簡単ですむよね、ということでslackに通知をさせてみました。

AWSは簡単だったのですがGCPはちょっと手間だったのでまとめておきたいと思います。

まあエンジニアとしては課金の具合を気にせず開発に集中できれば一番いいのですが、現実には会社の予算とかいろいろありますし私は課金周りの把握をする役回りもあるので、これはやっておきたいところです。


1. Pub/Subトピック作成

まずは適当なプロジェクトを用意し、Pub/Sub でトピックを作成します。

2.GCPで予算の作成

次は予算の作成です。GCPのメニューから「お支払い」→「予算とアラート」と進み、予算の作成をクリック。

予算と予算アラートの設定 | Cloud Billing | Google Cloud

  • まずは料金を集計する対象を選択します。
    プロジェクトやサービスを絞り込んだり、特定のラベルを付けたリソースだけを対象にすることもできます。

  • 予算は設定された額に対して実際の課金額が達したら通知されるようになっています。設定額は、「自由に設定」と「先月の額」から選べるんですね。
    ちなみに5000兆円は設定できませんでした🥺

  • 最後に設定額の何%になった時点でアラート出すかのしきい値を設定します。実値or予測値で、パーセンテージは自由に設定できます。

あと、先ほど作成したPub/Subトピックを忘れずに接続しておきましょう。


3. Slack App作成

Slackに通知するために、Appを作成してトークンを発行します。

コスト管理の自動レスポンスの例 | Cloud Billing | Google Cloud

https://api.slack.com/appsCreate New App をクリックし、適当なAPP名と導入したいワークスペース名を入力してアプリを作成します。

メニューの OAuth & Permissions を開き、 Bot Token Scopes chat:write スコープを追加します。


ページ上部の Install App to Workspace をクリックし、ワークスペースにAppをインストールします。

インストールして戻ってくれば、アクセストークンが得られます。



4. Function作成

さてCloud Functionsを作ります。ここがメインディッシュです。

GCPの料金アラートは、常に1時間に3回程度送信される仕様なようです。なのでPub/Subで受け取った通知をすべてslackに出しているといらない通知ばかり頻繁に来てしまいます😇

そこでFirestoreをちょこっと使って、しきい値突破時の通知だけをslackに投げるようにFunctionを作ってみました。

まず、Pub/Subに送られてくるデータは一例として以下のようになってます。

(詳しくはドキュメントを参照してください)

プログラムによる予算アラート通知を管理する | Cloud Billing | Google Cloud

{
  "budgetDisplayName": "sitateru",
  "alertThresholdExceeded": 0.8,
  "costAmount": 43210,
  "costIntervalStart": "2020-08-01T07:00:00Z",
  "budgetAmount": 50000,
  "budgetAmountType": "SPECIFIED_AMOUNT",
  "currencyCode": "JPY"
}

そこで、アルゴリズムとしては、

  • Pub/Subから来たデータを確認
  • しきい値に達したときの通知であれば
    alertThresholdExceeded か forecastThresholdExceeded というキーが含まれるので、キーの存在チェック
  • budgetDisplayNamealertThresholdExceededcostIntervalStartbudgetAmountType の値の組み合わせを見て、
    • Firestore上にその値のセットのデータがなければslackで通知+Firestoreに値セットを保存
    • Firestore上にその値のセットのデータがあれば何もしない

というような設計にすればだいじょうぶだあ、と思いつつそこから少し調整してこのようなコードになりました。

const slack = require('slack')
const Firestore = require('@google-cloud/firestore')

// slackbotのアクセストークン
const BOT_ACCESS_TOKEN = process.env.BOT_ACCESS_TOKEN
// 投稿先slackチャンネル名
const CHANNEL = process.env.SLACK_CHANNEL
// GCPの予算ページに飛べるようにURLを入れる
const BUDGET_URL = process.env.BUDGET_URL

const collection = new Firestore({
  projectId: process.env.PROJECT_ID,
  keyFilename: process.env.KEYFILE
}).collection('billing_alert_slack')

exports.billingAlertSlack = async (pubsubEvent, context) => {
  const pubsubData = JSON.parse(Buffer.from(pubsubEvent.data, 'base64').toString())

  const postSlack = async (fields) => {
    const messageFields = Object.entries(fields).map(datum => {
      return {
        "title": datum[0],
        "value": datum[1],
        "short": true
      }
    })
    await slack.chat.postMessage({
      token: BOT_ACCESS_TOKEN,
      channel: CHANNEL,
      text: '',
      attachments:  [{
        "color": "#1a73e8",
        "title": "GCP Billing Alert",
        "title_link": BUDGET_URL,
        "fields": messageFields
      }]
    })
    return 'Slack notification sent successfully'
  }

  if(pubsubData.hasOwnProperty("alertThresholdExceeded") || pubsubData.hasOwnProperty("forecastThresholdExceeded")) {
    const ThresholdExceeded = pubsubData.hasOwnProperty("alertThresholdExceeded") ? pubsubData.alertThresholdExceeded : pubsubData.forecastThresholdExceeded
    const querySnapshot = await collection.where('budgetDisplayName', '==', pubsubData.budgetDisplayName)
      .where('ThresholdExceeded', '==', ThresholdExceeded)
      .where('budgetAmountType', '==', pubsubData.budgetAmountType)
      .get()
    if (querySnapshot.docs.length) {
      const data = querySnapshot.docs[0].data()
      if (pubsubData.costIntervalStart != data.costIntervalStart) {
        // update document
        querySnapshot.docs[0].ref.update({ costIntervalStart: pubsubData.costIntervalStart })
        // post slack
        return await postSlack(pubsubData)
      }
    } else {
      // add document
      collection.add({
        budgetDisplayName: pubsubData.budgetDisplayName,
        ThresholdExceeded: ThresholdExceeded,
        budgetAmountType: pubsubData.budgetAmountType,
        costIntervalStart: pubsubData.costIntervalStart
      })
      // post slack
      return await postSlack(pubsubData)
    }
  } else {
    return 'Slack notification was not sent'
  }
}

上のコードで出てきた環境変数ですが、

  •  BOT_ACCESS_TOKEN 手順3で作ったSlack App用アクセストークン
  • SLACK_CHANNEL 通知先のSlackチャンネル名
  • BUDGET_URL Slackの通知からすぐ飛べるように、GCPの予算ページのURL
を入れています。

ということでGCPの予算通知がSlackに送られてくるようになりました。
このような見た目になります。

もう少し各項目の意味が分かりやすくなるように改良してもいいんじゃないかとは思いつつ、だいぶ課金の額を気にする作業が身近になったなあと感じています。
, , ,