DevOps

Terraform

Guia completo de Terraform — IaC, HCL, providers, resources, modules, state, workspaces e boas práticas para AWS

Terraform é uma ferramenta de Infrastructure as Code (IaC) da HashiCorp que permite declarar infraestrutura em arquivos HCL (HashiCorp Configuration Language) e aplicar mudanças de forma previsível e repetível em qualquer cloud provider.


Conceitos Fundamentais

Infrastructure as Code (IaC): infraestrutura descrita em arquivos de texto versionados. Mudanças passam por code review, pull requests e pipelines CI/CD — o mesmo fluxo do código da aplicação.

Provider: plugin que comunica o Terraform com uma API externa (AWS, GCP, Azure, Kubernetes, GitHub, etc.). Cada provider expõe resources e data sources específicos.

Resource: bloco que representa um objeto de infraestrutura gerenciado pelo Terraform (instância EC2, bucket S3, VPC, etc.). O Terraform cria, atualiza ou destrói resources para chegar ao estado desejado.

State: arquivo terraform.tfstate que mapeia os resources declarados no HCL aos objetos reais na cloud. O Terraform usa o state para calcular o diff entre o desejado e o atual.

Plan: operação de dry-run que mostra quais recursos serão criados, modificados ou destruídos antes de executar qualquer mudança.

Module: conjunto reutilizável de recursos Terraform agrupados em um diretório. Permite compartilhar padrões de infraestrutura entre projetos.

Fluxo de trabalho Terraform:

   Código HCL        terraform init      Download providers
  (*.tf files)  ──►  terraform plan  ──► Diff: desejado vs atual
                      terraform apply ──► Aplica mudanças na cloud
                      terraform destroy ► Remove todos os resources

HCL — Sintaxe Básica

HCL é tipado, legível por humanos e suporta expressões, loops e funções nativas.

# Bloco genérico:
# <BLOCK_TYPE> "<BLOCK_LABEL>" "<BLOCK_LABEL>" {
#   argumento = valor
# }

# Tipos primitivos
variable "exemplo" {
  type    = string  # string, number, bool
  default = "valor"
}

# Tipos complexos
variable "lista" {
  type    = list(string)
  default = ["a", "b", "c"]
}

variable "mapa" {
  type = map(string)
  default = {
    env    = "production"
    region = "us-east-1"
  }
}

variable "objeto" {
  type = object({
    name    = string
    port    = number
    enabled = bool
  })
}

# Comentários
# Linha única com #
/* Bloco
   multi-linha */

Locals

Locals definem valores intermediários calculados — útil para evitar repetição.

locals {
  # Valor simples
  ambiente = "producao"

  # Expressão composta
  nome_bucket = "${var.projeto}-${local.ambiente}-assets"

  # Map calculado com merge
  tags_padrao = {
    Environment = var.ambiente
    Project     = var.projeto
    ManagedBy   = "terraform"
    Owner       = "time-plataforma"
  }

  # Condicional
  instance_type = var.ambiente == "producao" ? "t3.medium" : "t3.micro"

  # Lista filtrada com for expression
  azs_disponiveis = [for az in data.aws_availability_zones.available.names : az]
}

# Usando um local
resource "aws_s3_bucket" "assets" {
  bucket = local.nome_bucket
  tags   = local.tags_padrao
}

Outputs

Outputs expõem valores do estado para uso externo ou em módulos pai.

# Output simples
output "bucket_id" {
  description = "ID do bucket S3 criado"
  value       = aws_s3_bucket.assets.id
}

# Output sensitivo (oculto no terminal, presente no state)
output "db_password" {
  description = "Senha do banco gerada"
  value       = random_password.db.result
  sensitive   = true
}

# Output de objeto completo
output "vpc_info" {
  description = "Informações da VPC principal"
  value = {
    id         = aws_vpc.main.id
    cidr       = aws_vpc.main.cidr_block
    subnet_ids = aws_subnet.public[*].id
  }
}

# Output condicional
output "alb_dns" {
  description = "DNS do ALB (apenas em producao)"
  value       = var.create_alb ? aws_lb.main[0].dns_name : null
}

Data Sources

Data sources leem informações de recursos já existentes sem gerenciá-los.

# Busca a AMI mais recente do Amazon Linux 2
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Busca as AZs disponíveis na região atual
data "aws_availability_zones" "available" {
  state = "available"
}

# Lê um secret do Secrets Manager
data "aws_secretsmanager_secret_version" "db_creds" {
  secret_id = "prod/myapp/db-credentials"
}

locals {
  # Decodifica o JSON retornado pelo secret
  db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)
}

# Referenciando data sources
resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
}

Providers

Configuração do Provider AWS

# versions.tf — sempre declare versões para builds reproduzíveis
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # aceita 5.x mas não 6.x
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.5"
    }
  }
}

# Configuração do provider principal
provider "aws" {
  region = var.aws_region

  # Tags aplicadas automaticamente em todos os recursos
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = var.ambiente
      Project     = var.projeto
    }
  }
}

# Provider alternativo para outra região (multi-region)
provider "aws" {
  alias  = "us_west"
  region = "us-west-2"
}

# Usando o provider alternativo num resource
resource "aws_s3_bucket" "backup" {
  provider = aws.us_west
  bucket   = "meu-backup-us-west"
}

Múltiplos Providers (Multi-Account)

# Assume role em conta diferente
provider "aws" {
  alias  = "producao"
  region = "us-east-1"

  assume_role {
    role_arn     = "arn:aws:iam::123456789012:role/TerraformRole"
    session_name = "terraform-deploy"
  }
}

provider "aws" {
  alias  = "staging"
  region = "us-east-1"

  assume_role {
    role_arn     = "arn:aws:iam::987654321098:role/TerraformRole"
    session_name = "terraform-deploy"
  }
}

Resources Principais AWS

EC2 — aws_instance

# Key pair para SSH
resource "aws_key_pair" "deployer" {
  key_name   = "${var.projeto}-deployer"
  public_key = file("~/.ssh/id_rsa.pub")
}

# Security group da instância
resource "aws_security_group" "app" {
  name        = "${var.projeto}-app-sg"
  description = "Security group da aplicacao"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP da internet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS da internet"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH apenas do bastion"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    # Referencia outro security group como source
    security_groups = [aws_security_group.bastion.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"  # -1 = todos os protocolos
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.projeto}-app-sg"
  }
}

# Instância EC2
resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = local.instance_type
  key_name               = aws_key_pair.deployer.key_name
  subnet_id              = aws_subnet.private[0].id
  vpc_security_group_ids = [aws_security_group.app.id]

  # Desabilita acesso a metadados IMDSv1 (segurança)
  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"  # Força IMDSv2
    http_put_response_hop_limit = 1
  }

  # Disco raiz
  root_block_device {
    volume_type           = "gp3"
    volume_size           = 20
    encrypted             = true
    delete_on_termination = true
  }

  # Script de inicialização
  user_data = base64encode(templatefile("${path.module}/templates/user_data.sh.tpl", {
    app_version = var.app_version
    db_endpoint = aws_db_instance.main.endpoint
  }))

  # Evita recriação da instância quando user_data muda em prod
  lifecycle {
    ignore_changes = [user_data, ami]
  }

  tags = {
    Name = "${var.projeto}-app-server"
  }
}

S3 — aws_s3_bucket

resource "aws_s3_bucket" "app" {
  bucket = "${var.projeto}-${var.ambiente}-storage"

  # Forçar deleção mesmo com objetos (cuidado em producao!)
  force_destroy = var.ambiente != "producao"
}

# Bloquear acesso público (separado desde AWS provider v4)
resource "aws_s3_bucket_public_access_block" "app" {
  bucket = aws_s3_bucket.app.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Versionamento
resource "aws_s3_bucket_versioning" "app" {
  bucket = aws_s3_bucket.app.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Criptografia server-side
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
  bucket = aws_s3_bucket.app.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
    bucket_key_enabled = true  # reduz custo de chamadas KMS
  }
}

# Lifecycle: arquiva objetos antigos
resource "aws_s3_bucket_lifecycle_configuration" "app" {
  bucket = aws_s3_bucket.app.id

  rule {
    id     = "archive-old-objects"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 90
      storage_class = "GLACIER"
    }

    expiration {
      days = 365
    }
  }
}

VPC — aws_vpc

# VPC principal
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.projeto}-vpc"
  }
}

# Subnets públicas (uma por AZ)
resource "aws_subnet" "public" {
  count = length(var.azs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = var.azs[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.projeto}-public-${var.azs[count.index]}"
    Tier = "public"
  }
}

# Subnets privadas (uma por AZ)
resource "aws_subnet" "private" {
  count = length(var.azs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.azs[count.index]

  tags = {
    Name = "${var.projeto}-private-${var.azs[count.index]}"
    Tier = "private"
  }
}

# Internet Gateway para subnets públicas
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.projeto}-igw"
  }
}

# Elastic IP para o NAT Gateway
resource "aws_eip" "nat" {
  count  = length(var.azs)
  domain = "vpc"

  tags = {
    Name = "${var.projeto}-nat-eip-${count.index}"
  }
}

# NAT Gateway (um por AZ para alta disponibilidade)
resource "aws_nat_gateway" "main" {
  count = length(var.azs)

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  depends_on = [aws_internet_gateway.main]

  tags = {
    Name = "${var.projeto}-nat-${var.azs[count.index]}"
  }
}

# Route table pública
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
  }

  tags = {
    Name = "${var.projeto}-rt-public"
  }
}

# Route tables privadas (uma por AZ, cada uma aponta para seu NAT)
resource "aws_route_table" "private" {
  count  = length(var.azs)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "${var.projeto}-rt-private-${var.azs[count.index]}"
  }
}

# Associações
resource "aws_route_table_association" "public" {
  count          = length(var.azs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count          = length(var.azs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

RDS — aws_db_instance

# Subnet group para o RDS
resource "aws_db_subnet_group" "main" {
  name       = "${var.projeto}-db-subnet-group"
  subnet_ids = aws_subnet.private[*].id

  tags = {
    Name = "${var.projeto}-db-subnet-group"
  }
}

# Security group do RDS
resource "aws_security_group" "rds" {
  name        = "${var.projeto}-rds-sg"
  description = "Security group do banco de dados"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "PostgreSQL apenas da aplicacao"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
}

# Gera senha aleatória
resource "random_password" "db" {
  length           = 32
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
}

# Armazena a senha no Secrets Manager
resource "aws_secretsmanager_secret" "db" {
  name                    = "${var.projeto}/${var.ambiente}/db-password"
  recovery_window_in_days = 7
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id     = aws_secretsmanager_secret.db.id
  secret_string = random_password.db.result
}

# Instância RDS PostgreSQL
resource "aws_db_instance" "main" {
  identifier     = "${var.projeto}-${var.ambiente}-db"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = var.db_instance_class

  db_name  = var.db_name
  username = var.db_username
  password = random_password.db.result

  allocated_storage     = 20
  max_allocated_storage = 100  # auto-scaling até 100 GB
  storage_type          = "gp3"
  storage_encrypted     = true

  multi_az               = var.ambiente == "producao"
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  backup_retention_period = var.ambiente == "producao" ? 7 : 1
  backup_window           = "03:00-04:00"
  maintenance_window      = "sun:04:00-sun:05:00"

  deletion_protection = var.ambiente == "producao"

  # Evita downtime em mudanças de minor version
  auto_minor_version_upgrade = true
  apply_immediately          = var.ambiente != "producao"

  # Previne destruição acidental em produção
  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name = "${var.projeto}-${var.ambiente}-db"
  }
}

Variables — Declaração e Validação

# variables.tf

# String simples com validação
variable "ambiente" {
  description = "Ambiente de deploy (dev, staging, producao)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "producao"], var.ambiente)
    error_message = "O ambiente deve ser 'dev', 'staging' ou 'producao'."
  }
}

# String com default
variable "aws_region" {
  description = "Região AWS onde os recursos serão criados"
  type        = string
  default     = "us-east-1"
}

# Número com range
variable "min_instances" {
  description = "Número mínimo de instâncias no ASG"
  type        = number
  default     = 1

  validation {
    condition     = var.min_instances >= 1 && var.min_instances <= 10
    error_message = "min_instances deve ser entre 1 e 10."
  }
}

# Boolean
variable "enable_deletion_protection" {
  description = "Habilita proteção contra deleção no RDS e ALB"
  type        = bool
  default     = false
}

# Lista
variable "azs" {
  description = "Availability Zones onde criar subnets"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

# Map de strings
variable "extra_tags" {
  description = "Tags adicionais aplicadas a todos os recursos"
  type        = map(string)
  default     = {}
}

# Objeto tipado
variable "db_config" {
  description = "Configuração do banco de dados"
  type = object({
    instance_class = string
    storage_gb     = number
    multi_az       = bool
  })
  default = {
    instance_class = "db.t3.micro"
    storage_gb     = 20
    multi_az       = false
  }
}

# Variável sem default — obrigatória, deve ser fornecida
variable "projeto" {
  description = "Nome do projeto (usado em todos os recursos)"
  type        = string
}

Arquivos tfvars

# terraform.tfvars — valores padrão (versionado no git)
projeto    = "minha-app"
aws_region = "us-east-1"

# environments/dev.tfvars
ambiente                   = "dev"
enable_deletion_protection = false
min_instances              = 1
azs                        = ["us-east-1a", "us-east-1b"]

db_config = {
  instance_class = "db.t3.micro"
  storage_gb     = 20
  multi_az       = false
}

# environments/producao.tfvars
ambiente                   = "producao"
enable_deletion_protection = true
min_instances              = 2
azs                        = ["us-east-1a", "us-east-1b", "us-east-1c"]

db_config = {
  instance_class = "db.t3.medium"
  storage_gb     = 100
  multi_az       = true
}

Modules

Criando um Módulo

modules/
└── vpc/
    ├── main.tf        # recursos do módulo
    ├── variables.tf   # inputs do módulo
    ├── outputs.tf     # outputs do módulo
    └── versions.tf    # terraform e provider requirements
# modules/vpc/variables.tf
variable "projeto" {
  description = "Nome do projeto"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block da VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "azs" {
  description = "Lista de AZs"
  type        = list(string)
}

variable "tags" {
  description = "Tags adicionais"
  type        = map(string)
  default     = {}
}

# modules/vpc/outputs.tf
output "vpc_id" {
  description = "ID da VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs das subnets públicas"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs das subnets privadas"
  value       = aws_subnet.private[*].id
}

Usando um Módulo Local

# main.tf do projeto raiz
module "vpc" {
  source = "./modules/vpc"

  projeto  = var.projeto
  vpc_cidr = "10.0.0.0/16"
  azs      = ["us-east-1a", "us-east-1b", "us-east-1c"]

  tags = {
    CreatedBy = "terraform"
  }
}

# Acessando outputs do módulo
resource "aws_instance" "app" {
  subnet_id = module.vpc.private_subnet_ids[0]
  # ...
}

Usando Módulo do Terraform Registry

# Módulo oficial do VPC da AWS no registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.projeto}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = false  # um por AZ para HA
  enable_vpn_gateway     = false
  enable_dns_hostnames   = true

  tags = local.tags_padrao
}

# EKS cluster usando módulo oficial
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${var.projeto}-cluster"
  cluster_version = "1.29"

  vpc_id                   = module.vpc.vpc_id
  subnet_ids               = module.vpc.private_subnets
  control_plane_subnet_ids = module.vpc.private_subnets

  eks_managed_node_groups = {
    main = {
      instance_types = ["t3.medium"]
      min_size       = 1
      max_size       = 5
      desired_size   = 2
    }
  }
}

State Management

State Local (padrão)

# Por padrão, o state é salvo em terraform.tfstate no diretório local
# Nunca commite terraform.tfstate no git — adicione ao .gitignore

State Remoto no S3 + DynamoDB

# backend.tf
terraform {
  backend "s3" {
    bucket         = "minha-empresa-terraform-state"
    key            = "producao/minha-app/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true

    # DynamoDB para locking — evita race conditions em equipes
    dynamodb_table = "terraform-state-lock"

    # IAM role com permissões mínimas para o state
    role_arn       = "arn:aws:iam::123456789:role/TerraformStateRole"
  }
}
# Criando a infraestrutura de backend (bootstrapping — executar antes)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "minha-empresa-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Comandos de State

# Lista todos os resources no state
terraform state list

# Mostra detalhes de um resource específico
terraform state show aws_instance.app

# Move um resource no state (renomear ou mover entre módulos)
terraform state mv aws_instance.app aws_instance.app_server

# Remove um resource do state sem destruí-lo na cloud
# Útil quando você quer parar de gerenciar um resource com Terraform
terraform state rm aws_s3_bucket.legado

# Importa um resource existente na cloud para o state
terraform import aws_s3_bucket.existente nome-do-bucket-ja-criado

# Força refresh do state (sincroniza com a realidade na cloud)
terraform refresh

# Faz pull do state remoto para um arquivo local
terraform state pull > terraform.tfstate.backup

# Força unlock de um state travado (use com cautela!)
terraform force-unlock <LOCK_ID>

Comandos CLI

# INICIALIZAÇÃO
# Baixa providers, inicializa backend e módulos
terraform init

# Atualiza providers para versões mais recentes dentro das constraints
terraform init -upgrade

# Inicializa com backend específico (útil em CI)
terraform init \
  -backend-config="bucket=meu-bucket-state" \
  -backend-config="key=prod/terraform.tfstate" \
  -backend-config="region=us-east-1"

# FORMATAÇÃO
# Formata todos os .tf no diretório atual
terraform fmt

# Formata recursivamente
terraform fmt -recursive

# Verifica se está formatado (exit code 0 = OK, 1 = precisa formatar)
terraform fmt -check -recursive

# VALIDAÇÃO
# Valida sintaxe e tipos (não conecta na cloud)
terraform validate

# PLANEJAMENTO
# Mostra o que seria criado/modificado/destruído
terraform plan

# Salva o plan num arquivo para aplicar depois
terraform plan -out=tfplan

# Plan com variáveis inline
terraform plan -var="ambiente=producao" -var="projeto=minha-app"

# Plan com arquivo de variáveis
terraform plan -var-file="environments/producao.tfvars"

# Plan para destruir tudo
terraform plan -destroy

# APLICAÇÃO
# Aplica as mudanças (pede confirmação interativa)
terraform apply

# Aplica um plan salvo (sem pedir confirmação)
terraform apply tfplan

# Aplica sem confirmação interativa (CI/CD)
terraform apply -auto-approve

# Aplica apenas um resource específico
terraform apply -target=aws_instance.app

# DESTRUIÇÃO
# Destrói todos os recursos gerenciados
terraform destroy

# Destrói sem confirmação
terraform destroy -auto-approve

# Destrói apenas um resource
terraform destroy -target=aws_instance.app

# IMPORT
# Importa resource existente para o state
terraform import aws_instance.app i-1234567890abcdef0

# TAINT / UNTAINT (deprecado no Terraform 1.x, substituído por -replace)
# Força recriação de um resource no próximo apply
terraform apply -replace=aws_instance.app

# GRAPH
# Gera grafo de dependências em formato DOT
terraform graph | dot -Tsvg > graph.svg

# OUTPUT
# Mostra todos os outputs do state
terraform output

# Mostra um output específico (útil em scripts)
terraform output -raw bucket_id

# Mostra outputs em JSON
terraform output -json

Workspaces

Workspaces permitem múltiplos states a partir do mesmo código, no mesmo backend.

# Lista workspaces
terraform workspace list

# Cria um novo workspace
terraform workspace new staging

# Muda para um workspace
terraform workspace select producao

# Mostra o workspace atual
terraform workspace show

# Deleta um workspace (deve estar vazio)
terraform workspace delete staging
# Usando o workspace no código
locals {
  # Seleciona configuração baseada no workspace
  config = {
    default = {
      instance_type = "t3.micro"
      min_size      = 1
    }
    staging = {
      instance_type = "t3.small"
      min_size      = 1
    }
    producao = {
      instance_type = "t3.medium"
      min_size      = 2
    }
  }

  # terraform.workspace retorna o nome do workspace atual
  env_config = local.config[terraform.workspace]
}

resource "aws_autoscaling_group" "app" {
  min_size          = local.env_config.min_size
  max_size          = local.env_config.min_size * 3
  desired_capacity  = local.env_config.min_size
  # ...
}

Provisioners

Provisioners executam scripts após a criação de um resource. Use como último recurso — prefira cloud-init, Ansible ou Packer.

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  # Arquivo: copia um arquivo local para o servidor
  provisioner "file" {
    source      = "scripts/setup.sh"
    destination = "/tmp/setup.sh"

    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("~/.ssh/id_rsa")
      host        = self.public_ip
    }
  }

  # Remote-exec: executa comandos no servidor remoto via SSH
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/setup.sh",
      "sudo /tmp/setup.sh",
      "sudo systemctl start myapp",
    ]

    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file("~/.ssh/id_rsa")
      host        = self.public_ip
    }
  }

  # Local-exec: executa um comando na máquina onde o Terraform roda
  provisioner "local-exec" {
    command = "echo 'Instância criada: ${self.id}' >> deploy.log"
  }

  # on_failure: continue ignora erros do provisioner (padrão: fail)
  provisioner "local-exec" {
    command    = "ansible-playbook -i '${self.public_ip},' playbook.yml"
    on_failure = continue
  }
}

terraform_remote_state

Acessa outputs de outro state Terraform (comunicação entre stacks).

# Stack A (networking) expõe a VPC
# Stack B (aplicacao) consome a VPC

# Em stack_b/main.tf:
data "terraform_remote_state" "networking" {
  backend = "s3"

  config = {
    bucket = "minha-empresa-terraform-state"
    key    = "producao/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

# Usa os outputs da stack de networking
resource "aws_instance" "app" {
  subnet_id              = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
  vpc_security_group_ids = [data.terraform_remote_state.networking.outputs.app_sg_id]
  # ...
}

Expressões e Funções Úteis

# FOR EXPRESSION — transforma listas e maps
locals {
  # Lista → lista transformada
  upper_tags = [for tag in var.tags : upper(tag)]

  # Lista → map
  subnet_map = { for s in aws_subnet.public : s.availability_zone => s.id }

  # Map → lista filtrada
  prod_instances = [for k, v in var.instances : v if v.environment == "producao"]
}

# SPLAT EXPRESSION — acessa atributos de todos os itens
output "subnet_ids" {
  value = aws_subnet.private[*].id  # equivale ao for expression acima
}

# COUNT — cria múltiplos recursos
resource "aws_subnet" "public" {
  count  = 3
  # count.index: 0, 1, 2
  cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
}

# FOR_EACH — cria recursos a partir de um map ou set
resource "aws_iam_user" "time" {
  for_each = toset(["alice", "bob", "charlie"])
  name     = each.key  # ou each.value para set
}

resource "aws_s3_bucket" "ambientes" {
  for_each = {
    dev     = "us-east-1"
    staging = "us-west-2"
  }
  bucket   = "${var.projeto}-${each.key}"
  # each.key: "dev" ou "staging"
  # each.value: "us-east-1" ou "us-west-2"
}

# FUNÇÕES BUILT-IN
locals {
  # String
  nome_upper   = upper("minha-app")          # "MINHA-APP"
  nome_replace = replace("my-app", "-", "_") # "my_app"
  nome_format  = format("%s-%s", var.projeto, var.ambiente)

  # Lista
  lista_unica  = distinct(["a", "b", "a", "c"]) # ["a", "b", "c"]
  lista_flat   = flatten([["a", "b"], ["c"]])    # ["a", "b", "c"]
  lista_length = length(var.azs)

  # Map
  map_merged  = merge(local.tags_padrao, var.extra_tags)
  map_keys    = keys(var.mapa)
  map_values  = values(var.mapa)

  # Numero
  max_val = max(1, 5, 3)  # 5
  min_val = min(1, 5, 3)  # 1

  # Condicional ternário
  tipo = var.ambiente == "producao" ? "t3.medium" : "t3.micro"

  # Coalesce — retorna o primeiro valor não-nulo
  regiao = coalesce(var.override_region, var.aws_region, "us-east-1")

  # CIDR helpers
  subnet_cidr = cidrsubnet("10.0.0.0/16", 8, 1)  # "10.0.1.0/24"
}

Boas Práticas

Estrutura de Projeto

infrastructure/
├── modules/               # módulos reutilizáveis
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── versions.tf
│   ├── rds/
│   └── ecs-service/
├── environments/
│   ├── dev/
│   │   ├── main.tf        # composição dos módulos para dev
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── backend.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   └── producao/
└── global/                # recursos globais (IAM roles, Route53 zones)
    ├── iam/
    └── dns/

Convenções de Naming

# Nome de resources: snake_case
resource "aws_s3_bucket" "app_assets" { }    # bom
resource "aws_s3_bucket" "AppAssets" { }     # ruim

# Evite nomes genéricos
resource "aws_vpc" "main" { }   # ok para projetos simples
resource "aws_vpc" "vpc" { }    # ruim — redundante

# Tags obrigatórias em todos os recursos
locals {
  tags_obrigatorias = {
    Project     = var.projeto
    Environment = var.ambiente
    ManagedBy   = "terraform"
    Owner       = var.equipe
    CostCenter  = var.cost_center
  }
}

# Lifecycle para recursos críticos
resource "aws_db_instance" "main" {
  lifecycle {
    prevent_destroy       = true   # bloqueia terraform destroy
    create_before_destroy = true   # zero-downtime em updates
    ignore_changes        = [password]  # ignora drift de senha
  }
}

DRY com Módulos e Locals

# Ruim — repete configuração em cada resource
resource "aws_instance" "web1" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  tags = {
    Environment = "prod"
    Project     = "minha-app"
    ManagedBy   = "terraform"
  }
}

resource "aws_instance" "web2" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
  tags = {
    Environment = "prod"
    Project     = "minha-app"
    ManagedBy   = "terraform"
  }
}

# Bom — usa locals e for_each
locals {
  tags = {
    Environment = "prod"
    Project     = "minha-app"
    ManagedBy   = "terraform"
  }

  web_servers = toset(["web1", "web2"])
}

resource "aws_instance" "web" {
  for_each      = local.web_servers
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"
  tags          = merge(local.tags, { Name = "${var.projeto}-${each.key}" })
}

Terraform com CI/CD

GitHub Actions

# .github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  TF_VERSION: "1.6.6"
  AWS_REGION: "us-east-1"

jobs:
  terraform:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # para OIDC com AWS
      contents: read
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC — sem chaves fixas)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: ${{ env.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        run: terraform init
        working-directory: environments/producao

      - name: Terraform Format Check
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        run: terraform validate
        working-directory: environments/producao

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        working-directory: environments/producao

      # Posta o plan como comentário no PR
      - name: Post Plan to PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan 📖
            \`\`\`\n${{ steps.plan.outputs.stdout }}\n\`\`\``;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

      # Apply apenas no merge para main
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan
        working-directory: environments/producao

Atlantis (GitOps para Terraform)

# atlantis.yaml — executado na raiz do repositório
version: 3
automerge: false

projects:
  - name: networking
    dir: environments/dev/networking
    workspace: default
    autoplan:
      when_modified: ["*.tf", "../../../modules/vpc/**/*.tf"]
      enabled: true
    apply_requirements: [approved, mergeable]

  - name: aplicacao
    dir: environments/producao/aplicacao
    workspace: default
    autoplan:
      when_modified: ["*.tf"]
      enabled: true
    apply_requirements: [approved, mergeable]
    # Requer aprovação manual antes de aplicar em producao
    workflow: producao

workflows:
  producao:
    plan:
      steps:
        - init
        - plan:
            extra_args: ["-var-file=terraform.tfvars"]
    apply:
      steps:
        - apply

Checklist de Boas Práticas

✓ Versione providers e Terraform em required_providers / required_version
✓ Use state remoto (S3 + DynamoDB) — nunca commite .tfstate
✓ Adicione terraform.tfstate* e .terraform/ ao .gitignore
✓ Separe ambientes em diretórios diferentes (não workspaces para prod)
✓ Use modules para padrões repetidos (VPC, RDS, ECS service)
✓ Declare tags obrigatórias em todos os resources
✓ Use prevent_destroy em recursos críticos (RDS, S3 state bucket)
✓ Gere senhas com random_password + guarde no Secrets Manager
✓ Evite provisioners — prefira cloud-init ou Ansible
✓ Use for_each em vez de count quando possível (mais seguro para remoção)
✓ Rode terraform fmt e terraform validate no CI
✓ Faça plan antes de apply — revise sempre antes de confirmar
✓ Use OIDC para autenticação AWS no CI/CD (sem chaves estáticas)
✓ Documente variáveis com description em todos os blocos variable

Terraform Cloud e HCP Terraform

O Terraform Cloud (agora chamado HCP Terraform) é a plataforma SaaS da HashiCorp para execução remota de planos e applies, gerenciamento de state e integração com VCS. Elimina a necessidade de configurar S3 + DynamoDB manualmente.

Autenticação e login

# Autenticar com o Terraform Cloud via browser
terraform login

# O token é salvo em ~/.terraform.d/credentials.tfrc.json
# Para CI/CD, use a variável de ambiente:
export TF_TOKEN_app_terraform_io="<seu-token-de-api>"

Remote Backend — configuração

# versions.tf
terraform {
  cloud {
    organization = "minha-org"

    workspaces {
      name = "producao-aws"
      # ou usar tag para múltiplos workspaces:
      # tags = ["aws", "producao"]
    }
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  required_version = ">= 1.6.0"
}
# Após configurar o bloco cloud, inicializar migra o state local para remoto
terraform init

VCS-Driven Runs

No Terraform Cloud, cada push para a branch conectada dispara automaticamente um plan. O apply pode ser manual (botão na UI) ou automático dependendo da configuração do workspace.

Fluxo VCS-driven:

  git push → Terraform Cloud detecta mudança
           → Executa terraform plan automaticamente
           → Notifica via Slack/email
           → Apply manual (ou automático se configurado)

Configuração recomendada para produção: Apply manual com política de aprovação por membros do time.

Variáveis sensíveis no TF Cloud

# No workspace do TF Cloud, variáveis podem ser:
# - Terraform variables  → lidas como var.nome no HCL
# - Environment variables → exportadas como env vars na execução

# Marque como "Sensitive" na UI para que o valor nunca seja exibido nos logs
# Exemplo: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DB_PASSWORD

# No HCL, consuma normalmente:
variable "db_password" {
  type      = string
  sensitive = true
  # Não defina default — o valor vem do TF Cloud
}

Diferença entre tiers

RecursoFreePlus
UsuáriosIlimitadoIlimitado
WorkspacesIlimitadoIlimitado
State remotoSimSim
Remote runsSimSim
Sentinel policiesNãoSim
Audit logsNãoSim
SSO/SAMLNãoSim
Equipes com permissões granularesLimitadoCompleto

Para projetos open-source ou times pequenos, o tier Free cobre todos os casos de uso principais.


Estimativa de Custo com Infracost

O Infracost analisa os arquivos .tf e estima o custo mensal na AWS, GCP ou Azure antes de aplicar qualquer mudança.

Instalação

# macOS
brew install infracost

# Linux
curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh

# Autenticar (gratuito para uso individual)
infracost auth login

Comandos principais

# Mostrar breakdown completo do custo do módulo atual
infracost breakdown --path .

# Comparar custo atual com uma mudança pendente (diff)
# Gera plan JSON primeiro:
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

# Depois calcula o diff de custo:
infracost diff --path plan.json

# Saída em JSON (útil para CI/CD)
infracost breakdown --path . --format json --out-file infracost.json

# Saída em tabela amigável
infracost breakdown --path . --format table

Exemplo de output

Name                                     Monthly Qty  Unit   Monthly Cost
─────────────────────────────────────────────────────────────────────────
aws_instance.web
 ├─ Instance usage (Linux, on-demand, t3.medium)   730  hours        $30.37
 ├─ root_block_device
 │   └─ Storage (general purpose SSD, gp3)          20  GB            $1.60
 └─ ebs_block_device[0]
     └─ Storage (general purpose SSD, gp3)         100  GB            $8.00

OVERALL TOTAL                                                         $39.97

Integração em GitHub Actions

# .github/workflows/infracost.yml
name: Infracost

on:
  pull_request:
    branches: [main]

jobs:
  infracost:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - name: Setup Infracost
        uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}

      - name: Checkout base branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.ref }}
          path: base

      - name: Generate Infracost cost estimate (base)
        run: infracost breakdown --path=base --format=json --out-file=/tmp/infracost-base.json

      - name: Generate Infracost cost estimate (PR)
        run: infracost breakdown --path=. --format=json --out-file=/tmp/infracost-pr.json

      - name: Post Infracost comment
        run: |
          infracost diff \
            --path=/tmp/infracost-pr.json \
            --compare-to=/tmp/infracost-base.json \
            --format=github-comment \
            --github-token=${{ secrets.GITHUB_TOKEN }} \
            --pull-request=${{ github.event.pull_request.number }} \
            --repo=${{ github.repository }}

Formato JSON do output

{
  "version": "0.2",
  "currency": "USD",
  "projects": [
    {
      "name": "meu-projeto",
      "metadata": { "path": "." },
      "pastBreakdown": null,
      "breakdown": {
        "resources": [
          {
            "name": "aws_instance.web",
            "monthlyCost": "39.97",
            "costComponents": [
              {
                "name": "Instance usage (Linux, on-demand, t3.medium)",
                "unit": "hours",
                "monthlyCost": "30.37"
              }
            ]
          }
        ],
        "totalMonthlyCost": "39.97"
      },
      "diff": {
        "totalMonthlyCost": "0.00"
      }
    }
  ],
  "totalMonthlyCost": "39.97"
}

Workspace Isolation Avançada

Workspaces do Terraform permitem manter múltiplos states dentro do mesmo diretório de configuração, útil para ambientes efêmeros ou variações pequenas da mesma infraestrutura.

Comandos de workspace

# Listar workspaces (default sempre existe)
terraform workspace list

# Criar novo workspace
terraform workspace new staging

# Trocar de workspace
terraform workspace select producao

# Mostrar workspace atual
terraform workspace show

# Deletar workspace (deve estar vazio)
terraform workspace delete staging

Usar workspace name em recursos

# O nome do workspace atual fica disponível como terraform.workspace
locals {
  env = terraform.workspace  # "default", "staging", "producao"

  instance_type = {
    default    = "t3.micro"
    staging    = "t3.small"
    producao   = "t3.large"
  }

  tags = {
    Environment = terraform.workspace
    ManagedBy   = "terraform"
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type[terraform.workspace]

  tags = local.tags
}

resource "aws_s3_bucket" "dados" {
  # Nome único por workspace
  bucket = "minha-app-${terraform.workspace}-dados"
  tags   = local.tags
}

Estrutura de diretórios por ambiente (alternativa a workspaces)

Para ambientes com configurações muito diferentes (ex: prod tem WAF, multi-AZ, autoscaling; dev tem instância única), a abordagem de diretórios separados é mais segura:

infra/
├── modules/
│   ├── vpc/
│   ├── rds/
│   └── ecs-service/
├── environments/
│   ├── dev/
│   │   ├── main.tf          # Importa módulos com configs simples
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf       # State bucket: infra-dev-state
│   ├── staging/
│   │   ├── main.tf
│   │   └── backend.tf       # State bucket: infra-staging-state
│   └── producao/
│       ├── main.tf          # Importa módulos com configs de prod
│       └── backend.tf       # State bucket: infra-prod-state
# Cada ambiente tem seu próprio state e init
cd environments/producao
terraform init
terraform plan
terraform apply

Quando NÃO usar workspaces

Evite workspaces quando:

  • Ambientes têm configurações estruturalmente diferentes (ex: prod tem recursos que dev não tem). Com workspaces, você usa count = terraform.workspace == "producao" ? 1 : 0 em dezenas de recursos — vira caos.
  • Isolamento de blast radius é crítico: um terraform destroy no workspace errado pode destruir produção. Com diretórios separados, o risco é menor.
  • Times diferentes gerenciam ambientes diferentes: diretórios permitem permissões IAM e pipelines CI/CD distintos por ambiente.
  • Muitas diferenças entre ambientes: workspaces são ideais para ambientes quase idênticos (como feature branches de infra efêmera).

Regra prática: use workspaces para infra efêmera (PR environments, testes de carga temporários). Use diretórios separados para dev/staging/producao.


Taint, Replace e Moved

terraform apply -replace (substituto do taint)

O comando terraform taint foi deprecated no Terraform 0.15.2. O substituto moderno é a flag -replace diretamente no apply.

# Forma antiga (deprecated — não use)
terraform taint aws_instance.web
terraform apply

# Forma atual — força recriação de um recurso específico
terraform apply -replace="aws_instance.web"

# Funciona com recursos indexados (for_each / count)
terraform apply -replace="aws_instance.web[\"servidor-1\"]"
terraform apply -replace="aws_instance.web[0]"

# Combinar com target para recriar apenas esse recurso
terraform apply -replace="aws_instance.web" -target="aws_instance.web"

Quando usar: quando uma instância EC2 está com estado corrompido, quando o AMI de uma instância precisa ser atualizado forçadamente, ou quando um certificado TLS precisa ser renovado fora do ciclo normal.

Bloco moved {} — refatorar sem destruir

O bloco moved permite renomear ou mover recursos no state sem destruí-los e recriá-los. Essencial em refatorações de código HCL.

# Cenário 1: renomear um resource
# Antes: resource "aws_instance" "servidor_web"
# Depois: resource "aws_instance" "web"
moved {
  from = aws_instance.servidor_web
  to   = aws_instance.web
}

# Cenário 2: mover resource para dentro de um módulo
moved {
  from = aws_security_group.app
  to   = module.app.aws_security_group.this
}

# Cenário 3: mover de count para for_each (refatoração comum)
# Antes: resource "aws_iam_user" "devs" { count = 3 }
# Depois: resource "aws_iam_user" "devs" { for_each = toset(["alice","bob","carol"]) }
moved {
  from = aws_iam_user.devs[0]
  to   = aws_iam_user.devs["alice"]
}
moved {
  from = aws_iam_user.devs[1]
  to   = aws_iam_user.devs["bob"]
}
moved {
  from = aws_iam_user.devs[2]
  to   = aws_iam_user.devs["carol"]
}
# O terraform plan mostra os movimentos antes de aplicar:
# aws_instance.servidor_web → aws_instance.web (moved)
terraform plan   # valida os blocos moved
terraform apply  # efetiva a mudança no state (sem recriar recursos)

# Após aplicar e confirmar que está correto, remova os blocos moved do código
# (eles só precisam existir durante a transição)

terraform state mv vs bloco moved {}

Critérioterraform state mvBloco moved {}
VersionamentoNão — comando manualSim — arquivo .tf versionado
Revisão em PRNãoSim
AuditoriaSó em histórico de shellVisível no diff do PR
Módulos remotosLimitadoSuportado
Reversível facilmenteNãoSim (remover o bloco)
Quando usarCorreção urgente no state sem tempo de PRRefatorações planejadas
# terraform state mv — uso direto (sem arquivo .tf)
terraform state mv aws_instance.servidor_web aws_instance.web

# Mover para dentro de módulo
terraform state mv aws_security_group.app module.app.aws_security_group.this

# Listar resources no state (útil antes de mover)
terraform state list

# Ver detalhes de um resource no state
terraform state show aws_instance.web

Recomendação: prefira sempre o bloco moved {} em projetos com time — ele cria um registro versionado e revisável da refatoração.