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

Cloud Intelligence™

FinOps as Code, parte 2: patrones avanzados y tu tenant gestionado con Terraform

Esta página también está disponible en English, Deutsch, Français, Italiano, 日本語 y Português.

Hannes HayashiBy Hannes HayashiOct 12, 202618 min read

En la primera parte pusimos presupuestos, asignaciones, alertas y reportes bajo control de versiones. Desde entonces, el provider de Terraform de DoiT ha publicado cinco versiones menores en unos dos meses (de la v1.6 a la v1.10).

Tanto el número de organizaciones que usan el provider como su tráfico de API han crecido considerablemente desde entonces. Las asignaciones concentran la mayor parte de ese tráfico, y todas las organizaciones que leen sus presupuestos a través del provider también lo usan para escribirlos, ya sea creando, actualizando o eliminando presupuestos.

Así que este artículo va en dos direcciones:

  1. Más a fondo en los recursos principales. Son los patrones que se descubren después de unos meses de FinOps as code: generar asignaciones, presupuestos y alertas a partir de un único mapa de equipos, verificaciones de estado en tiempo de plan y reportes que salen de la consola como PDF.
  2. Más amplio, hacia el tenant. Configuración a nivel de organización, roles personalizados, incorporación de usuarios y service accounts con tokens rotativos. Al final, un reporte semanal de FinOps correrá en GitHub Actions sin la API key personal de nadie.

El provider ya cubre 20 recursos y 85 data sources. Cada fragmento de código que ves abajo se probó contra un tenant real de DoiT antes de entrar a este artículo (se aplicó, se volvió a planear para detectar drift y se destruyó donde era seguro), y la mayoría también aparece en los ejemplos de la documentación del provider.

Actualizar desde una versión v1.x anterior

Si configuraste todo después del primer artículo, probablemente estés en la v1.5 o una versión anterior. La mayoría de los equipos actualiza rápido (la gran mayoría de las organizaciones activas ya está en la v1.6 o posterior) y la actualización suele ser sencilla. Eso sí, por el camino se eliminaron algunos atributos obsoletos, y estos son los que podrían darte problemas:

Versión Cambio Qué hacer
v1.7 max_results en los data sources de listas es un número Quita las comillas: max_results = 50
v1.8 Se eliminó doit_budget.scope Usa scopes: un filtro cuya lista values contenga todos los IDs de asignación
v1.8 Se eliminó doit_alert.config.attributions Usa config.scopes
v1.8 Se eliminó doit_report.config.metric Usa config.metrics (hasta cuatro)
v1.8 Se eliminó inverse_selection del componente de doit_allocation Usa inverse
v1.8 Se eliminaron los layouts de reporte csv_export / sheets_export Elige un layout de visualización, como table
v1.8 request_timeout debe ser mayor a 120s (por defecto 150s) Sube o elimina los valores personalizados
v1.8 Se eliminaron los recursos y data sources de Insights La API de Insights fue descontinuada
v1.10 Los operadores de limit_by_change usan códigos cortos gt, gte, lt, lte, b, nb en lugar de >, >=, …
v1.10 Se eliminó doit_user.job_function Usa job_title

La guía de eliminación de atributos obsoletos tiene los detalles de migración para cada caso.

Desde la v1.8 también obtienes mejoras de robustez sin hacer nada. Los reportes corren a través de la API de ejecución asíncrona: el provider envía la solicitud, hace polling y recupera el resultado por ti. Las solicitudes afectadas por límites de tasa aplican backoff correctamente, y las solicitudes lentas muestran el error de la propia API en lugar de agotar primero el tiempo de espera local.

Patrones avanzados para los recursos principales de FinOps

Las asignaciones, los presupuestos, las alertas y los reportes son los cuatro recursos principales de la primera parte, y concentran la mayor parte del tráfico de API del provider.

Asignaciones a escala: un solo mapa para todos los equipos

Si tu configuración tiene una asignación, un presupuesto y una alerta por cada equipo, no los copies y pegues. Mejor define tus equipos una sola vez:

locals {
teams = {
payments = { budget = 12000, label = "payments" }
search = { budget = 8000, label = "search" }
platform = { budget = 20000, label = "platform" }
}
}
# Una sola asignación por equipo, con la detección de anomalías activada
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]
}]
}
}
# Los mismos equipos como una asignación de grupo que selecciona las individuales de arriba
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 pasó a ser editable en las asignaciones individuales con la v1.7, así que el monitoreo de anomalías vive en tu configuración en lugar de ser una casilla que alguien tiene que acordarse de marcar. La asignación de grupo, además, selecciona las asignaciones de los equipos en lugar de redefinirlas.

Consejo: construye siempre los grupos con select. También puedes definir reglas de grupo inline con action = "create", y la API de DoiT crea exactamente las mismas asignaciones individuales tras bambalinas. Pero esas asignaciones no son recursos de Terraform por sí mismas, así que las reglas inline no pueden configurar cosas como anomaly_detection o folder_id. Las asignaciones individuales separadas más select te dan recursos reales y gestionados.

Como ahora cada equipo tiene una asignación real con detección de anomalías, puedes preguntar qué equipos están generando anomalías y obtener una explicación con IA de la más costosa:

# Todas las anomalías (sin max_results, así que se recuperan todas las páginas),
# ordenadas para que la más costosa aparezca primero
data "doit_anomalies" "by_cost" {
sort_by = "costOfAnomaly"
sort_order = "desc"
}
locals {
# Una anomalía puede pertenecer a varias asignaciones; se cuenta en cada una
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
}

El atributo allocations de las anomalías (v1.10) lista todas las asignaciones a las que pertenece una anomalía, y doit_anomaly_explanation (v1.8) devuelve la explicación generada junto con los datos que la respaldan. En nuestro tenant de prueba, la explicación de la anomalía principal fue: "probablemente impulsada por un pico en el volumen de solicitudes de Amazon SNS Tier 1, especialmente en las regiones US West 2 y US West 1".

Presupuestos que siguen a las asignaciones

Con el mapa de equipos listo, los presupuestos son un for_each más. Los presupuestos también pueden notificar por Slack, y desde la v1.8 el provider valida la configuración de Slack antes del 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 en milisegundos
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"
}]
}

Agregar un equipo ahora es un cambio de una sola línea en local.teams, y la asignación, el presupuesto y la alerta (siguiente sección) lo siguen automáticamente.

Desde la v1.8, doit_budgets también expone la clasificación de riesgo que calcula DoiT del lado del servidor: un risk_status por presupuesto (onTrack, atRisk o unknown), y puedes filtrar por él. Combínalo con un bloque check de Terraform y cada terraform plan te avisa cuando el presupuesto de un equipo va camino a problemas:

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])}"
}
}

Los bloques check producen advertencias, no errores, así que nunca bloquean un apply, pero quien revise el plan ve el problema. Para presupuestos sugeridos con IA, el data source doit_budget_suggestions (v1.7) lista las recomendaciones pendientes con su nivel de confianza y el monto sugerido, para que una persona pueda revisarlas en un pull request.

Alertas que vale la pena activar

Según nuestros datos de uso, las alertas son el recurso menos utilizado de los cuatro principales, y muchas menos organizaciones gestionan alertas con Terraform que presupuestos. Es una brecha que vale la pena cerrar, porque las alertas sumaron dos de las mejoras más prácticas desde julio.

El problema clásico de las alertas por cambio porcentual es el ruido. Un servicio que cuesta $2 al día salta a $10 y recibes una alerta de "+400%" por ocho dólares. ignore_values_range (v1.9) omite cualquier evaluación donde el valor de la métrica quede dentro de los límites, y recipients_slack_channels envía la alerta al canal donde tu equipo de verdad mira:

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 # porcentaje
currency = "USD"
ignore_values_range = {
lower_bound = 0
upper_bound = 50 # USD por día
}
# Evalúa cada servicio por separado; basta con que uno solo se dispare
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
}]
}

Ten en cuenta que los límites están en unidades de la métrica (aquí, USD por día), no en porcentaje, y solo aplican a condition = "percentage-change". Un canal compartido de Slack lleva su ID y shared = true; un canal en un workspace conectado lleva su ID y workspace = "<name>".

Reportes: Top Movers, pronósticos y nuevas visualizaciones

De la v1.6 destacan dos novedades en los reportes.

Top Movers. limit_by_change filtra filas por el cambio entre períodos, y limit_aggregation controla cómo se muestran las filas excluidas. Este reporte conserva solo los servicios cuyo costo total supera los $1,000 y creció un 50% o más:

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"
}
}

Pronósticos y temas. forecast_settings proyecta el gasto futuro a partir de los datos históricos, y los layouts de la v1.8 (column_and_line_chart, sankey_chart, trend_board y cumulative_comparison) te dan más formas de mostrarlo. Agrega display_settings.theme_id y el reporte usa los colores de tu marca:

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"
}
}
}

Una salvedad: los valores del pronóstico todavía no se incluyen en los resultados ni en las exportaciones de los reportes. El tema, en cambio, se mantiene de principio a fin, como muestra la siguiente sección.

Los reportes salen de la consola: exportación a PDF y PNG

Ahora puedes sacar un reporte de DoiT como archivo, sin que nadie abra la consola. Desde la v1.10, doit_report_result y doit_report_query aceptan file_output = "pdf" o "png" y devuelven una URL de descarga firmada:

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
}

El PDF es un documento pulido de varias páginas con el nombre de tu organización en el encabezado, y tu tema personalizado se conserva: como el reporte de pronóstico de arriba define display_settings.theme_id, el PDF también se renderiza con los colores de tu marca.

Cambia local_sensitive_file por un aws_s3_object (acepta el mismo content_base64) y el PDF termina en un bucket. Las ejecuciones de reportes pasan por la API de ejecución asíncrona de DoiT, así que los reportes pesados pueden tardar: si lo necesitas, sube el timeout de lectura del data source con timeouts = { read = "30m" }.

Ni siquiera necesitas un reporte guardado. doit_report_query renderiza igual de bien una consulta ad-hoc, lo cual es genial para una gráfica en un README, un comentario en un pull request o una página de 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" },
]
# Top 5 de servicios por costo
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"
}
}

Una consulta ad-hoc renderizada como gráfica PNG

Consejo: define sort_dimensions = "a_to_z" para las exportaciones. Sin esto, la gráfica renderizada ordena el eje de tiempo por valor, así que septiembre puede terminar antes que agosto. Con esto, los meses quedan en orden de calendario.

La URL de descarga es sensible y válida hasta por siete días. Cada plan renderiza un archivo nuevo, que es exactamente lo que quieres para un reporte recurrente. Lo pondremos a trabajar en el gran final.

Tu tenant como código

Hasta aquí todo ha sido sobre datos de costos. Pero una de las funcionalidades más adoptadas del provider es el onboarding de AWS CloudConnect, a través del recurso y el módulo del artículo anterior. Está claro que los equipos quieren su configuración de DoiT en código, no solo sus reportes, y las últimas tres versiones cubren el resto del tenant.

Configuración a nivel de organización

doit_customer (v1.8) gestiona la configuración a nivel de organización. No creas un customer: adoptas el que ya tienes, así que empieza con un 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"]
}
}

Tu primer plan muestra exactamente en qué difiere la configuración actual de lo que escribiste, lo cual ya es una auditoría útil por sí sola. A partir de ahí, desactivar MFA o permitir un nuevo dominio de invitación es un pull request que alguien tiene que aprobar, no un interruptor que cualquiera puede mover.

Roles personalizados y usuarios

doit_role (v1.10) gestiona roles personalizados. Los IDs de permisos son opacos, así que no los escribas a mano: derívalos de los roles predefinidos. Este rol combina dos de ellos en uno:

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"],
)))
}

El onboarding de usuarios es entonces un mapa, igual que los equipos de antes:

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
}

Las configuraciones como esta necesitan una identidad con permisos suficientes para gestionar usuarios. Desde la v1.9, doit_current_user expone los permissions efectivos de quien llama, así que puedes fallar temprano con un mensaje legible en lugar de recibir un 403 a mitad de un apply:

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 y tokens rotativos

Los service accounts son la funcionalidad estrella de la v1.10. Los pipelines que hablan con la API de DoiT suelen correr con la API key personal de alguien, con todos los permisos de esa persona. Un service account le da a la automatización su propia identidad con solo los permisos que necesita, y doit_service_account y doit_service_account_token gestionan tanto la cuenta como sus tokens en Terraform.

Controla el nombre y la expiración del token con un recurso time_rotating y Terraform emite un token nuevo cada vez que pasa la fecha de rotación:

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
}
}

Cuatro detalles hacen que esto funcione:

  • Cambiar name o expires_time reemplaza el token, así que la marca de tiempo de rotación en el nombre es lo que dispara un token nuevo. También mantiene los nombres únicos, algo obligatorio mientras el token viejo y el nuevo coexisten brevemente.
  • create_before_destroy significa que el token nuevo existe antes de que se elimine el viejo. Forzamos una rotación durante las pruebas: Terraform creó el token nuevo, eliminó el viejo y el siguiente plan salió limpio.
  • La expiración es una semana después de la fecha de rotación, así el token sigue funcionando aunque nadie ejecute el siguiente apply a tiempo.
  • access_token se devuelve una sola vez, al momento de la creación. Está marcado como sensible. Entrégalo a donde haga falta dentro del mismo apply.

El token es un token de API normal. Configúralo como DOIT_API_TOKEN y el provider (o cualquier otro cliente de la API de DoiT) se autentica como el service account y ve únicamente los permisos que le diste.

Consejo: los service accounts no pueden tener más permisos que su creador. Solicitar permisos que quien llama no tiene falla con un 403. Fíjate también en la nomenclatura: los permisos de los service accounts usan camelCase (cloudAnalyticsUser), mientras que doit_current_user.permissions los reporta en PascalCase (CloudAnalyticsUser).

Cloud bill shouldn't be a mystery

One platform for AI and Cloud optimization.

Novedades rápidas

Algunas adiciones más que vale la pena conocer:

  • doit_service_quotas muestra la utilización de cuotas de AWS y GCP. Combínalo con un bloque check y cada plan te advierte antes de que llegues a un límite:
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}%)"])}"
}
}

En nuestro tenant de prueba esto se disparó de inmediato por una cuota de VPC de AWS al 80%. Ten en cuenta que leer cuotas requiere el permiso Manage Settings.

  • doit_billing_explainer desglosa una factura por payer, incluidos créditos, descuentos, ahorros y cargos de marketplace.
  • doit_widget / doit_widgets exponen los datos de los widgets del dashboard de Cloud Analytics.
  • doit_cloudconnect_supported_features muestra qué funcionalidades y permisos están activos en cada cuenta de AWS conectada.
  • Los filtros name_contains en doit_alerts, doit_allocations, doit_budgets, doit_labels y doit_reports convierten "encuéntrame todo lo que pertenece al equipo X" en una sola línea.
  • Los data sources de PerfectScale for Commitments (doit_ps4c_*) y el recurso doit_support_request_tags cierran la lista de novedades.

Todo junto: un reporte semanal de FinOps sin API keys personales

El último ejemplo combina un service account, un token rotativo, un bloque check, una exportación a PDF y Ava, la asistente de IA de DoiT, en un pipeline de GitHub Actions. Corre cada lunes y publica un resumen de FinOps con el reporte completo adjunto, usando dos configuraciones pequeñas de Terraform:

  • Bootstrap, que un admin ejecuta una sola vez: crea un service account con permisos mínimos, un token rotativo y el reporte, y luego entrega el token y el ID del reporte a GitHub. Es el fragmento del service account de arriba más el reporte, con dos recursos del provider de GitHub al final:
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
}

Volver a ejecutar el bootstrap después de la fecha de rotación emite un token nuevo y actualiza el secreto en el mismo apply, así que el pipeline ni se entera.

  • Reporting, que ejecuta el pipeline: un bloque check de presupuestos, la exportación a PDF de antes y una pregunta para Ava. No contiene más recursos que el archivo descargado, así que no necesita estado remoto:
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
}

El workflow en sí es corto:

name: Weekly FinOps report
on:
schedule:
- cron: "0 7 * * 1" # Lunes, 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

Aquí tienes una ejecución real. El bloque check marcó un presupuesto en riesgo en el log, el resumen de Ava aparece en la página de la ejecución y el PDF quedó adjunto como artefacto:

La página de la ejecución de GitHub Actions con el resumen de Ava

Página 1 del PDF adjunto a la ejecución

Mira el tercer punto del resumen de Ava y luego la gráfica: el salto de Cloud SQL en la semana del 7 de septiembre es exactamente el pico morado de la semana 37. El resumen de Ava se genera con IA, así que la redacción cambia de una ejecución a otra, pero es un primer párrafo muy útil para un lunes por la mañana.

Lo que aprendimos al construir esto:

  • Mantén mínimos los permisos del robot. El service account del pipeline tiene solo dos permisos: cloudAnalyticsUser y budgetsReadOnly. Por eso mismo la verificación de cuotas se queda fuera de este pipeline: necesitaría Manage Settings.
  • El robot solo ve presupuestos públicos. Los service accounts no tienen dirección de correo, así que no puedes agregar uno como colaborador de un presupuesto. Los presupuestos que el pipeline deba revisar necesitan public = "viewer" o superior en el doit_budget.
  • Nadie copia un token a mano. El token va de la API de DoiT directo al secreto de GitHub dentro de un mismo apply. La única otra copia está en el estado de Terraform del bootstrap, así que guarda ese estado en un lugar seguro.

Para cerrar

En resumen:

  • Los recursos principales ganaron profundidad: genera asignaciones, presupuestos y alertas a partir de un solo mapa, construye asignaciones de grupo con select y deja que los bloques check revisen tus presupuestos en cada plan.
  • Los reportes salen de la consola: renderiza reportes guardados y consultas ad-hoc como PDF y PNG, con tu propio tema.
  • Tu tenant ahora es código: configuración, roles, usuarios y service accounts con tokens rotativos, para que tus pipelines dejen de pedir prestada la API key de una persona.

Próximos pasos:

  • Actualiza a la v1.10 con la ayuda de la tabla del inicio de este artículo.
  • Empieza por el mapa de equipos. Con él, las asignaciones, los presupuestos y las alertas quedan en una sola configuración.
  • Saca tus pipelines de las API keys personales y pásalos a un service account con token rotativo.
  • Sigue las releases del repositorio (Watch → Custom → Releases). El provider soporta todo lo que ofrece la API de DoiT, así que las nuevas capacidades de la API aparecen ahí a medida que se agregan.

El provider es open source en github.com/doitintl/terraform-provider-doit, y la documentación incluye la mayoría de los ejemplos de este artículo, y muchos más. Si algo falta o resulta incómodo, abre un issue y cuéntanos.