DevOps10 de outubro de 2026 9 min de leitura

O custo da infraestrutura na pull request: estimar um plano de Terraform antes do merge

O custo de uma alteração de infraestrutura costuma descobrir-se na fatura, um mês depois. Mostramos como o AAA Sentinel comenta em cada pull request quanto um plano de Terraform faz subir ou descer o gasto mensal, o que sai da sua máquina, como o montar no GitHub Actions e o que ainda não sabe orçamentar.

Este artigo também está disponível em:esenfr

Uma linha num ficheiro .tf troca m5.large por m5.xlarge. A revisão concentra-se em saber se o código está correto, ninguém pergunta quanto custa e a alteração é integrada. Um mês depois, a fatura de EC2 subiu e é preciso reconstruir o que aconteceu. Neste artigo mostramos como pôr o preço de cada alteração na própria pull request com o AAA Sentinel, quando ainda é barato mudar de ideias.

O problema: o custo chega um mês atrasado

Na maioria das equipas, o custo da infraestrutura só se vê depois de estar implementada. Isso tem três consequências:

  • Quem decide a alteração não vê o seu preço: o programador escolhe um tamanho de instância sem saber quanto custa por mês.
  • Quem vê o preço não pode decidir: as finanças descobrem a subida na fatura, quando o recurso já está em produção e é preciso convencer alguém a alterá-lo.
  • Corrigir tarde custa mais: um tamanho muda-se em segundos numa pull request, e em produção exige uma janela de manutenção.

É o que se conhece como FinOps «shift-left»: levar a informação de custo ao momento em que se toma a decisão técnica, tal como se fez antes com os testes ou a segurança.

O que ganha com isso

Com o custo na pull request:

  • Cada alteração de infraestrutura chega à revisão com o seu delta mensal estimado.
  • A conversa sobre o custo acontece entre quem escreve a alteração e quem a revê, e não um mês depois entre finanças e engenharia.
  • Vê também o que não foi possível orçamentar e porquê, em vez de um número que parece completo sem o ser.
  • Não acrescenta um ponto de falha à sua implementação: se a estimativa falhar, o pipeline continua.

Como o AAA Sentinel o resolve

A peça é uma CLI publicada no npm, @aaa-sentinel/iac-cost. Lê a saída de terraform show -json, reduz-a na sua máquina ao indispensável, pede a estimativa à API do AAA Sentinel e escreve na saída padrão um comentário em Markdown pronto a publicar na pull request. Vem acompanhada de exemplos para GitHub Actions e Bitbucket Pipelines que publicam o comentário e o atualizam em cada push.

Planos: é preciso Pro ou Business e uma aplicação OAuth2 com a permissão iac:estimate. O Pro permite duas aplicações OAuth2 e o Business, dez.

O que sai da sua máquina (e o que não sai)

A saída de terraform show -json contém intervalos CIDR, identificadores de conta e, muitas vezes, segredos em claro. Por isso o plano não sai da sua máquina. Antes de enviar o que quer que seja, a CLI reduz-o localmente a uma lista branca:

  • O tipo de recurso e a ação: criação, alteração, remoção ou substituição.
  • Quantas instâncias o bloco produz: count e for_each são agrupados numa quantidade.
  • Os poucos atributos que mexem no preço: o tamanho da instância e, quando o recurso a tem, a região.

Nem sequer viajam os endereços de recurso, como aws_instance.web[0]: num repositório público, o comentário seria um mapa da sua infraestrutura. A projeção também nunca é escrita no log do CI. E a permissão iac:estimate não dá acesso a mais nada: não pode ler as suas contas, descobertas nem relatórios, pelo que é seguro guardá-la num CI que terceiros podem disparar.

Caso prático: a pull request da ACME

A ACME, uma empresa fictícia, tem três servidores de aplicação na AWS, em us-east-1, com o tamanho m5.large. Uma pull request passa-os para m5.xlarge e, de caminho, acrescenta uma base de dados de staging e uma regra de security group. O bloco provider "aws" fixa a região como valor constante. Com preços de tabela aproximados de EC2 a pedido em Linux:

ACME · alteração de tamanho na pull request

m5.large (antes)
cerca de 0,096 USD/hora × 730 h = 70,08 USD/mês por instância
m5.xlarge (depois)
cerca de 0,192 USD/hora × 730 h = 140,16 USD/mês por instância
Delta por instância
+70,08 USD/mês
Delta das 3 instâncias
+210,24 USD/mês
aws_db_instance (nova)
sem preço: o RDS ainda não tem catálogo
aws_security_group_rule (nova)
sem custo por definição

Passo 1: criar a aplicação OAuth2

A CLI autentica-se com OAuth2 Client Credentials. Um proprietário ou administrador da organização cria a aplicação em Desenvolvedor → «Aplicações OAuth2»:

  • Em «Minhas aplicações», carregue em «Nova aplicação».
  • Em «Nova aplicação OAuth2», preencha «Nome da aplicação» (por exemplo, ci-iac-cost).
  • Em «Permissões (scopes)», dentro do grupo «Leitura», marque só iac:estimate, «Estimar o custo de um plano de Terraform».
  • Carregue em «Criar aplicação» e copie o client_id e o client_secret.

O client_secret é mostrado uma única vez. Guarde-o logo como segredo cifrado do CI: no GitHub, como IAC_COST_CLIENT_ID e IAC_COST_CLIENT_SECRET nos segredos do repositório.

Passo 2: testar em local

Antes de mexer no pipeline, convém testá-lo na sua máquina com Node.js 20 ou 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

Variáveis de ambiente da CLI

IAC_COST_CLIENT_ID
Obrigatória · client_id da aplicação OAuth2
IAC_COST_CLIENT_SECRET
Obrigatória · client_secret, guardado como segredo cifrado
IAC_COST_BASE_URL
Opcional · por omissão https://api.aaa-sentinel.com; tem de ser https
  • O comentário sai pela saída padrão e os avisos pela de erro. Se não houver nada a comentar, a saída padrão fica vazia.
  • Também aceita o plano pela entrada padrão, encaminhado para npx @aaa-sentinel/iac-cost.

Passo 3: comentar cada pull request com GitHub Actions

Este é o workflow do README do pacote. Publica um comentário por pull request e atualiza-o nos push seguintes, em vez de empilhar um novo de cada vez. Usa o token que a própria execução já injeta: não é preciso um token pessoal nem instalar uma 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
  • Acrescente os passos de credenciais da sua nuvem antes de terraform plan, como em qualquer pipeline de Terraform.
  • O continue-on-error não é decoração: sem ele, uma falha da API do GitHub ao publicar o comentário faria falhar o job.
  • Numa pull request vinda de um fork, o GitHub dá à execução um token só de leitura e o passo do comentário é saltado. É uma restrição do GitHub que se aplica a qualquer ferramenta que comente em pull requests.

E se usar Bitbucket Pipelines

O Bitbucket não injeta um token com permissão para escrever no repositório. Crie um Repository Access Token com o scope pullrequest:write e guarde-o como variável segura do repositório com o nome BB_TOKEN. O set +e e o exit 0 final cumprem a mesma função que o continue-on-error no 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

Passo 4: ler o comentário

Para a pull request da ACME, o comentário fica assim (excerto; o comentário é sempre escrito em inglês):

Comentário na pull request (excerto)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>
  • Primeiro vem a cobertura, não o total: 3 de 4 recursos faturáveis têm preço. A regra de security group não conta, porque não gera fatura.
  • A alteração de tamanho é orçamentada dos dois lados: o delta é o custo do depois menos o custo do antes, multiplicado pelas 3 instâncias. Uma substituição é tratada da mesma forma, como uma remoção mais uma criação.
  • A base de dados aparece como não orçamentada, com o seu motivo. Nunca é contada como se fosse gratuita.

Porque nunca bloqueia o merge

O código de saída da CLI é sempre 0. Uma credencial em falta, um erro de rede, um plano ilegível ou uma falha do AAA Sentinel imprimem um aviso de uma linha e o pipeline continua. É uma decisão deliberada: um comentário de custo não é um portão de qualidade, e a disponibilidade de um terceiro não deve tornar-se uma dependência das suas implementações. A CLI não oferece um modo que faça falhar o build quando o custo passa de um limiar; a decisão fica na revisão humana da pull request.

Como ler o número

O valor do comentário tem um significado preciso:

  • É preço de tabela. Nem a AWS nem o Azure devolvem o que paga: os descontos contratuais e as reservas ou Savings Plans não se veem na pull request. Se uma reserva já cobre o recurso novo, o custo marginal real é zero.
  • Está em USD, a moeda em que os fornecedores publicam. Convertê-lo seria uma estimativa sobre outra estimativa, e a mesma pull request mostraria outro valor todas as manhãs.
  • Se menos de metade dos recursos faturáveis tiverem preço, publica-se o detalhe mas não o total: um total feito com 2 recursos em 30 não é uma estimativa parcial, é ruído.
  • Uma região desconhecida é tratada como não suportada. Na AWS a região vive no bloco provider; se vier de uma variável ou de um provider com alias, o plano não a resolve e o comentário di-lo em vez de usar o preço de outra região.

O que ainda não faz

Os limites são importantes, porque um comentário que parece completo sem o ser é pior do que nenhum:

  • Hoje só orçamenta duas coisas: instâncias EC2 a pedido na AWS (aws_instance) e máquinas virtuais do Azure (azurerm_linux_virtual_machine, azurerm_windows_virtual_machine e os respetivos scale sets).
  • Tudo o resto aparece como não orçamentado, com o seu motivo: RDS, volumes EBS, balanceadores, NAT, armazenamento e qualquer outro tipo de recurso.
  • O Google Cloud é reconhecido, mas o seu catálogo de preços ainda não está ligado. Outros fornecedores, como Kubernetes, Datadog ou Cloudflare, não estão cobertos.
  • O aws_instance é orçamentado como Linux: o plano não diz o sistema operativo da AMI.
  • O preço de EC2 é só o da instância, sem o volume raiz nem a transferência de dados.
  • Um mesmo plano admite no máximo 5 regiões com preço, somando as duas nuvens. Acima disso, a estimativa falha e o pipeline continua sem comentário.

Checklist para começar

Com isto montado, cada alteração de infraestrutura chega à revisão com o seu preço:

  • Plano Pro ou Business.
  • Uma aplicação OAuth2 só com a permissão iac:estimate, criada em Desenvolvedor → «Aplicações OAuth2».
  • O client_id e o client_secret guardados como segredos cifrados do CI.
  • Um teste em local com terraform show -json e npx @aaa-sentinel/iac-cost.
  • O workflow de GitHub Actions, ou o pipeline de Bitbucket, no repositório de Terraform.
  • A região da AWS fixada como constante no bloco provider, para que o plano a resolva.
  • Uma equipa que sabe ler o comentário: cobertura primeiro, preço de tabela, em USD.