Ao final deste artigo você vai conseguir descrever uma pequena infraestrutura na AWS em código, ver o que vai mudar antes de aplicar, guardar o state de forma segura e derrubar tudo quando acabar. Os conceitos valem para qualquer nuvem, porque mudam só os providers.
Por que infra como código
Quando você cria servidores clicando no console, a infraestrutura vira memória de quem clicou. Staging e produção divergem sem ninguém notar, a documentação envelhece e um recurso apagado por engano deixa a equipe sem saber o que existia.
Com Terraform a infraestrutura fica em arquivos no Git. Mudanças passam por pull request, terraform plan mostra o efeito antes de qualquer alteração, e reproduzir um ambiente é rodar o mesmo código com outras variáveis. Reverter costuma ser um git revert seguido de terraform apply, com a ressalva de que nem toda mudança é reversível (apagar um banco de dados, por exemplo, não volta com o revert).
Uma observação sobre o ecossistema. O Terraform é mantido pela HashiCorp (hoje parte da IBM) sob licença BUSL, e existe o OpenTofu, um fork mantido pela Linux Foundation com licença aberta. Os comandos e a linguagem HCL são praticamente os mesmos, então tudo aqui funciona trocando terraform por tofu.
Como o Terraform pensa
Terraform é declarativo. Você descreve o estado desejado e ele calcula o que criar, alterar ou remover para chegar lá. Um exemplo enxuto, com rede e um servidor:
# main.tf
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
# Busca a AMI mais recente do Ubuntu 24.04 em vez de fixar um ID
data "aws_ssm_parameter" "ubuntu" {
name = "/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "demo-vpc" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "demo-igw" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = { Name = "demo-public" }
}
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
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "app" {
name_prefix = "demo-app-"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH só do seu IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.allowed_ssh_cidr]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
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"]
}
}
resource "aws_instance" "app" {
ami = data.aws_ssm_parameter.ubuntu.value
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.app.id]
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y docker.io
systemctl enable --now docker
EOF
root_block_device {
volume_size = 30
volume_type = "gp3"
}
tags = { Name = "demo-app" }
}
resource "aws_eip" "app" {
instance = aws_instance.app.id
domain = "vpc"
depends_on = [aws_internet_gateway.main]
}
São 8 recursos: VPC, internet gateway, subnet, tabela de rotas, associação da rota, security group, instância e IP elástico. Repare que nenhum ID aparece fixo. Cada recurso referencia o atributo do outro (aws_vpc.main.id), e é isso que permite ao Terraform montar o grafo de dependências e criar tudo na ordem certa.
Em produção você não deixaria a porta 22 aberta, nem mesmo para um IP. O caminho mais seguro é o AWS Systems Manager Session Manager, que dispensa SSH. Para um primeiro estudo, restringir ao seu IP com /32 é aceitável.
As variáveis usadas acima ficam em outro arquivo:
# variables.tf
variable "region" {
description = "Região AWS"
type = string
default = "sa-east-1"
}
variable "instance_type" {
description = "Tipo da instância EC2"
type = string
default = "t3.medium"
}
variable "allowed_ssh_cidr" {
description = "CIDR com acesso SSH, por exemplo 203.0.113.10/32"
type = string
}
variable "environment" {
description = "Ambiente"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment deve ser dev, staging ou prod."
}
}
O allowed_ssh_cidr não tem default, então o Terraform vai exigir o valor. Você passa por -var, por um arquivo terraform.tfvars ou pela variável de ambiente TF_VAR_allowed_ssh_cidr.
O ciclo: init, plan, apply, destroy
init
terraform init
Baixa os providers, configura o backend e cria a pasta .terraform/. Gera também o .terraform.lock.hcl, que trava as versões dos providers. Esse arquivo deve ir para o Git, para que todos usem as mesmas versões.
plan
terraform plan -out=tfplan
O Terraform compara o seu código com o state e com a infraestrutura real e mostra o que vai fazer. Cada linha traz um símbolo: + cria, ~ altera no lugar, - destrói e -/+ recria. O resumo final diz algo como Plan: 8 to add, 0 to change, 0 to destroy. Leia com atenção, principalmente os -/+, porque recriar um banco de dados ou um disco pode significar perda de dados.
apply
terraform apply tfplan
Executa exatamente o plano salvo. Sem o arquivo de plano, terraform apply gera um plano novo e pede confirmação. Em pipelines de CI/CD o padrão é salvar o plano na etapa de revisão e aplicá-lo na etapa de deploy.
destroy
terraform destroy
Remove tudo o que o state controla, depois de pedir confirmação. É ótimo para ambientes de estudo, para não pagar por recursos esquecidos, e perigoso em produção. Em recursos críticos, use lifecycle { prevent_destroy = true } como trava de segurança.
State: o arquivo que liga código à realidade
O Terraform guarda no terraform.tfstate quais recursos reais correspondem a cada bloco do código. Sem ele, não saberia o que já criou.
Dois cuidados essenciais. O state pode conter dados sensíveis em texto claro, como senhas de banco geradas pelo próprio Terraform, então nunca vá para o Git. E, em equipe, duas pessoas aplicando ao mesmo tempo corrompem o state, por isso se usa um backend remoto com lock.
printf 'terraform.tfstate*\n.terraform/\n*.tfplan\n' >> .gitignore
Um backend S3 com lock nativo fica assim:
# backend.tf
terraform {
backend "s3" {
bucket = "meu-time-tf-state"
key = "prod/terraform.tfstate"
region = "sa-east-1"
encrypt = true
use_lockfile = true
}
}
O use_lockfile = true, disponível a partir do Terraform 1.10, grava o lock como um arquivo no próprio bucket. Ele substitui a tabela DynamoDB que tutoriais mais antigos recomendam, e que hoje está marcada como obsoleta. Se você usa uma versão anterior, confira a documentação do backend para a sua versão.
O bucket precisa existir antes do init, com versionamento e criptografia ligados:
aws s3api create-bucket \
--bucket meu-time-tf-state \
--region sa-east-1 \
--create-bucket-configuration LocationConstraint=sa-east-1
aws s3api put-bucket-versioning \
--bucket meu-time-tf-state \
--versioning-configuration Status=Enabled
Os nomes de bucket são globais na AWS, então troque meu-time-tf-state por um nome seu.
Outputs: pegar valores depois do apply
# outputs.tf
output "instance_id" {
description = "ID da instância EC2"
value = aws_instance.app.id
}
output "public_ip" {
description = "IP público do servidor"
value = aws_eip.app.public_ip
}
Depois do apply, terraform output lista todos e terraform output -raw public_ip devolve só o valor, útil em scripts. Para valores secretos, marque o output com sensitive = true.
Armadilhas comuns
Drift. Alguém altera um recurso pelo console e o código passa a mentir. Para ver a divergência sem alterar nada, use terraform plan -refresh-only. O comando terraform refresh isolado ainda existe, mas está obsoleto, porque atualiza o state sem mostrar o que mudou antes. Depois de detectar o drift, decida se o código ou a realidade é o certo.
Segredos no código. Não escreva senhas em .tf nem em .tfvars versionados. Use variáveis de ambiente, um cofre (AWS Secrets Manager, Vault) ou deixe o serviço gerenciar a credencial.
Aplicar sem ler o plan. O plano existe para ser lido. Em time, comente-o no pull request antes do merge.
Versões soltas. Sem required_version e sem a restrição de versão do provider, atualizações podem mudar o comportamento. Fixe com ~> e versione o lock file.
Quando não usar Terraform
Para uma única máquina que você nunca vai recriar, o custo de manter código e state pode não compensar. E o Terraform é ruim para configurar o interior do servidor (pacotes, arquivos de configuração), onde Ansible ou imagens prontas funcionam melhor. O uso típico é provisionar a infraestrutura e entregar a aplicação por imagem de container.
Próximos passos
Com a infraestrutura em código, o passo seguinte é decidir o que roda nela. Veja Serverless ou containers para cargas de IA e, se o destino for um cluster, Kubernetes para quem vem do Docker Compose. Para controlar o que a infraestrutura custa, leia FinOps para IA e use tags em todos os recursos.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria