Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Construir um agente LLM que gera código Terraform HCL a partir de linguagem natural
Implementar validação de sintaxe Terraform como tool do agente
Implementar verificação de policy OPA como tool do agente
Conectar 4 tools (gerar, validar, checar policy, estimar custo) em loop de agente
Completar os 2 TODOs do starter: validar_terraform e verificar_policy_opa
Por que isso importa
Terraform HCL tem uma curva de aprendizado alta: sintaxe de blocos, referências entre recursos (aws_instance.web.id), providers, módulos, state. Um copilot que gera HCL correto e válido a partir de linguagem natural reduz drasticamente o tempo de um dev para provisionar infra.
O problema real: LLMs geram HCL plausível mas frequentemente incorreto — chaves desbalanceadas, argumentos inexistentes na versão do provider, recursos sem tags de conformidade. A solução é um pipeline de validação automática: gerar → validar → checar policy → estimar custo, tudo antes de qualquer terraform apply.
Conceitos Fundamentais
Estrutura do Agente IaC Copilot
O agente tem 4 tools em sequência mandatória:
1. gerar_terraform → LLM gera o HCL
2. validar_terraform → sintaxe OK?
3. verificar_policy_opa → segurança OK?
4. estimar_custo → quanto vai custar?O system prompt do agente instrui explicitamente essa ordem:
system='Você é um IaC Copilot. Sempre: 1) Gere o Terraform, 2) Valide, 3) Verifique policies, 4) Estime custo.'O LLM entende a instrução e executa as 4 tools automaticamente — sem lógica condicional no orquestrador.
TODO 1: validar_terraform(codigo_hcl)
A versão completa valida sintaxe básica sem precisar do binário terraform:
def validar_terraform(codigo_hcl: str) -> str:
erros = []
# Verificação de blocos obrigatórios
if 'resource' not in codigo_hcl and 'module' not in codigo_hcl:
erros.append('Nenhum bloco resource ou module encontrado')
# Verificação de chaves balanceadas
if codigo_hcl.count('{') != codigo_hcl.count('}'):
erros.append('Chaves desbalanceadas')
# Verificação de aspas (HCL usa aspas duplas)
linhas = codigo_hcl.split('\n')
for i, linha in enumerate(linhas, 1):
if linha.count('"') % 2 != 0:
erros.append(f'Linha {i}: aspas não fechadas')
if erros:
return f'INVÁLIDO: {"; ".join(erros)}'
return 'VÁLIDO: sintaxe correta'Para validação real (quando terraform está instalado):
import subprocess, tempfile, os
def validar_terraform_real(codigo_hcl: str) -> str:
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, 'main.tf'), 'w') as f:
f.write(codigo_hcl)
resultado = subprocess.run(
['terraform', 'validate', '-json'],
cwd=tmpdir,
capture_output=True,
text=True,
)
data = json.loads(resultado.stdout)
if data['valid']:
return 'VÁLIDO: terraform validate OK'
return f"INVÁLIDO: {data['error_count']} erros"TODO 2: verificar_policy_opa(codigo_hcl, politicas)
OPA (Open Policy Agent) verifica políticas de segurança. O mock analisa o HCL:
def verificar_policy_opa(codigo_hcl: str, politicas: list[str] = None) -> str:
politicas = politicas or ['no-public-s3', 'require-tags', 'no-root-iam']
resultados = []
for politica in politicas:
if politica == 'no-public-s3':
if 'acl = "public-read"' in codigo_hcl or 'acl = "public-read-write"' in codigo_hcl:
resultados.append(f'FALHA: {politica} — bucket S3 público detectado')
else:
resultados.append(f'OK: {politica}')
elif politica == 'require-tags':
if 'tags' not in codigo_hcl:
resultados.append(f'AVISO: {politica} — recurso sem tags obrigatórias')
else:
resultados.append(f'OK: {politica}')
elif politica == 'no-root-iam':
if '"arn:aws:iam::*:root"' in codigo_hcl or '/* All */' in codigo_hcl:
resultados.append(f'FALHA: {politica} — permissão root detectada')
else:
resultados.append(f'OK: {politica}')
else:
resultados.append(f'OK: {politica} (política não implementada no mock)')
aprovado = all('FALHA' not in r for r in resultados)
prefixo = '✓ APROVADO' if aprovado else '✗ REPROVADO'
return f'{prefixo}\n' + '\n'.join(resultados)Para OPA real (com servidor OPA rodando):
import requests
def verificar_policy_opa_real(codigo_hcl: str, politica: str) -> dict:
payload = {'input': {'resource': codigo_hcl}}
response = requests.post(
f'http://localhost:8181/v1/data/terraform/{politica}/deny',
json=payload,
)
return response.json()Aprofundamento Técnico
Por que o Agente Funciona sem Lógica Condicional
O orquestrador do lab é simples:
for i in range(6):
response = client.messages.create(tools=TOOLS, messages=mensagens, ...)
if response.stop_reason == 'end_turn':
return resultado
# processa tool_use, adiciona a mensagensO LLM decide a sequência. Quando você instrui “sempre faça 1, 2, 3, 4”, ele segue. Mas e se a validação falhar? O LLM irá pedir ao agente para regenerar — automaticamente, sem você programar isso.
Trace de exemplo:
[AGENTE] → gerar_terraform(...)
← resource "aws_instance" com chave desbalanceada
[AGENTE] → validar_terraform(...)
← INVÁLIDO: chaves desbalanceadas
[AGENTE → pensa: "preciso gerar de novo corretamente"]
[AGENTE] → gerar_terraform(...) ← segunda tentativa
← resource "aws_instance" { ... } (correto)
[AGENTE] → validar_terraform(...)
← VÁLIDO
[AGENTE] → verificar_policy_opa(...)
[AGENTE] → estimar_custo(...)
[AGENTE] end_turn: "Terraform gerado, válido e aprovado. Custo: $8.50/mês"estimar_custo: Parsing de HCL sem Parser
A tool de custo analisa HCL como string. Isso funciona para casos simples mas falha para HCL com variáveis (var.instance_type) ou módulos. O insight: para fins de estimativa rápida, string matching é suficiente para os tipos mais comuns.
custos = {'aws_instance': {'t3.micro': 8.5, 't3.medium': 34}, 'aws_s3': 2.5}
for recurso, tipos in custos.items():
if recurso in codigo_hcl:
# detectou o tipo de recurso → verifica o tipo de instância
for tipo, custo in tipos.items():
if tipo in codigo_hcl:
total += custoPara produção, use Infracost CLI: infracost breakdown --path=. gera estimativa precisa considerando regions, preços spot, reserved instances.
Exemplos Anotados
Exemplo 1: Fluxo Completo do Agente
import anthropic
import json
import os
client = anthropic.Anthropic()
# As 4 tools do pipeline
TOOLS = [
{'name': 'gerar_terraform', ...},
{'name': 'validar_terraform', ...},
{'name': 'verificar_policy_opa', ...},
{'name': 'estimar_custo', ...},
]
def iac_copilot(requisito: str) -> str:
mensagens = [{'role': 'user', 'content': f'Gere, valide e estime custo para: {requisito}'}]
for i in range(6):
response = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=1500,
tools=TOOLS,
# instrução explícita de sequência
system='Você é um IaC Copilot. Sempre: 1) Gere o Terraform, 2) Valide, 3) Verifique policies, 4) Estime custo.',
messages=mensagens,
)
if response.stop_reason == 'end_turn':
return next((b.text for b in response.content if hasattr(b, 'text')), 'Concluído.')
tool_blocks = [b for b in response.content if b.type == 'tool_use']
if not tool_blocks:
break
# adiciona resposta completa (TextBlock + ToolUseBlock) ao histórico
mensagens.append({'role': 'assistant', 'content': response.content})
results = []
for block in tool_blocks:
resultado = executar_tool(block.name, block.input)
results.append({
'type': 'tool_result',
'tool_use_id': block.id, # usa block.id, não block.name
'content': resultado,
})
mensagens.append({'role': 'user', 'content': results})
return 'Loop finalizado'Exemplo 2: HCL com Policy Violation
# Este HCL vai falhar em 'no-public-s3'
hcl_problematico = '''
resource "aws_s3_bucket" "dados" {
bucket = "empresa-dados-publicos"
acl = "public-read" # ← policy violation!
}
'''
resultado = verificar_policy_opa(hcl_problematico)
# REPROVADO
# FALHA: no-public-s3 — bucket S3 público detectado
# AVISO: require-tags — recurso sem tags obrigatórias
# OK: no-root-iamPadrões e Armadilhas
Padrões
Padrão 1: Sequência mandatória via system prompt
system='Sempre siga esta sequência: 1) gerar_terraform 2) validar_terraform 3) verificar_policy_opa 4) estimar_custo. Nunca pule etapas.'O LLM respeita a instrução. Para maior garantia, adicione lógica no orquestrador que rejeita end_turn sem todas as 4 tools terem sido chamadas.
Padrão 2: HCL como string no tool_result
{'type': 'tool_result', 'tool_use_id': block.id, 'content': hcl_gerado}
# O LLM recebe o HCL como texto e pode passá-lo para a próxima toolPadrão 3: Dry-run antes de apply
kubectl_apply(manifest) # sempre dry-run no lab
terraform_plan(config) # equivalente Terraform: plan antes de applyArmadilhas
⚠️ Armadilha 1: block.name vs block.id no tool_result
# ERRADO — vai retornar erro da API
results.append({'type': 'tool_result', 'tool_use_id': block.name, ...})
# CORRETO — sempre use o ID único do bloco
results.append({'type': 'tool_result', 'tool_use_id': block.id, ...})⚠️ Armadilha 2: Não incluir response.content no histórico
# ERRADO — perde os TextBlocks antes dos ToolUseBlocks
mensagens.append({'role': 'assistant', 'content': tool_blocks})
# CORRETO — inclui tudo
mensagens.append({'role': 'assistant', 'content': response.content})⚠️ Armadilha 3: HCL com variáveis quebra o parser de custo
resource "aws_instance" "web" {
instance_type = var.instance_type # string matching não detecta isso
}O parser de custo do lab só funciona com valores literais. Para produção, use Infracost ou o plano Terraform em JSON (terraform show -json).
Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
2 TODOs no starter.py:
TODO 1 — validar_terraform(codigo_hcl): A implementação mínima já está: verifica resource/module e conta chaves. Expanda adicionando verificação de aspas não fechadas:
for i, linha in enumerate(codigo_hcl.split('\n'), 1):
if linha.count('"') % 2 != 0:
erros.append(f'Linha {i}: aspas não fechadas')TODO 2 — verificar_policy_opa(codigo_hcl, politicas): Adicione mais políticas além das 3 do starter: - no-encrypted-storage: verifica se S3 tem server_side_encryption_configuration - require-backup: verifica se RDS tem backup_retention_period
Rode com python starter.py e observe o agente executar as 4 tools em sequência.
Agora você está pronto para o lab.