Attribute™ at re:Invent, Nov 30 to Dec 4.Book my merch
Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

FinOps as Code 第2回:応用パターンとTerraformで運用するテナント

このページはEnglish、Deutsch、Español、Français、Italiano、Portuguêsでもご覧いただけます。

Hannes HayashiBy Hannes HayashiOct 12, 202618 min read

第1回では、予算、アロケーション、アラート、レポートをバージョン管理下に置きました。その後、DoiT Terraformプロバイダーは約2か月で5つのマイナーリリース(v1.6〜v1.10)を公開しています。

プロバイダーを利用する組織の数も、そのAPIトラフィックも、当時から大きく増加しました。トラフィックの大半を占めるのはアロケーションで、プロバイダー経由で予算を読み取っている組織はすべて、作成・更新・削除を問わず、書き込みにもプロバイダーを使っています。

そこで本記事では、2つの方向から掘り下げます。

  1. コアリソースをより深く。 FinOps as codeを数か月運用してはじめて見えてくるパターンです。1つのチームマップからアロケーション、予算、アラートを生成し、plan時にヘルスチェックを行い、レポートをPDFとしてコンソールの外へ持ち出します。
  2. テナントそのものへと、より広く。 顧客全体の設定、カスタムロール、ユーザーのオンボーディング、トークンをローテーションするサービスアカウント。最後には、誰の個人APIキーも使わずに、週次のFinOpsレポートがGitHub Actionsで動くようになります。

プロバイダーは現在20のリソースと85のデータソースをカバーしています。以下のスニペットはすべて、この記事に掲載する前に実際のDoiTテナントでテスト済みです(適用し、ドリフトの有無を再planで確認し、安全な範囲でdestroyしています)。その多くはプロバイダーのドキュメントのサンプルにも含まれています。

旧v1.x系リリースからのアップグレード

第1回の記事のあとにセットアップした場合は、おそらくv1.5以前を使っているはずです。ほとんどのチームはすぐにアップグレードしており(アクティブな組織の大多数はすでにv1.6以降)、アップグレード自体も通常はスムーズです。ただし、途中でいくつかの非推奨属性が削除されており、つまずきやすいのは次の点です。

バージョン 変更内容 対応方法
v1.7 リストデータソースのmax_resultsが数値に 引用符を削除: max_results = 50
v1.8 doit_budget.scopeを削除 scopesを使用: すべてのアロケーションIDをvaluesリストに持つ1つのフィルター
v1.8 doit_alert.config.attributionsを削除 config.scopesを使用
v1.8 doit_report.config.metricを削除 config.metricsを使用(最大4つ)
v1.8 doit_allocationコンポーネントのinverse_selectionを削除 inverseを使用
v1.8 レポートレイアウトのcsv_export / sheets_exportを削除 tableなどの可視化レイアウトを選択
v1.8 request_timeoutは120s超が必須(デフォルト150s) カスタム値を引き上げるか削除
v1.8 Insightsのリソースとデータソースを削除 上流のInsights APIが非推奨化されたため
v1.10 limit_by_changeの演算子は短縮コードに >、>=、…の代わりにgt、gte、lt、lte、b、nb
v1.10 doit_user.job_functionを削除 job_titleを使用

非推奨属性の削除ガイドに、各削除項目の移行手順が記載されています。

また、v1.8以降では追加の作業なしで堅牢性も向上しています。レポートは非同期実行APIを通じて実行され、プロバイダーが送信・ポーリング・結果の取得を代行します。レート制限されたリクエストは適切にバックオフし、遅いリクエストはローカルで先にタイムアウトするのではなく、API自身のエラーを表示します。

コアFinOpsリソースの応用パターン

アロケーション、予算、アラート、レポートは第1回で扱った4つのコアリソースで、プロバイダーのAPIトラフィックの大半を占めています。

アロケーションのスケール運用:1つのマップですべてのチームを

チームごとにアロケーション、予算、アラートを定義している構成なら、コピー&ペーストはやめましょう。代わりに、チームを一度だけ定義します。

locals {
teams = {
payments = { budget = 12000, label = "payments" }
search = { budget = 8000, label = "search" }
platform = { budget = 20000, label = "platform" }
}
}
# チームごとに1つのシングルアロケーション。異常検知を有効化
resource "doit_allocation" "team" {
for_each = local.teams
name = "Team: ${each.key}"
description = "All costs labeled owner=${each.value.label}"
anomaly_detection = true
rule = {
formula = "A"
components = [{
type = "label"
key = "owner"
mode = "is"
values = [each.value.label]
}]
}
}
# 同じチームを、上のシングルアロケーションをselectする1つのグループアロケーションとして定義
resource "doit_allocation" "by_team" {
name = "Cost by Team"
description = "Team allocations combined into one group"
unallocated_costs = "Unowned"
rules = [for team in doit_allocation.team : {
action = "select"
id = team.id
}]
}

anomaly_detectionはv1.7でシングルアロケーションに対して書き込み可能になったため、異常検知の監視は、誰かが覚えておかなければならないチェックボックスではなく、構成の中に組み込めます。グループアロケーションも、チームアロケーションを再定義するのではなくselectで選択しています。

ヒント:グループは必ずselectで構築しましょう。 action = "create"でグループルールをインラインに定義することもでき、DoiT APIは裏側でまったく同じシングルアロケーションを作成します。しかし、それらのアロケーションは独立したTerraformリソースではないため、インラインルールではanomaly_detectionやfolder_idなどを設定できません。シングルアロケーションを分けて定義しselectすれば、きちんと管理された本物のリソースが手に入ります。

すべてのチームが異常検知付きの本物のアロケーションを持つようになったので、どのチームが異常を発生させているかを問い合わせ、最もコストの高い異常についてAIによる解説を取得できます。

# すべての異常(max_resultsなしのため全ページを取得)。
# コストの高いものが先頭に来るようにソート
data "doit_anomalies" "by_cost" {
sort_by = "costOfAnomaly"
sort_order = "desc"
}
locals {
# 1つの異常は複数のアロケーションに属する場合があるため、それぞれに計上する
anomaly_cost_by_allocation = {
for name, costs in {
for pair in flatten([
for a in data.doit_anomalies.by_cost.anomalies : [
for al in a.allocations : { name = al.name, cost = a.cost_of_anomaly }
]
]) : pair.name => pair.cost...
} : name => { anomalies = length(costs), total_cost = sum(costs) }
}
}
data "doit_anomaly_explanation" "costliest" {
count = length(data.doit_anomalies.by_cost.anomalies) > 0 ? 1 : 0
id = data.doit_anomalies.by_cost.anomalies[0].id
}
output "costliest_anomaly_explanation" {
value = one(data.doit_anomaly_explanation.costliest[*].explanation.text)
sensitive = true
}

異常のallocations属性(v1.10)は、その異常が属するすべてのアロケーションを一覧し、doit_anomaly_explanation(v1.8)は生成された解説を、その根拠となる事実とともに返します。テストテナントでは、最上位の異常について「Amazon SNS Tier 1のリクエスト量の急増、特にUS West 2およびUS West 1リージョンでの増加が原因と考えられる」という結果が返ってきました。

アロケーションに追従する予算

チームマップができていれば、予算はfor_eachをもう1つ書くだけです。予算はSlackにも通知でき、v1.8以降はapply前にSlack設定が検証されます。

resource "doit_budget" "team" {
for_each = local.teams
name = "Team: ${each.key}"
type = "recurring"
time_interval = "month"
currency = "USD"
amount = each.value.budget
start_period = 1767225600000 # 2026-01-01T00:00:00Z(ミリ秒)
alerts = [{ percentage = 80 }, { percentage = 100 }]
scopes = [{
type = "allocation_rule"
id = "allocation_rule"
mode = "is"
values = [doit_allocation.team[each.key].id]
}]
collaborators = [{ email = data.doit_current_user.me.email, role = "owner" }]
recipients = [data.doit_current_user.me.email]
recipients_slack_channels = [{
id = "C0123456789"
customer_id = "your-customer-id"
workspace = "your-slack-workspace"
}]
}

チームの追加はlocal.teamsへの1行の変更で済み、アロケーション、予算、そして(次のセクションで扱う)アラートが自動的に追従します。

v1.8以降、doit_budgetsはDoiTのサーバーサイドのリスク分類も公開しています。予算ごとのrisk_status(onTrack、atRisk、unknown)が取得でき、フィルタリングも可能です。これをTerraformのcheckブロックと組み合わせれば、terraform planのたびに、どのチーム予算が危険な状況に向かっているかを教えてくれます。

check "team_budgets_on_track" {
data "doit_budgets" "team_at_risk" {
name_contains = "Team: "
filter = "riskStatus:atRisk"
}
assert {
condition = data.doit_budgets.team_at_risk.row_count == 0
error_message = "Budgets at risk: ${join(", ", [for b in data.doit_budgets.team_at_risk.budgets : b.budget_name])}"
}
}

checkブロックが出すのはエラーではなく警告なので、applyをブロックすることはありませんが、planをレビューする人には問題が見えます。AIが提案する予算については、doit_budget_suggestionsデータソース(v1.7)が、信頼度と提案金額つきで保留中の推奨事項を一覧するので、プルリクエスト上で人間がレビューできます。

有効にする価値のあるアラート

当社の利用データでは、アラートは4つのコアリソースの中で最も使われておらず、Terraformでアラートを管理している組織は予算に比べてはるかに少数です。これは埋める価値のあるギャップです。というのも、7月以降で最も実用的な追加機能のうち2つがアラートに入ったからです。

パーセンテージ変化アラートの古くからの課題はノイズです。1日2ドルのサービスが10ドルに跳ね上がると、わずか8ドルのために「+400%」のアラートが届きます。ignore_values_range(v1.9)は、メトリクス値が範囲内にある評価をすべてスキップし、recipients_slack_channelsはチームが実際に見ている場所にアラートを届けます。

resource "doit_alert" "team_spike" {
for_each = local.teams
name = "Team ${each.key}: daily cost spike"
config = {
metric = { type = "basic", value = "cost" }
time_interval = "day"
condition = "percentage-change"
operator = "gt"
value = 30 # パーセント
currency = "USD"
ignore_values_range = {
lower_bound = 0
upper_bound = 50 # 1日あたりのUSD
}
# サービスごとに個別評価するため、1つのサービスの急増だけでも発火する
evaluate_for_each = "fixed:service_description"
scopes = [{
type = "label"
id = "owner"
mode = "is"
values = [each.value.label]
}]
}
recipients = [data.doit_current_user.me.email]
recipients_slack_channels = [{
id = "C0123456789"
shared = true
}]
}

境界値はメトリクスの単位(ここでは1日あたりのUSD)であり、パーセントではなく、condition = "percentage-change"にのみ適用される点に注意してください。共有SlackチャンネルにはIDとshared = trueを、接続済みワークスペースのチャンネルにはIDとworkspace = "<name>"を指定します。

レポート:Top Movers、予測、新しいビジュアル

v1.6のレポート関連の追加機能のうち、2つが際立っています。

Top Movers。 limit_by_changeは期間比の変化で行をフィルタリングし、limit_aggregationは除外された行の表示方法を制御します。このレポートは、合計コストが1,000ドル超で、かつ50%以上成長したサービスだけを残します。

resource "doit_report" "top_movers" {
name = "Top Movers"
description = "Services with significant month-over-month growth"
config = {
metrics = [{ type = "basic", value = "cost" }]
aggregation = "total"
data_source = "billing"
time_interval = "month"
dimensions = [{ id = "service_description", type = "fixed" }]
time_range = {
mode = "last"
amount = 3
include_current = true
unit = "month"
}
metric_filter = {
metric = { type = "basic", value = "cost" }
operator = "gt"
values = [1000]
operand = "series_total"
}
limit_by_change = {
metric = { type = "basic", value = "cost" }
change_type = "percentage"
operator = "gte"
values = [50]
include_incomplete_data = false
}
limit_aggregation = "top"
display_values = "actuals_only"
layout = "table"
currency = "USD"
}
}

予測とテーマ。 forecast_settingsは過去データから将来の支出を予測し、v1.8のレイアウト(column_and_line_chart、sankey_chart、trend_board、cumulative_comparison)で見せ方の幅が広がりました。display_settings.theme_idを追加すれば、レポートにブランドカラーが適用されます。

resource "doit_custom_theme" "brand" {
name = "Brand"
primary_color = "#1A73E8"
colors = {
light = ["#1A73E8", "#34A853", "#FBBC04", "#EA4335", "#A142F4", "#24C1E0"]
dark = ["#8AB4F8", "#81C995", "#FDD663", "#F28B82", "#C58AF9", "#78D9EC"]
}
}
resource "doit_report" "forecast" {
name = "Spend Forecast"
description = "Last 12 months of spend with a 3-month forecast"
config = {
metrics = [{ type = "basic", value = "cost" }]
aggregation = "total"
time_interval = "month"
currency = "USD"
data_source = "billing"
time_range = {
mode = "last"
amount = 12
unit = "month"
include_current = true
}
dimensions = [
{ id = "year", type = "datetime" },
{ id = "month", type = "datetime" },
]
forecast_settings = {
mode = "totals"
historical_time_intervals = 12
future_time_intervals = 3
}
layout = "column_and_line_chart"
display_values = "actuals_only"
sort_dimensions = "a_to_z"
display_settings = {
theme_id = doit_custom_theme.brand.id
number_scale = "thousands"
}
}
}

1つ注意点があります。予測値はまだレポートの結果やエクスポートには含まれません。一方、テーマは最後まで引き継がれます。次のセクションで紹介します。

レポートがコンソールの外へ:PDFとPNGのエクスポート

誰もコンソールを開くことなく、DoiTからレポートをファイルとして取り出せるようになりました。v1.10以降、doit_report_resultとdoit_report_queryはfile_output = "pdf"または"png"を受け付け、署名付きダウンロードURLを返します。

data "doit_report_result" "forecast_pdf" {
id = doit_report.forecast.id
file_output = "pdf"
lifecycle {
postcondition {
condition = self.file_output_url != null
error_message = "PDF rendering failed. Try applying again."
}
}
}
data "http" "forecast_pdf" {
url = data.doit_report_result.forecast_pdf.file_output_url
}
resource "local_sensitive_file" "forecast_pdf" {
filename = "${path.module}/spend-forecast.pdf"
content_base64 = data.http.forecast_pdf.response_body_base64
}

PDFは、ヘッダーに組織名が入った洗練された複数ページのドキュメントで、カスタムテーマも引き継がれます。上の予測レポートはdisplay_settings.theme_idを設定しているため、PDFもブランドカラーで描画されます。

local_sensitive_fileをaws_s3_objectに差し替えれば(同じcontent_base64を受け取ります)、PDFはバケットに保存されます。レポートの実行はDoiTの非同期実行APIを経由するため、重いレポートには時間がかかることがあります。必要に応じて、timeouts = { read = "30m" }でデータソースの読み取りタイムアウトを引き上げてください。

保存済みレポートすら必要ありません。doit_report_queryはアドホックなクエリも同じようにレンダリングできるため、READMEのグラフ、プルリクエストのコメント、Wikiページに最適です。

data "doit_report_query" "top_services_png" {
file_output = "png"
config = {
metrics = [{ type = "basic", value = "cost" }]
aggregation = "total"
time_interval = "month"
currency = "USD"
time_range = {
mode = "last", amount = 3, unit = "month", include_current = false
}
dimensions = [
{ id = "year", type = "datetime" },
{ id = "month", type = "datetime" },
]
# コスト上位5サービス
group = [{
id = "service_description"
type = "fixed"
limit = { value = 5, sort = "desc", metric = { type = "basic", value = "cost" } }
}]
layout = "stacked_column_chart"
sort_dimensions = "a_to_z"
}
}

PNGグラフとしてレンダリングされたアドホッククエリ

ヒント:エクスポートではsort_dimensions = "a_to_z"を設定しましょう。 設定しないと、レンダリングされたグラフは時間軸を値の大きさ順に並べるため、9月が8月より前に来ることがあります。設定すれば、月はカレンダー順に並びます。

ダウンロードURLは機密情報として扱われ、有効期間は最大7日です。planのたびに新しいファイルがレンダリングされます。これは定期レポートにはまさに望ましい挙動です。最後の総仕上げで活用しましょう。

テナントをコードに

ここまではすべてコストデータの話でした。しかし、プロバイダーで最も広く採用されている機能のひとつは、前回の記事で紹介したリソースとモジュールによるAWS CloudConnectのオンボーディングです。チームが求めているのは、レポートだけでなくDoiTのセットアップそのもののコード化であり、直近3つのリリースでテナントの残りの部分がカバーされました。

顧客全体の設定

doit_customer(v1.8)は組織レベルの設定を管理します。カスタマーを新規作成するのではなく、既存のものを取り込む形になるので、まずはimportから始めます。

import {
to = doit_customer.main
id = "your-customer-id"
}
resource "doit_customer" "main" {
url_slug = "my-company"
settings = {
currency = "USD"
allowed_invite_domains = ["mycompany.com"]
mfa_required = true
}
contact = {
emails = ["finops@mycompany.com"]
}
}

最初のplanは、現在の設定があなたの書いた内容とどこが違うのかを正確に示してくれます。これだけでも有用な監査になります。その後は、MFAの無効化や新しい招待ドメインの許可は、誰かが承認しなければならないプルリクエストになり、誰でも気軽に切り替えられるトグルではなくなります。

カスタムロールとユーザー

doit_role(v1.10)はカスタムロールを管理します。権限IDは不透明な文字列なので、手で入力してはいけません。プリセットロールから導出しましょう。次のロールは、2つのプリセットを1つにまとめたものです。

data "doit_roles" "all" {}
locals {
preset_permissions = {
for r in data.doit_roles.all.roles : r.name => r.permissions if r.type == "preset"
}
}
resource "doit_role" "finops_analyst" {
name = "FinOps Analyst"
description = "View Only plus everything a Finance User can see"
permissions = sort(distinct(concat(
local.preset_permissions["View Only"],
local.preset_permissions["Finance User"],
)))
}

オンボーディングは、先ほどのチームと同じく、マップで表現できます。

locals {
analysts = {
"ana@mycompany.com" = { first_name = "Ana", last_name = "Lopez" }
"ravi@mycompany.com" = { first_name = "Ravi", last_name = "Patel" }
}
}
resource "doit_user" "analyst" {
for_each = local.analysts
email = each.key
first_name = each.value.first_name
last_name = each.value.last_name
job_title = "Finance / Accounting"
role_id = doit_role.finops_analyst.id
}

このような構成には、ユーザーを管理できる十分な権限を持つアイデンティティが必要です。v1.9以降、doit_current_userは呼び出し元の実効的なpermissionsを公開しているため、applyの途中で403に遭遇する代わりに、読みやすいメッセージとともに早期に失敗させられます。

data "doit_current_user" "me" {
lifecycle {
postcondition {
condition = alltrue([for p in ["UsersManager", "ServiceAccountManager"] : contains(self.permissions, p)])
error_message = "The identity running Terraform needs the UsersManager and ServiceAccountManager permissions."
}
}
}

サービスアカウントとトークンのローテーション

サービスアカウントはv1.10の目玉機能です。DoiT APIを呼ぶパイプラインは、しばしば誰かの個人APIキーで動いており、その人の権限をすべて持ってしまいます。サービスアカウントを使えば、自動化に必要最小限の権限だけを持つ専用のアイデンティティを与えられ、doit_service_accountとdoit_service_account_tokenでアカウントとトークンの両方をTerraformで管理できます。

トークンの名前と有効期限をtime_rotatingリソースから導出すれば、ローテーション日を過ぎるたびにTerraformが新しいトークンを発行します。

resource "doit_service_account" "ci" {
name = "ci-pipeline"
description = "Runs Terraform from CI"
permissions = ["cloudAnalyticsUser", "budgetsReadOnly"]
}
resource "time_rotating" "ci_token" {
rotation_days = 90
}
resource "doit_service_account_token" "ci" {
service_account_id = doit_service_account.ci.id
name = "ci-${formatdate("YYYYMMDD-hhmmss", time_rotating.ci_token.rfc3339)}"
expires_time = timeadd(time_rotating.ci_token.rotation_rfc3339, "168h")
lifecycle {
create_before_destroy = true
}
}

これが機能するポイントは4つあります。

  • nameまたはexpires_timeを変更するとトークンが置き換えられるため、名前に含まれるローテーションのタイムスタンプが新しいトークン発行のトリガーになります。また、新旧のトークンが一時的に併存する間、名前を一意に保つ役割も果たします。
  • **create_before_destroy**により、古いトークンが削除される前に新しいトークンが存在します。テスト中に強制的にローテーションを実行したところ、Terraformは新しいトークンを作成し、古いトークンを削除し、次のplanはクリーンでした。
  • 有効期限はローテーション日の1週間後なので、次のapplyが時間どおりに実行されなくてもトークンは使い続けられます。
  • access_tokenが返されるのは作成時の一度だけです。sensitiveとしてマークされています。同じapplyの中で、必要な場所に渡してください。

このトークンは通常のAPIトークンです。DOIT_API_TOKENとして設定すれば、プロバイダー(あるいは他のDoiT APIクライアント)はサービスアカウントとして認証され、付与した権限だけが見える状態になります。

ヒント:サービスアカウントは作成者を超える権限を持てません。 呼び出し元が持っていない権限を要求すると403で失敗します。命名規則にも注意してください。サービスアカウントの権限はcamelCase(cloudAnalyticsUser)ですが、doit_current_user.permissionsはPascalCase(CloudAnalyticsUser)で報告します。

Cloud bill shouldn't be a mystery

One platform for AI and Cloud optimization.

そのほかの注目ポイント

知っておく価値のある追加機能をいくつか紹介します。

  • **doit_service_quotas**はAWSとGCPのクォータ使用率を表示します。checkブロックと組み合わせれば、上限に達する前にplanのたびに警告が出ます。
check "service_quota_headroom" {
data "doit_service_quotas" "hot" {
min_utilization_percent = 80
}
assert {
condition = length(data.doit_service_quotas.hot.items) == 0
error_message = "Quotas at or above 80%: ${join(", ", [for q in data.doit_service_quotas.hot.items : "${q.cloud_provider}/${q.service}/${q.quota} (${q.utilization_percent}%)"])}"
}
}

テストテナントでは、使用率80%のAWS VPCクォータに対して即座に発火しました。クォータの読み取りには_Manage Settings_権限が必要な点に注意してください。

  • **doit_billing_explainer**は、クレジット、割引、削減額、マーケットプレイスの請求を含め、請求書を支払いアカウントごとに分解します。
  • **doit_widget / doit_widgets**はCloud Analyticsダッシュボードのウィジェットデータを公開します。
  • **doit_cloudconnect_supported_features**は、接続された各AWSアカウントで有効な機能と権限を表示します。
  • **name_contains**フィルター(doit_alerts、doit_allocations、doit_budgets、doit_labels、doit_reports)により、「チームXに属するものをすべて探す」が1行で書けます。
  • PerfectScale for Commitmentsのデータソース(doit_ps4c_*)とdoit_support_request_tagsリソースが、これらのリリースを締めくくります。

総仕上げ:個人のAPIキーを一切使わない週次FinOpsレポート

最後の例では、サービスアカウント、ローテーションするトークン、checkブロック、PDFエクスポート、そしてDoiTのAIアシスタントであるAvaを、GitHub Actionsパイプラインにまとめます。毎週月曜日に実行され、完全なレポートを添付したFinOpsサマリーを投稿します。使うのは2つの小さなTerraform構成です。

  • ブートストラップ(管理者が一度だけ実行):最小権限のサービスアカウント、ローテーションするトークン、レポートを作成し、トークンとレポートIDをGitHubに渡します。先ほどのサービスアカウントのスニペットにレポートを加えたもので、最後にGitHubプロバイダーの2つのリソースが付きます。
resource "github_actions_secret" "doit_api_token" {
repository = "doit-finops-demo"
secret_name = "DOIT_API_TOKEN"
plaintext_value = doit_service_account_token.reporting.access_token
}
resource "github_actions_variable" "report_id" {
repository = "doit-finops-demo"
variable_name = "DOIT_REPORT_ID"
value = doit_report.weekly.id
}

ローテーション日を過ぎてからブートストラップを再実行すると、新しいトークンが発行され、同じapplyの中でGitHubシークレットも更新されるため、パイプラインは何も気づきません。

  • レポーティング(パイプラインが実行):予算のcheckブロック、先ほどのPDFエクスポート、そしてAvaへの1つの質問です。ダウンロードしたファイル以外のリソースを含まないため、リモートステートは不要です。
data "doit_ava" "summary" {
question = "Summarize report ${var.report_id} in three short bullet points for an engineering audience. Mention the biggest week-over-week change."
}
output "summary" {
value = <<-EOT
## Weekly FinOps report
${data.doit_ava.summary.answer}
_The full report is attached to this run as `weekly-cost-report`._
EOT
}

ワークフロー自体は短いものです。

name: Weekly FinOps report
on:
schedule:
- cron: "0 7 * * 1" # 毎週月曜 07:00 UTC
workflow_dispatch:
jobs:
report:
runs-on: ubuntu-latest
defaults:
run:
working-directory: reporting
env:
DOIT_API_TOKEN: ${{ secrets.DOIT_API_TOKEN }}
TF_VAR_report_id: ${{ vars.DOIT_REPORT_ID }}
steps:
- uses: actions/checkout@v7
- uses: hashicorp/setup-terraform@v4
with:
terraform_wrapper: false
- run: terraform init -input=false
- run: terraform apply -auto-approve -input=false
- run: terraform output -raw summary >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v7
with:
name: weekly-cost-report
path: reporting/weekly-cost-report.pdf

実際の実行結果がこちらです。checkブロックはログでリスクのある予算をフラグし、Avaのサマリーは実行ページに表示され、PDFはアーティファクトとして添付されています。

Avaのサマリーが表示されたGitHub Actionsの実行ページ

実行に添付されたPDFの1ページ目

Avaのサマリーの3つ目の箇条書きとグラフを見比べてください。9月7日の週のCloud SQLの急増は、まさに第37週の紫色のスパイクです。AvaのサマリーはAIによる生成なので、文言は実行のたびに変わりますが、月曜の朝の最初の一段落としては十分に役立ちます。

この仕組みを構築して学んだことは次のとおりです。

  • ロボットの権限は最小限に。 パイプラインのサービスアカウントが持つ権限はcloudAnalyticsUserとbudgetsReadOnlyの2つだけです。クォータチェックをこのパイプラインに含めていないのもそのためで、含めると_Manage Settings_が必要になってしまいます。
  • ロボットには公開予算しか見えません。 サービスアカウントにはメールアドレスがないため、予算のコラボレーターとして追加できません。パイプラインにチェックさせたい予算は、doit_budgetでpublic = "viewer"以上に設定する必要があります。
  • トークンを手でコピーする人はいません。 トークンは1回のapplyの中で、DoiT APIからGitHubシークレットへ直接渡されます。唯一の別コピーはブートストラップのTerraformステートなので、ステートは安全な場所に保管してください。

まとめ

要点を振り返りましょう。

  • コアリソースがより深くなりました: 1つのマップからアロケーション、予算、アラートを生成し、グループアロケーションはselectで構築し、checkブロックにplanのたびに予算をレビューさせましょう。
  • レポートがコンソールの外へ: 保存済みレポートもアドホッククエリも、独自テーマを適用したPDFやPNGとしてレンダリングできます。
  • テナントもコードになりました: 設定、ロール、ユーザー、そしてローテーションするトークン付きのサービスアカウント。パイプラインが個人のAPIキーを借りる必要はもうありません。

ここからの進め方は次のとおりです。

  • 本記事冒頭の表を参考にv1.10へアップグレードしましょう。
  • チームマップから始めましょう。 アロケーション、予算、アラートが1つの構成にまとまります。
  • パイプラインを個人のAPIキーから卒業させ、ローテーションするトークン付きのサービスアカウントへ移行しましょう。
  • リポジトリのリリースをウォッチしましょう(Watch → Custom → Releases)。プロバイダーはDoiT APIが提供するものをすべてサポートするため、新しいAPI機能は追加され次第そこに現れます。

プロバイダーはgithub.com/doitintl/terraform-provider-doitでオープンソースとして公開されており、ドキュメントには本記事のほとんどの例と、さらに多くのサンプルが掲載されています。足りないものや使いにくい点があれば、issueを作成してぜひ教えてください。