Infrastructure as Code (Terraform)
Princípios de Infrastructure as Code
IaC trata infraestrutura com as mesmas práticas de código de aplicação: versionamento, code review, testes, CI/CD. A infraestrutura se torna reproduzível, auditável e recuperável.
Princípios fundamentais:
┌──────────────────────────────────────────────────────────────┐
│ 1. Declarativo │ Descreva O QUE quer, não COMO fazer │
│ 2. Idempotente │ Aplicar N vezes = mesmo resultado │
│ 3. Versionado │ Toda mudança passa por Git + code review │
│ 4. Reproduzível │ Mesma config = mesma infra (dev/staging) │
│ 5. Imutável │ Não modifique — destrua e recrie │
│ 6. Testável │ Validate, plan, compliance checks │
└──────────────────────────────────────────────────────────────┘
Arquitetura do Terraform
┌── Workflow ────────────────────────────────────────────┐
│ │
│ terraform init → Baixa providers e inicializa backend│
│ │ │
│ terraform plan → Compara config vs state vs real │
│ │ Mostra diff do que vai mudar │
│ │ │
│ terraform apply → Executa as mudanças via API │
│ │ Atualiza o state │
│ │ │
│ terraform destroy→ Remove todos os recursos do state │
│ │
└──────────────────────────────────────────────────────────┘
┌── Componentes ─────────────────────────────────────────┐
│ │
│ Providers → Plugins que falam com APIs (AWS, GCP, │
│ Azure, Cloudflare, Datadog, GitHub...) │
│ │
│ State → Mapeamento entre config e recursos reais │
│ terraform.tfstate (JSON) │
│ │
│ Resources → Objetos gerenciados (instâncias, VPCs...) │
│ Data Sources→ Dados lidos (AMI mais recente, AZ list) │
│ Modules → Composição reutilizável de resources │
│ │
└──────────────────────────────────────────────────────────┘
HCL: Sintaxe em Profundidade
Variables, Locals e Outputs
# variables.tf — Entradas parametrizáveis
variable "environment" {
type = string
description = "Ambiente de deploy (dev, staging, production)"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment deve ser dev, staging ou production."
}
}
variable "instance_config" {
type = object({
instance_type = string
min_size = number
max_size = number
desired_size = number
})
default = {
instance_type = "t3.medium"
min_size = 2
max_size = 10
desired_size = 3
}
}
variable "tags" {
type = map(string)
default = {}
}
variable "db_password" {
type = string
sensitive = true # Nunca aparece em logs ou plan output
}
# locals.tf — Valores computados internos
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project
Team = "platform"
})
is_production = var.environment == "production"
# Condicionais
instance_type = local.is_production ? "t3.large" : "t3.small"
multi_az = local.is_production ? true : false
# Construção de map dinâmico
subnet_cidrs = {
for idx, az in data.aws_availability_zones.available.names :
az => cidrsubnet(var.vpc_cidr, 8, idx)
}
}
# outputs.tf — Valores exportados
output "vpc_id" {
value = aws_vpc.main.id
description = "ID da VPC criada"
}
output "database_endpoint" {
value = aws_db_instance.main.endpoint
description = "Endpoint do RDS"
sensitive = true # Esconde do output padrão
}
output "load_balancer_dns" {
value = aws_lb.main.dns_name
}
Data Sources
# Buscar AMI mais recente do Amazon Linux 2023
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Buscar AZs disponíveis na região
data "aws_availability_zones" "available" {
state = "available"
}
# Buscar VPC existente por tag
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["main-vpc"]
}
}
# Buscar secret do Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/myapp/db-password"
}
# Buscar account ID atual
data "aws_caller_identity" "current" {}
# Uso: data.aws_caller_identity.current.account_id
Loops e Condicionais
# for_each com map — cria recursos dinâmicos
variable "services" {
type = map(object({
port = number
cpu = number
memory = number
desired_count = number
}))
default = {
api = {
port = 3000
cpu = 512
memory = 1024
desired_count = 3
}
worker = {
port = 0
cpu = 256
memory = 512
desired_count = 2
}
}
}
resource "aws_ecs_service" "services" {
for_each = var.services
name = "${local.name_prefix}-${each.key}"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.tasks[each.key].arn
desired_count = each.value.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.services[each.key].id]
}
}
# count com condicional — criar recurso apenas em produção
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
count = local.is_production ? 1 : 0
alarm_name = "${local.name_prefix}-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = 60
statistic = "Average"
threshold = 80
alarm_actions = [aws_sns_topic.alerts[0].arn]
}
# Dynamic blocks
resource "aws_security_group" "api" {
name = "${local.name_prefix}-api"
vpc_id = aws_vpc.main.id
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
Resource Lifecycle
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
lifecycle {
# Cria novo ANTES de destruir o antigo (zero downtime)
create_before_destroy = true
# Previne destruição acidental (ex: banco de dados)
prevent_destroy = true
# Ignora mudanças feitas fora do Terraform
ignore_changes = [
tags["LastModified"], # Tag atualizada por scripts externos
ami, # AMI atualizada por processo separado
]
# Substituir recurso se atributo mudar
replace_triggered_by = [
aws_security_group.app.id # Nova SG → nova instância
]
}
}
# Taint — força recriação de um recurso no próximo apply
terraform taint aws_instance.app
# Equivalente moderno:
terraform apply -replace="aws_instance.app"
# Mover recurso no state (refactor sem destruir)
terraform state mv aws_instance.old aws_instance.new
# Remover recurso do state (sem destruir na cloud)
terraform state rm aws_instance.orphan
# Listar recursos no state
terraform state list
# Ver detalhes de um recurso no state
terraform state show aws_instance.app
State Management
Remote Backend com S3 + DynamoDB
# backend.tf — Configuração de state remoto
terraform {
backend "s3" {
bucket = "empresa-terraform-state"
key = "production/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # Lock para evitar apply simultâneo
# Opcional: KMS key para criptografia do state
kms_key_id = "arn:aws:kms:us-east-1:123456:key/xxx"
}
}
# Bootstrap — criar bucket e tabela (antes do primeiro init)
# Geralmente feito manualmente ou com CloudFormation
resource "aws_s3_bucket" "terraform_state" {
bucket = "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" # Permite recuperar states anteriores
}
}
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 = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
State: O que Acontece Internamente
terraform plan executa:
1. Lê o state remoto (S3)
2. Adquire lock (DynamoDB)
3. Para cada recurso no state, chama a API do provider para ler estado real
4. Compara: config desejada vs state vs estado real
5. Gera diff (create, update, destroy)
6. Libera lock
Se estado real diverge do state (drift):
- Alguém mudou manualmente na console → Terraform quer reverter
- terraform refresh atualiza o state para refletir o real
- terraform plan -refresh-only mostra drift sem propor mudanças
Módulos: Composição e Reutilização
# modules/vpc/main.tf — Módulo reutilizável de VPC
variable "name" { type = string }
variable "cidr" { type = string }
variable "azs" { type = list(string) }
variable "private_subnets" { type = list(string) }
variable "public_subnets" { type = list(string) }
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = var.name }
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnets[count.index]
availability_zone = var.azs[count.index]
tags = { Name = "${var.name}-private-${var.azs[count.index]}" }
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = { Name = "${var.name}-public-${var.azs[count.index]}" }
}
output "vpc_id" { value = aws_vpc.this.id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }
output "public_subnet_ids" { value = aws_subnet.public[*].id }
# Uso do módulo — chamada limpa e reutilizável
module "vpc" {
source = "./modules/vpc"
# Ou de registry: source = "terraform-aws-modules/vpc/aws"
# Ou de Git: source = "git::https://github.com/empresa/modules.git//vpc?ref=v2.1.0"
name = "${local.name_prefix}-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"]
}
# Referenciando outputs do módulo
resource "aws_ecs_service" "api" {
network_configuration {
subnets = module.vpc.private_subnet_ids
}
}
Workspaces
# Workspaces permitem múltiplos states para a mesma configuração
terraform workspace new staging
terraform workspace new production
terraform workspace select staging
# State separado por workspace:
# s3://bucket/env:/staging/terraform.tfstate
# s3://bucket/env:/production/terraform.tfstate
# No código, use terraform.workspace para diferenciação
locals {
env_config = {
staging = {
instance_type = "t3.small"
min_size = 1
max_size = 3
}
production = {
instance_type = "t3.large"
min_size = 3
max_size = 20
}
}
config = local.env_config[terraform.workspace]
}
Import de Infraestrutura Existente
# Import clássico — adiciona ao state
terraform import aws_instance.app i-1234567890abcdef0
# Import com bloco (Terraform 1.5+) — gera código automaticamente
# import.tf
import {
to = aws_instance.app
id = "i-1234567890abcdef0"
}
import {
to = aws_security_group.app
id = "sg-0123456789abcdef0"
}
# Gerar código HCL a partir dos imports
terraform plan -generate-config-out=generated.tf
# Review e ajuste do código gerado, depois:
terraform apply
Terraform vs Alternativas
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ │ Terraform │ Pulumi │ CloudFormation│
├──────────────┼──────────────┼──────────────┼──────────────┤
│ Linguagem │ HCL │ TS/Python/Go │ JSON/YAML │
│ Multi-cloud │ ✅ Sim │ ✅ Sim │ ❌ Só AWS │
│ State │ Remoto (S3) │ Pulumi Cloud │ AWS-managed │
│ Comunidade │ Enorme │ Crescendo │ Grande (AWS) │
│ Learning │ Médio │ Baixo (prog) │ Alto │
│ Maturidade │ Muito alta │ Alta │ Muito alta │
│ Licença │ BSL 1.1* │ Apache 2.0 │ Proprietário │
└──────────────┴──────────────┴──────────────┴──────────────┘
* OpenTofu (fork CNCF) mantém licença open source
Boas Práticas e Estrutura de Diretório
infra/
├── modules/ # Módulos reutilizáveis
│ ├── vpc/
│ ├── ecs-service/
│ ├── rds/
│ └── monitoring/
├── environments/ # Configuração por ambiente
│ ├── production/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ └── ...
│ └── dev/
│ └── ...
├── global/ # Recursos compartilhados
│ ├── iam/
│ ├── dns/
│ └── ecr/
└── scripts/
├── plan.sh
└── apply.sh
# CI/CD para Terraform (GitHub Actions)
# 1. PR aberto → terraform plan (comentário no PR)
# 2. PR aprovado + merged → terraform apply
# plan.sh
#!/bin/bash
set -euo pipefail
terraform init -backend-config="key=${ENVIRONMENT}/terraform.tfstate"
terraform validate
terraform plan -out=tfplan -var-file="${ENVIRONMENT}.tfvars"
terraform show -json tfplan > plan.json
# Compliance checks com OPA/Conftest
conftest test plan.json -p policies/
# Ex: "Nenhum Security Group pode ter 0.0.0.0/0 em ingress"
# apply.sh
#!/bin/bash
set -euo pipefail
terraform apply -auto-approve tfplan
# Convenções de naming e tagging
# Padrão: {project}-{environment}-{component}-{resource}
# Ex: myapp-production-api-sg
# Tags obrigatórias em toda organização
locals {
required_tags = {
Project = var.project
Environment = var.environment
Team = var.team
ManagedBy = "terraform"
CostCenter = var.cost_center
}
}
Terraform é a ferramenta de IaC mais adotada pela indústria por boas razões: ecossistema de providers massivo, workflow de plan/apply claro e módulos reutilizáveis. O investimento em aprender HCL, gestão de state e boas práticas de organização se paga rapidamente em confiabilidade e velocidade de provisionamento.
Referencias e Fontes
- Terraform Official Documentation — https://developer.hashicorp.com/terraform/docs — referência oficial
- “Terraform: Up & Running” — Yevgeniy Brikman — o livro mais prático sobre Terraform
- Terraform Registry — https://registry.terraform.io/ — providers e módulos oficiais e comunitários
- “Infrastructure as Code” — Kief Morris — princípios de IaC além de uma ferramenta específica
- OpenTofu — https://opentofu.org/ — fork open source do Terraform