Pular para o conteúdo
fernando.moretes.com
BlogEstudosCursosE-booksOpen SourcePodcasts
Loading…
Fernando Azevedo

Arquiteto de TI Especialista

Arquitetura, AWS, IA em produção, sistemas financeiros e FinOps, escritos a partir do que foi operado, com os números.

  • LinkedIn
  • GitHub
  • E-mail

Conteúdo

  • Blog
  • Estudos de arquitetura
  • Cursos
  • E-books
  • Open Source
  • Podcasts
  • Assuntos
  • Trilhas

Ferramentas

  • Moretes Architect MCP
  • Well-Architected self-check
  • Arquitetura deste site
  • Números públicos
  • Histórico de estudo

Sobre

  • Comunidade
  • IA Builder
  • Perfil
  • Currículo (CV)
  • Trabalhe comigo
  • Media kit
  • RSS do blog
  • RSS dos estudos
  • Artigos narrados (podcast)
  • llms.txt
  • Termos, privacidade e uso de IA

(c) 2026 Fernando Francisco Azevedo

MCP para Arquitetos: o Moretes Architect na prática/IaC e código no pipeline: Terraform, CloudFormation, Kubernetes e o CLI
Módulo 2 · Trabalhar· Aula 07/10

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 quiser

Dica: 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.

Assista a esta aula
Narrado por Fernando AzevedoMP4PDF
Ouça esta aula
0:0014:39
Velocidade
Baixar

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:

  1. terraform plan -out tfplan grava o plano binário.
  2. terraform show -json tfplan > plan.json exporta no formato que a HashiCorp documenta: format_version, terraform_version, resource_changes com change.actions, change.after e replace_paths.
  3. terraform_plan_review lê esse arquivo. Ele conta as ações (create, update, delete, replace, no-op, read), trata delete + create como replace, 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 de after com padrões: acesso público, 0.0.0.0/0, criptografia desligada, credencial literal, política com Action: *, provider sem version_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.

Exportar o plano e revisar localmente, sem cliente MCP: o CLI carrega as mesmas ferramentas no próprio processo
bash
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 entrada

O 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 bloco moved ou lifecycle.prevent_destroy era a intenção.
  • high aws_db_instance.orders: replacement forçado por engine_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.

Resposta real de terraform_plan_review para o plano do Orders (encurtada; campos e valores são os de docs/examples)
json
{
  "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: true num AWS::RDS::DBInstance; validar a intenção de rede e de security group.
  • medium Resources.Invoices.Properties.PublicAccessBlockConfiguration.RestrictPublicBuckets: a flag não está explicitamente true. A mensagem pede para resolver herança da conta e funções intrínsecas antes de julgar exposição, porque a ferramenta não avalia Ref, Fn::If ou parâmetros: o que vem de função intrínseca é marcado para revisão humana, não resolvido.
  • medium Resources.Api: AWS::Lambda::Function sem ReservedConcurrentExecutions, 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.

Job do GitHub Actions: plano exportado, revisão com --fail-on high, Markdown no Summary e SARIF no code scanning
text
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.sarif
Ordenar

Monte o job na ordem

Coloque os passos do job de revisão de IaC na ordem em que eles precisam rodar.

  1. 1node dist/cli.js call terraform_plan_review --input plan.json --format sarif --fail-on high
  2. 2actions/checkout e actions/setup-node com Node 22
  3. 3terraform show -json tfplan > plan.json
  4. 4upload-sarif com if: always() e Markdown no $GITHUB_STEP_SUMMARY
  5. 5npm ci --ignore-scripts && npm run build no clone do MCP
  6. 6terraform plan -out tfplan

O que você leva desta aula

Revise o plano (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.
As cinco revisões só falam do que falta: um manifesto com probes, limits e PDB e dois achados é um time que fez o trabalho.
O CLI roda as ferramentas no próprio processo, sem rede e sem cota, e devolve 0, 1 ou 2; o 2 impede que um arquivo quebrado pareça um plano limpo.
SARIF vira anotação no PR (upload-sarif@v3, precisa de security-events: write); Markdown no $GITHUB_STEP_SUMMARY não precisa de permissão extra.
Heurística é triagem. A trava é OPA/Conftest, Checkov, cfn-lint/Guard, Kyverno, Semgrep/CodeQL, cada uma no seu artefato.
Quiz

Checagem rápida

1. Por que terraform_plan_review lê o JSON de `terraform show -json` em vez do diff dos arquivos .tf?
2. O passo `node dist/cli.js call terraform_plan_review --input plan.json --format sarif --fail-on high` terminou com código 2. O que aconteceu?
3. O campo `gate` veio como "Block: high findings need a human decision before apply". O que isso significa no pipeline?
4. code_review_architecture devolveu zero achados num arquivo de 400 linhas. Qual é a conclusão correta?

Referências

HashiCorp: JSON Output Format (terraform show -json)Kubernetes: Pod Security StandardsGitHub Docs: Uploading a SARIF file to GitHubGitHub Docs: Workflow commands for GitHub Actions (job summary)Moretes Architect MCP: docs/tools.mdConftest: OPA policies for Terraform plans, CloudFormation and KubernetesKyverno: Kubernetes policy engine
Concluir e ir para a próxima Aula anterior