Teu progresso
0 / 83 módulos0%
Estágio 03 · 03-06
BloqueadoClickOps cresce até dor: ninguém lembra como aquele LB foi configurado; staging "esquece" um security group; reproduzir prod em outra região leva uma semana de cliques. IaC promete reverter isso, código declarativo, versionado, reviewable, automated. Mas adoção sem disciplina vira lava: state corrompido, drift entre código e cloud, módulos copy-paste com 2k linhas, secrets em state files git.
Este módulo é IaC com profundidade: Terraform (de fato standard), Pulumi (linguagens reais), AWS CDK, Crossplane (K8s-native). State management, módulos reutilizáveis, drift detection, multi-env, secrets, blast radius. Você sai sabendo desenhar IaC que sobrevive 2+ anos de mudança.
Sem IaC, infra é tribal knowledge.
Em 2026, Terraform/OpenTofu domina. Pulumi cresce em times JS/Python/Go. CDK forte em AWS-only shops. Crossplane em times K8s-heavy.
HashiCorp mudou licença (BSL) em 2023; comunidade forkou pra OpenTofu (Linux Foundation), drop-in compatible.
Em projetos novos: OpenTofu pra evitar lock-in. Compatibilidade com Terraform providers e modules é total.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "logistica-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "tflock"
}
}
provider "aws" { region = var.region }
resource "aws_s3_bucket" "uploads" {
bucket = "logistica-${var.env}-uploads"
}
Comandos:
terraform init, baixa providers, configura backend.terraform plan, diff entre código e state. Reviewa antes.terraform apply, aplica.terraform destroy, remove tudo (cuidado).State é a fonte de verdade do que Terraform criou. Mapeia código → resources reais. Sem state, Terraform não sabe se recurso já existe.
Backend:
terraform.tfstate no diretório. Não use em time.Locking: evita 2 apply simultâneos corromperem state.
Encryption: state pode conter secrets (passwords, etc.). Encripte at rest sempre.
Drift: state vs realidade. Alguém mudou no console → Terraform detecta no plan.
Reutilização. Módulo é diretório com main.tf, variables.tf, outputs.tf.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
...
}
Module registry público (HashiCorp registry, GitHub) tem módulos battle-tested. Use antes de escrever próprio.
Módulo próprio quando: pattern recorrente do seu projeto que merece abstraction. Não vire arquiteto cego, module-of-modules virou anti-padrão famoso.
Terraform Workspaces: múltiplos states por module config. Útil pra envs simples.
Padrão melhor: directory per env (environments/dev/, environments/staging/, environments/prod/) cada um com seu state file e variables. Workspaces escondem ambiguidade. Directory explicit.
variable "env" { type = string }
locals { name_prefix = "logistica-${var.env}" }
output "vpc_id" { value = module.vpc.vpc_id }
Variable values:
terraform.tfvars (gitignored em alguns casos).*.auto.tfvars (auto-loaded).-var ou -var-file.TF_VAR_env=prod.Outputs expõem valores; outros modules consomem via data.terraform_remote_state ou via module.x.output.
Linguagens reais (TS, Python, Go, .NET, Java). Mesma abstração de resources, mas você usa loops, ifs, funções, types nativos.
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("uploads", {
bucket: `logistica-${env}-uploads`,
});
Pros:
Cons:
Em times TS/JS-heavy, Pulumi vira escolha natural.
CDK constrói CloudFormation templates a partir de código (TS, Python, Java, Go, .NET).
Layers:
Cons: locked em AWS. CFN tem limites (timeouts, error handling difícil). Drift detection fraca. Pros: integração profunda em AWS. Constructs ricas. Para AWS-only shops, vale.
CDKTF (CDK for Terraform): CDK que sai Terraform em vez de CFN. Híbrido.
K8s-native IaC. Você define resources como CRDs no cluster:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
metadata: { name: prod-db }
spec:
forProvider:
region: us-east-1
engine: postgres
instanceClass: db.t4g.small
Controllers Crossplane reconciliam, mesmo modelo do K8s.
Pros: GitOps natural; uma plataforma pra app + infra; Compositions (módulos) reutilizáveis. Cons: curva de aprendizado; ecosystem menor que Terraform; nem todo provider tem qualidade Crossplane.
Anti-padrão #1: secret em .tf versionado.
Padrão:
Em CI: Terraform assume role via OIDC.
Drift: console mudou recurso → plan mostra. Decisão: trazer pra código ou reverter.
Import: trazer recurso existente pra state sem destruir/recriar. terraform import aws_s3_bucket.x bucket-name. Em Terraform 1.5+, import {} blocks declarativos.
Refactor: renomear resource sem destroy/recreate. moved {} block (Terraform 1.1+).
terraform plan: review humano é o teste primário.terraform validate: syntax checks.tflint: linter, regras best practices.tfsec, checkov: security scanners (overly permissive, encryption off, etc.).terraform-docs: gera docs.Em CI: validate + lint + scan em PR; plan visível; apply só com approval.
Cross-env data: outputs de "shared" stack lidos por env stacks via data.terraform_remote_state.
State único = mudança em 04-03 bucket pode forçar replan de DB. Separe por blast radius:
Cada layer com state separado. Apps não precisam re-plan VPC.
Gate típico: nenhum SG com 0.0.0.0/0 em porta 22. RDS sempre encrypted. Tags obrigatórias.
Visibilidade de custo no review evita surpresa.
Fork status. HashiCorp trocou Terraform pra BSL em ago/2023; Linux Foundation forkou OpenTofu em jan/2024. OpenTofu 1.6+ é drop-in compat com Terraform 1.5.x — state file format idêntico, providers AWS/GCP/Azure oficiais funcionam, terraform-aws-modules mantém compat dual. Migration = CLI swap (terraform init → tofu init). OpenTofu adicionou features que Terraform OSS não terá: state encryption nativo (TF só via Terraform Cloud), test framework rico, for_each em provider blocks. Decisão 2026: novo greenfield = OpenTofu (sem licença restritiva); legacy heavy em TF Cloud = manter até migrar workflows pra Spacelift / Env0 / Atlantis.
Module design. Composition over inheritance: módulos pequenos consumidos via module "x" { source = ... } em root. Inputs = contract estável, validados; outputs = API estável, nomes consistentes (arn, id, name). Anti-pattern famoso: provider block dentro de module — quebra reuse multi-region. Module declara required_providers, root configura provider.
Versioning + registry. Source pinning obrigatório:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.3"
}
module "ecs_service" {
source = "git::https://github.com/logistica/tf-modules.git//ecs-service?ref=v2.1.3"
}
Bare source = "git@.../module.git" sem ?ref= = floating master = build não-determinístico. Semver: input remove/rename = major; additive = minor; bugfix = patch.
State organization por blast radius. networking (raro, manual approval), data (RDS/S3, cuidado), compute (EKS/ECS, frequente), app (CI deploy contínuo). Cross-state via terraform_remote_state cria coupling forte — alternativa: SSM Parameter Store / Secrets Manager pra valores compartilhados, lookup runtime sem state dependency.
for_each vs count. count (TF 0.12) usa index posicional: deletar elemento 1 de lista de 4 re-cria índices 2 e 3. Catastrófico em prod. for_each (TF 0.13+) com map keys estáveis: delete elimina só essa key.
# ERRADO — count com lista
resource "aws_instance" "app" {
count = length(var.subnets)
subnet_id = var.subnets[count.index]
}
# CERTO — for_each com map
resource "aws_instance" "app" {
for_each = { for s in var.subnets : s.name => s }
subnet_id = each.value.id
}
Regra: 99% prefer for_each. count só pra count = var.enabled ? 1 : 0.
Refactoring sem destroy/recreate.
# moved (TF 1.1+, OpenTofu 1.6+) — rename sem touch
moved {
from = aws_instance.foo
to = aws_instance.bar
}
# removed (TF 1.7+, OpenTofu 1.7+) — drop do state, recurso continua existindo
removed {
from = aws_instance.legacy
lifecycle { destroy = false }
}
# import (TF 1.5+, OpenTofu 1.6+) — declarativo, substitui CLI manual
import {
to = aws_instance.foo
id = "i-0abc1234"
}
moved precisa apply na mesma run em que código muda; deletar block após apply é correto. import {} declarativo elimina drift de "alguém importou via CLI e não documentou".
Lifecycle meta-arguments.
resource "aws_db_instance" "prod" {
# ...
lifecycle {
prevent_destroy = true # bloqueia destroy accidental
ignore_changes = [tags["last_deployed"]] # CI tag injection fora do TF
create_before_destroy = true # zero-downtime swap
replace_triggered_by = [aws_kms_key.rds.key_id] # força recreate em key rotate
}
}
Logística applied — ecs-service module.
# modules/ecs-service/variables.tf
variable "cpu" {
type = string
validation {
condition = contains(["256", "512", "1024", "2048"], var.cpu)
error_message = "cpu must be one of: 256, 512, 1024, 2048."
}
}
variable "name" { type = string }
variable "cluster_arn" { type = string }
variable "image" { type = string }
variable "memory" { type = string }
variable "port" { type = number }
variable "health_check_path" { type = string }
variable "env_vars" { type = map(string) }
variable "secrets_arns" { type = list(string) }
variable "desired_count" { type = number }
# modules/ecs-service/main.tf
terraform { required_version = ">= 1.6" }
resource "aws_ecs_task_definition" "this" {
# ...
lifecycle { create_before_destroy = true }
}
resource "aws_ecs_service" "this" {
task_definition = aws_ecs_task_definition.this.arn
# ...
lifecycle {
ignore_changes = [task_definition, desired_count] # CI deploya nova revision via SDK
}
}
# modules/ecs-service/outputs.tf
output "service_arn" { value = aws_ecs_service.this.id }
output "task_def_arn" { value = aws_ecs_task_definition.this.arn }
output "target_group_arn" { value = aws_lb_target_group.this.arn }
Reuso: shipments-api, tracking-api, billing-api consomem o mesmo módulo, configs distintas.
Anti-patterns observados:
count = length(var.list) — delete index 0 destrói TODOS subsequentes.source = "git@..." sem ?ref= — build não-determinístico.provider block dentro de module — quebra reuse multi-region.terraform plan sem -out=plan.tfplan em CD — race entre plan e apply..tfvars commitado — use SOPS / data.aws_secretsmanager_secret.terraform { required_version = ">= 1.6" } — apply em CLI antiga corrompe state.prevent_destroy ausente em RDS/S3 prod — dia ruim apaga tudo.import via CLI manual sem import {} block — próximo apply destrói.moved block deletado mid-rollout — state inconsistente, fix manual.Cruza com 03-05 (AWS, IAM/VPC consumidos por modules), 03-04 (CI/CD, plan + apply gates), 03-08 (security, OPA/Sentinel + tfsec/checkov), 04-09 (scaling, multi-region module patterns), 04-12 (tech leadership, módulo strategy = central platform team).
Module + provider design (§2.19) é metade. Outra metade = operação: state file é precioso (corrompeu, dia ruim), drift mata convergência (recurso mexido fora do TF passa silencioso), e plan/apply sem CI estruturado vira race condition em PR concorrente. Em 2026 o padrão consolidou: PR-driven flow (plan no PR, apply no merge), state remoto encriptado, drift detection agendado com alerta, plataforma gerenciada (Spacelift/env0/HCP) ou self-hosted (Atlantis) — escolha por custo + compliance. Versões base: Terraform 1.10+ (Q4 2024 — for_each em module + ephemeral resources GA), OpenTofu 1.9+ (Q4 2024 — state encryption GA, AWS provider parity), Atlantis 0.30+, Terragrunt 0.69+ (engine plugin pra OpenTofu), HCP Terraform (rebrand de Terraform Cloud em 2024).
Drift detection. terraform plan -refresh-only -detailed-exitcode em cron — exit code 0 (no drift), 1 (error), 2 (drift detected). Cadência: prod diário, staging semanal, dev sob demanda. Quem detecta sem dono = ignorado, então alerta vai pro time de ownership do state (tag owner no S3 prefix).
# CI cron (GitHub Actions ou Atlantis scheduled run)
terraform plan -refresh-only -detailed-exitcode -out=drift.tfplan
case $? in
0) echo "no drift" ;;
1) echo "error" && exit 1 ;;
2) echo "DRIFT" && curl -X POST $SLACK_WEBHOOK -d '{"text":"drift in '"$WORKSPACE"'"}' ;;
esac
Plataformas gerenciadas built-in: Spacelift drift_detection { schedule = ["0 4 * * *"] reconcile = true }, env0 cron + auto-remediation policy, HCP Terraform health assessments (paid tier). Política: reconcile = true (auto-revert ao state) só em compute efêmero; em data/networking = alert-only, humano aprova.
State management at scale. Self-hosted: S3 + DynamoDB lock, padrão desde TF 0.12, ainda funciona.
terraform {
backend "s3" {
bucket = "tf-state-logistica-prod"
key = "shipments/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:111:key/abc"
}
}
OpenTofu 1.9+ adiciona state encryption nativo no client (TF OSS não tem — só HCP):
terraform {
encryption {
key_provider "aws_kms" "primary" {
kms_key_id = "arn:aws:kms:us-east-1:111:key/state-enc"
region = "us-east-1"
key_spec = "AES_256"
}
method "aes_gcm" "primary" { keys = key_provider.aws_kms.primary }
state { method = method.aes_gcm.primary }
plan { method = method.aes_gcm.primary }
}
}
Tamanho importa: state >10MB começa a degradar (plan lento, lock contention). Regra: 1 state por env por service (shipments/dev, shipments/prod, tracking/dev, ...) — blast radius pequeno + state pequeno. Não junte 8 services em 1 state pra "simplificar".
Terragrunt — DRY sem perder controle. terragrunt.hcl wrappa Terraform/OpenTofu, herança via include, dependency entre módulos sem cross-state hack:
# infra/terragrunt.hcl (root — herdado por todos)
remote_state {
backend = "s3"
generate = { path = "backend.tf" if_exists = "overwrite" }
config = {
bucket = "tf-state-logistica-${get_env("ENV")}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks"
encrypt = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${get_env("AWS_REGION")}"
default_tags { tags = { managed_by = "terragrunt" env = "${get_env("ENV")}" } }
}
EOF
}
# infra/prod/us-east-1/shipments/terragrunt.hcl
include "root" { path = find_in_parent_folders() }
terraform {
source = "git::https://github.com/logistica/tf-modules.git//ecs-service?ref=v2.1.3"
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = { vpc_id = "vpc-mock" private_subnets = ["subnet-mock"] }
}
inputs = {
name = "shipments"
vpc_id = dependency.vpc.outputs.vpc_id
private_subnets = dependency.vpc.outputs.private_subnets
cpu = "1024"
desired_count = 4
}
Pin Terragrunt no CI (terragrunt --version em pre-flight) — upgrade silencioso quebra generate blocks. terragrunt run-all plan aplica em ordem topológica de dependencies.
CI platforms 2026 — matriz. Escolha por modelo + custo + compliance:
| Plataforma | Modelo | Custo aprox | Drift | Forte em |
|---|---|---|---|---|
| Atlantis 0.30+ | Self-hosted (K8s/EC2) | grátis + ops | scheduled plan | controle total, on-prem, compliance estrito |
| Spacelift | Managed | $$ por run + worker | nativo + auto-remediate | enterprise policy (OPA), stack dependency, drift |
| env0 | Managed | $ por run | nativo | dev-friendly, RBAC simples, custom flows |
| HCP Terraform | Managed (HashiCorp) | grátis até 500 resources, depois ~$0.00014/resource-hour | health assessment (paid) | private registry oficial, policy Sentinel |
Tradeoff real: Atlantis = baixo custo direto, paga em operação (atualizar, monitorar, patch). Spacelift/env0 = $$ mas zero ops + drift built-in. HCP free é generoso até overrun — passar 500 resources sem alerta vira surpresa de fatura.
Plan-and-apply gating com locked plan. Plano salvo (-out=plan.tfplan) carrega versão exata do state — se outro PR aplicou entre seu plan e seu apply, apply falha (state version mismatch). Sem locked plan = race garantida.
# .github/workflows/tf-pr.yml (Atlantis-style fallback ou direto via gh actions)
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: terraform init
- run: terraform plan -out=plan.tfplan -detailed-exitcode
- uses: actions/upload-artifact@v4
with: { name: plan, path: plan.tfplan }
apply:
needs: plan
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: prod # GitHub manual approval gate
steps:
- uses: actions/download-artifact@v4
with: { name: plan }
- run: terraform apply plan.tfplan # exato, não re-plan
Atlantis equivalente — atlantis.yaml no repo, bot comenta plan no PR, atlantis apply no merge:
# atlantis.yaml
version: 3
projects:
- name: shipments-prod
dir: infra/prod/us-east-1/shipments
workflow: terragrunt
apply_requirements: [approved, mergeable]
autoplan: { when_modified: ["**/*.hcl", "../../../modules/**/*.tf"] }
workflows:
terragrunt:
plan: { steps: [{ run: "terragrunt plan -out=$PLANFILE -lock-timeout=5m" }] }
apply: { steps: [{ run: "terragrunt apply $PLANFILE" }] }
State migration patterns. Refactor sem destroy: moved block (TF 1.1+) > terraform state mv CLI — moved fica versionado no Git, state mv é manual + não auditável. import block (TF 1.5+) > terraform import CLI pelo mesmo motivo. terraform state rm apenas pra desacoplar (recurso continua no cloud, sai do state — útil antes de removed block ou pra recriar via import limpo). Cross-workspace: precisa state pull → editar JSON → state push (manual, com backup obrigatório).
# migration cross-workspace (NUNCA sem backup)
terraform workspace select source
terraform state pull > source.tfstate.bak
terraform state pull | jq '.resources[] | select(.name=="rds")' > rds.json
# manual edit + reinjection no destino
terraform workspace select target
terraform state push merged.tfstate
Stack Logística aplicada. Atlantis self-hosted em EKS, GitHub PR webhooks. State em S3 (encriptado KMS) + DynamoDB locks, 1 state por (env × region × service) = 36 states (6 services × 3 envs × 2 regions). Terragrunt root terragrunt.hcl define backend + provider — leaf terragrunt.hcl só inputs. Drift detection: cron diário em prod (Atlantis scheduled plan -refresh-only), alerta no canal #infra-drift com tag owner. PR flow: developer abre PR → Atlantis comenta plan → reviewer aprova → atlantis apply no merge → state versionado no S3 com versioning ON (rollback via aws s3api list-object-versions).
Anti-patterns observados:
dynamodb_table lock — dois applies concorrentes corrompem state.generate blocks silenciosamente.moved block deletado mid-rollout — state inconsistente, corrige na mão (já em §2.13, repete porque dói toda vez).-out=plan.tfplan artifact — race entre PRs concorrentes.state push errado destrói histórico, sem rollback.Cruza com 03-06 §2.5 (state foundation), §2.7 (workspaces), §2.11 (Crossplane K8s alternative), §2.13 (drift/importing/refactor intro), §2.14 (testing IaC), §2.15 (multi-environment), §2.16 (blast radius), §2.17 (policy as code), §2.19 (modules + OpenTofu); 03-04 §2.21 (release-please pra IaC modules); 03-08 §2.23 (sigstore + cosign pra module signing); 03-15 §2.20 (incident response se state corromper); 04-12 (platform team owna IaC strategy + plataforma escolhida).
Você precisa, sem consultar:
Reproduzir infra do 03-05 com Terraform/OpenTofu, dividida por blast radius e versionada.
infra/
bootstrap/ # cria backend bucket + DDB table
modules/
vpc/
ecs-service/
rds-postgres/
elasticache-redis/
environments/
staging/
networking/
data/
compute/
prod/
networking/
data/
compute/
terraform_remote_state.vpc: 3 AZs, pub/priv subnets, NAT controlado por flag.ecs-service: task definition, service, target group, security group, com inputs (image, cpu, memory, env vars, secrets ARNs).rds-postgres: instância Multi-AZ, password gerado random + Secrets Manager, parameter group.elasticache-redis: cluster + replicas.random_password, armazenado em Secrets Manager.moved {} renomeando recurso sem destroy.import {} trazendo um recurso "criado clickado" pro state.terraform apply em prod sem terraform plan review.plan mostrando mudança em compute sem afetar networking.moved e import aplicadas.plan mostra; reverte via apply.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Por que `for_each` é preferido a `count` em recursos Terraform iterados?
Q2Qual a maneira correta de manejar password de RDS em IaC?
Q3Por que separar state files por blast radius (networking/data/compute)?
Q4Qual o problema de Terraform `count = length(var.subnets)` ao receber lista variável?
Q5O que o `moved {}` block (TF 1.1+) resolve em refactor de código IaC?
Destrava
03-06 é prereq dos seguintes módulos: