Terraform IaC com Claude Code: AWS, modules, state, CI e revisão de plan
Guia prático de Terraform IaC com Claude Code na AWS: modules, backend, state, CI, policy, secrets e armadilhas comuns.
Terraform facilita escrever infraestrutura como código, mas um arquivo HCL que passa no primeiro teste ainda pode ser perigoso. Um VPC gerado pode usar state local, misturar dev e prod na mesma chave de backend, criar subnets públicas sem revisão ou esconder um replace no terraform plan. Claude Code ajuda bastante nesse trabalho, desde que seja usado como parceiro de implementação e revisão, não como botão automático de apply.
IaC significa Infrastructure as Code: recursos de nuvem descritos em arquivos versionados e revisáveis. HCL é a linguagem de configuração do Terraform. Um module é uma peça reutilizável de infraestrutura. State é o registro que liga o código aos recursos reais. Backend é onde esse state fica salvo. Esses conceitos parecem básicos, mas a maioria dos problemas de Terraform nasce justamente entre código, state, permissões e revisão humana.
Para este artigo, Masa testou um repositório AWS pequeno. Claude Code foi útil para dividir o VPC em module, organizar variables, criar validações e escrever uma base de CI. Mesmo assim, o prompt precisou ser explícito: não colocar secrets em tfvars, não aprovar destroy, não usar exemplos antigos de locking com DynamoDB para novos backends S3.
Fluxo e casos de uso
O fluxo seguro é: escrever o briefing, gerar HCL, validar, rodar plan, revisar e só então aplicar.
flowchart LR
A["Escrever requisitos"] --> B["Claude Code gera HCL"]
B --> C["terraform fmt / validate"]
C --> D["terraform plan"]
D --> E["Revisão com IA e pessoa"]
E --> F["Apply aprovado"]
Casos comuns no dia a dia:
| Caso | O que Claude Code prepara | O que a pessoa revisa |
|---|---|---|
| Nova rede AWS | VPC, subnets, rotas, tags | CIDR, custo de NAT Gateway |
| Module reutilizável | modules/vpc, variables, outputs | Se o module ficou grande demais |
| Separação de ambientes | envs/dev.tfvars, envs/prod.tfvars | Backend key e permissões de produção |
| CI e policy | fmt, validate, plan, checks JSON | destroy, replace e IAM amplo |
Para o produto, veja adocumentação oficial do Claude Code. Para modules, use adocumentação do Terraform Modules. Se permissões AWS forem o ponto fraco, leia também o guia interno deAWS IAM.
Prompt com limites de segurança
Não peça apenas “crie Terraform”. Defina escopo, proibições e comandos de verificação.
claude -p "
Crie Terraform apenas para um module de VPC na AWS.
Não apague arquivos existentes sem pedido explícito.
Requisitos:
- Assumir Terraform 1.10 ou posterior
- Usar provider hashicorp/aws
- Criar VPC, 2 public subnets, 2 private subnets, Internet Gateway e NAT Gateway
- Colocar código reutilizável em modules/vpc
- Separar dev e prod com tfvars
- Não escrever secrets nem AWS access keys em tfvars
- Incluir terraform fmt -recursive, terraform validate e terraform plan
- Se o plan tiver destroy ou replacement, tratar como não aprovado
"
As regras “não apagar”, “sem secrets” e “destroy não aprovado” mudam a qualidade da resposta. Em Terraform, um default genérico pode gerar custo, abrir rede ou ampliar IAM. Claude Code acelera, mas a responsabilidade pelo ambiente continua sendo do time.
Estrutura Terraform para começar
A estrutura abaixo é pequena o bastante para um primeiro projeto. Com credenciais AWS e um bucket de state real, ela pode ser formatada, validada e planejada. Em junho de 2026, o backend S3 do Terraform suporta locking com use_lockfile; o locking baseado em DynamoDB aparece como deprecated na documentação oficial. Consulte oS3 backend oficial antes de copiar tutoriais antigos.
infra/
versions.tf
variables.tf
main.tf
envs/dev.tfvars
modules/vpc/main.tf
modules/vpc/variables.tf
modules/vpc/outputs.tf
# versions.tf
terraform {
required_version = ">= 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 7.0"
}
}
backend "s3" {
bucket = "replace-with-your-tfstate-bucket"
key = "claude-code-terraform-iac/dev/terraform.tfstate"
region = "sa-east-1"
encrypt = true
use_lockfile = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = var.common_tags
}
}
# variables.tf
variable "aws_region" {
type = string
description = "AWS region to deploy into."
default = "sa-east-1"
}
variable "environment" {
type = string
description = "Environment name such as dev, staging, or prod."
}
variable "common_tags" {
type = map(string)
description = "Tags applied to every supported AWS resource."
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the VPC."
}
variable "public_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
}
variable "private_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
}
# main.tf
module "network" {
source = "./modules/vpc"
project = "claude-code-iac"
environment = var.environment
vpc_cidr = var.vpc_cidr
public_subnets = var.public_subnets
private_subnets = var.private_subnets
common_tags = var.common_tags
}
Desenho do module e variables
O module de VPC deve cuidar de rede. Se ALB, ECS, RDS e IAM entram no mesmo module, qualquer ajuste vira um plan difícil de revisar. Também prefira for_each com nomes estáveis em vez de count.index para recursos duradouros.
# modules/vpc/variables.tf
variable "project" {
type = string
description = "Project name used in resource names."
}
variable "environment" {
type = string
description = "Environment name used in tags and names."
}
variable "vpc_cidr" {
type = string
description = "CIDR block for the VPC."
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr must be a valid CIDR block."
}
}
variable "public_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
validation {
condition = length(var.public_subnets) >= 2 && alltrue([for s in var.public_subnets : can(cidrhost(s.cidr, 0))])
error_message = "Define at least two valid public subnet CIDR blocks."
}
}
variable "private_subnets" {
type = list(object({
name = string
cidr = string
az = string
}))
validation {
condition = length(var.private_subnets) >= 2 && alltrue([for s in var.private_subnets : can(cidrhost(s.cidr, 0))])
error_message = "Define at least two valid private subnet CIDR blocks."
}
}
variable "common_tags" {
type = map(string)
description = "Common tags."
}
# modules/vpc/main.tf
locals {
public_subnets_by_name = { for subnet in var.public_subnets : subnet.name => subnet }
private_subnets_by_name = { for subnet in var.private_subnets : subnet.name => subnet }
name_prefix = "${var.project}-${var.environment}"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-vpc"
})
}
resource "aws_subnet" "public" {
for_each = local.public_subnets_by_name
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
map_public_ip_on_launch = true
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-public-${each.key}"
Tier = "public"
})
}
resource "aws_subnet" "private" {
for_each = local.private_subnets_by_name
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = merge(var.common_tags, {
Name = "${local.name_prefix}-private-${each.key}"
Tier = "private"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = values(aws_subnet.public)[0].id
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
}
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
Backend, state e separação de ambientes
State não é cache descartável. Ele contém IDs e alguns atributos sensíveis. Use bucket S3 com versioning, locking, permissões restritas e keys separadas por ambiente.
# envs/dev.tfvars
aws_region = "sa-east-1"
environment = "dev"
vpc_cidr = "10.40.0.0/16"
common_tags = {
Project = "claude-code-iac"
Environment = "dev"
Owner = "masa"
ManagedBy = "terraform"
}
public_subnets = [
{ name = "a", cidr = "10.40.0.0/24", az = "sa-east-1a" },
{ name = "b", cidr = "10.40.1.0/24", az = "sa-east-1b" }
]
private_subnets = [
{ name = "a", cidr = "10.40.10.0/24", az = "sa-east-1a" },
{ name = "b", cidr = "10.40.11.0/24", az = "sa-east-1b" }
]
Em production, mude CIDR, tags e backend key. Workspaces podem ajudar times avançados, mas tfvars explícitos são mais fáceis para iniciantes revisarem.
Verificação com fmt, validate e plan
O Terraform documenta fmt, validate e plan. Primeiro valide sem backend remoto.
cd infra
terraform init -backend=false
terraform fmt -recursive
terraform validate
Depois inicialize o backend real e salve o plan.
terraform init -reconfigure
terraform plan -var-file=envs/dev.tfvars -out=tfplan
terraform show -no-color tfplan > plan.txt
Peça ao Claude Code uma revisão focada em risco.
claude -p "
Revise plan.txt.
Priorize destroy, replace, force replacement, ampliação de IAM, exposição pública e alteração de backend key.
Se houver delete, não aprove o plan.
Liste perguntas objetivas para a pessoa revisora.
"
CI, policy, permissões e secrets
No GitHub Actions, evite AWS access keys de longa duração. Use OIDC para assumir uma IAM role limitada; a orientação oficial está emGitHub Actions OIDC in AWS. A role deve seguirAWS IAM best practices.
name: terraform-plan
on:
pull_request:
paths:
- "infra/**"
permissions:
contents: read
id-token: write
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.10.0"
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-terraform-plan
aws-region: sa-east-1
- run: terraform init -backend=false
- run: terraform fmt -check -recursive
- run: terraform validate
- run: terraform init -reconfigure
- run: terraform plan -var-file=envs/dev.tfvars -out=tfplan
- run: terraform show -json tfplan > tfplan.json
Uma policy mínima pode bloquear deletes.
package terraform.deny
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] == "delete"
msg := sprintf("Destroy action requires manual approval: %s", [rc.address])
}
Para aprofundar, leiaClaude Code CI/CD eSecrets Management com Claude Code.
Armadilhas comuns e resultado do teste
A primeira armadilha é tratar state como arquivo temporário. Não faça commit, não use a mesma key para dev e prod e limite permissões. A segunda é usar count.index em recursos de vida longa; mudar a ordem de uma lista pode gerar replace. A terceira é gravar senhas, tokens ou access keys em tfvars. A quarta é aplicar um plan só porque Claude Code fez um bom resumo.
No repositório de teste, terraform fmt -recursive e terraform validate passaram após a separação em module. Para um plan real, é preciso trocar o bucket do backend e configurar credenciais AWS. Os riscos mais fáceis de esquecer foram custo de NAT Gateway, separação da key de state e replacements implícitos. A melhor instrução para Claude Code foi priorizar destroy e replace antes de comentários de estilo.
Se sua equipe precisa padronizar prompts de revisão, checklists de CI e onboarding de IaC, consulte abiblioteca Claude Code ou otreinamento de implementação. Mesmo em projeto individual, colocar estas regras em CLAUDE.md deixa cada PR de Terraform mais previsível.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.