Resumo para os apressados#
| Tópico | Resposta curta |
|---|---|
| Sintaxe | HCL declarativa: resource, variable, output, module |
| Providers | Plugins que traduzem HCL para chamadas de API (AWS, GCP, Azure, etc.) |
| Módulos | Reuso de código — próprios ou do Terraform Registry |
| tfvars | Arquivos .tfvars separam valores de ambiente da lógica de infra |
| Ambientes | Use workspaces (simples) ou file structure (recomendado para times) |
| Trunk-based | Commits frequentes na main, feature flags para controlar ambientes |
| Branch-based | Branches por ambiente (staging, prod), risco de merge hell |
| Estado | .tfstate + remote backend (S3+GCS+Azure) com locking |
| Drift | terraform plan detecta; terraform apply -refresh-only reconcilia |
1. Sintaxe do Terraform em 5 minutos#
Terraform usa HCL (HashiCorp Configuration Language), uma linguagem declarativa onde você descreve o estado desejado da infraestrutura, não os passos para chegar lá.
Os quatro blocos que você vai usar todo santo dia:
# 1. Provider — com quem o Terraform vai falar
provider "aws" {
region = "us-east-1"
}
# 2. Variable — valores parametrizáveis
variable "instance_type" {
description = "Tipo da instância EC2"
type = string
default = "t2.micro"
}
# 3. Resource — o que você quer criar
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
tags = {
Name = "servidor-web"
}
}
# 4. Output — valores exportados após apply
output "instance_ip" {
value = aws_instance.web.public_ip
}Algumas expressões que salvam tempo:
# Interpolação de variáveis
name = "app-${var.environment}"
# if ternário (sim, HCL tem)
instance_type = var.env == "prod" ? "t2.large" : "t2.micro"
# Loop com for_each
resource "aws_s3_bucket" "buckets" {
for_each = toset(["logs", "backups", "media"])
bucket = "${each.key}-${var.project}"
}E os comandos que você vai digitar toda hora:
terraform init # Baixa providers e módulos
terraform plan # Preview do que será alterado (sem aplicar)
terraform apply # Aplica as mudanças
terraform fmt # Formata código HCL
terraform validate # Valida sintaxeO fluxo básico é init → plan → apply. Nunca dê apply sem antes ver o plan. Já vi gente derrubar produção porque confiou no -auto-approve. Não seja essa pessoa.
2. Providers: a ponte entre código e cloud#
O Terraform por si só não cria nada. Ele delega a execução para providers,plugins que traduzem HCL em chamadas de API reais.
Ou seja, quando você declara provider "aws", o Terraform baixa o binário do provider AWS e usa ele pra autenticar e chamar a API. Cada resource pertence a um provider (aws_instance → provider aws).
Os providers que mais aparecem no dia a dia:
| Provider | Use case |
|---|---|
aws | EC2, S3, RDS, Lambda, VPC, IAM |
azurerm | Azure VMs, Storage, AKS, CosmosDB |
google | GCP Compute, GKE, Cloud Storage |
kubernetes | Deployments, Services, ConfigMaps no K8s |
helm | Charts Helm gerenciados como código |
cloudflare | DNS, Workers, Pages |
github | Repositórios, teams, branch protection |
datadog | Dashboards, monitores, alerts |
O Terraform Registry tem mais de 3.000 providers. Dificilmente você vai precisar de algo que não exista lá.#
3. Módulos: pare de copiar e colar#
Módulos são como funções para infraestrutura. Em vez de repetir 50 linhas de VPC em todo projeto, você chama um módulo.
Módulos do Registry#
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "meu-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}Três cuidados que eu tomaria com módulos de terceiros:
- Sempre fixe a versão (
version = "5.0.0"), nunca uselatest. Breaking change em módulo popular é mais comum do que parece. - Leia o código fonte do módulo antes de usar em produção. Popular não significa bem escrito.
- Se o módulo for crítico, faça fork. Melhor depender do seu próprio repositório do que acordar domingo com o módulo deprecado.
Módulos próprios#
Convenção que funciona bem:
modules/
├── networking/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── database/
├── main.tf
├── variables.tf
└── outputs.tfmodule "database" {
source = "./modules/database"
engine = "postgres"
instance_class = "db.t3.medium"
environment = var.environment
}A separação main.tf (recursos), variables.tf (inputs) e outputs.tf (retornos) não é obrigatória, mas times com mais de duas pessoas agradecem.
4. tfvars: código é lógica, tfvars é configuração#
.tfvars são arquivos que atribuem valores a variáveis sem mexer no código. Isso separa o que é lógica de infra do que é valor de ambiente.
# variables.tf — declara as variáveis e defaults
variable "environment" {
type = string
default = "dev"
}
variable "instance_count" {
type = number
default = 1
}# staging.tfvars
environment = "staging"
instance_count = 2# production.tfvars
environment = "prod"
instance_count = 5Uso no terminal:
terraform plan -var-file="staging.tfvars"
terraform apply -var-file="staging.tfvars"
terraform plan -var-file="production.tfvars"
terraform apply -var-file="production.tfvars"Nunca commite secrets em .tfvars. Use sensitive = true na variável e passe valores via variável de ambiente (TF_VAR_db_password) ou secrets manager. Crie um terraform.tfvars.example com valores dummy pro time se orientar. E coloque *.tfvars (exceto o .example) no .gitignore.
5. Ambientes no Terraform: qual estratégia usar?#
Gerenciar dev, staging e produção com Terraform tem três abordagens principais.
Workspaces (a mais simples)#
terraform workspace new staging
terraform workspace new production
terraform workspace select staging
terraform apply -var-file="staging.tfvars"Um backend, múltiplos state files. Rápido e nativo, mas todos os ambientes compartilham o mesmo backend e as mesmas credenciais. Um terraform destroy no workspace errado e você deletou produção. Já vi acontecer.
File Structure (a que eu recomendo pra times)#
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── modules/
├── networking/
├── compute/
└── database/Cada ambiente tem seu próprio diretório, seu próprio state e suas próprias credenciais. Isolamento de verdade. O preço é um pouco de duplicação de código, mas pra times de 2 a 10 pessoas vale muito a pena.
Terragrunt (pra times grandes)#
Terragrunt é um wrapper que elimina a duplicação mantendo o isolamento:
# terragrunt.hcl
terraform {
source = "git::git@github.com:org/modules.git//networking?ref=v1.0.0"
}
inputs = {
environment = "staging"
cidr_block = "10.1.0.0/16"
}Resumo rápido:
| Estratégia | Tamanho do time | Risco | Complexidade |
|---|---|---|---|
| Workspaces | 1-2 pessoas | Médio | Baixa |
| File Structure | 2-10 pessoas | Baixo | Média |
| Terragrunt | 10+ pessoas | Baixo | Alta |
Minha sugestão: comece com file structure. Se o time crescer a ponto da duplicação doer, migre pra Terragrunt.
6. Trunk-Based Development com Terraform#
Trunk-based development (TBD) é commitar direto na main várias vezes ao dia, usando feature flags e proteção de branches pra controlar deploys.
main ──●──●──●──●──●──●──●── (commits frequentes)
│ │
▼ ▼
[plan] [plan]
│ │
▼ ▼
staging productionNa prática:
# 1. Altera o tfvars do staging
vim environments/staging/terraform.tfvars
# 2. Commit direto na main
git add . && git commit -m "staging: sobe instance_count pra 3"
git push origin main
# 3. O CI detecta o push, roda plan no staging
# 4. Plan limpo → CI dá apply no staging
# 5. Validou em staging → promove pra prod (mesmo código, outro tfvars)Uma pipeline de exemplo com GitHub Actions:
name: Terraform CI
on:
push:
branches: [main]
jobs:
terraform:
strategy:
matrix:
environment: [staging, production]
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Plan
working-directory: environments/${{ matrix.environment }}
run: terraform plan -input=false
- name: Terraform Apply (prod só com aprovação manual)
if: matrix.environment == 'staging' || github.event_name == 'workflow_dispatch'
working-directory: environments/${{ matrix.environment }}
run: terraform apply -auto-approve -input=falseAs vantagens do TBD são claras: sem branches long-lived, sem merge hell, a main sempre reflete o que está (ou estará) em produção, e toda mudança pequena é revisada e deployada rápido.
Só toma cuidado com três coisas:
- feature flags: se a feature está incompleta, controle com variável condicional, não com branch separada.
- Proteção de produção: exija aprovação manual antes do apply em prod.
- testes no CI:
terraform validateeterraform fmt -check(No mínimo, o correto é ter SAST, secret scan, linters mais avançados, etc..) em TODO commit.
7. Branch-Based Development: o modelo tradicional#
No modelo branch-based, cada ambiente tem uma branch de longa duração:
main ──────●────────────●────────────●── (produção)
\\ \\ \\
staging ────●───●──●────●────────────●── (homologação)
\\
dev ──────────────●──●──●──●──●──●──●── (desenvolvimento)Fluxo típico:
git checkout dev
# ... altera recursos ...
git commit -m "adiciona cache redis"
git checkout staging
git merge dev
terraform plan && terraform apply
git checkout main
git merge staging
terraform plan && terraform applyParece organizado, mas na prática traz uns problemas sérios:
| Problema | Por que dói |
|---|---|
| Merge hell | As branches dev e staging divergem e resolver conflito em HCL não é como resolver em Python. |
| State vs Branch | O state file não sabe que branches existem. Se dev e staging apontam pro mesmo backend, o caos é certo. |
| Promoção manual | Depende de alguém lembrar de mergear. Automação frágil e propensa a erro humano. |
| Ilusão de isolamento | A branch tá separada mas o state não. Sem file structure, branches são cosméticas. |
Branch-based ainda faz sentido em times pequenos (1-3 pessoas) sem CI/CD maduro ou em monorepos onde Terraform é só uma parte. Mas a real é que a maioria dos times está migrando pra trunk-based com file structure, isolamento real e deploy contínuo.
8. Estado e Drift: onde a mágica acontece (e os problemas também)#
O state file#
O .tfstate é um JSON que mapeia recursos declarados no .tf para recursos reais na cloud. Sem ele, o Terraform não sabe o que destruir ou alterar.
main.tf ──► terraform apply ──► aws_instance.web (i-0a1b2c3d4e5f)
│
└── terraform.tfstate
resource "aws_instance" "web" {
id = "i-0a1b2c3d4e5f"
...
}Remote Backend (obrigatório se tem mais de uma pessoa)#
State nunca deve ser local. Use backend remoto com locking:
terraform {
backend "s3" {
bucket = "meu-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}| Backend | Locking | Custo |
|---|---|---|
| S3 | Sim (nativo) | Baixo |
| GCS | Sim (nativo) | Baixo |
| Azure Storage | Sim (lease) | Baixo |
| HCP Terraform | Sim (nativo) | Free tier |
Regra de ouro: duas pessoas nunca podem dar apply ao mesmo tempo no mesmo state. O locking impede isso, mas só funciona se você configurar direito.
Drift: quando a realidade sai do código#
Drift acontece quando alguém muda um recurso fora do Terraform (console, CLI, outro time). O state fica desatualizado e você só descobre quando algo quebra.
terraform plan -detailed-exitcode
# exit 0 = tudo em sync
# exit 1 = erro
# exit 2 = drift detectadoPra corrigir:
Reconciliar (importar a mudança):
terraform apply -refresh-only # Atualiza o state sem mexer na infra
terraform plan # Confere se bateu — se não, ajusta o .tf e roda applySobrescrever (forçar o código):
terraform apply # Altera a infra de volta pro que está no códigoPra prevenir, o combo que funciona: bloqueie acesso de escrita nos consoles (IAM read-only pra humanos), toda mudança passa pelo Terraform, e rode plan no CI periodicamente, um cron job diário que alerta se detectar drift. Simples e eficaz.
9. Estrutura que escala#
Juntando tudo que falamos, essa estrutura funciona de 1 a 50 pessoas:
terraform/
├── environments/
│ ├── dev/
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── backend.tf
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── terraform.tfvars
├── modules/
│ ├── networking/
│ ├── compute/
│ ├── database/
│ └── monitoring/
├── .github/
│ └── workflows/
│ ├── terraform-plan.yml
│ ├── terraform-apply.yml
│ └── drift-detection.yml
├── .gitignore
├── .terraform-version
└── README.mdPara fechar#
Terraform é enorme, mas a essência tá em seis conceitos:
- HCL descreve: você declara o que quer, o Terraform resolve como chegar lá
- Providers executam: o motor que transforma código em recurso real
- Módulos organizam: reuso de código, seus ou do Registry
- tfvars separam: código é lógica, tfvars é configuração de ambiente
- Ambientes isolam: file structure > workspaces pra times sérios
- State é a verdade: proteja com backend remoto + locking, monitore drift
Se tem uma dica que eu daria pra quem tá começando: comece pequeno e itere. Cria uma instância EC2 simples, depois adiciona um módulo, depois separa ambientes. Não tenta montar a estrutura perfeita no dia 1, ela vai evoluir conforme o time cresce.
Este artigo faz parte da série IaC Tools. O anterior foi Terraform vs Pulumi: Qual Escolher em 2026?.
Dúvidas ou sugestões? Me encontra no LinkedIn.
