Terraform é uma ferramenta de Infrastructure as Code (IaC) desenvolvida pela HashiCorp. Permite gerenciar recursos de provedores de nuvem como AWS através de código declarativo. Neste artigo, aprenderemos desde os fundamentos do Terraform até o nível de operação em produção através da construção prática de infraestrutura AWS.
O que você aprenderá neste artigo
- Conceitos básicos do Terraform e sintaxe HCL
- Construção de recursos AWS (VPC, EC2, RDS, S3)
- Design de módulos e melhores práticas
- Gerenciamento de estado e trabalho em equipe
- Integração com pipelines CI/CD
Configuração do Ambiente
Instalação
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Verificar versão
terraform version
Configuração de Autenticação AWS
# Configuração AWS CLI
aws configure
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: ap-northeast-1
# Ou variáveis de ambiente
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-northeast-1"
Estrutura do Projeto
terraform-project/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ ├── database/
│ └── storage/
├── .terraform-version
└── .gitignore
Sintaxe Básica HCL
Configuração do Provider
# main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Backend remoto (explicado mais adiante)
backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
}
}
Definição de Variáveis
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
description = "Project name"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "db_config" {
description = "Database configuration"
type = object({
instance_class = string
allocated_storage = number
engine_version = string
})
default = {
instance_class = "db.t3.micro"
allocated_storage = 20
engine_version = "16.1"
}
}
Arquivo de Valores de Variáveis
# terraform.tfvars
aws_region = "ap-northeast-1"
environment = "dev"
project_name = "my-app"
vpc_cidr = "10.0.0.0/16"
instance_type = "t3.small"
db_config = {
instance_class = "db.t3.small"
allocated_storage = 50
engine_version = "16.1"
}
Construção de Rede VPC
Design do Módulo
# modules/networking/main.tf
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# Subnets públicas
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-${local.azs[count.index]}"
Tier = "Public"
}
}
# Subnets privadas
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(local.azs))
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-${local.azs[count.index]}"
Tier = "Private"
}
}
# NAT Gateway (para ambiente de produção)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(local.azs) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-${var.environment}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(local.azs) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${var.environment}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
# Tabela de rotas (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.project_name}-${var.environment}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Tabela de rotas (privada)
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
tags = {
Name = "${var.project_name}-${var.environment}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
Variáveis e Outputs do Módulo
# modules/networking/variables.tf
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "enable_nat_gateway" {
type = bool
default = false
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
Construção de Instância EC2
Security Group
# modules/compute/main.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-${var.environment}-web-sg"
description = "Security group for web servers"
vpc_id = var.vpc_id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-sg"
}
}
Launch Template e Auto Scaling
# Data source AMI
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Launch Template
resource "aws_launch_template" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
image_id = data.aws_ami.amazon_linux_2023.id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.web.id]
}
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh", {
environment = var.environment
region = var.aws_region
}))
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-${var.environment}-web"
}
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
min_size = var.asg_min_size
max_size = var.asg_max_size
desired_capacity = var.asg_desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-web"
propagate_at_launch = true
}
}
# Políticas de Scaling
resource "aws_autoscaling_policy" "web_scale_up" {
name = "${var.project_name}-${var.environment}-web-scale-up"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
cooldown = 300
}
resource "aws_autoscaling_policy" "web_scale_down" {
name = "${var.project_name}-${var.environment}-web-scale-down"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = -1
cooldown = 300
}
Construção de Banco de Dados RDS
# modules/database/main.tf
# Subnet Group
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-${var.environment}-db-subnet"
}
}
# Security Group
resource "aws_security_group" "db" {
name = "${var.project_name}-${var.environment}-db-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from web servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.web_security_group_id]
}
tags = {
Name = "${var.project_name}-${var.environment}-db-sg"
}
}
# Parameter Group
resource "aws_db_parameter_group" "main" {
family = "postgres16"
name = "${var.project_name}-${var.environment}-pg16"
parameter {
name = "log_min_duration_statement"
value = "1000" # Log de queries acima de 1 segundo
}
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
}
}
# Instância RDS
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}-db"
engine = "postgres"
engine_version = var.db_config.engine_version
instance_class = var.db_config.instance_class
allocated_storage = var.db_config.allocated_storage
max_allocated_storage = var.db_config.allocated_storage * 2
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password # Recomendado usar Secrets Manager
multi_az = var.environment == "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main.name
backup_retention_period = var.environment == "prod" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.project_name}-${var.environment}-final" : null
deletion_protection = var.environment == "prod"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
tags = {
Name = "${var.project_name}-${var.environment}-db"
}
}
Gerenciamento de Estado
Configuração de Backend Remoto
# backend-setup/main.tf
# Criação do bucket S3 e tabela DynamoDB para gerenciamento de estado
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-${data.aws_caller_identity.current.account_id}"
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 = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Integração CI/CD
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
branches: [main]
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'ap-northeast-1'
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
working-directory: terraform/environments/dev
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -input=false
working-directory: terraform/environments/dev
continue-on-error: true
- name: Update Pull Request
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format 🖌 \`${{ steps.fmt.outcome }}\`
#### Terraform Init ⚙️ \`${{ steps.init.outcome }}\`
#### Terraform Validate 🤖 \`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖 \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
working-directory: terraform/environments/dev
Melhores Práticas
Convenção de Nomenclatura
# Convenção de nomenclatura consistente
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_vpc" "main" {
tags = {
Name = "${local.name_prefix}-vpc"
}
}
resource "aws_subnet" "public" {
tags = {
Name = "${local.name_prefix}-public-subnet-${count.index + 1}"
}
}
Uso de Data Sources
# Referência a recursos existentes
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["existing-vpc"]
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.existing.id]
}
filter {
name = "tag:Tier"
values = ["Private"]
}
}
Lógica Condicional
# Lógica condicional baseada no ambiente
resource "aws_nat_gateway" "main" {
count = var.environment == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
# Blocos dinâmicos
resource "aws_security_group" "example" {
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Referência de Comandos
# Inicialização
terraform init
# Formatação
terraform fmt -recursive
# Validação
terraform validate
# Verificar plano
terraform plan -out=plan.tfplan
# Aplicar
terraform apply plan.tfplan
# Destruir
terraform destroy
# Verificar estado
terraform state list
terraform state show aws_vpc.main
# Importar
terraform import aws_vpc.main vpc-12345678
# Verificar outputs
terraform output
terraform output -json
Resumo
Resumo dos pontos-chave para construção de infraestrutura com Terraform.
Princípios de Design
- Modularização: Design de componentes reutilizáveis
- Separação de Ambientes: Gerenciamento de estado independente por ambiente
- Convenção de Nomenclatura: Nomenclatura consistente
- Documentação: Descrições detalhadas das variáveis
Pontos de Operação
- Backend Remoto: Gerenciamento de estado com S3 + DynamoDB
- Integração CI/CD: Automação com GitHub Actions
- Revisão de Plan: Sempre verificar antes de aplicar
- Controle de Versão: Fixar versões do provider e Terraform
A codificação da infraestrutura possibilita reprodutibilidade, rastreabilidade e automação.