Una línea en un fichero .tf cambia m5.large por m5.xlarge. La revisión se centra en si el código es correcto, nadie pregunta cuánto cuesta y el cambio se fusiona. Un mes después, la factura de EC2 ha subido y hay que reconstruir qué pasó. En este artículo te contamos cómo poner el precio de cada cambio en la propia pull request con AAA Sentinel, cuando todavía es barato cambiar de idea.
El problema: el coste llega un mes tarde
En la mayoría de equipos, el coste de la infraestructura se mira cuando ya está desplegada. Eso tiene tres consecuencias:
- Quien decide el cambio no ve su precio: el desarrollador elige un tamaño de instancia sin saber cuánto cuesta al mes.
- Quien ve el precio no puede decidir: finanzas descubre la subida en la factura, cuando el recurso ya está en producción y hay que convencer a alguien de cambiarlo.
- Corregir tarde cuesta más: un tamaño se cambia en segundos en una pull request, y en producción exige una ventana de mantenimiento.
Es lo que se conoce como FinOps desplazado a la izquierda: llevar la información de coste al momento en que se toma la decisión técnica, igual que se hizo antes con las pruebas o la seguridad.
Qué ganas con ello
Con el coste en la pull request:
- Cada cambio de infraestructura llega a la revisión con su delta mensual estimado.
- La conversación sobre el coste ocurre entre quien escribe el cambio y quien lo revisa, no un mes después entre finanzas e ingeniería.
- Ves también lo que no se ha podido preciar y por qué, en lugar de una cifra que parece completa sin serlo.
- No añades un punto de fallo a tu despliegue: si la estimación falla, el pipeline sigue.
Cómo lo resuelve AAA Sentinel
La pieza es un CLI publicado en npm, @aaa-sentinel/iac-cost. Lee la salida de terraform show -json, la reduce en tu máquina a lo imprescindible, pide la estimación a la API de AAA Sentinel y escribe en la salida estándar un comentario en Markdown listo para publicar en la pull request. Con él vienen ejemplos para GitHub Actions y Bitbucket Pipelines que publican el comentario y lo actualizan en cada push.
Planes: hace falta Pro o Business y una aplicación OAuth2 con el permiso iac:estimate. Pro permite dos aplicaciones OAuth2 y Business, diez.
Qué sale de tu máquina (y qué no)
La salida de terraform show -json lleva rangos CIDR, identificadores de cuenta y, a menudo, secretos en claro. Por eso el plan no sale de tu máquina. Antes de enviar nada, el CLI lo reduce localmente a una lista blanca:
- El tipo de recurso y la acción: alta, modificación, baja o sustitución.
- Cuántas instancias produce el bloque: count y for_each se pliegan en una cantidad.
- Los pocos atributos que mueven el precio: el tamaño de la instancia y, cuando el recurso la lleva, la región.
Ni siquiera viajan las direcciones de recurso, como aws_instance.web[0]: en un repositorio público, el comentario sería un mapa de tu infraestructura. La proyección tampoco se escribe en el log del CI. Y el permiso iac:estimate no da acceso a nada más: no puede leer tus cuentas, hallazgos ni informes, así que es seguro guardarlo en un CI que pueden disparar terceros.
Caso práctico: la pull request de ACME
ACME, una empresa ficticia, tiene tres servidores de aplicación en AWS, en us-east-1, con el tamaño m5.large. Una pull request los sube a m5.xlarge y, de paso, añade una base de datos de staging y una regla de security group. El bloque provider "aws" fija la región como valor constante. Con precios de lista aproximados de EC2 bajo demanda en Linux:
ACME · cambio de tamaño en la pull request
- m5.large (antes)
- unos 0,096 US$/hora × 730 h = 70,08 US$/mes por instancia
- m5.xlarge (después)
- unos 0,192 US$/hora × 730 h = 140,16 US$/mes por instancia
- Delta por instancia
- +70,08 US$/mes
- Delta de las 3 instancias
- +210,24 US$/mes
- aws_db_instance (nueva)
- sin precio: RDS aún no tiene catálogo
- aws_security_group_rule (nueva)
- sin coste por definición
Paso 1: crear la aplicación OAuth2
El CLI se autentica con OAuth2 Client Credentials. Un propietario o administrador de la organización crea la aplicación en Developer → «Aplicaciones OAuth2»:
- En «Mis aplicaciones», pulsa «Nueva aplicación».
- En «Nueva aplicación OAuth2», rellena «Nombre de la aplicación» (por ejemplo, ci-iac-cost).
- En «Permisos (scopes)», dentro del grupo «Lectura», marca sólo iac:estimate, «Estimar el coste de un plan de Terraform».
- Pulsa «Crear aplicación» y copia el client_id y el client_secret.
El client_secret se muestra una sola vez. Guárdalo directamente como secreto cifrado del CI: en GitHub, como IAC_COST_CLIENT_ID e IAC_COST_CLIENT_SECRET en los secretos del repositorio.
Paso 2: probarlo en local
Antes de tocar el pipeline, conviene probarlo en tu máquina con Node.js 20 o posterior:
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
export IAC_COST_CLIENT_ID=<client_id>
export IAC_COST_CLIENT_SECRET=<client_secret>
npx @aaa-sentinel/iac-cost plan.jsonVariables de entorno del CLI
- IAC_COST_CLIENT_ID
- Obligatoria · client_id de la aplicación OAuth2
- IAC_COST_CLIENT_SECRET
- Obligatoria · client_secret, guardado como secreto cifrado
- IAC_COST_BASE_URL
- Opcional · por defecto https://api.aaa-sentinel.com; tiene que ser https
- El comentario sale por la salida estándar y los avisos, por la de error. Si no hay nada que comentar, la salida estándar queda vacía.
- También acepta el plan por la entrada estándar, canalizándolo hacia npx @aaa-sentinel/iac-cost.
Paso 3: comentarlo en cada pull request con GitHub Actions
Este es el workflow del README del paquete. Publica un comentario por pull request y lo actualiza en los siguientes push, en lugar de apilar uno nuevo cada vez. Usa el token que el propio run ya inyecta: no hace falta un token personal ni instalar una app.
name: Infrastructure cost
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
cost:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: actions/setup-node@v4
with:
node-version: 20
- run: terraform init
- run: terraform plan -out=plan.tfplan
- run: terraform show -json plan.tfplan > plan.json
- name: Estimate cost
env:
IAC_COST_CLIENT_ID: ${{ secrets.IAC_COST_CLIENT_ID }}
IAC_COST_CLIENT_SECRET: ${{ secrets.IAC_COST_CLIENT_SECRET }}
run: npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
- name: Comment on the pull request
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.number }}
run: |
[ -s comment.md ] || exit 0
id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
--jq '.[] | select((.body // "") | contains("<!-- aaa-sentinel:iac-cost -->")) | .id' \
| head -n1)
if [ -n "$id" ]; then
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md
else
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.md
fi- Añade los pasos de credenciales de tu nube antes de terraform plan, igual que en cualquier pipeline de Terraform.
- continue-on-error no es decoración: sin él, un fallo de la API de GitHub al publicar el comentario haría fallar el job.
- En una pull request desde un fork, GitHub da al run un token de sólo lectura y el paso del comentario se salta. Es una restricción de GitHub que afecta a cualquier herramienta que comente en pull requests.
Y si usas Bitbucket Pipelines
Bitbucket no inyecta un token con permiso para escribir en el repositorio. Crea un Repository Access Token con el permiso pullrequest:write y guárdalo como variable segura del repositorio con el nombre BB_TOKEN. El set +e y el exit 0 final cumplen la misma función que continue-on-error en GitHub.
pipelines:
pull-requests:
'**':
- step:
name: Infrastructure cost
image: hashicorp/terraform:latest
script:
- apk add --no-cache nodejs npm curl jq
- terraform init
- terraform plan -out=plan.tfplan
- terraform show -json plan.tfplan > plan.json
- npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
- |
set +e
[ -s comment.md ] || exit 0
API="https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests/$BITBUCKET_PR_ID/comments"
BODY=$(jq -Rs . < comment.md)
ID=$(curl -sf -H "Authorization: Bearer $BB_TOKEN" "$API?pagelen=100" \
| jq -r '[.values[] | select((.content.raw // "") | contains("<!-- aaa-sentinel:iac-cost -->"))][0].id // empty')
if [ -n "$ID" ]; then
curl -sf -X PUT -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
-d "{\"content\":{\"raw\":$BODY}}" "$API/$ID"
else
curl -sf -X POST -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
-d "{\"content\":{\"raw\":$BODY}}" "$API"
fi
exit 0Paso 4: leer el comentario
Para la pull request de ACME, el comentario queda así (extracto; el comentario se escribe siempre en inglés):
**Coverage: 3 of 4 billable resources priced** (1 more have no cost by design).
**Estimated change: +$210.24/mo** (list price, USD)
| Resource | Change | Qty | Delta |
|---|---|---|---|
| `aws_instance` | update | 3 | +$210.24/mo |
<details><summary>Not priced (1 resources)</summary>
| Resource | Change | Qty | Why |
|---|---|---|---|
| `aws_db_instance` | new | 1 | no matching price found in the catalog |
</details>- Primero va la cobertura, no el total: 3 de 4 recursos facturables tienen precio. La regla de security group no cuenta, porque no genera factura.
- El cambio de tamaño se precia por los dos lados: el delta es lo que cuesta el después menos lo que costaba el antes, multiplicado por las 3 instancias. Una sustitución se trata igual, como una baja más un alta.
- La base de datos aparece como no preciada, con su motivo. Nunca se cuenta como si fuera gratis.
Por qué nunca bloquea el merge
El código de salida del CLI es siempre 0. Una credencial que falta, un error de red, un plan ilegible o una caída de AAA Sentinel imprimen un aviso de una línea y el pipeline continúa. Es una decisión deliberada: un comentario de coste no es una puerta de calidad, y la disponibilidad de un tercero no debe convertirse en una dependencia de tus despliegues. El CLI no ofrece un modo que falle el build si el coste supera un umbral; la decisión queda en la revisión humana de la pull request.
Cómo leer la cifra
El número del comentario tiene un significado preciso:
- Es precio de lista. Ni AWS ni Azure devuelven lo que pagas tú: los descuentos por contrato y las reservas o Savings Plans no se ven en la pull request. Si una reserva ya cubre el recurso nuevo, el coste marginal real es cero.
- Está en USD, la moneda en la que publican los proveedores. Convertirla sería una estimación sobre otra estimación, y la misma pull request mostraría otra cifra cada mañana.
- Si menos de la mitad de los recursos facturables tienen precio, se publica el desglose pero no el total: un total hecho con 2 recursos de 30 no es una estimación parcial, es ruido.
- Una región desconocida se trata como no soportada. En AWS la región vive en el bloque provider; si viene de una variable o de un provider con alias, el plan no la resuelve y el comentario lo dice en lugar de usar el precio de otra región.
Qué no hace todavía
Los límites son importantes, porque un comentario que parece completo sin serlo es peor que ninguno:
- Hoy sólo precia dos cosas: instancias EC2 bajo demanda en AWS (aws_instance) y máquinas virtuales de Azure (azurerm_linux_virtual_machine, azurerm_windows_virtual_machine y sus scale sets).
- Todo lo demás sale como no preciado, con su motivo: RDS, discos EBS, balanceadores, NAT, almacenamiento y cualquier otro tipo de recurso.
- Google Cloud se reconoce, pero su catálogo de precios todavía no está conectado. Otros proveedores, como Kubernetes, Datadog o Cloudflare, no están cubiertos.
- aws_instance se precia como Linux: el plan no dice el sistema operativo de la AMI.
- El precio de EC2 es sólo el de la instancia, sin el volumen raíz ni la transferencia de datos.
- Un mismo plan admite como mucho 5 regiones con precio, sumando las dos nubes. Por encima, la estimación falla y el pipeline sigue sin comentario.
Checklist para empezar
Con esto montado, cada cambio de infraestructura llega a la revisión con su precio:
- Plan Pro o Business.
- Una aplicación OAuth2 con sólo el permiso iac:estimate, creada en Developer → «Aplicaciones OAuth2».
- El client_id y el client_secret guardados como secretos cifrados del CI.
- Una prueba en local con terraform show -json y npx @aaa-sentinel/iac-cost.
- El workflow de GitHub Actions, o el pipeline de Bitbucket, en el repositorio de Terraform.
- La región de AWS fijada como constante en el bloque provider, para que el plan la resuelva.
- El equipo sabe leer el comentario: cobertura primero, precio de lista y en USD.