GCP予算アラートをSlackに通知する

# 目的

GCPで予算を使いすぎた(使いすぎそうになった)ときにアラートをSlackに飛ばしたい。
そのための管理コストも最小限にしたい。

# 通知の流れ

1. 予算アラート -> 
1. GCP pub/sub -> 
1. GCP functions -> 
1. Slack Incoming webhook ->
1. Slack channel

になります。こんな設定、エンジニア以外出来ないだろと行った感じですね。エンジニアがやっても面倒でした。Googleどうにかしろ。

# それぞれの設定

## 1. 予算アラートの作成

予算が超過した場合、または、超過しそうな場合にアラートを発生させ、アカウントユーザーに対してメールを送信できます。
**しかし、予算通知の場合は、トリガーの設定は反映されません。**
常に1日のうちに何度も通知が実行されます。
そのため、トリガー相当の機能は通知プログラム内で実装する必要があります。
(ただし、トリガーを設定しておくと条件を満たした場合のみ後述の特定のフィールドが含まれるようになります。)

予算アラートの作成は、お支払い > 予算とアラート から行います。

![GCP_budget_alert_alert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/8ded7ee5-f4bb-ee27-223d-6086df218e36.png)

予算の額だけ設定しておいてください。「指定額」と「先月の使用額」を選べます。

## 2. GCP pub/subのトピック作成

[Pub/sub console](https://console.cloud.google.com/cloudpubsub/topic/list)から、予算アラートを受け取るTopicを作成してください。
作成はトピックIDだけを入力して後はデフォルトで大丈夫です。

![GCP_budget_alert_pubsub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/68e2284b-2356-70df-619c-464ae91ff3cd.png)

作成が完了したら、先程の予算アラート設定の最下部で、先ほど作成したトピックを登録します。

![GCP_budget_alert_register_pubsub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/2115db92-b226-2d86-dbf5-5aeca6df193f.png)


## 3. Slack incoming webhookの作成

[Slackの説明](https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8)に従って、通知を出したいチャンネルにIncoming webhookを設定してWebhook URLを生成してください。こちらのURLをこの後に使います。

また、Webhookには[新旧のWebhook](https://qiita.com/kshibata101/items/0e13c420080a993c5d16)が存在します。Payloadの仕様が違います。本投稿は新しいWebhookで作成しています。

## 4. GCP functionsの関数作成

Pub/subからイベントを受け取り、Slackに通知を行う処理を[Functions](https://console.cloud.google.com/functions/list)に作成します。

名前は任意、トリガーをCloud Pub/Subに変更し、先程作成したトピックを指定してください。ソースコードは、下載のコードで、Incoming webhookのURL部分だけを置換して、設定してください。実行する関数は”alertBudget"を指定してください。

![GCP_budget_alert_create_functions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/d3306134-1844-30e4-f0a1-022ba4e35fcb.png)


```javascript:index.js
const { IncomingWebhook } = require("@slack/client");

/**
 * Background Cloud Function to be triggered by Pub/Sub.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} context
 */
exports.alertBudget = async (event, context) => {
  const pubsubMessage = event.data;
  const decoded = pubsubMessage.data ? Buffer.from(pubsubMessage.data, 'base64').toString() : '{}';
  const data = JSON.parse( decoded );

  // 予算をオーバーした場合はSlackに通知を投げる
  if (data.costAmount > data.budgetAmount) {
    const body = {
      "text": "GCP予算超過があります\n" + 
      `*現在の消化予算*:${data.costAmount} ${data.currencyCode}`
    };
    // こちらのURLを、各自のIncoming webhookのURLに置き換えてください。
    const webhook = new IncomingWebhook('https://hooks.slack.com/services/XXXXX');
    await webhook.send(body);
  } else {
    console.log(`${data.costAmount}/${data.budgetAmount}`);
  }

  return 'Slack notification sent successfully';
  
};
```

```json:package.json
{
    "name": "alert-budget-http",
    "version": "0.0.1",
    "dependencies": {
        "@slack/client": "^3.15.0" 
    }
}
```

## 5. 通知のテスト

このようなJsonがPub/Subからは送られてきます。
dataに指定されている文字列は、下記のような内容がBase64でエンコードされた文字列が入っています。

```json:RequestBodyFromPub/Sub
{
  "data": "ew0KICAgICJidWRnZXREaXNwbGF5TmFtZSI6ICJuYW1lLW9mLWJ1ZGdldCIsDQogICAgImFsZXJ0VGhyZXNob2xkRXhjZWVkZWQiOiAxLjAsDQogICAgImZvcmVjYXN0VGhyZXNob2xkRXhjZWVkZWQiOiAxLjAsDQogICAgImNvc3RBbW91bnQiOiAxMDAuMDEsDQogICAgImNvc3RJbnRlcnZhbFN0YXJ0IjogIjIwMTktMDEtMDFUMDA6MDA6MDBaIiwNCiAgICAiYnVkZ2V0QW1vdW50IjogMTAwLjAwLA0KICAgICJidWRnZXRBbW91bnRUeXBlIjogIlNQRUNJRklFRF9BTU9VTlQiLA0KICAgICJjdXJyZW5jeUNvZGUiOiAiVVNEIg0KfQ=="
}
```


```json:RawDataBody
{
    "budgetDisplayName": "name-of-budget",
    "alertThresholdExceeded": 1.0,
    "forecastThresholdExceeded": 1.0,
    "costAmount": 100.01,
    "costIntervalStart": "2019-01-01T00:00:00Z",
    "budgetAmount": 100.00,
    "budgetAmountType": "SPECIFIED_AMOUNT",
    "currencyCode": "USD"
}
    
```
alertThresholdExceededはトリガーに設定している実値の閾値を超えている場合のみ含まれます。(閾値を超えていない場合は、このフィールド含まれません。)
forecastThresholdExceededはトリガーに設定している推定値を超えている場合のみに含まれます。


通知のテストを行いたい場合は、ファンクションのテストで、RequestBodyFromPub/Subの内容をコピペして「関数をテストする」を実行してください。

![GCP_budget_alert_test_send.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/eec103e1-93ae-09df-de3b-3abbb4e99c7f.png)

正しく動いていれば、Slackに次のような通知が飛びます。(文面は自分用に多少いじっています。)
![GCP_budget_alert_message_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12647/95fda9b9-080c-9361-ff1d-1486940ec9eb.png)



以上になりますが、長かった。。。
しかし、これ一度予算上限を超えてしまうと何度も通知されちゃうな。トリガー相当の実装は骨が折れすぎる。。。


# その他みつけた実現手段

* 旧Webhookを使ったものが比較的多くGoogle検索で引っかかります。基本的には同じなので、今回のFunctionsの実装は、旧Webhookでも動作するものになっています。
* [Googleの公式](https://cloud.google.com/billing/docs/how-to/notify?hl=ja)では、Slackへの通知に、Slackボットユーザーを利用しています。

# 参考

* [中野智文のブログ](http://nakano-tomofumi.hatenablog.com/entry/2018/08/22/190752)
* 見た目をいじりたい場合 -> [Payloadドキュメント](https://api.slack.com/reference/messaging/payload)