Geração de Infraestrutura com IA

[332] Geração de Infraestrutura com IA

Geração de Terraform e manifestos Kubernetes por LLM com validação no laço: os quatro componentes de um prompt de infraestrutura, terraform validate e kubeconform como portão automático, o que essa validação não vê, e por que a checagem de segurança por regex linha a linha falha em silêncio.
DevOps

19 min de leitura

O Estado da Arte em 2025

A geração de infraestrutura via prompts de linguagem natural avançou significativamente nos últimos dois anos. Em 2023, a ideia de "descrever em português o que você quer e receber Terraform funcional" era mais experimento do que prática. Em 2025, é uma ferramenta usada regularmente por engenheiros — não como substituto do conhecimento de infraestrutura, mas como acelerador.

A distinção é fundamental. Um engenheiro que não entende Terraform, VPCs, IAM e subnets não consegue usar geração por IA de forma produtiva — não tem capacidade de avaliar se o código gerado está correto, seguro e adequado. Um engenheiro que domina esses conceitos consegue usar a IA para ir de zero a rascunho funcional em minutos em vez de horas, e então aplicar seu julgamento para ajustar, melhorar e validar.

Todo o currículo principal desta série existia, entre outras razões, para construir o conhecimento necessário para usar esta ferramenta de forma responsável.

Padrões de Prompts para Infraestrutura

Prompts eficazes para gerar infraestrutura têm quatro componentes: contexto (o que já existe), objetivo (o que precisa ser criado), restrições (o que não pode ser feito) e formato esperado (módulo, recurso avulso, variáveis separadas, etc.).

Um exemplo de prompt completo para gerar um módulo Terraform:

Contexto: Tenho uma VPC existente com ID referenciado via data source,
subnets privadas em 3 AZs, e um cluster EKS já provisionado.
Tenho o módulo terraform-aws-modules/rds/aws disponível.

Objetivo: Criar um módulo Terraform reutilizável para RDS PostgreSQL 16
que siga as melhores práticas de produção.

Restrições:
- Usar manage_master_user_password = true (sem senha no state)
- Multi-AZ obrigatório em produção, opcional em staging via variável
- Performance Insights habilitado com retenção de 7 dias
- Backup com retenção mínima de 7 dias
- Sem acesso público
- Tags obrigatórias: ambiente, projeto, gerenciado_por
- Variável de tipo de instância com default razoável para produção

Formato: Módulo com main.tf, variables.tf, outputs.tf.
Incluir comentários explicando decisões não óbvias.

Esse nível de especificidade produz código que precisa de ajustes mínimos. Prompts vagos ("crie um RDS PostgreSQL") produzem código genérico que pode funcionar mas provavelmente não segue os padrões do projeto.

Ferramenta de Geração com Validação

O fluxo mais produtivo combina geração via LLM com validação automática:

# infra_generator.py
# Gera e valida infraestrutura como código via LLM

import anthropic
import subprocess
import tempfile
import os
import re
from pathlib import Path
from dataclasses import dataclass


@dataclass
class ResultadoGeracao:
    codigo: str
    valido: bool
    erros_validacao: list[str]
    avisos_seguranca: list[str]
    arquivo_gerado: str


SISTEMA_TERRAFORM = """Você é um especialista em Terraform e AWS com foco em produção.

Ao gerar código Terraform:
1. SEMPRE use versões explícitas nos required_providers
2. SEMPRE adicione tags nos recursos (pelo menos: ambiente, projeto, gerenciado_por="terraform")
3. NUNCA hardcode credenciais, passwords ou secrets — use Secrets Manager ou variables sensíveis
4. SEMPRE use backend remoto (não gere o bloco backend mas mencione que é necessário)
5. Prefira data sources a recursos hardcoded quando o recurso já existe
6. Use locals para evitar repetição de valores
7. Adicione outputs para valores que outros módulos possam precisar
8. Inclua comentários para decisões não óbvias

Retorne APENAS o código Terraform, sem explicações antes ou depois.
O código deve estar pronto para ser salvo em arquivo .tf e executado."""


def gerar_terraform(descricao: str, contexto: str = "") -> str:
    """Gera código Terraform baseado em descrição em linguagem natural."""
    cliente = anthropic.Anthropic()

    prompt_completo = descricao
    if contexto:
        prompt_completo = f"Contexto existente:\n{contexto}\n\nObjetivo:\n{descricao}"

    resposta = cliente.messages.create(
        model="claude-opus-5",
        max_tokens=4000,
        system=SISTEMA_TERRAFORM,
        messages=[{"role": "user", "content": prompt_completo}]
    )

    codigo = resposta.content[0].text
    # Remover blocos de código markdown se presentes
    codigo = re.sub(r'^```(?:hcl|terraform)?\n', '', codigo, flags=re.MULTILINE)
    codigo = re.sub(r'\n```$', '', codigo, flags=re.MULTILINE)
    return codigo.strip()


def validar_terraform(codigo: str) -> tuple[bool, list[str]]:
    """
    Valida sintaxe do Terraform usando terraform validate.
    Retorna (válido, lista_de_erros).
    """
    with tempfile.TemporaryDirectory() as tmpdir:
        arquivo = Path(tmpdir) / "main.tf"
        arquivo.write_text(codigo)

        # Inicializar sem backend para validação local
        init = subprocess.run(
            ["terraform", "init", "-backend=false", "-no-color"],
            cwd=tmpdir,
            capture_output=True,
            text=True
        )

        if init.returncode != 0:
            return False, [f"terraform init falhou: {init.stderr}"]

        validate = subprocess.run(
            ["terraform", "validate", "-no-color", "-json"],
            cwd=tmpdir,
            capture_output=True,
            text=True
        )

        if validate.returncode == 0:
            return True, []

        import json
        try:
            resultado = json.loads(validate.stdout)
            erros = [
                f"Linha {d.get('range', {}).get('start', {}).get('line', '?')}: {d.get('summary', '')}"
                for d in resultado.get("diagnostics", [])
                if d.get("severity") == "error"
            ]
            return False, erros
        except Exception:
            return False, [validate.stdout or validate.stderr]


def verificar_seguranca_basica(codigo: str) -> list[str]:
    """
    Verificações de segurança básicas sem dependências externas.
    Para produção, usar Checkov ou tfsec.
    """
    avisos = []
    linhas = codigo.split('\n')

    padroes_problematicos = [
        (r'0\.0\.0\.0/0.*ingress', "Security group com entrada de qualquer IP (0.0.0.0/0)"),
        (r'publicly_accessible\s*=\s*true', "RDS com acesso público habilitado"),
        (r'password\s*=\s*"[^"]{3,}"', "Possível senha hardcoded no código"),
        (r'secret_key\s*=\s*"[A-Za-z0-9+/]{20,}"', "Possível chave secreta hardcoded"),
        (r'encrypted\s*=\s*false', "Recurso com criptografia explicitamente desabilitada"),
        (r'skip_final_snapshot\s*=\s*true', "RDS sem snapshot final ao deletar"),
        (r'deletion_protection\s*=\s*false', "Recurso sem proteção contra deleção"),
    ]

    for i, linha in enumerate(linhas, 1):
        for padrao, descricao in padroes_problematicos:
            if re.search(padrao, linha, re.IGNORECASE):
                avisos.append(f"Linha {i}: {descricao}")

    return avisos


def gerar_e_validar(
    descricao: str,
    contexto: str = "",
    arquivo_saida: str = "gerado.tf",
    max_tentativas: int = 3
) -> ResultadoGeracao:
    """
    Gera Terraform, valida e tenta corrigir automaticamente se inválido.
    """
    codigo = gerar_terraform(descricao, contexto)

    for tentativa in range(max_tentativas):
        valido, erros = validar_terraform(codigo)

        if valido:
            break

        if tentativa < max_tentativas - 1:
            print(f"Tentativa {tentativa + 1}: código inválido. Solicitando correção...")
            # Pedir ao LLM que corrija os erros específicos
            cliente = anthropic.Anthropic()
            correcao = cliente.messages.create(
                model="claude-opus-5",
                max_tokens=4000,
                system=SISTEMA_TERRAFORM,
                messages=[
                    {"role": "user", "content": descricao},
                    {"role": "assistant", "content": codigo},
                    {
                        "role": "user",
                        "content": f"O código tem erros de validação. Corrija:\n" +
                                   "\n".join(erros) +
                                   "\n\nRetorne apenas o código corrigido."
                    }
                ]
            )
            codigo = correcao.content[0].text
            codigo = re.sub(r'^```(?:hcl|terraform)?\n', '', codigo, flags=re.MULTILINE)
            codigo = re.sub(r'\n```$', '', codigo, flags=re.MULTILINE)
            codigo = codigo.strip()

    avisos = verificar_seguranca_basica(codigo)

    Path(arquivo_saida).write_text(codigo)

    return ResultadoGeracao(
        codigo=codigo,
        valido=valido,
        erros_validacao=erros if not valido else [],
        avisos_seguranca=avisos,
        arquivo_gerado=arquivo_saida
    )

Geração de Manifestos Kubernetes

O mesmo padrão se aplica a manifestos Kubernetes. A vantagem aqui é que o Kubernetes tem um schema bem definido e ferramentas de validação (kubeval, kubeconform) que podem verificar o YAML gerado antes de aplicar no cluster:

# k8s_generator.py
# Gera e valida manifestos Kubernetes

import anthropic
import subprocess
import tempfile
import re
import yaml
from pathlib import Path


SISTEMA_KUBERNETES = """Você é um especialista em Kubernetes com foco em segurança e produção.

Ao gerar manifestos Kubernetes:
1. SEMPRE defina resource requests e limits
2. SEMPRE configure liveness e readiness probes
3. SEMPRE use securityContext com runAsNonRoot: true e readOnlyRootFilesystem: true
4. SEMPRE configure PodDisruptionBudget para Deployments críticos
5. SEMPRE use topologySpreadConstraints para distribuir pods entre zonas
6. NUNCA use a imagem com tag 'latest' — use SHA ou versão específica
7. Use ConfigMaps para configuração não-sensível e ExternalSecrets/SecretProviderClass para sensível
8. Adicione preStop hook com sleep para graceful shutdown

Retorne APENAS o YAML, sem explicações. Múltiplos recursos separados por ---."""


def gerar_manifesto_k8s(descricao: str, namespace: str = "default") -> str:
    """Gera manifesto Kubernetes baseado em descrição."""
    cliente = anthropic.Anthropic()

    prompt = f"Namespace: {namespace}\n\n{descricao}"

    resposta = cliente.messages.create(
        model="claude-opus-5",
        max_tokens=4000,
        system=SISTEMA_KUBERNETES,
        messages=[{"role": "user", "content": prompt}]
    )

    manifesto = resposta.content[0].text
    manifesto = re.sub(r'^```yaml\n', '', manifesto, flags=re.MULTILINE)
    manifesto = re.sub(r'\n```$', '', manifesto, flags=re.MULTILINE)
    return manifesto.strip()


def validar_manifesto_k8s(manifesto: str) -> tuple[bool, list[str]]:
    """
    Valida manifesto Kubernetes com kubeconform.
    Requer: pip install kubeconform ou instalação do binário.
    """
    with tempfile.NamedTemporaryFile(
        mode='w', suffix='.yaml', delete=False
    ) as f:
        f.write(manifesto)
        arquivo_tmp = f.name

    try:
        resultado = subprocess.run(
            [
                "kubeconform",
                "-strict",
                "-summary",
                "-kubernetes-version", "1.29.0",
                arquivo_tmp
            ],
            capture_output=True,
            text=True
        )

        if resultado.returncode == 0:
            return True, []
        else:
            erros = [
                linha for linha in resultado.stdout.split('\n')
                if 'error' in linha.lower() or 'invalid' in linha.lower()
            ]
            return False, erros or [resultado.stdout]

    except FileNotFoundError:
        # kubeconform não instalado — validação básica com yaml.safe_load
        try:
            list(yaml.safe_load_all(manifesto))
            return True, ["kubeconform não disponível — apenas sintaxe YAML validada"]
        except yaml.YAMLError as e:
            return False, [str(e)]
    finally:
        Path(arquivo_tmp).unlink(missing_ok=True)


def dry_run_kubectl(manifesto: str, namespace: str = "default") -> tuple[bool, str]:
    """
    Executa kubectl apply --dry-run=server para validar contra o cluster real.
    Requer kubectl configurado e acesso ao cluster.
    """
    with tempfile.NamedTemporaryFile(
        mode='w', suffix='.yaml', delete=False
    ) as f:
        f.write(manifesto)
        arquivo_tmp = f.name

    try:
        resultado = subprocess.run(
            [
                "kubectl", "apply",
                "--dry-run=server",
                "--namespace", namespace,
                "-f", arquivo_tmp,
            ],
            capture_output=True,
            text=True
        )
        return resultado.returncode == 0, resultado.stdout + resultado.stderr
    finally:
        Path(arquivo_tmp).unlink(missing_ok=True)

O Fluxo Completo: Prompt → Validação → Revisão Humana

O fluxo de trabalho mais produtivo combina geração, validação automática e revisão humana supervisionada:

# cli_infra.py
# Interface de linha de comando para geração supervisionada de infraestrutura

import click
from infra_generator import gerar_e_validar
from k8s_generator import gerar_manifesto_k8s, validar_manifesto_k8s


@click.group()
def cli():
    """Gerador de infraestrutura como código assistido por IA."""
    pass


@cli.command()
@click.argument('descricao')
@click.option('--contexto', '-c', default='', help='Arquivo com contexto existente')
@click.option('--output', '-o', default='gerado.tf', help='Arquivo de saída')
@click.option('--auto-aplicar', is_flag=True, help='Aplicar sem confirmação humana')
def terraform(descricao: str, contexto: str, output: str, auto_aplicar: bool):
    """Gera código Terraform a partir de descrição em linguagem natural."""

    contexto_str = ""
    if contexto:
        from pathlib import Path
        contexto_str = Path(contexto).read_text()

    click.echo(f"⏳ Gerando Terraform para: {descricao[:60]}...")

    resultado = gerar_e_validar(descricao, contexto_str, output)

    if resultado.avisos_seguranca:
        click.echo(f"\n⚠️  {len(resultado.avisos_seguranca)} aviso(s) de segurança:")
        for aviso in resultado.avisos_seguranca:
            click.echo(f"   • {aviso}")

    if resultado.valido:
        click.echo(f"\n✅ Código gerado e validado: {resultado.arquivo_gerado}")
        click.echo("\n--- PRÉVIA ---")
        click.echo(resultado.codigo[:1000] + ("..." if len(resultado.codigo) > 1000 else ""))
        click.echo("--- FIM DA PRÉVIA ---\n")

        if not auto_aplicar:
            click.echo("⚠️  REVISÃO HUMANA OBRIGATÓRIA antes de aplicar.")
            click.echo("   Execute: terraform plan")
            click.echo("   Verifique: terraform plan -out=plano.tfplan")
            click.echo("   Aplique:   terraform apply plano.tfplan")
        else:
            click.echo("🚀 Iniciando terraform plan (auto-aplicar habilitado)...")
            import subprocess
            subprocess.run(["terraform", "plan", f"-out={output}.plan"])
    else:
        click.echo(f"\n❌ Código inválido após {3} tentativas:")
        for erro in resultado.erros_validacao:
            click.echo(f"   • {erro}")


if __name__ == '__main__':
    cli()

Conclusão: IA como Ferramenta, Não como Substituto

Ao longo dos quatro artigos desta extensão sobre AIOps e IA aplicada a DevOps, um padrão consistente emergiu: a IA é mais útil onde o domínio é estruturado, onde há convenções estabelecidas e onde os erros podem ser verificados automaticamente.

Detecção de anomalias em métricas funciona porque há décadas de literatura em estatística aplicada a séries temporais. Análise de logs funciona porque os padrões linguísticos em mensagens de erro são estruturados e repetitivos. Geração de Terraform funciona porque a linguagem tem schema, as melhores práticas são documentadas e terraform validate captura erros de sintaxe imediatamente.

O que não funciona sem supervisão humana cuidadosa: decisões de arquitetura que envolvem trade-offs de negócio, configurações de segurança em contextos regulatórios específicos, e qualquer coisa que afete produção sem validação prévia.

O engenheiro que termina esta série tem o conhecimento necessário para usar essas ferramentas produtivamente — e, mais importante, para reconhecer seus limites. A IA acelera o trabalho de quem sabe o que está fazendo. Para quem não sabe, ela acelera a chegada aos problemas.

Referências para Aprofundamento

52 artigos do currículo principal. 12 artigos de extensão. 64 artigos no total cobrindo do terminal Linux ao AIOps, passando por Docker, Kubernetes, AWS, Azure, Terraform, CI/CD, observabilidade, segurança, FinOps e muito mais.

Exercícios

Exercício 1

O verificar_seguranca_basica percorre codigo.split('\n') e testa cada linha contra r'0\.0\.0\.0/0.*ingress'. Escreva o security group mais perigoso que você conseguir e verifique se esse padrão dispara.

Ver resposta

✓ Resposta: Ele nunca dispara. Em HCL, ingress e 0.0.0.0/0 estão sempre em linhas diferentes — e o loop compara uma linha por vez:

resource "aws_security_group" "banco" {
  ingress {
    from_port   = 0
    to_port     = 65535
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # o mundo inteiro, todas as portas
  }
}

A linha do cidr_blocks tem o CIDR mas não a palavra ingress; a linha do ingress não tem o CIDR. Nenhuma das duas casa. E ainda que estivessem juntas, a ordem do padrão está invertida — ele exige ingress depois do CIDR, e o HCL escreve na ordem oposta.

O que faz disso um problema sério não é a regra falhar, é como ela falha: em silêncio, e para o lado errado. A saída da CLI é "✅ Código gerado e validado", sem nenhum aviso — indistinguível do caso em que o código está mesmo seguro. Uma verificação que só produz falsos negativos é pior do que não ter verificação nenhuma, porque produz confiança.

A correção não é consertar o regex — é abandonar a análise por linha. Regras de segurança em IaC precisam ver a estrutura: que aquele cidr_blocks pertence àquele bloco ingress, daquele recurso. É exatamente o que Checkov e tfsec fazem ao trabalhar sobre o HCL parseado, e é por isso que o próprio docstring da função diz "para produção, usar Checkov ou tfsec". Vale ler essa frase como o que ela é: um aviso de que a função não deveria estar no caminho de decisão.

Exercício 2

O fluxo confia no terraform validate para decidir se o código gerado está bom. O LLM inventa enable_super_encryption = true num aws_db_instance, e também gera um bucket sem criptografia. Quais desses dois o validate pega?

Ver resposta

✓ Resposta: Pega o primeiro, não pega o segundo — e essa divisão é a coisa mais importante a entender sobre o fluxo inteiro.

O terraform validate confere o código contra o schema do provider: sintaxe HCL, tipos, referências entre recursos e nomes de argumentos. Um argumento que não existe em aws_db_instance é rejeitado na hora. É por isso que o laço de auto-correção funciona bem para alucinação — o erro volta como mensagem específica e o modelo costuma acertar na segunda tentativa.

O que ele não olha é se a configuração é boa. Um bucket sem criptografia é HCL perfeitamente válido; o mesmo vale para uma role com AdministratorAccess, um RDS com publicly_accessible = true ou uma retenção de backup de um dia. Todos passam com nota máxima.

Repare que -backend=false aperta ainda mais o escopo: sem state e sem credenciais, a validação é puramente local. Ela não sabe se a VPC referenciada existe, se o nome do bucket já foi tomado, se a instância cabe na cota da conta. Isso não é defeito — é o que torna o passo rápido e seguro num pipeline. Mas define o que ele significa.

A leitura correta do "✅ validado" é: "este código provavelmente aplica". Não "este código está certo", e muito menos "este código é seguro". Cobrir o resto exige camadas diferentes — política com Checkov/OPA, terraform plan contra a conta real, e revisão humana.

Exercício 3

Leia o SISTEMA_TERRAFORM inteiro, em especial a regra 4 e a instrução final. Elas podem ser cumpridas ao mesmo tempo? O que o modelo provavelmente vai fazer — e por que isso quebra o passo seguinte do fluxo?

Ver resposta

✓ Resposta: Não podem. A regra 4 manda "não gere o bloco backend mas mencione que é necessário", e a instrução final manda "retorne APENAS o código Terraform, sem explicações antes ou depois". Mencionar é explicar.

A saída mais provável é o modelo resolver o conflito com um comentário HCL:

# NOTA: configure um backend remoto (S3 + DynamoDB) antes de aplicar.
terraform {
  required_providers { ... }
}

Que é, honestamente, a melhor saída possível — cumpre as duas regras. Mas é sorte, não garantia. Uma parte das gerações vai escrever a menção como prosa antes do bloco terraform, e é aí que o fluxo quebra: a limpeza de markdown só remove cercas ```. Texto solto em português não é removido, vai inteiro para main.tf, e o terraform validate falha com erro de sintaxe.

O efeito prático é pior do que uma falha simples: o laço de correção gasta uma das três tentativas corrigindo um problema que o próprio prompt criou. E, como cada tentativa é uma chamada nova, o comportamento é intermitente — a mesma descrição às vezes funciona, às vezes não.

A lição vale para além deste script: instruções contraditórias num system prompt não produzem erro, produzem variância. Aqui a correção é trivial — pedir a menção explicitamente como comentário HCL, que é código e não viola o "apenas código".

Exercício 4

Compare o que gerar_terraform envia ao modelo com o que o laço de correção em gerar_e_validar envia na segunda tentativa. Falta alguma coisa? Que erro isso pode produzir?

Ver resposta

✓ Resposta: Falta o contexto. O gerar_terraform monta prompt_completo = f"Contexto existente:\n{contexto}\n\nObjetivo:\n{descricao}", mas o laço de correção manda só descricao como primeira mensagem:

messages=[
    {"role": "user",      "content": descricao},   # ← o contexto sumiu
    {"role": "assistant", "content": codigo},
    {"role": "user",      "content": "O código tem erros..."},
]

Na hora de corrigir, o modelo perde a informação de que já existem VPC, subnets e cluster EKS. O erro típico que isso produz é ele recriar o que deveria referenciar: onde havia um data "aws_vpc", aparece um resource "aws_vpc" novo.

E esse é o pior tipo de erro no fluxo, porque valida perfeitamente. Um recurso novo é HCL correto. O validate aprova, a CLI imprime o ✅, e o problema só se revela no terraform plan — na forma de uma VPC extra a criar — ou, pior, não se revela para quem não lê o plan com atenção.

O conserto é de uma linha: montar prompt_completo uma vez e reusá-lo nas duas chamadas. O caso geral vale registrar — quando você reconstrói um histórico à mão, qualquer coisa que ficar de fora desaparece silenciosamente, e a correção fica pior do que a geração original.

Exercício 5

O prompt de exemplo exige manage_master_user_password = true e justifica com "sem senha no state". Que problema isso resolve, e por que ele não seria resolvido marcando a variável da senha como sensitive = true?

Ver resposta

✓ Resposta: Resolve o fato de que o state do Terraform guarda em texto claro todo valor que o provider devolve — a senha do banco inclusive. Quem consegue ler o arquivo de state consegue ler a senha, ponto.

sensitive = true não ajuda porque atua em outro lugar: ele só censura a saída no terminal, trocando o valor por (sensitive value) no plan e no apply. No terraform.tfstate o valor continua lá, legível: sensitive não muda nada do que é gravado, só do que é impresso. É proteção contra ombro e contra log de CI — não contra quem tem o arquivo.

Com manage_master_user_password = true a senha nunca passa pelo Terraform: o RDS a gera, guarda no Secrets Manager e devolve apenas o ARN do segredo. O que entra no state é uma referência, e o controle de acesso vira um problema de IAM — que é onde ele deve estar. A rotação, de quebra, passa a ser gerenciada.

Duas consequências que valem o registro:

  • Se uma senha passou pelo state alguma vez, ela está comprometida em todos os backups e versões daquele arquivo. Trocar o código depois não desfaz isso — a credencial precisa ser rotacionada.
  • Vale para muito mais que senha de RDS: chave privada gerada por tls_private_key, token de service account, saída de random_password. Sempre que um recurso gera um segredo, pergunte onde ele repousa depois do apply.
Comentários

Mais em DevOps

Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento
Platform Engineering: Construindo a Plataforma Interna de Desenvolvimento

A disciplina que nasce quando o time de plataforma vira gargalo: o conceito de…

Grafana: Dashboards e Alertas que Fazem Sentido
Grafana: Dashboards e Alertas que Fazem Sentido

Dashboards que respondem perguntas em vez de acumular gráficos…

Azure Kubernetes Service (AKS)
Azure Kubernetes Service (AKS)

O Kubernetes é o mesmo; o que muda são as integrações com a nuvem: o cluster…