O problema real
Você vai no console AWS, clica em EC2, cria instância, abre porta 22 pra seu IP, instala software. Pronto, servidor online. Semana depois, precisa de outro servidor igual. Clica, clica, clica... 30 minutos. Colega clica diferente, faz variação. Produção tem setup X, staging tem setup Y. Documentação fica desatualizada. Alguém deleta recurso por acidente e ninguém sabe o que era.
Terraform resolve isso: infra fica em código, versionada no Git, revisável em Pull Request. terraform plan mostra exatamente o que vai mudar antes de acontecer. Rollback é um git revert + terraform apply.
O que é Terraform e como funciona
Terraform é um orquestrador de infra declarativo. Você diz o que quer, não como fazê-lo. Ele gera plano de ação, mostra antes de executar, depois aplica.
# terraform/main.tf — declaração: "quero esta infra"
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "sa-east-1" # São Paulo
}
# VPC (rede privada)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "ml-vpc"
}
}
# Subnet (sub-rede dentro da VPC)
resource "aws_subnet" "main" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "sa-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "ml-subnet"
}
}
# Internet Gateway (acesso à internet)
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "ml-igw"
}
}
# Route table (tabela de roteamento)
resource "aws_route_table" "main" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "ml-rt"
}
}
resource "aws_route_table_association" "main" {
subnet_id = aws_subnet.main.id
route_table_id = aws_route_table.main.id
}
# Security Group (firewall)
resource "aws_security_group" "ml_app" {
name_prefix = "ml-app-"
vpc_id = aws_vpc.main.id
# SSH (entrada): só seu IP
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["SEU_IP/32"] # Troque por seu IP real
}
# HTTP (entrada)
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# HTTPS (entrada)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Tudo pode sair (saída)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ml-app-sg"
}
}
# EC2 instance
resource "aws_instance" "ml_server" {
ami = "ami-0ae8f15ae066f6fc6" # Ubuntu 22.04 LTS sa-east-1
instance_type = "t3.medium" # CPU genérica, OK pra app
subnet_id = aws_subnet.main.id
security_groups = [aws_security_group.ml_app.id]
# Script que roda na inicialização
user_data = base64encode(<<-EOF
#!/bin/bash
apt-get update
apt-get install -y docker.io git
usermod -aG docker ubuntu
systemctl start docker
systemctl enable docker
EOF
)
root_block_device {
volume_size = 30 # 30 GB disco
volume_type = "gp3"
delete_on_termination = true
}
tags = {
Name = "ml-api-server"
}
}
# Elastic IP (IP fixo, não muda quando reinicia)
resource "aws_eip" "ml_server" {
instance = aws_instance.ml_server.id
domain = "vpc"
depends_on = [aws_internet_gateway.main]
tags = {
Name = "ml-api-eip"
}
}
Este arquivo declara: 1 VPC, 1 subnet, Internet Gateway, security group (firewall), 1 EC2 instance, 1 Elastic IP. Isso é infra completa pra rodar uma aplicação.
O ciclo: init → plan → apply → destroy
1. Terraform init
cd terraform/
terraform init
Isso:
- Baixa provider AWS (plugin pra conversar com AWS API)
- Cria
.terraform/(cache local) - Inicializa state (arquivo que rastreia infraestrutura atual)
Initializing the backend...
Downloading registry.terraform.io/hashicorp/aws/5.10.0 ...
Terraform has been successfully initialized!
2. Terraform plan
terraform plan -out=tfplan
Terraform conecta na AWS, vê o que já existe, compara com seu .tf, gera plano:
Terraform will perform the following actions:
# aws_eip.ml_server will be created
+ resource "aws_eip" "ml_server" {
+ associate_with_primary_network_interface_id = (known after apply)
+ domain = "vpc"
+ id = (known after apply)
+ instance = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ tags = {
+ "Name" = "ml-api-eip"
}
}
# aws_instance.ml_server will be created
+ resource "aws_instance" "ml_server" {
+ ami = "ami-0ae8f15ae066f6fc6"
+ instance_type = "t3.medium"
+ ...
}
# ... (mais 7 recursos)
Plan: 10 to add, 0 to change, 0 to destroy.
Aqui você pode revisar: vai criar 10 recursos. Se estiver errado, Ctrl+C, corrige o .tf, roda plan de novo.
3. Terraform apply
terraform apply tfplan
Executa o plano:
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0a1b2c3d]
aws_subnet.main: Creating...
aws_subnet.main: Creation complete after 1s [id=subnet-0e1f2g3h]
aws_internet_gateway.main: Creating...
aws_internet_gateway.main: Creation complete after 1s [id=igw-0i1j2k3l]
aws_security_group.ml_app: Creating...
aws_security_group.ml_app: Creation complete after 2s [id=sg-0m1n2o3p]
aws_instance.ml_server: Creating...
aws_instance.ml_server: Creation complete after 25s [id=i-0q1r2s3t]
aws_eip.ml_server: Creating...
aws_eip.ml_server: Creation complete after 1s [id=eipalloc-0u1v2w3x]
Apply complete! Resources: 10 added, 0 unchanged, 0 destroyed.
Seu servidor já tá online.
4. Terraform state
Terraform cria terraform.tfstate:
{
"version": 4,
"terraform_version": "1.5.0",
"serial": 1,
"lineage": "abc123...",
"outputs": {},
"resources": [
{
"type": "aws_instance",
"name": "ml_server",
"instances": [
{
"attributes": {
"id": "i-0q1r2s3t",
"ami": "ami-0ae8f15ae066f6fc6",
"instance_type": "t3.medium",
...
}
}
]
}
]
}
Este é o arquivo mais perigoso do repositório. Ele contém IDs reais de recursos AWS. Nunca commite em Git. Sempre use backend remoto com lock.
Backend remoto (state seguro em time)
State local funciona pra experimento pessoal. Num time, se 2 pessoas rodarem terraform apply ao mesmo tempo, desastre (conflito).
Use S3 + DynamoDB pra lock:
# terraform/backend.tf
terraform {
backend "s3" {
bucket = "ml-team-tf-state" # Crie esse bucket antes
key = "prod/terraform.tfstate"
region = "sa-east-1"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
Crie bucket e tabela uma vez manualmente (ou com Terraform bootstrap):
# Criar bucket S3
aws s3api create-bucket \
--bucket ml-team-tf-state \
--region sa-east-1 \
--create-bucket-configuration LocationConstraint=sa-east-1
# Habilitar versionamento
aws s3api put-bucket-versioning \
--bucket ml-team-tf-state \
--versioning-configuration Status=Enabled
# Criar tabela DynamoDB pra lock
aws dynamodb create-table \
--table-name terraform-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region sa-east-1
Agora, quando alguém roda terraform plan:
- State é baixado do S3
- Tabela DynamoDB trava (lock)
- Executa plano
- Libera lock
- Persiste novo state no S3
Se outra pessoa tentar rodar apply no meio do caminho:
Error: resource locked
Reason: ml-team-1629304894 trying to lock
Variáveis: entrada flexível
Hardcode é inimigo. Use variáveis:
# terraform/variables.tf
variable "instance_type" {
description = "Tipo de instância EC2"
type = string
default = "t3.medium"
}
variable "region" {
description = "Região AWS"
type = string
default = "sa-east-1"
}
variable "environment" {
description = "Environment (dev/staging/prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment deve ser dev, staging ou prod"
}
}
variable "allowed_ssh_cidr" {
description = "CIDR permitido SSH"
type = string
}
Use em main.tf:
provider "aws" {
region = var.region
}
resource "aws_instance" "ml_server" {
instance_type = var.instance_type
# ...
}
Execute com:
terraform apply -var="instance_type=t3.large" -var="allowed_ssh_cidr=200.1.2.3/32"
Ou arquivo terraform.tfvars:
instance_type = "t3.large"
allowed_ssh_cidr = "200.1.2.3/32"
environment = "prod"
Outputs: publicar valores importantes
Depois de apply, você quer saber o IP da máquina:
# terraform/outputs.tf
output "instance_id" {
description = "ID da instância EC2"
value = aws_instance.ml_server.id
}
output "public_ip" {
description = "IP público do servidor"
value = aws_eip.ml_server.public_ip
}
output "vpc_id" {
description = "ID da VPC"
value = aws_vpc.main.id
}
Após apply:
$ terraform output
instance_id = "i-0q1r2s3t"
public_ip = "200.1.2.3"
vpc_id = "vpc-0a1b2c3d"
Ou recupere depois:
terraform output public_ip
# 200.1.2.3
Destroy: derruba tudo (com cuidado)
terraform destroy
Terraform pede confirmação:
Plan: 0 to add, 0 to change, 10 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure.
There is no undo.
Enter a value: yes
Todos os 10 recursos são deletados. AWS desliga servidor, deleta VPC, deleta security group. 2 minutos, tudo some.
Armadilhas comuns
State commitado no Git: terraform.tfstate tem secrets, IDs reais. Nunca commite. Use backend "s3" + .gitignore.
echo "terraform.tfstate*" >> .gitignore
echo ".terraform/" >> .gitignore
Drift: você cria infra com Terraform, depois clica no console e muda security group manualmente. Agora Terraform e AWS estão divergentes. Próximo plan fica confuso.
Solução: terraform refresh recarrega state atual, terraform plan -refresh=true mostra o que divergiu.
Dependência implícita: você cria 2 recursos que dependem um do outro, mas não declara. Terraform não sabe a ordem.
# RUIM: Terraform não sabe se vpc deve ser criada primeiro
resource "aws_subnet" "main" {
vpc_id = "vpc-12345" # Hardcode
}
# BOM: Terraform vê referência, cria vpc antes
resource "aws_subnet" "main" {
vpc_id = aws_vpc.main.id # Referência
}
Quando NÃO usar Terraform
Infra muito simples (1 servidor, mantenha manualmente).
Coisas que mudam constantemente (configuração de app, métricas). Use ferramentas de config management (Ansible, Chef).
Primeira vez testando: aprenda com projeto pequeno, depois escala.
Próximos passos
Automatize deployment com Kubernetes para quem vem do Docker Compose, gerenciando cluster K8s todo em Terraform.
Monitore custos com FinOps para IA: como não estourar o orçamento, tagueando todos os recursos criados.
Implante workloads de IA com Serverless ou containers para cargas de IA?, definindo filas e auto-scale em HCL.