GCPのBillingをDatadogに送るアプリをGoで作った

# 対象者(読むと嬉しくなるかもしれない人)
- GCPのBilling周りの通知をメール以外のプロダクトに飛ばしたい人
- Datadogを使用している人

# 前書き
GCPのBillingは現状メールアドレス宛てにしか飛ばせず、`予算とアラート`の機能でPub/Sub トピックにデータを送ったりもできるんですが、これが予算を超えると1時間毎にPub/Subにデータが送られてくるんですよね。アプリ側で1度送信されたらそのフラグを管理するようにすればよいのですが、フラグを管理するためだけにデータソースを用意するのも大仰だな。と思ったことと折角BigQueryに出力しているのであればそれを使えばよいか。と思ったので作ってみました。
また、現状AWSのコストもDatadogに送っていることやDatadog側でMonitoringの種別を色々便利に使える(例えばForcastMonitoringで使用料金の上がり幅が急激だったり)といったことも理由にあります。

ちなみにメールからZapier等を使ってメールからslackに飛ばしてもいいんですが、自分の会社の環境だと連携が定期的に削除されてしまい、通知が来ないこともあったので作ってみました。同じ問題で困っていてかつDatadogを利用している方と割と対象者が限定的ですが送信先を変えれば何にでも使えると思うので参考になればと思います。

# 実装
コードは[こちら](https://github.com/s-okayama/gcp-billing-sender/blob/main/app/main.go)においておきました。

内容は解説するほどもないくらいシンプルで`cloud.google.com/go/bigquery`ライブラリを使って当月の各サービスごとの料金を取得し、`github.com/zorkian/go-datadog-api`ライブラリを使ってDatadogのCustom Metricsに送っているだけです。
成功するとこのようにMetricsにデータが送信されます。
![スクリーンショット 2024-08-16 23.03.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1313152/9752b1da-8309-fc95-c360-12b8ff5fc615.png)

また、`from`に `description:datadog` といった形でサービス名をセットすることでサービス毎の料金が確認できます。
![スクリーンショット 2024-08-16 23.04.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1313152/dc79ca66-f5b9-b0e1-70ab-071cf544e0f7.png)

# デプロイ
自分たちのサービスはKubernetesなので簡単にHelmにしておきました。
コードは[こちら](https://github.com/s-okayama/gcp-billing-sender/tree/main/environment/helm/templates)です。
```
helm template ./ | kubectl apply -f -
```

## 権限周り(ハマったところ)
前項のkubernetes manifestで記述しているとおり、`serviceAccountName: gcp-billing` でworkloadIdentity経由で権限を付与してあげます。
GKEを使っていればIAMを直接kubernetesのServiceAccountに割り当てられるので↓のようなSAは作らなくてもよいのですが、BigQueryの場合は`google_bigquery_dataset_iam_member` を使ってdataset単位で権限付与してあげる必要があるので、今回はGoogleServiceAccountを払い出してそこに権限付与しています。

```
# gcp-billing
resource "google_service_account" "gcp_billing" {
  account_id   = "gcp-billing"
  display_name = "gcp-billing"
}

resource "google_bigquery_dataset_iam_member" "gcp_billing_bigquery_dataviewer" {
  for_each = toset([
    "roles/bigquery.dataViewer"
  ])
  dataset_id = "billing"
  role       = each.key
  member     = "serviceAccount:${google_service_account.gcp_billing.email}"
}

resource "google_project_iam_member" "gcp_billing_bigquery_jobuser" {
  project = var.project
  for_each = toset([
    "serviceAccount:${google_service_account.gcp_billing.email}"
  ])
  role   = "roles/bigquery.jobUser"
  member = each.key
}

resource "google_service_account_iam_member" "gcp_billing_workload_identity_user" {
  service_account_id = google_service_account.gcp_billing.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project}.svc.id.goog[sre/gcp-billing]"
}
```

# 監視設定
Datadogの監視設定も貼っておきます。`datadog`としている部分をサービス毎に増やしていけば自動的に監視が追加できます。
```
variable "gcp_billing" {
  type = map(map(string))

  default = {
    datadog = {
      message_budget_1 = <<-EOT
        {{#is_alert}}
        @slack-Org-channelName
        {{/is_alert}}
        {{#is_warning}}
        @slack-Org-channelName
        {{/is_warning}}
        {{#is_recovery}}
        {{/is_recovery}}
      EOT
      budget           = "0"
    }
  }
}

resource "datadog_monitor" "gcp_budget_1" {
  for_each = var.gcp_billing

  name  = "{{#is_alert}}[Budget] ${each.key}のコストが$ {{eval \"int(threshold)\"}}(100％)を超過しました。現在の利用金額は$ {{eval \"int(value)\"}} です。{{/is_alert}}{{#is_warning}}[Budget] ${each.key}のコストが$ {{eval \"int(warn_threshold)\"}}(70％)を超過しました。現在の利用金額は$ {{eval \"int(value)\"}} です。{{/is_warning}}"
  type  = "metric alert"
  query = "max(last_1d):max:gcp.billing.total{description:${each.key}} >= ${each.value.budget * 1.0}."

  monitor_thresholds {
    critical = each.value.budget * 1.0
    warning  = each.value.budget * 0.7
  }

  require_full_window = false
  notify_no_data      = false
  timeout_h           = 0
  evaluation_delay    = 660

  include_tags = false
  message      = each.value.message_budget_1
}
```
`gcp_budget_1` と記述しているのは、`gcp_budget_n`には以下のような形でアラートを増やしたいからです。
```
  monitor_thresholds {
    critical = each.value.budget * 0.9
    warning  = each.value.budget * 0.5
  }
```

# おまけ
自社では最近`bullseye`を使うことが多く、自分も最初はbullseyeを使っていたのですが、最後にDockerfile周りのベストプラクティスを見ながら細かい調整していたところ、作業中にやたらとimageのpushとpullに時間が掛かっていたことを思い出しました。そういえばサイズってどのくらいなんだっけ？と思って確認してみると799MBとかなりでかい。
マルチステージビルドもしていなかったので、まぁこんなもんかなと思っていたのですが、alpineと比べると結構な差がありました。
```
golang                                            1.23.0-bullseye     87f0914bbaf7   2 days ago     740MB
golang                                            1.23.0-alpine3.20   ec9c7c392a17   2 days ago     244MB
```

マルチステージビルドにしてalpineでbuildしてみると21倍という結構な差になりましたね。
```
docker-registry.uzabase.com/sre/gcp_billing       v1.1.3            fd9e33de801b   2 hours ago    37.8MB
docker-registry.uzabase.com/sre/gcp_billing       v1.1.2            4584d536e6a5   5 hours ago    799MB
docker-registry.uzabase.com/sre/gcp_billing       v1.1.1            4ccedd3b10fb   11 hours ago   769MB
```
imageが増えてくるとディスクも結構食ってしまうのでセキュリティを優先しつつ軽量化も考えていきたいですね。