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