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 resourcesHCL — 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 .gitignoreState 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 -jsonWorkspaces
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/producaoAtlantis (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:
- applyChecklist 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 variableTerraform 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 initVCS-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
| Recurso | Free | Plus |
|---|---|---|
| Usuários | Ilimitado | Ilimitado |
| Workspaces | Ilimitado | Ilimitado |
| State remoto | Sim | Sim |
| Remote runs | Sim | Sim |
| Sentinel policies | Não | Sim |
| Audit logs | Não | Sim |
| SSO/SAML | Não | Sim |
| Equipes com permissões granulares | Limitado | Completo |
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 loginComandos 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 tableExemplo 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.97Integraçã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 stagingUsar 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 applyQuando 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 : 0em dezenas de recursos — vira caos. - Isolamento de blast radius é crítico: um
terraform destroyno 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ério | terraform state mv | Bloco moved {} |
|---|---|---|
| Versionamento | Não — comando manual | Sim — arquivo .tf versionado |
| Revisão em PR | Não | Sim |
| Auditoria | Só em histórico de shell | Visível no diff do PR |
| Módulos remotos | Limitado | Suportado |
| Reversível facilmente | Não | Sim (remover o bloco) |
| Quando usar | Correção urgente no state sem tempo de PR | Refatoraçõ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.webRecomendação: prefira sempre o bloco moved {} em projetos com time — ele cria um registro versionado e revisável da refatoração.