Na prática, fazendo um projeto Terraform com um AI Agent do início ao fim
No post anterior, dissecamos a anatomia de um AI Agent para Terraform: a estrutura de arquivos, o modelo de guides e sensors, o steering loop. Foi preciso um pouco de teoria para chegarmos aqui. Mas como isso funciona quando você senta na frente do terminal com um request real?
Neste post, vou fazer um passo a passo pegar um pedido de infraestrutura, passar por cada stage do processo, e mostrar como o agente opera em cada momento. O projeto é simples de propósito (uma VPC com subnets e security groups na AWS) porque o objetivo não é impressionar com complexidade de Terraform. É mostrar como o processo e os mecanismos do agente funcionam juntos numa entrega real.
Vou simular as interações, mostrando o que o agente consulta, o que ele produz, e quando ele para para pedir ajuda. Ao final, você vai ter uma ideia clara de como isso funciona no dia a dia.
O cenário
Chega o seguinte request do time de desenvolvimento:
“Precisamos de uma VPC isolada para o ambiente de staging do projeto Atlas. Três subnets privadas, três públicas, em AZs diferentes. Um security group para os ALBs e outro para as instâncias da aplicação. Conta AWS: 123456789012. Região: us-east-1.”
Simples, direto, bem definido. É o tipo de tarefa que todo mundo já fez dezenas de vezes e que, por ser “fácil”, é onde mais se acumulam desleixos.
Fluxo de trabalho
Antes de mergulhar em cada etapa, vale conhecer cada etapa do trabalho. O processo definido no PROCESS.md é uma sequência de seis stages, executados em ordem, sem pulos. Cada stage tem uma responsabilidade clara e um portão de saída, só se avança para o próximo quando o atual cumpre seus critérios. É esse encadeamento que garante que o agente entenda antes de projetar, projete antes de construir, e valide antes de aplicar.
Intake
A porta de entrada. O objetivo não é escrever código, é entender o problema e restaurar contexto: ler HANDOFF.md e MEMORY.md, levantar restrições conhecidas e traduzir um pedido vago numa lista clara de critérios de aceitação. É aqui que o agente pergunta quando não sabe, em vez de adivinhar.
Design
Define como resolver antes de sair escrevendo. O agente propõe a estrutura de arquivos, toma decisões arquiteturais (registrando o porquê no MEMORY.md) e decompõe o trabalho em tasks verificáveis no TASKS.md. Etapa de baixo custo e alto impacto: mudar uma decisão num documento é trivial, mudá-la depois de 20 recursos criados é caro.
Build
Onde o código nasce, guiado pelos guides e validado por sensors a cada passo. O agente trabalha uma task por vez e roda um micro-loop de auto-correção (escreve → formata → valida → corrige) antes de avançar. O feedforward e o feedback computacional (fmt, validate) se combinam para produzir código que segue as convenções do time sem supervisão linha a linha.
Validate
O portão antes de tocar na infraestrutura real. Vai além do terraform validate do Build e examina o que vai acontecer de fato: roda terraform plan e checa contra as Pre-Apply Guards. Aparece algum destroy inesperado? Algum recurso será recriado? O custo está dentro do esperado? Nada avança sem passar por esse crivo.
Deploy
A execução propriamente dita, com uma regra de ouro: aplicar só o plan que foi aprovado e verificar logo depois. Um apply que retorna “sucesso” ainda pode ter deixado drift, então o agente roda um plan imediatamente após para confirmar que o estado real bate com o desejado.
Close
O stage que a maioria dos processos esquece, e é justamente o que faz o sistema melhorar com o tempo. O agente confirma os critérios de aceitação, deixa o HANDOFF.md num estado retomável e decide o que aprendeu que merece virar conhecimento permanente. É o fechamento do steering loop.
Com o mapa em mãos, vamos percorrer cada stage em detalhe, começando pelo Intake.
Stage 1 — Intake, entendendo o pedido
O Intake é a porta de entrada do processo. O objetivo aqui não é escrever código, é entender o problema e carregar contexto antes de qualquer decisão. É o momento em que o agente restaura a memória de trabalho: o que já foi feito, o que o time já aprendeu, quais restrições existem. Pular essa etapa é o que faz um agente (ou uma pessoa) repetir erros que já foram resolvidos antes. Um bom intake transforma um pedido vago numa lista clara de critérios de aceitação.
O agente começa lendo HANDOFF.md (vazio, projeto novo) e MEMORY.md. No MEMORY.md, encontra uma entrada relevante de um projeto anterior:
| 2025-07-15 | projeto-orion | VPC com 3 AZs us-east-1 tem AZ us-east-1e com capacidade limitada para alguns instance types | Evitar us-east-1e para subnets de compute | active |
Isso é feedforward em ação. Antes de escrever uma linha de código, o agente já sabe de uma restrição que alguém descobriu meses atrás.
Em seguida, o agente consulta FEEDFORWARD.md e encontra na seção “Known Pitfalls”:
| FF-003 | VPC creation | Subnets sem tags Name causam confusão no console | Sempre tagar subnets com Name incluindo AZ | medium |
Com essas informações, o agente define scope e acceptance criteria:
- VPC com CIDR /16
- 3 subnets públicas + 3 privadas (evitando us-east-1e)
- Security group para ALB (porta 443 inbound, aberto)
- Security group para app (porta 8080 inbound, source = SG do ALB)
- Todas as subnets com tag Name incluindo a AZ
- Tags padrão em todos os recursos
Pergunta ao humano: “O CIDR deve ser 10.1.0.0/16 ou há conflito com VPCs existentes na conta?”
Isso é o agente reconhecendo os limites do que ele sabe. Não adivinha: pergunta.
O humano responde: “Use 10.20.0.0/16, os ranges 10.0-10.19 já estão em uso.”
O agente registra no HANDOFF.md e avança.
Com o pedido agora traduzido em critérios de aceitação claros e sem ambiguidades pendentes, o agente tem tudo que precisa para sair do “o quê” e entrar no “como”. É a deixa para o Design.
Stage 2 — Design, definindo a solução
Com o problema entendido, o Design é onde se decide como resolver antes de sair escrevendo. Aqui o agente define a estrutura de arquivos, as decisões arquiteturais e o desmembramento do trabalho em tasks. É uma etapa de baixo custo e alto impacto, mudar uma decisão de design num documento é trivial, mudá-la depois de 20 recursos criados é caro. O ponto-chave é que cada decisão relevante fica registrada com o seu porquê no MEMORY.md, para que ninguém (humano ou agente) precise adivinhar meses depois por que as coisas foram feitas de determinada forma.
Com os requisitos claros, o agente propõe a estrutura:
environments/staging/atlas/
├── main.tf — VPC, subnets, route tables
├── security.tf — Security groups
├── variables.tf — Inputs
├── outputs.tf — VPC ID, subnet IDs, SG IDs
├── providers.tf — AWS provider pinado
├── backend.tf — S3 state
└── terraform.tfvars — Valores para staging
Decisões tomadas (registradas no MEMORY.md):
| 2025-08-29 | atlas | Subnets criadas com for_each sobre as AZs (chave = AZ) em vez de count | Evita recriação em cascata ao remover/reordenar AZs; segue o anti-pattern do FEEDFORWARD | active |
| 2025-08-29 | atlas | SG da app referencia SG do ALB por ID, não por CIDR | Mais seguro e resiliente a mudanças de IP | active |
O agente verifica contra FEEDFORWARD.md e não detecta nenhum anti-pattern na abordagem proposta. O design principles section reforça: “explicit over implicit, declare dependencies, don’t rely on ordering.”
TASKS.md é populado:
| 1 | Criar VPC e subnets com routing | Build | high | terraform validate passa, plan mostra 17 recursos |
| 2 | Criar security groups com regras | Build | high | terraform validate passa, plan mostra 5 recursos |
| 3 | Configurar outputs | Build | medium | Outputs exportam vpc_id, subnet_ids, sg_ids |
| 4 | Validar plan completo | Validate | high | Plan limpo, sem destroys inesperados |
Repare que o design já embute os critérios de verificação de cada task (quantos recursos o plan deve mostrar, o que os outputs precisam exportar). Isso não é detalhe: são esses critérios que vão dar ao agente um alvo objetivo no Build. Sem eles, “criar a VPC” seria vago; com eles, cada task tem uma definição de pronto que um sensor consegue checar. Com o plano de trabalho decomposto e verificável, começa a construção.
Stage 3 — Build, escrevendo o Terraform
O Build é onde o código nasce, mas com uma diferença importante em relação ao “só pedir pro LLM gerar”: o agente escreve guiado pelos guides e validado por sensors a cada passo. Ele trabalha uma task por vez e roda um micro-loop de auto-correção (escreve → formata → valida → corrige) antes de avançar. Não precisa acertar de primeira; precisa ter um loop rápido que capture o erro cedo. É aqui que o feedforward (o que ele já sabe) e o feedback computacional (fmt, validate) se combinam para produzir código que segue as convenções do time sem supervisão linha a linha.
Vale entender por que o agente insiste em uma task por vez. Um LLM tende a gerar tudo de uma vez quando você pede “crie a VPC completa”: VPC, subnets, route tables, gateways e security groups num único bloco. O problema é que, se algo falha no meio, fica difícil isolar a causa, e o contexto do agente enche de código que ele precisa reprocessar a cada iteração. Quebrar em tasks pequenas (definidas lá no Stage 2) mantém cada passo verificável de forma independente: se a task 1 passa no validate e a task 2 falha, você sabe exatamente onde está o problema. É a mesma lógica de commits pequenos versus um commit gigante.
O agente pega a task 1 e escreve. Aqui vai um trecho do que ele produz:
locals {
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.common_tags, {
Name = "${var.project}-${var.environment}-vpc"
})
}
resource "aws_subnet" "private" {
for_each = { for idx, az in local.azs : az => idx }
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.value)
availability_zone = each.key
tags = merge(var.common_tags, {
Name = "${var.project}-${var.environment}-private-${each.key}"
Tier = "private"
})
}
Note que o agente aplicou automaticamente o pitfall FF-003 (tag Name com AZ) e evitou us-east-1e (lesson do MEMORY.md). Ele também usou for_each em vez de count, seguindo o anti-pattern documentado no FEEDFORWARD.md (“nunca use count para resources com lifecycle dependencies; use for_each”). Com for_each indexado pela AZ, remover ou reordenar uma AZ no futuro não recria as outras subnets, o que aconteceria com count. Ele não precisou que ninguém lembrasse: a informação estava nos guides.
As subnets públicas seguem exatamente o mesmo padrão (outro aws_subnet com for_each sobre as AZs, offset diferente no cidrsubnet() e Tier = "public"), então omiti o bloco aqui para não repetir. O importante é que ambas as camadas, 3 privadas e 3 públicas, uma por AZ, são geradas com a mesma técnica.
Roda terraform fmt e passa. Roda terraform validate:
Error: Reference to undeclared variable "common_tags" on main.tf line 9
Sensor computational detectou o problema. O agente auto-corrige: declara a variável em variables.tf, roda terraform validate novamente. Passa.
Vale desmontar esse micro-loop, porque é o coração do Build. Ele tem quatro passos e roda até o código ficar limpo:
- Escreve o trecho da task atual.
- Formata com
terraform fmt. É barato e determinístico; padroniza a indentação antes de qualquer coisa, evitando ruído no diff e no review depois. - Valida com
terraform validate. Aqui o sensor pega erros de sintaxe, referências a variáveis ou recursos inexistentes, tipos incompatíveis. Foi o que aconteceu acima: uma variável usada mas não declarada. - Corrige com base na mensagem de erro e volta ao passo 2. O agente não “chuta” a correção às cegas; ele lê o erro específico (
undeclared variable "common_tags"), entende a causa e aplica o remédio exato.
O ponto importante é que esse loop é rápido e local. fmt e validate rodam em milissegundos, sem tocar em nenhuma API da AWS e sem custo. O agente pode errar e corrigir cinco vezes seguidas que ninguém percebe, porque nada disso chega perto da infraestrutura real. O erro é capturado no lugar mais barato possível de resolver.
Mas atenção ao que o validate não pega. Ele confirma que o código é sintaticamente válido e internamente coerente, não que ele faz a coisa certa. Um cidr_block sobreposto entre duas subnets passa no validate. Uma regra de security group perigosamente permissiva passa no validate. Uma tag faltando passa no validate. Esses problemas semânticos só serão capturados por outros sensors: linters de política (tfsec, checkov), pela revisão do plan no Stage 4, ou pela análise inferencial do próprio agente contra os guides. Por isso o Build não é o único portão; ele é o primeiro de vários.
E quando o agente não consegue se auto-corrigir? O loop tem um limite. Se depois de algumas tentativas o mesmo erro persiste, ou se o erro exige uma decisão que ele não tem contexto para tomar (por exemplo, “esse módulo interno não existe na versão que você referenciou, qual usar?”), ele para e escala para o humano em vez de insistir num beco sem saída. Um agente que fica tentando a mesma correção dez vezes é um agente que precisava ter pedido ajuda na terceira.
Task 1 marcada como done. Puxa task 2. Mesmo ciclo: escreve, formata, valida, corrige se necessário. Tasks 3 e 4 seguem o mesmo padrão.
Ao fim do Build, todas as tasks passaram no fmt e no validate, o código está escrito e internamente coerente. Mas coerência sintática não é garantia de que o resultado na infraestrutura será o esperado. Antes de aplicar qualquer coisa, o agente precisa olhar o retrato completo do que vai mudar. É exatamente o que o próximo stage faz com o terraform plan.
Stage 4 — Validate, revisando o plan
Validate é o portão antes de tocar na infraestrutura real. O terraform validate do Build garante que o código é sintaticamente válido; o Validate vai além e examina o que vai acontecer de fato quando aplicar. É o momento de olhar o plan com desconfiança: aparece algum destroy que ninguém pediu? Algum recurso vai ser recriado (must-replace)? O custo está dentro do esperado? O agente roda um checklist explícito (as “Pre-Apply Guards”) em vez de confiar no “parece ok”. Esse é o stage que separa uma automação madura de um apply no escuro.
Com o build completo, o agente roda terraform plan:
Plan: 22 to add, 0 to change, 0 to destroy.
Antes de apresentar o resultado, ele verifica a seção “Pre-Apply Guards” do FEEDFORWARD.md:
- State backend acessível? Sim.
- Provider pinado? Sim,
~> 5.60. - Sem destroys inesperados? Sim, tudo é criação.
- Sem hardcoded secrets? Sim.
- Custo? VPC e subnets não têm custo direto. NAT Gateway (se adicionado depois) sim.
O agente apresenta o plan ao humano com um resumo:
## Plan Summary — atlas/staging
22 resources to create:
- 1 VPC
- 6 subnets (3 private, 3 public)
- 3 route tables
- 6 route table associations
- 1 internet gateway
- 2 security groups
- 3 security group rules
Destructive changes: none
Cost impact: ~$0/month (no NAT gateway, no EIPs)
Risks: none identified
Awaiting approval to apply.
O humano aprova. O agente registra: “Approval received from your-company-id, 2025-08-29 14:30.”
Esse registro de aprovação não é formalidade: ele marca a fronteira entre o que foi revisado e o que será executado. Só o plan aprovado pode ser aplicado, e é com ele em mãos, e não com uma nova geração de código, que o agente entra no Deploy.
Stage 5 — Deploy, aplicando o Terraform
O Deploy é a execução propriamente dita, e a regra de ouro é: aplicar só o plan que foi aprovado, e verificar logo depois. Aplicar não é o fim do trabalho. Um apply que retorna “sucesso” ainda pode ter deixado drift, então o agente roda um plan imediatamente após para confirmar que o estado real bate com o desejado. É um stage curto, mas é onde a operação toca o mundo real e, portanto, onde a disciplina de verificar em vez de assumir mais importa.
terraform apply
Apply complete! Resources: 22 added, 0 changed, 0 destroyed.
O agente imediatamente verifica. Roda terraform plan novamente:
No changes. Your infrastructure matches the configuration.
Zero drift. Tudo consistente.
Registra no FEEDBACK.md:
### 2025-08-29 — atlas — staging — low (success)
**Stage:** Deploy
**Action:** terraform apply — VPC, subnets, SGs for atlas staging
**Expected:** 22 resources created
**Actual:** 22 resources created successfully
**Result:** success
**Time to Resolve:** N/A
**Blast Radius:** N/A
**Lesson:** for_each com chave = AZ mantém subnets estáveis a mudanças na lista de AZs
**Propagated:** Not yet (first occurrence — observing pattern)
Com os recursos criados e o drift confirmado como zero, a parte técnica da entrega acabou. Mas a entrega em si ainda não, falta capturar o que esse trabalho ensinou e deixar o projeto num estado que qualquer pessoa consiga retomar. Esse é o papel do último stage, o Close.
Stage 6 — Close, propagando conhecimento
Close é o stage que a maioria dos processos esquece, e é justamente o que faz o sistema melhorar com o tempo. Aqui o agente confirma que os critérios de aceitação foram cumpridos, deixa o HANDOFF.md num estado que permite a qualquer pessoa retomar o trabalho e, o mais importante, decide o que do que aprendeu merece virar conhecimento permanente. Sem essa etapa, cada entrega termina e o aprendizado evapora. Com ela, o que foi descoberto num projeto vira prevenção para o próximo. É o fechamento do steering loop.
O agente verifica acceptance criteria:
- VPC criada com CIDR 10.20.0.0/16? Sim.
- 6 subnets em 3 AZs? Sim.
- SGs com regras corretas? Sim.
- Tags em todos os recursos? Sim.
Atualiza HANDOFF.md com o estado final:
| Field | Value |
|-------|-------|
| Project | atlas |
| Environment | staging |
| Process Stage | Completed |
| Status | Delivered |
| Terraform Snapshot | 22 resources, zero drift |
Atualiza TASKS.md e marca todas as tasks como done e verificadas.
Não houve falha nesta entrega, então não há novo pattern para promover. Mas se o pattern do for_each indexado por AZ se confirmar em mais projetos, ele vai para FEEDFORWARD.md como “proven pattern”.
E com isso o ciclo fecha: o pedido virou infraestrutura, as decisões ficaram registradas e o pouco que se aprendeu já está pronto para ser reaproveitado. Foi uma entrega tranquila, sem sustos. Mas o valor real do processo aparece mesmo quando algo dá errado, e é esse cenário que vamos ver a seguir.
Quando as coisas dão errado
Vamos supor um cenário alternativo. No Stage 5, o apply falhou:
Error: creating Security Group Rule: InvalidGroup.NotFound
O agente não retenta cegamente. Ele registra no FEEDBACK.md com severity medium, identifica que o security group não existia ainda quando a rule tentou ser criada (race condition na API da AWS), e aplica a correção: adiciona depends_on explícito entre o SG rule e o SG.
Reaplica. Funciona.
Agora verifica: isso já aconteceu antes? Busca no MEMORY.md e FEEDBACK.md. Encontra uma entry similar de três meses atrás num projeto diferente. Segunda ocorrência. Regra de propagação ativada:
Para MEMORY.md:
| 2025-08-29 | atlas | SG rules podem falhar com NotFound se criadas muito rápido após o SG | Usar depends_on explícito ou aws_security_group_rule com retry | E-004 |
Para FEEDFORWARD.md:
| FF-007 | SG rules creation | Race condition: rule tenta criar antes do SG propagar na API | Adicionar depends_on explícito entre rule e SG | medium | E-004 |
Da próxima vez que qualquer projeto criar security group rules, o agente vai ler FF-007 no FEEDFORWARD.md e já aplicar o depends_on preventivamente. O problema não vai se repetir uma terceira vez.
O que o agente NÃO fez
Vale notar o que o agente não fez nesse passo a passo:
- Não adivinhou o CIDR: perguntou.
- Não ignorou o checklist pré-apply: verificou item por item.
- Não descartou a falha como “transient”: registrou e investigou.
- Não expandiu o scope: quando percebeu que NAT Gateway poderia ser necessário, registrou como “discovered work” no TASKS.md e esperou o humano decidir.
Isso é o harness funcionando. O agente é capaz, mas os guides e sensors mantêm ele nos trilhos.
Conclusão
O processo completo (intake, design, build, validate, deploy, close) não é burocracia. É o que garante que:
- O agente consulta conhecimento antes de agir (feedforward)
- O agente registra resultados depois de agir (feedback)
- O agente para quando não tem certeza (escalation)
- O conhecimento se acumula e previne problemas futuros (propagation)
Um projeto simples como esse leva menos tempo com o agente do que sem. Não porque o agente digita mais rápido. Mas porque ele não esquece de verificar o plan, não pula o tagging, e não ignora um pattern que já foi documentado.
Mas talvez o ponto mais importante seja o que o agente não faz. Ele não decide sozinho o CIDR de uma rede, não aprova o próprio plan, não expande o escopo por conta própria e não aplica nada que não tenha passado por revisão. Esses não são limites que atrapalham; são o que torna o agente confiável. Um agente que decide tudo sozinho é um agente que, mais cedo ou mais tarde, vai destruir um recurso de produção com total convicção de que estava certo. O harness existe justamente para que as decisões de maior impacto (o que aprovar, o que aplicar, quando expandir o escopo) permaneçam com quem tem contexto e responsabilidade: o humano.
Ese é um ponto que eu acho que vale insistir: a revisão humana não é um resquício do processo antigo, é uma etapa de primeira classe. O agente prepara o terreno, ou seja, gera o código, roda os sensors, monta um resumo honesto do plan com destaque para riscos e mudanças destrutivas, e então para e espera. Quem lê o plan, pondera o impacto no negócio e diz “pode aplicar” é a pessoa. O agente otimiza o trabalho da revisão (chega tudo formatado, validado e explicado), mas não a substitui. A responsabilidade pelo que vai para produção continua sendo humana, porque é o humano que responde pela auditoria, que é acordado às 3h da manhã se algo cair, e que carrega o contexto que nenhum guide captura por completo. O agente executa; a pessoa dirige e decide.
No próximo e último post da série, vou mostrar como transformar esse sistema em algo que o time inteiro usa, como distribuir o agente como um template, como evoluir o catálogo, e como fazer a adoção de forma incremental sem forçar ninguém.
Abraços!
Vida longa e próspera a todos!!
Referências
- Parte 1 - A anatomia de um AI Agent para projetos de IaC
- Harness engineering: leveraging Codex in an agent-first world — OpenAI, Fev 2026
- Harness design for long-running application development — Anthropic, Mar 2026
- Harness engineering for coding agent users — Martin Fowler / Birgitta Böckeler, Abr 2026
Entre em contato:
NewsLetter - https://engineeringmanager.com.br/Linkdin - linkedin.com/in/leonardoml/
Twitter: @infraascode_br
Te convido a ver os outros posts do blog Infra-as-Code garanto que tem coisas legais lá!!
|
|
