ギックスのデータエンジニアの緒方です。
ギックスの Google Cloud の組織における想定外課金対策となる仕組みを作り導入しました。
このブログではその経緯と実装、運用を紹介します。
目次
想定外課金対策
想定外課金対策に至る経緯
想定外課金の発生
先日ギックスでは Google Cloud の利用料でそこそこ大きな想定外課金(金額はご想像におまかせします。)が発生しました。
原因は BigQuery ML において検証時と比較してデータ量が著しく増大していたにも関わらず料金的に問題ないと判断したことにありました。
このような想定外の課金は Cloud を利用している限りいつでも誰でも発生させうるものであり、今後同じようなことがあったときに一定金額以上の課金防ぐ必要があると判断し、仕組みを導入しました。
余談ですがいろんなデータエンジニアの方とお話しているとデータアナリストやデータサイエンティストが何十万円とか何百万円とかいう額の無駄な課金を発生させた、という話を時々聞きます。
規模の大きい組織の場合は何十万円くらいは気にならない、何なら個人が自由に使える範囲に吸収される、なんて話も聞きますが、ベンチャー企業にとっては死活問題です。
この記事がまだ想定外課金防止の仕組みを導入していない企業様の役に立つことを切に願います。
想定外課金防止の検討
ギックスが必要とする想定外課金対策
まずはどのような仕組みが必要か検討し、以下の観点が必要という結論が出ました。
- 想定外の課金が発生しないようにすること。
- 想定外の課金が発生した場合には速やかに自動的に課金を停止できること。
- 設定漏れが無いようにすべての Google Cloud プロジェクトに対して自動的に仕組みが導入されること。
1 については API 制限を設定することで課金の上限を設けることができます。
ギックスにおいても以前から BigQuery の API 制限をかけているプロジェクトが複数あります。
例えば Query usage per day per user
に対して TB 単位での制限をかけていることがあります。これによって、一部のユーザーが異常な使い方をしていても、設定値を上限とした課金に抑えることができます。
ただしこのやり方は各プロジェクトの1つ1つのAPIに設定しなければならず、どの API に対してどれくらいの制限を設けるか検討するだけでも時間がかかってしまいます。
BigQuery のようなギックス内では非常によく使うサービスに限定すれば一律に制限をかけることも可能ですが、その他多数のサービスの想定外課金を防ぐことができません。
1についても BigQuery や課金額が大きくなることが予想できるものについては設定するものの、2と3を満たす仕組みを別途導入したいところです。
そこで、今回はすべてのプロジェクトに対して課金額が設定を超えたらそのプロジェクトと請求アカウントを切り離すという仕組みを導入することにしました。
Google Cloud の公式ドキュメントに記載の方法との対象の違い
課金額が設定を超えた場合に請求アカウントを切り離す処理は Google Cloud の公式ドキュメントにも掲載されています。
参考:コスト管理の自動レスポンスの例
Cloud Cloud の公式ドキュメントを出発点として、課金停止の仕組みを検討しました。
今回は処理の対象となるものが異なります。
公式ドキュメント | ギックス | |
---|---|---|
対象プロジェクト | 1つ | 組織内のすべてのプロジェクト |
対象期間 | 月間(カスタム可) | 1日 |
対象期間については明確な記述はありませんが、予算通知ではカスタム期間以外の最小期間が月間のため、月間を想定しているものと思われます。
想定外課金対策の仕組み
想定外課金対策は大きく分けて3つの仕組みがあります。
- 超過課金検知
- あるプロジェクトの想定外課金が発生したことを検知する。
- 課金停止
- 請求アカウントと対象のプロジェクトを切り離す。
- 課金停止機能ステータスチェック
- この仕組みがうまく動いていることを検知する。
請求アカウントと対象のプロジェクトの切り離し方については公式ドキュメントの方法をほぼそのまま採用しました。
一方で想定外課金の発生の検知については公式ドキュメントでは予算アラートを使用する方法が掲載されていますが、ギックスでは BigQuery への Billing データのエクスポート を利用し、その集計結果をもとにアラートを出す方法を採用しました。
また想定外課金対策がなんらかの事情で動かなくなる可能性を考慮し、仕組みが動かなくなっていることを検知する仕組みを作りました。
アーキテクチャ図
アーキテクチャ図は次のようになります。
![](https://bst-image.imgix.net/prod-gixo/content/uploads/2023/11/Untitled-1.png?auto=compress%2Cformat&fit=scale&h=541&ixlib=php-3.3.0&w=1024&wpsize=large)
上記のようなアーキテクチャになった経緯を説明します。
超過課金検知
超過課金検知の検討
超過課金検知については公式ドキュメントに記載の方法をそのまま真似ることができないため、複数案を比較検討しました。
予算アラートを使用する場合の課題
予算アラートの場合、アラート対象となる予算の期間として設定できるものは次の4つになります。
- 年間
- 四半期
- 月間
- カスタム期間
ギックスでは課金額が設定以上になることを速やかに検知することを目標にしています。月間以上の期間になると、その検知が難しくなります。
例えば一律ですべてのプロジェクトに対して1日あたり10万円以上の課金があった場合にアラートを出したいとします。
月間での設定でこれを実現しようとすると、基本的に10万円の設定をせざるを得ません。しかしこの設定にしてしまうと1日5000円の課金があるプロジェクトなら20日目にアラートが出てしまいます。
またカスタム期間であれば1日単位で設定できるものの、具体的な日付を指定するため、毎日の設定が必要になってしまいます。
その場合は次のようなアーキテクチャを検討していました。
![](https://bst-image.imgix.net/prod-gixo/content/uploads/2023/11/Untitled-2.png?auto=compress%2Cformat&fit=scale&h=401&ixlib=php-3.3.0&w=1024&wpsize=large)
毎日予算超過アラートを設定するあたりの筋が悪そうで、これが主な原因となりこの方針はとりませんでした。
Billing API を使う場合の課題
Billing API で Google Cloud プロジェクトの課金額を取得してアラートとする方法も検討しました。
しかし探した限りではプロジェクト単位での課金額をそのまま取得できる API は存在せず、大きな粒度でもサービス単位での課金額しか取得できませんでした。
すべてのサービスに対して課金額を取得して集計することも可能ですが、それなりに煩雑な仕組みになりそうな点が懸念でした。
そこで考えたのが Billing データのエクスポートを用いた方法です。
Cloud Billing のデータエクスポート を用いた仕組み
Cloud Billing のデータエクスポートは BigQuery に課金状況を出力するサービスです。
参考:Cloud Billing データを BigQuery にエクスポートする
ギックスでは元々、課金状況をこの機能で BigQuery に出力しており、それを集計して Looker studio でレポートを作成して slack に出力しています。想定外課金が発生したのに素早く気づけたものこのレポートのおかげでした。
![](https://bst-image.imgix.net/prod-gixo/content/uploads/2023/11/Untitled-3.png?auto=compress%2Cformat&fit=scale&h=404&ixlib=php-3.3.0&w=1024&wpsize=large)
billing export では発生した課金に紐づく形で、プロジェクトや sku, 発生時刻などのカラムがあります。
ギックスでは標準の使用料金テーブルを利用しており、出力されるカラムは次のページで確認できます。
標準データのエクスポートの構造
これを使えばわざわざ Billing API を使わずとも楽に課金データを使うことができるため、この方法を採用することにしました。
課金停止
課金停止機能については公式ドキュメントに記載の方法をほとんどそのまま利用しています。
課金超過検知機能ステータスチェック
課金超過検知の workflows がずっと止まっていると困るためステータスチェックを作成します。
ただし毎時間実行する workflows が一度でも止まったら困るというものではないため、比較的ゆるい仕組みで十分です。
あまり悩むこともなく、Cloud Logging と Cloud Monitoring で手軽に作成することにしました。
実装
課金超過検知
課金超過検知は Cloud Scheduler から毎時間 Workflows を起動し、課金データを集計して一定金額を超えていたら Functions を呼び出します。
スケジューリング
「想定外の課金が発生した場合には速やかに自動的に課金を停止できること」を目標にしているため、毎時間呼び出すことにしました。
課金データの集計クエリ
次のようなクエリを使っています。
1 2 3 4 5 6 7 8 9 10 11 12 |
SELECT COALESCE(project.id, 'no_project') AS project_id, SUM(cost) AS sum_cost FROM `project_id.dataset.gcp_billing_export_v1_billing_id` WHERE TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) >= TIMESTAMP( TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL -2 DAY) ) GROUP BY project.id; |
_PARTITIONTIME はパーティショニング列で、スキャン料を軽減できるため使用します。
注意点として、_PARTITIONTIME は課金が発生したタイミングの記録ではなくレコードが入ってきたときのタイミングが記録されます。
Cloud Billing のエクスポートは課金が発生すると同時に出力されるものではなく、課金の発生と検知にはタイムラグがあることを認識しておくことが必要です。
絞り込みは INTERVAL -2 DAY
のように現在時刻から2日前としています。このようにすると実際の1日あたりの課金額に近くなります。
今回は BigQuery のクエリの責務は各プロジェクトのコストの取得に限定しました。しかし workflows の変数に格納できるメモリは512KBと小さく、プロジェクトが多い場合は事前に BigQuery で課金料の小さいプロジェクトに対して絞り込みを入れる必要があるかもしれません。
課金停止
Functions の実装
課金停止の Functions については公式ドキュメント記載の実装をほぼそのまま使っているため、そちらを参照してください。
権限についてハマったところがあるためその点だけ共有します。
- サンプルコード中の
getBillingInfo
メソッドについて- Functions に紐づくサービスアカウントに対して課金停止対象プロジェクト内の
resourcemanager.projects.get
権限が必要です。これが含まれるロールはプロジェクトの閲覧者です。- 課金停止のFunctions が使うサービスアカウントに対して、すべてのプロジェクトの
resourcemanager.projects.get
権限をつけるのは手間がかかるため、getBillingInfo
メソッドは使わない方針としました。
- 課金停止のFunctions が使うサービスアカウントに対して、すべてのプロジェクトの
- 一方で
updateBillingInfo
メソッドについては課金停止対象プロジェクトに対する権限は不要です。このあたりの権限については戸惑いを覚えました。
- Functions に紐づくサービスアカウントに対して課金停止対象プロジェクト内の
- Functions に紐付けるサービスアカウントが持つべき課金停止に必要な権限を持つロールの組み合わせの一例としては次になります。
- 請求先管理者(組織レベルの権限)
- プロジェクト請求管理者(プロジェクトレベルの権限)
課金超過検知機能ステータスチェック
Cloud Logging と Cloud Monitoring を使ってステータスチェックを実装しました。
方針としては workflows の一定時間内の実行回数が一定以下になった場合異常があるとして通知します。
以下のように手軽に実装できました。
Cloud Logging による指標作成
超過課金検知の実行成功回数の指標を作成しました。
Cloud Logging で次のようなクエリで指標を作成します。
1 2 3 4 |
resource.type="workflows.googleapis.com/Workflow" jsonPayload.state="SUCCEEDED" resource.labels.workflow_id="超過課金検知の workflows の ID" |
この設定により成功回数をカウントできます。
Cloud Monitoring のアラート設定
Cloud Monitoring のアラートで、6時間の window での実行成功回数が5回を下回った場合に課金超過検知機能が停止していると判断し、slack に通知するよう設定しました。
たまに実行が失敗する分には大きな問題はないため、成功回数を5回未満に設定しています。
テスト
実装中にうっかりすべてのプロジェクトの課金を停止すると大事になります。したがって意図しない課金停止を発生させないためにテストしながら実装を進めます。
今回のケースではステージング環境を用意することは実質できません。細心の注意を払いつつ、次の手順でテストしました。
- 万が一すべてのプロジェクトに対して課金停止した場合でも請求アカウントとプロジェクトをすぐに紐付けられるスクリプトを作成する。
- 課金停止の仕組みをテスト用の1つのプロジェクトに対して動作確認する。
- 課金停止を動作しないようにして、超過課金検知が想定通り動作することを確認する。
- テスト用のプロジェクトを作成し、設定値の境界値テストをする。
- 本番の課金額を設定する時に人と一緒にやって桁を間違えないようにする。
運用
権限管理
今回の仕組みは各プロジェクトの課金を停止することができます。課金を停止できるということはプロジェクトに大して多大な影響をもたらしうるということです。
したがって今回の仕組みに関連するリソースは気軽に触っていいものではなく、ごく限られた人間のみ編集できる状態にしなければなりません。
機微なリソースとその扱い
今回の例では次のリソースが該当します。
- 課金停止の権限を持つサービスアカウント
- 課金停止のサービスアカウントが紐づいた Functions
- 各プロジェクトの課金上限が記された設定ファイル
今回は以下の理由から設定ファイルのみ拒否ポリシーを用いてアクセス制限をかけました。
- 設定ファイルは慣れていない人がうっかり誤った更新をする可能性がある。
- 例えば課金上限の申請を忘れたりめんどくさがって自分で設定ファイルを更新する際にうっかりするケースを想定しています。
- サービスアカウントと Functions についてはうっかり使ったり更新することがまずありえない。
- 悪意を持った人が意図的に更新する可能性は残っているものの、監査ログがあるためいつ誰が課金停止のサービスアカウントを悪用したかがわかるようになっています。
- またそもそも想定外課金対策のプロジェクトにアクセスできる人が極めて少数なため、現時点では悪用される心配がありません。
アクセス制限のかけかた
拒否ポリシーは GA になってからまだ1年程度しか経っておらず、記事もあまり多くありません。詳細はドキュメントを読んでいただくとして、ハマったポイントを共有します。
- denial_condition の expression で使用できるのはタグのみ。
- 公式ドキュメント中に
ただし、拒否条件はリソースタグ関数のみを認識します。
という一文があるのを見逃しており、ハマりました。 - したがって拒否条件にはタグを使うしかなく、タグを付けられないリソースは拒否ポリシーを設定できません。
- 参考:タグをサポートするサービスリソースの一覧
- 公式ドキュメント中に
- denial_condition の expression のタグで
matchTag
が想定通りに動かない。- これは実装が悪かったのかもしれませんが、
matchTag
でキーとタグを指定しても true が返ってきませんでした。 - 代わりに
matchTagId
を利用しました。 - 参考:IAM Conditions 属性のリファレンス リソースタグ
- これは実装が悪かったのかもしれませんが、
社内への通知
仕組みの導入の通知
全社会議で仕組みを導入することを通知しました。
通知内容としてはざっくりと以下の通りです。
- 今後すべての GCP プロジェクトで1日あたりの課金額の上限を自動的に20万円に設定する。
- したがって20万円を超えると自動的に課金が止まる。
- 予め20万円を超えることが予想されるプロジェクトは申請必須。
また合わせて課金に対する啓蒙として、課金額が大きくなりがちかつ事前に課金額を予測するのがむずかしい AI 系のサービスを使う時は特に課金に気をつける旨を伝えました。
申請手順の整理
2つの申請手順を整理しました。
- 想定外課金の上限の設定申請
- ごく一部の設定をできる権限を持つ人に slack で申請するという手順をドキュメントにまとめました。
- 想定外課金が発生してしまった場合の課金有効化申請
- 想定外課金の原因を特定した後に課金を有効化する必要があります。課金の有効化もごく一部の人しか権限を持たないため、申請制にして手順をドキュメントにまとめました。
終わりに
実装する立場としては機微なリソースがあったり間違った設定をした場合に一大事になってしまうのでなかなか緊張感がありました。
この仕組みがあることで今後は GCP は比較的安心して使うことができます。
とはいえこの仕組みでは日頃の小さな無駄遣いまでは防ぐことができません。
引き続き啓蒙活動をしていくとともに、定期的なリソースの棚卸しにも挑戦したいと思いました。