DevOps10 de octubre de 2026 9 min de lectura

El coste de la infraestructura, en la pull request: estimar un plan de Terraform antes del merge

El coste de un cambio de infraestructura suele descubrirse en la factura, un mes después. Te contamos cómo AAA Sentinel comenta en cada pull request cuánto sube o baja el gasto mensual de un plan de Terraform, qué sale de tu máquina, cómo montarlo en GitHub Actions y qué no sabe preciar todavía.

Este artículo también está disponible en:enfrpt

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:

Terminalbash
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.json

Variables 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.

.github/workflows/infrastructure-cost.ymlyaml
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.

bitbucket-pipelines.ymlyaml
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 0

Paso 4: leer el comentario

Para la pull request de ACME, el comentario queda así (extracto; el comentario se escribe siempre en inglés):

Comentario en la pull request (extracto)markdown
**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.