Pular para o conteúdo
iauaiiauai — portal de tecnologia, IA e Cloud
CloudIniciante

Terraform na prática: sua primeira infra versionada

Crie infra com código, não com clicks. Init, plan, apply, destroy: o ciclo de vida explicado com exemplo real.

Por Equipe iauai · 8 de julho de 2026 · 10 min de leitura

Nesta página

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:

  1. State é baixado do S3
  2. Tabela DynamoDB trava (lock)
  3. Executa plano
  4. Libera lock
  5. 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.

Aprenda jogando

Datacenter

Percorra o corredor de racks e resolva falhas antes que o uptime despenque.

Jogar Datacenter

Teste seu conhecimento

Teste o que você aprendeu sobre Terraform

Pergunta 1 de 5

Seu retriever devolve um trecho que corta no meio de uma frase importante, perdendo contexto. Qual é a causa mais provável?

Continue lendo