IaC e código no pipeline: Terraform, CloudFormation, Kubernetes e o CLI
Cinco revisões heurísticas sobre artefatos exportados (plano do Terraform, template do CloudFormation, manifestos do Kubernetes, código e package.json) e o CLI que transforma cada uma em código de saída, Markdown e SARIF dentro do GitHub Actions.
12 min de leitura
Escolha como aprender
combine como quiserDica: o vídeo é um resumo visual rápido; o áudio e o texto trazem a aula completa. Terminar o áudio ou o vídeo, ou passar no quiz, já conta a aula como concluída.
Nas aulas anteriores o MCP respondeu a perguntas de desenho. Nesta ele entra no pipeline: revisa o plano do Terraform, o template do CloudFormation, os manifestos do Kubernetes, o código e o package.json da plataforma Orders, e devolve um código de saída que o CI entende. A regra que sustenta tudo: heurística faz triagem, política como código faz a trava.
Revise o plano, não o diff
O diff de um pull request mostra o que mudou no texto dos arquivos .tf. O plano mostra o que o Terraform vai fazer na conta. São coisas diferentes, e a distância entre elas cresce com módulos, variáveis por ambiente e IaC gerada por agente. Na plataforma Orders, trocar engine_version num tfvars aparece no diff como uma linha; no plano, aparece como delete seguido de create do banco de dados.
Por isso a revisão trabalha sobre o JSON do plano, e não sobre o HCL:
terraform plan -out tfplangrava o plano binário.terraform show -json tfplan > plan.jsonexporta no formato que a HashiCorp documenta:format_version,terraform_version,resource_changescomchange.actions,change.afterereplace_paths.terraform_plan_reviewlê esse arquivo. Ele conta as ações (create,update,delete,replace,no-op,read), tratadelete+createcomoreplace, mantém uma lista de tipos com estado (db_instance,dynamodb_table,s3_bucket,kms_key,eks_cluster,iam_role, e os equivalentes de Azure, GCP e OCI) e varre os atributos deaftercom padrões: acesso público,0.0.0.0/0, criptografia desligada, credencial literal, política comAction: *, provider semversion_constraint.
O que ele não enxerga também está escrito na resposta: valores known after apply não existem no JSON, então não são avaliados. O limite é de 2000 mudanças de recurso por chamada, e qualquer provider serve, porque o formato do plano é o mesmo para AWS, Azure ou GCP. O mesmo arquivo funciona com OpenTofu.
terraform plan -out tfplan -input=false
terraform show -json tfplan > plan.json
# local, sem servidor e sem cota: as ferramentas rodam no proprio processo
node dist/cli.js call terraform_plan_review --input plan.json --format markdown
# o mesmo, com codigo de saida para o pipeline
node dist/cli.js call terraform_plan_review --input plan.json --format sarif --fail-on high > review.sarif
echo "exit=$?" # 0 ok · 1 achado em ou acima de high · 2 erro de uso ou de entradaO que o plano do Orders devolveu
O plano de exemplo em docs/examples/terraform_plan_review.json tem três recursos. aws_db_instance.orders sai como ["delete","create"] com replace_paths: [["engine_version"]] e skip_final_snapshot: true. aws_security_group_rule.api_ingress é um create na porta 443 com cidr_blocks: ["0.0.0.0/0"]. aws_s3_bucket.invoices é um update que deixa versioning.enabled = false.
A ferramenta devolveu cinco achados, summary: {high: 2, medium: 2, info: 1} e o campo gate em Block: high findings need a human decision before apply:
- high
aws_db_instance.orders: replace de um recurso com estado; confirmar backup, migração de dados e se um blocomovedoulifecycle.prevent_destroyera a intenção. - high
aws_db_instance.orders: replacement forçado porengine_version; esperar indisponibilidade. - medium
aws_db_instance.orders:skip_final_snapshot=true, um destroy não guarda snapshot. - medium
aws_security_group_rule.api_ingress: ingress da internet inteira; verificar portas e intenção. - info
aws_s3_bucket.invoices: versionamento desligado.
A leitura que eu faço: a porta 443 aberta para o mundo numa API pública é intenção, e a mensagem diz exatamente isso, "verifique portas e intenção". Os dois high são o problema real. Um upgrade de engine que recria o banco de 300 pedidos por minuto, com skip_final_snapshot ligado, é uma troca sem rede de proteção. A decisão possível: upgrade in-place com janela combinada, ou blue/green com snapshot antes. A ferramenta não escolhe; ela bloqueia até alguém escrever a escolha. É isso que o gate significa, e é por isso que o pipeline sai com código 1 e não com um deny.
{
"kind": "terraform-plan-heuristic-review",
"terraformVersion": "1.9.5",
"resources": 3,
"actions": { "create": 1, "update": 1, "delete": 0, "replace": 1, "no-op": 0, "read": 0 },
"findings": [
{ "severity": "high", "target": "aws_db_instance.orders",
"message": "replace of a stateful resource (aws_db_instance); confirm backups, data migration and whether a moved block or lifecycle prevent_destroy was intended." },
{ "severity": "high", "target": "aws_db_instance.orders",
"message": "Replacement forced by: engine_version; expect downtime for this resource." },
{ "severity": "medium", "target": "aws_db_instance.orders",
"message": "skip_final_snapshot=true: a destroy will not keep a snapshot." },
{ "severity": "medium", "target": "aws_security_group_rule.api_ingress",
"message": "Ingress from the entire internet; verify ports and intent." },
{ "severity": "info", "target": "aws_s3_bucket.invoices",
"message": "Versioning disabled on storage." }
],
"summary": { "high": 2, "medium": 2, "info": 1 },
"gate": "Block: high findings need a human decision before apply",
"limitations": [
"Pattern matching on planned attribute values; unknown-after-apply values are invisible",
"Not a replacement for policy as code (OPA/Conftest, Sentinel, Checkov, tfsec) or for reading the plan diff",
"A clean result is not an approval to apply"
]
}CloudFormation e Kubernetes: a mesma leitura, outro formato
cloudformation_review recebe o template em JSON (Resources é obrigatório, máximo de 200 recursos) e aplica um subconjunto fixo de checagens. No template do Orders em docs/examples/cloudformation_review.json vieram três achados:
- high
Resources.Db:PubliclyAccessible: truenumAWS::RDS::DBInstance; validar a intenção de rede e de security group. - medium
Resources.Invoices.Properties.PublicAccessBlockConfiguration.RestrictPublicBuckets: a flag não está explicitamentetrue. A mensagem pede para resolver herança da conta e funções intrínsecas antes de julgar exposição, porque a ferramenta não avaliaRef,Fn::Ifou parâmetros: o que vem de função intrínseca é marcado para revisão humana, não resolvido. - medium
Resources.Api:AWS::Lambda::FunctionsemReservedConcurrentExecutions, ou seja, sem orçamento explícito de concorrência numa conta compartilhada.
Security group com 0.0.0.0/0 sai como medium com a nota "public HTTP is not automatically a defect". As limitações dizem o resto: não é cfn-lint, não é CloudFormation Guard, não é Access Analyzer.
kubernetes_manifest_review recebe um array de objetos JSON (converta o YAML, um objeto por documento; kind: List é desmontado; máximo de 300). O Deployment/orders-api de exemplo faz nove coisas certas: 3 réplicas, RollingUpdate, requests e limits, readinessProbe e livenessProbe, runAsNonRoot, drop: [ALL], readOnlyRootFilesystem, imagem fixada em 1.14.2 e um PodDisruptionBudget com minAvailable: 2 casando o label. Nada disso aparece na resposta: a ferramenta só fala do que falta. Sobraram dois achados: high Deployment/orders-api/api, segredo literal em env DB_PASSWORD, usar secretKeyRef ou um cofre externo; e info, nenhuma NetworkPolicy no namespace orders, todo tráfego pod a pod liberado. Dois achados num manifesto que faz nove coisas certas é a assinatura de um time que fez o trabalho; a heurística mostra onde ficou o risco restante.
Código e dependências: o que uma regex enxerga
code_review_architecture recebe código como texto (filename, language, code até 60.000 caracteres ou 3.000 linhas) e aplica cerca de vinte regras de expressão regular, linha a linha. O orders-service.ts de exemplo tem dez linhas e rendeu cinco achados: linha 1 hardcoded-secret (high, const DB_PASSWORD = 'pr0d-...'), linha 3 sql-concat (high, SELECT montado com + id +), linha 7 plain-http e no-timeout (medium, fetch('http://notify.internal/send') sem timeout visível), linha 9 swallowed-error (medium, catch (e) {}). Junto vêm metrics e quatro architectureQuestions, do tipo "onde vivem timeout, retry e idempotência de cada chamada de saída?".
As limitações são explícitas: regex por linha, sem parsing, sem fluxo de dados, sem semântica; um exemplo documentado pode ser falso positivo. Para impor regra, use Semgrep, CodeQL, Bandit, gosec ou o linter da linguagem. Esta ferramenta serve para o agente ler um trecho colado e te devolver perguntas de arquitetura antes de abrir a conversa com o time.
dependency_manifest_review recebe o manifesto como texto (package.json, requirements.txt, pyproject.toml, go.mod, pom.xml, Gemfile, Cargo.toml) e a flag lockfilePresent. O package.json do Orders tem quatro dependências e cinco achados: left-pad: "*" (high, aceita qualquer versão), internal-lib: github:example/internal-lib (medium, fonte git, fixar num hash de commit e ler o código), scripts.postinstall: node scripts/telemetry.js (medium, script que roda na instalação), express: ^4.19.2 (info, o lockfile fixa, confirme que o install usa npm ci) e engines ausente (info). É exatamente por causa do postinstall que o próprio MCP se instala com npm ci --ignore-scripts. Limitação: texto puro, sem consulta a registry, sem dados de vulnerabilidade; pareie com npm audit, pip-audit ou osv-scanner.
Heurística é triagem; política como código é a trava
O gate: Block e o código de saída 1 param o pipeline para um humano decidir. Isso não é uma política. Um padrão que não está na regex passa em silêncio, um valor known after apply é invisível, e summary zerado não é aprovação (a própria resposta diz "a clean result is not an approval to apply"). O que impõe regra nos pipelines que eu opero é outra camada: OPA/Conftest ou Sentinel sobre o JSON do plano, Checkov ou tfsec sobre o HCL, cfn-lint e CloudFormation Guard sobre o template, Kyverno ou Pod Security Admission dentro do cluster. O MCP fica antes, como primeira leitura barata que o agente consegue explicar em linguagem natural. Localmente, pelo CLI, ele roda sem rede e sem cota; pelo endpoint público, valem os 300 pedidos por dia por chamador.
O CLI no CI: formato, código de saída e SARIF
O CLI (node dist/cli.js) carrega as 48 ferramentas públicas no próprio processo: não abre servidor, não chama a nuvem, não gasta cota. Três comandos: list, describe <tool> (descrição e JSON Schema de entrada) e call <tool>. Em call, --input aceita o arquivo de argumentos ou o documento cru: para terraform_plan_review ele embrulha como plan, para cloudformation_review como template, para iam_policy_review, openapi_review e mcp_config_review no campo esperado, para code_review_architecture o texto vai em code com o nome do arquivo em filename, e kubernetes_manifest_review aceita um array JSON direto. - lê da entrada padrão.
--format escolhe json (padrão), markdown ou sarif. --fail-on high|medium|info define o limiar. Códigos de saída: 0 sem achado no limiar, 1 achado em ou acima de --fail-on, 2 erro de uso ou de entrada (JSON inválido, plano sem resource_changes, ferramenta desconhecida). Esse 2 importa: um arquivo quebrado não pode parecer um plano limpo.
O SARIF 2.1.0 mapeia high para error, medium para warning e info para note; o ruleId é a regra da ferramenta (sql-concat) ou <tool>/<severity>; locations aponta para o arquivo de entrada e, nas revisões de código, para a linha. Com github/codeql-action/upload-sarif@v3, os achados viram anotações no pull request e aparecem na aba Security. Isso exige security-events: write no job; se a sua organização trava as permissões de workflow em read, como a minha, fique com o Markdown no $GITHUB_STEP_SUMMARY, que aparece no Summary do run sem permissão extra. Como ainda não existe publicação no npm, o job clona o repositório e roda npm ci --ignore-scripts && npm run build antes de chamar o CLI; o npx moretes-architect que aparece no skill iac-change-review é a forma futura.
jobs:
iac-review:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # so para upload-sarif; sem ele, fique no Summary
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build the workbench CLI
run: |
git clone --depth 1 https://github.com/fernando-moretes/moretes-aws-architect-mcp .workbench
cd .workbench && npm ci --ignore-scripts && npm run build
- uses: hashicorp/setup-terraform@v3
- name: Export the plan
run: |
terraform init -input=false
terraform plan -out tfplan -input=false
terraform show -json tfplan > plan.json
- name: Review the plan (markdown to the job summary)
run: node .workbench/dist/cli.js call terraform_plan_review --input plan.json --format markdown >> "$GITHUB_STEP_SUMMARY"
- name: Review the plan (SARIF, fails on high)
run: node .workbench/dist/cli.js call terraform_plan_review --input plan.json --format sarif --fail-on high > review.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always() # publica mesmo quando o passo anterior saiu com 1
with:
sarif_file: review.sarifMonte o job na ordem
Coloque os passos do job de revisão de IaC na ordem em que eles precisam rodar.
- 1node dist/cli.js call terraform_plan_review --input plan.json --format sarif --fail-on high
- 2actions/checkout e actions/setup-node com Node 22
- 3terraform show -json tfplan > plan.json
- 4upload-sarif com if: always() e Markdown no $GITHUB_STEP_SUMMARY
- 5npm ci --ignore-scripts && npm run build no clone do MCP
- 6terraform plan -out tfplan
O que você leva desta aula
terraform show -json), não o diff: um engine_version numa variável é uma linha no diff e um replace do banco no plano.terraform_plan_review devolve actions, findings, summary e gate; Block significa decisão humana antes do apply, não um deny.upload-sarif@v3, precisa de security-events: write); Markdown no $GITHUB_STEP_SUMMARY não precisa de permissão extra.