対象者(読むと嬉しくなるかもしれない人)
- GCPのBilling周りの通知をメール以外のプロダクトに飛ばしたい人
- Datadogを使用している人
前書き
GCPのBillingは現状メールアドレス宛てにしか飛ばせず、予算とアラート
の機能でPub/Sub トピックにデータを送ったりもできるんですが、これが予算を超えると1時間毎にPub/Subにデータが送られてくるんですよね。アプリ側で1度送信されたらそのフラグを管理するようにすればよいのですが、フラグを管理するためだけにデータソースを用意するのも大仰だな。と思ったことと折角BigQueryに出力しているのであればそれを使えばよいか。と思ったので作ってみました。
また、現状AWSのコストもDatadogに送っていることやDatadog側でMonitoringの種別を色々便利に使える(例えばForcastMonitoringで使用料金の上がり幅が急激だったり)といったことも理由にあります。
ちなみにメールからZapier等を使ってメールからslackに飛ばしてもいいんですが、自分の会社の環境だと連携が定期的に削除されてしまい、通知が来ないこともあったので作ってみました。同じ問題で困っていてかつDatadogを利用している方と割と対象者が限定的ですが送信先を変えれば何にでも使えると思うので参考になればと思います。
実装
コードはこちらにおいておきました。
内容は解説するほどもないくらいシンプルでcloud.google.com/go/bigquery
ライブラリを使って当月の各サービスごとの料金を取得し、github.com/zorkian/go-datadog-api
ライブラリを使ってDatadogのCustom Metricsに送っているだけです。
成功するとこのようにMetricsにデータが送信されます。
また、from
に description:datadog
といった形でサービス名をセットすることでサービス毎の料金が確認できます。
デプロイ
自分たちのサービスはKubernetesなので簡単にHelmにしておきました。
コードはこちらです。
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が増えてくるとディスクも結構食ってしまうのでセキュリティを優先しつつ軽量化も考えていきたいですね。