In Teil eins haben wir Budgets, Allocations, Alerts und Reports unter Versionskontrolle gestellt. Seitdem hat der DoiT Terraform Provider in rund zwei Monaten fünf Minor-Releases veröffentlicht (v1.6 bis v1.10).
Sowohl die Zahl der Organisationen, die den Provider nutzen, als auch sein API-Traffic sind seitdem deutlich gewachsen. Allocations machen den Großteil dieses Traffics aus, und jede Organisation, die ihre Budgets über den Provider liest, nutzt ihn auch zum Schreiben – sei es beim Erstellen, Aktualisieren oder Löschen von Budgets.
Dieser Beitrag geht daher in zwei Richtungen:
- Tiefer in die Kern-Ressourcen. Das sind Patterns, die man nach einigen Monaten FinOps as Code entdeckt: Allocations, Budgets und Alerts aus einer einzigen Team-Map generieren, Health-Checks zur Plan-Zeit und Reports, die die Konsole als PDF verlassen.
- Breiter, hin zum Tenant selbst. Organisationsweite Einstellungen, Custom Roles, User-Onboarding und Service Accounts mit rotierenden Tokens. Am Ende läuft ein wöchentlicher FinOps-Report in GitHub Actions – ganz ohne persönlichen API-Key.
Der Provider deckt inzwischen 20 Ressourcen und 85 Data Sources ab. Jedes Snippet in diesem Beitrag wurde vor Veröffentlichung gegen einen echten DoiT-Tenant getestet (angewendet, auf Drift erneut geplant und, wo gefahrlos möglich, wieder zerstört), und die meisten finden sich auch in den Beispielen der Provider-Dokumentation.
Upgrade von einem früheren v1.x-Release
Wenn Sie nach dem ersten Beitrag eingestiegen sind, sind Sie vermutlich auf v1.5 oder älter. Die meisten Teams aktualisieren schnell (die große Mehrheit der aktiven Organisationen ist bereits auf v1.6 oder neuer), und das Upgrade selbst verläuft in der Regel reibungslos. Unterwegs wurden allerdings einige veraltete Attribute entfernt – über diese könnten Sie stolpern:
| Version | Änderung | Was zu tun ist |
|---|---|---|
| v1.7 | max_results auf List-Data-Sources ist eine Zahl |
Anführungszeichen entfernen: max_results = 50 |
| v1.8 | doit_budget.scope entfernt |
scopes verwenden: ein Filter, dessen values-Liste alle Allocation-IDs enthält |
| v1.8 | doit_alert.config.attributions entfernt |
config.scopes verwenden |
| v1.8 | doit_report.config.metric entfernt |
config.metrics verwenden (bis zu vier) |
| v1.8 | doit_allocation-Komponente inverse_selection entfernt |
inverse verwenden |
| v1.8 | Report-Layouts csv_export / sheets_export entfernt |
Ein Visualisierungs-Layout wählen, z. B. table |
| v1.8 | request_timeout muss über 120s liegen (Standard 150s) |
Eigene Werte erhöhen oder entfernen |
| v1.8 | Insights-Ressourcen und -Data-Sources entfernt | Die zugrunde liegende Insights-API wurde eingestellt |
| v1.10 | limit_by_change-Operatoren verwenden Kurzcodes |
gt, gte, lt, lte, b, nb statt >, >=, … |
| v1.10 | doit_user.job_function entfernt |
job_title verwenden |
Der Leitfaden zur Entfernung veralteter Attribute enthält Migrationsdetails zu jeder einzelnen Änderung.
Seit v1.8 gibt es außerdem mehr Robustheit frei Haus. Reports laufen über die asynchrone Execution-API: Der Provider übermittelt die Anfrage, pollt und holt das Ergebnis für Sie ab. Rate-limitierte Requests ziehen sich mit sauberem Backoff zurück, und langsame Requests liefern die Fehlermeldung der API selbst, statt vorher lokal in einen Timeout zu laufen.
Fortgeschrittene Patterns für die FinOps-Kern-Ressourcen
Allocations, Budgets, Alerts und Reports sind die vier Kern-Ressourcen aus Teil eins und machen den Großteil des API-Traffics des Providers aus.
Allocations im großen Stil: Eine Map für alle Teams
Wenn Ihre Konfiguration für jedes Team eine Allocation, ein Budget und einen Alert enthält, verzichten Sie auf Copy-and-paste. Definieren Sie Ihre Teams stattdessen genau einmal:
locals { teams = { payments = { budget = 12000, label = "payments" } search = { budget = 8000, label = "search" } platform = { budget = 20000, label = "platform" } }}
# Eine einzelne Allocation pro Team, mit aktivierter Anomalie-Erkennungresource "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] }] }}
# Dieselben Teams als eine Gruppen-Allocation, die die einzelnen Allocations oben auswähltresource "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 ist seit v1.7 auf einzelnen Allocations beschreibbar – das Anomalie-Monitoring lebt damit in Ihrer Konfiguration, statt eine Checkbox zu sein, an die jemand denken muss. Die Gruppen-Allocation selektiert außerdem die Team-Allocations, statt sie neu zu definieren.
Tipp: Bauen Sie Gruppen immer über
select. Sie können Gruppen-Regeln auch inline mitaction = "create"definieren, und die DoiT-API legt im Hintergrund exakt dieselben einzelnen Allocations an. Diese Allocations sind dann aber keine eigenen Terraform-Ressourcen, weshalb Inline-Regeln Dinge wieanomaly_detectionoderfolder_idnicht setzen können. Separate einzelne Allocations plusselectliefern Ihnen echte, verwaltete Ressourcen.
Da jedes Team jetzt eine echte Allocation mit Anomalie-Erkennung hat, können Sie abfragen, welche Teams Anomalien erzeugen – und sich die teuerste per KI erklären lassen:
# Alle Anomalien (ohne max_results, es werden also alle Seiten geladen),# so sortiert, dass die teuerste zuerst kommtdata "doit_anomalies" "by_cost" { sort_by = "costOfAnomaly" sort_order = "desc"}
locals { # Eine Anomalie kann zu mehreren Allocations gehören; sie zählt bei jeder davon 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}Das Attribut allocations an Anomalien (v1.10) listet jede Allocation auf, zu der eine Anomalie gehört, und doit_anomaly_explanation (v1.8) liefert die generierte Erklärung samt der Fakten dahinter. Auf unserem Test-Tenant kam die Top-Anomalie zurück als "vermutlich getrieben durch einen sprunghaften Anstieg des Amazon-SNS-Tier-1-Request-Volumens, insbesondere in den Regionen US West 2 und US West 1".
Budgets, die den Allocations folgen
Mit der Team-Map sind Budgets nur ein weiteres for_each. Budgets können auch Slack benachrichtigen, und seit v1.8 validiert der Provider die Slack-Konfiguration vor dem Apply:
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 in Millisekunden
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" }]}Ein Team hinzuzufügen ist jetzt eine Ein-Zeilen-Änderung an local.teams – Allocation, Budget und (siehe nächster Abschnitt) Alert folgen automatisch.
Seit v1.8 stellt doit_budgets außerdem DoiTs serverseitige Risiko-Klassifizierung bereit: einen risk_status pro Budget (onTrack, atRisk oder unknown), nach dem Sie filtern können. Kombinieren Sie das mit einem Terraform-check-Block, und jeder terraform plan meldet Ihnen, wenn ein Team-Budget in Schieflage gerät:
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-Blöcke erzeugen Warnungen, keine Fehler – sie blockieren also nie ein Apply, aber wer den Plan reviewt, sieht das Problem. Für KI-vorgeschlagene Budgets listet die Data Source doit_budget_suggestions (v1.7) ausstehende Empfehlungen samt Konfidenz und vorgeschlagenem Betrag auf, sodass ein Mensch sie in einem Pull Request prüfen kann.
Alerts, die man einschalten sollte
In unseren Nutzungsdaten sind Alerts die am wenigsten genutzte der vier Kern-Ressourcen, und deutlich weniger Organisationen verwalten Alerts über Terraform als Budgets. Diese Lücke zu schließen lohnt sich, denn Alerts haben seit Juli zwei der praktischsten Neuerungen erhalten.
Das klassische Problem bei Alerts auf prozentuale Veränderung ist Rauschen. Ein Service, der 2 $ am Tag kostet, springt auf 10 $ – und Sie bekommen einen "+400 %"-Alert für acht Dollar. ignore_values_range (v1.9) überspringt jede Auswertung, bei der der Metrikwert innerhalb der Grenzen liegt, und recipients_slack_channels schickt den Alert dorthin, wo Ihr Team tatsächlich hinschaut:
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 # Prozent currency = "USD" ignore_values_range = { lower_bound = 0 upper_bound = 50 # USD pro Tag } # Jeden Service einzeln auswerten, sodass schon ein einzelner Ausreißer genügt 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 }]}Beachten Sie: Die Grenzen sind in Metrik-Einheiten angegeben (hier USD pro Tag), nicht in Prozent, und sie gelten nur für condition = "percentage-change". Ein geteilter Slack-Channel braucht seine ID und shared = true; ein Channel in einem verbundenen Workspace stattdessen seine ID und workspace = "<name>".
Reports: Top Movers, Forecasts und neue Visualisierungen
Zwei Report-Neuerungen aus v1.6 stechen hervor.
Top Movers. limit_by_change filtert Zeilen nach der Veränderung von Periode zu Periode, und limit_aggregation steuert, wie die ausgeschlossenen Zeilen dargestellt werden. Dieser Report behält nur Services, deren Gesamtkosten über 1.000 $ liegen und die um 50 % oder mehr gewachsen sind:
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" }}Forecasts und Themes. forecast_settings projiziert zukünftige Ausgaben aus historischen Daten, und die v1.8-Layouts (column_and_line_chart, sankey_chart, trend_board und cumulative_comparison) bieten mehr Darstellungsmöglichkeiten. Fügen Sie display_settings.theme_id hinzu, und der Report verwendet Ihre Markenfarben:
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" } }}Eine Einschränkung: Forecast-Werte sind in Report-Ergebnissen und -Exporten noch nicht enthalten. Das Theme hingegen zieht sich komplett durch, wie der nächste Abschnitt zeigt.
Reports verlassen die Konsole: PDF- und PNG-Export
Sie können einen Report jetzt als Datei aus DoiT herausholen, ohne dass jemand die Konsole öffnet. Seit v1.10 akzeptieren doit_report_result und doit_report_query file_output = "pdf" oder "png" und liefern eine signierte Download-URL zurück:
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}Das PDF ist ein sauber gestaltetes, mehrseitiges Dokument mit dem Namen Ihrer Organisation im Header, und Ihr Custom Theme wird übernommen: Da der Forecast-Report oben display_settings.theme_id setzt, erscheint auch das PDF in Ihren Markenfarben.
Tauschen Sie local_sensitive_file gegen ein aws_s3_object (es akzeptiert dasselbe content_base64), und das PDF landet stattdessen in einem Bucket. Report-Läufe gehen über DoiTs asynchrone Execution-API, umfangreiche Reports können also eine Weile dauern: Erhöhen Sie bei Bedarf den Read-Timeout der Data Source mit timeouts = { read = "30m" }.
Sie brauchen nicht einmal einen gespeicherten Report. doit_report_query rendert eine Ad-hoc-Query genauso gut – ideal für ein Diagramm in einem README, einem Pull-Request-Kommentar oder auf einer Wiki-Seite:
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" }, ] # Top-5-Services nach Kosten 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" }}
Tipp: Setzen Sie
sort_dimensions = "a_to_z"für Exporte. Ohne diese Einstellung ordnet das gerenderte Diagramm die Zeitachse nach Wert – September kann also vor August landen. Mit ihr stehen die Monate in Kalenderreihenfolge.
Die Download-URL ist als sensitiv markiert und bis zu sieben Tage gültig. Jeder Plan rendert eine frische Datei – genau das, was Sie für einen wiederkehrenden Report wollen. Im Finale setzen wir das in die Praxis um.
Ihr Tenant als Code
Bisher ging es ausschließlich um Kostendaten. Eines der meistgenutzten Features des Providers ist aber das AWS-CloudConnect-Onboarding über die Ressource und das Modul aus dem letzten Beitrag. Teams wollen offensichtlich ihr gesamtes DoiT-Setup in Code, nicht nur ihre Reports – und die letzten drei Releases decken den Rest des Tenants ab.
Organisationsweite Einstellungen
doit_customer (v1.8) verwaltet Einstellungen auf Organisationsebene. Sie erstellen keinen Customer, Sie übernehmen den vorhandenen – starten Sie also mit einem 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"] }}Ihr erster Plan zeigt genau, wie die heutigen Einstellungen von dem abweichen, was Sie geschrieben haben – schon das ist für sich genommen ein nützliches Audit. Danach ist das Abschalten von MFA oder das Zulassen einer neuen Invite-Domain ein Pull Request, den jemand genehmigen muss – kein Schalter, den jemand einfach umlegen kann.
Custom Roles und User
doit_role (v1.10) verwaltet Custom Roles. Permission-IDs sind opak, tippen Sie sie also nicht ab, sondern leiten Sie sie aus den Preset-Rollen ab. Diese Rolle kombiniert zwei Presets zu einer:
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"], )))}Das Onboarding ist dann eine Map, genau wie bei den Teams vorhin:
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}Konfigurationen wie diese brauchen eine Identität mit ausreichenden Rechten zur User-Verwaltung. Seit v1.9 stellt doit_current_user die effektiven permissions des Aufrufers bereit, sodass Sie früh mit einer lesbaren Meldung abbrechen können, statt mitten im Apply in einen 403 zu laufen:
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." } }}Service Accounts und rotierende Tokens
Service Accounts sind das Highlight von v1.10. Pipelines, die mit der DoiT-API sprechen, laufen oft mit dem persönlichen API-Key einer einzelnen Person – inklusive aller Berechtigungen dieser Person. Ein Service Account gibt der Automatisierung eine eigene Identität mit genau den Berechtigungen, die sie braucht, und doit_service_account sowie doit_service_account_token verwalten sowohl den Account als auch seine Tokens in Terraform.
Steuern Sie Name und Ablaufdatum des Tokens über eine time_rotating-Ressource, und Terraform erzeugt automatisch ein frisches Token, sobald das Rotationsdatum überschritten ist:
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 }}Vier Details machen das rund:
- Eine Änderung von
nameoderexpires_timeersetzt das Token – der Rotations-Zeitstempel im Namen ist also das, was ein neues Token auslöst. Außerdem bleiben die Namen so eindeutig – das müssen sie sein, solange altes und neues Token kurzzeitig nebeneinander existieren. create_before_destroysorgt dafür, dass das neue Token existiert, bevor das alte gelöscht wird. Wir haben beim Testen eine Rotation erzwungen: Terraform legte das neue Token an, löschte das alte, und der nächste Plan war sauber.- Das Ablaufdatum liegt eine Woche nach dem Rotationsdatum, sodass das Token weiter funktioniert, selbst wenn niemand das nächste Apply pünktlich ausführt.
access_tokenwird nur ein einziges Mal zurückgegeben, bei der Erstellung. Es ist als sensitiv markiert. Übergeben Sie es im selben Apply dorthin, wo es gebraucht wird.
Das Token ist ein ganz normales API-Token. Setzen Sie es als DOIT_API_TOKEN, und der Provider (oder jeder andere DoiT-API-Client) authentifiziert sich als Service Account und sieht nur die Berechtigungen, die Sie vergeben haben.
Tipp: Service Accounts können nicht mehr Rechte haben als ihr Ersteller. Das Anfordern von Berechtigungen, die der Aufrufer selbst nicht besitzt, schlägt mit einem 403 fehl. Beachten Sie auch die Schreibweise: Service-Account-Berechtigungen verwenden camelCase (
cloudAnalyticsUser), währenddoit_current_user.permissionssie in PascalCase meldet (CloudAnalyticsUser).
Cloud bill shouldn't be a mystery
One platform for AI and Cloud optimization.
Kurz notiert
Ein paar weitere Neuerungen, die man kennen sollte:
doit_service_quotaszeigt die Quota-Auslastung in AWS und GCP. Kombiniert mit einem Check-Block warnt jeder Plan, bevor Sie an ein Limit stoßen:
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}%)"])}" } }Auf unserem Test-Tenant schlug das sofort für eine AWS-VPC-Quota bei 80 % an. Beachten Sie, dass das Lesen von Quotas die Berechtigung Manage Settings erfordert.
doit_billing_explainerschlüsselt eine Rechnung pro Payer auf – inklusive Credits, Rabatten, Einsparungen und Marketplace-Posten.doit_widget/doit_widgetsstellen Widget-Daten der Cloud-Analytics-Dashboards bereit.doit_cloudconnect_supported_featureszeigt, welche Features und Berechtigungen auf jedem verbundenen AWS-Account aktiv sind.name_contains-Filter aufdoit_alerts,doit_allocations,doit_budgets,doit_labelsunddoit_reportsmachen aus "Finde alles, was zu Team X gehört" einen Einzeiler.- PerfectScale-for-Commitments-Data-Sources (
doit_ps4c_*) und die Ressourcedoit_support_request_tagsrunden die Releases ab.
Alles im Zusammenspiel: Ein wöchentlicher FinOps-Report ohne persönlichen API-Key
Das letzte Beispiel kombiniert einen Service Account, ein rotierendes Token, einen Check-Block, einen PDF-Export und Ava, DoiTs KI-Assistentin, zu einer GitHub-Actions-Pipeline. Sie läuft jeden Montag und veröffentlicht eine FinOps-Zusammenfassung mit dem vollständigen Report im Anhang – auf Basis von zwei kleinen Terraform-Konfigurationen:
- Bootstrap, einmalig von einem Admin ausgeführt: erstellt einen Service Account mit minimalen Berechtigungen, ein rotierendes Token und den Report und übergibt Token und Report-ID an GitHub. Das ist das Service-Account-Snippet von oben plus der Report, am Ende ergänzt um zwei Ressourcen aus dem GitHub-Provider:
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 }Ein erneuter Bootstrap-Lauf nach dem Rotationsdatum erzeugt ein neues Token und aktualisiert das Secret im selben Apply – die Pipeline bekommt davon nichts mit.
- Reporting, von der Pipeline ausgeführt: ein Budget-Check-Block, der PDF-Export von vorhin und eine Frage an Ava. Außer der heruntergeladenen Datei enthält die Konfiguration keine Ressourcen und braucht daher keinen Remote State:
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 }Der Workflow selbst ist kurz:
name: Weekly FinOps report
on: schedule: - cron: "0 7 * * 1" # Montags 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.pdfHier ein echter Lauf. Der Check-Block hat im Log ein gefährdetes Budget markiert, Avas Zusammenfassung steht auf der Run-Seite, und das PDF hängt als Artefakt an:


Sehen Sie sich den dritten Punkt in Avas Zusammenfassung an und dann das Diagramm: Der Cloud-SQL-Sprung in der Woche vom 7. September ist exakt die violette Spitze in Woche 37. Avas Zusammenfassung ist KI-generiert, der Wortlaut variiert also von Lauf zu Lauf – aber als erster Absatz für einen Montagmorgen ist sie durchaus nützlich.
Was wir dabei gelernt haben:
- Halten Sie die Berechtigungen des Roboters minimal. Der Pipeline-Service-Account hat genau zwei Berechtigungen:
cloudAnalyticsUserundbudgetsReadOnly. Deshalb ist der Quota-Check auch nicht Teil dieser Pipeline, denn er bräuchte Manage Settings. - Der Roboter sieht nur öffentliche Budgets. Service Accounts haben keine E-Mail-Adresse, Sie können sie also nicht als Budget-Collaborator hinzufügen. Budgets, die die Pipeline prüfen soll, brauchen
public = "viewer"oder höher auf demdoit_budget. - Niemand kopiert ein Token von Hand. Das Token wandert innerhalb eines einzigen Applys direkt von der DoiT-API in das GitHub-Secret. Die einzige weitere Kopie liegt im Terraform-State des Bootstraps – bewahren Sie diesen State also sicher auf.
Fazit
Noch einmal im Überblick:
- Mehr Tiefe bei den Kern-Ressourcen: Allocations, Budgets und Alerts aus einer Map generieren, Gruppen-Allocations mit
selectbauen und Check-Blöcke bei jedem Plan über Ihre Budgets wachen lassen. - Reports verlassen die Konsole: Gespeicherte Reports und Ad-hoc-Queries als PDF und PNG rendern – im eigenen Theme.
- Ihr Tenant ist jetzt Code: Einstellungen, Rollen, User und Service Accounts mit rotierenden Tokens, sodass sich Ihre Pipelines keinen persönlichen API-Key mehr ausleihen müssen.
Wie es weitergeht:
- Upgraden Sie auf v1.10 mithilfe der Tabelle am Anfang dieses Beitrags.
- Starten Sie mit der Team-Map. Sie bringt Allocations, Budgets und Alerts in eine einzige Konfiguration.
- Stellen Sie Ihre Pipelines von persönlichen API-Keys auf einen Service Account mit rotierendem Token um.
- Beobachten Sie die Releases des Repositorys (Watch → Custom → Releases). Der Provider unterstützt alles, was die DoiT-API bietet – neue API-Funktionen tauchen dort auf, sobald sie hinzukommen.
Der Provider ist Open Source unter github.com/doitintl/terraform-provider-doit, und die Dokumentation enthält die meisten Beispiele aus diesem Beitrag – und viele mehr. Wenn etwas fehlt oder hakt, eröffnen Sie ein Issue und sagen Sie uns Bescheid.