Use Cases (Atualizado: 02/06/2026)

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 IaC com Claude Code: AWS, modules, state, CI e revisão de plan

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:

CasoO que Claude Code preparaO que a pessoa revisa
Nova rede AWSVPC, subnets, rotas, tagsCIDR, custo de NAT Gateway
Module reutilizávelmodules/vpc, variables, outputsSe o module ficou grande demais
Separação de ambientesenvs/dev.tfvars, envs/prod.tfvarsBackend key e permissões de produção
CI e policyfmt, validate, plan, checks JSONdestroy, 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.

#Claude Code #Terraform #IaC #AWS #infrastructure
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.