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:
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.jsonVariá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.
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.
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 0Passo 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):
**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.