Introdução ao Terraform
Terraform é uma ferramenta de Infrastructure as Code (IaC) que permite definir, visualizar e gerenciar infraestrutura através de código declarativo. Diferente de ferramentas imperativas, onde você descreve como fazer algo, Terraform permite descrever o que você quer que exista. Isso traz reproducibilidade, versionamento e automação para suas operações de infraestrutura.
O grande diferencial do Terraform é sua abordagem agnóstica em relação aos provedores de nuvem. Você usa a mesma sintaxe e lógica para gerenciar recursos em AWS, Azure, Google Cloud, ou até infraestrutura on-premises. Essa versatilidade o tornou a ferramenta padrão da indústria para IaC, especialmente em ambientes multi-cloud.
Providers: A Ponte Entre Você e a Infraestrutura
O que é um Provider?
Um Provider é um plugin do Terraform que traduz suas instruções em chamadas de API específicas de um serviço (AWS, Azure, Google Cloud, etc.). O provider é responsável por autenticar-se, comunicar-se com a plataforma alvo e executar as operações necessárias. Sem providers, o Terraform seria apenas um interpretador de sintaxe.
Cada provider mantém documentação sobre quais recursos ele suporta e quais argumentos cada recurso aceita. Você precisa declarar explicitamente qual provider usar antes de criar qualquer recurso. O Terraform automaticamente faz download do binário do provider da versão especificada quando você executa terraform init.
Configurando Providers
A configuração de providers é feita no bloco terraform ou diretamente no arquivo de configuração. Veja um exemplo prático com AWS:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Environment = "development"
ManagedBy = "terraform"
}
}
}
Neste exemplo, estamos dizendo explicitamente que queremos a versão 5.x do provider AWS. O bloco default_tags é particularmente útil porque adiciona tags automaticamente a todos os recursos criados, evitando repetição. A autenticação do AWS é feita através de variáveis de ambiente (AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY) ou arquivo de credenciais.
Um exemplo com múltiplos providers (cenário multi-cloud):
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
Nesse cenário, você pode criar recursos tanto em AWS quanto em Azure no mesmo projeto, mantendo o código organizado e centralizado.
Resources: Definindo Sua Infraestrutura
Estrutura Fundamental de um Resource
Um resource no Terraform é a unidade básica de infraestrutura que você deseja criar e gerenciar. Cada resource tem um tipo (como aws_instance, aws_s3_bucket, etc.) e um nome local que você define. O formato é resource "tipo" "nome_local".
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "Production Web Server"
}
}
Neste exemplo, criamos uma instância EC2 da AWS. O tipo é aws_instance e o nome local é web_server. Esse nome local é usado para referenciar este recurso em outras partes do seu código Terraform. Os argumentos (ami, instance_type, etc.) são específicos de cada tipo de resource e definidos pelo provider.
Referências Entre Resources
Um dos poderes do Terraform é a capacidade de referenciar outputs de um resource como inputs de outro. Isso cria dependências implícitas que o Terraform entende automaticamente:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
Repare que em aws_subnet usamos aws_vpc.main.id — isso referencia o atributo id do VPC que criamos. O Terraform compreende que o subnet depende do VPC e ordena a criação corretamente. Se você deletar o VPC, o subnet será deletado também porque o Terraform rastreia essa dependência.
Argumentos Opcionais e Computed
Nem todos os argumentos de um resource são obrigatórios. Alguns são opcionais e outros são "computed" (calculados pelo provider). Argumentos computed são gerados pelo provedor após a criação do recurso:
resource "aws_s3_bucket" "data_storage" {
bucket = "my-unique-bucket-name-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "Data Storage"
}
}
# 'arn', 'region' e 'hosted_zone_id' são computed
# Você não pode definir, apenas ler após criação
data "aws_caller_identity" "current" {}
output "bucket_arn" {
value = aws_s3_bucket.data_storage.arn
description = "ARN do bucket S3 criado"
}
Neste exemplo, arn é um atributo computed — o AWS gera automaticamente quando o bucket é criado. Você não pode especificar qual ARN deseja; apenas lê o valor após a criação. Usamos data source aws_caller_identity para obter o ID da conta AWS atual, demonstrando como combinar dados e recursos.
State: O Coração do Terraform
Entendendo o State File
O state file é um arquivo JSON que o Terraform mantém para rastrear qual infraestrutura ele criou e qual é o estado atual. É absolutamente crítico: sem o state, o Terraform não sabe que ele já criou um recurso e tentaria criá-lo novamente. O state mapeia sua configuração (código) com os recursos reais da sua infraestrutura.
{
"version": 4,
"terraform_version": "1.5.0",
"serial": 3,
"lineage": "abc123def456",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "aws_vpc",
"name": "main",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "vpc-0123456789abcdef",
"cidr_block": "10.0.0.0/16",
"enable_dns_hostnames": false
}
}
]
}
]
}
Este é um exemplo simplificado de um state file. Cada recurso criado tem uma entrada aqui com seus atributos atuais. Quando você executa terraform plan, o Terraform compara seu código com este state para determinar o que precisa mudar.
State Local vs. Remoto
Por padrão, o Terraform armazena o state localmente em terraform.tfstate. Para ambientes de equipe ou produção, você nunca deve usar state local — é inseguro e causa problemas de concorrência. Use state remoto:
terraform {
backend "s3" {
bucket = "meu-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Aqui, o state é armazenado em um bucket S3 com criptografia ativada. O DynamoDB table é usado para locking — apenas uma operação Terraform pode rodar por vez, evitando race conditions. Sem locking, duas pessoas poderiam executar terraform apply simultaneamente e corromper o state.
Manipulando State com Cautela
Às vezes, você precisa modificar o state manualmente. O Terraform fornece comandos para isso, mas use com cuidado:
# Ver o state atual
terraform state list
terraform state show aws_instance.web_server
# Remover um recurso do state (sem deletar a infraestrutura real!)
terraform state rm aws_instance.web_server
# Importar um recurso existente ao state
terraform import aws_instance.web_server i-0123456789abcdef
Um cenário comum: você criou uma instância EC2 manualmente (fora do Terraform) e agora quer que o Terraform a gerencie. Use terraform import com o ID do recurso real. O Terraform consultará a AWS, aprenderá os atributos e adicionará ao state. Depois, você precisa escrever a configuração Terraform correspondente.
Plan: Visualizando Mudanças Antes de Aplicar
O Comando Plan
Antes de fazer qualquer mudança real, execute terraform plan. Este comando analisa sua configuração, consulta o state atual e calcula exatamente o que mudará na infraestrutura real. É sua oportunidade de revisar antes de comprometer.
terraform plan -out=tfplan
O parâmetro -out salva o plano em arquivo binário tfplan, que pode ser reutilizado exatamente no apply. Isto garante que você aplica exatamente o que revisou.
Interpretando a Saída do Plan
Terraform will perform the following actions:
# aws_instance.web_server will be created
+ resource "aws_instance" "web_server" {
+ ami = "ami-0c55b159cbfafe1f0"
+ availability_zone = (known after apply)
+ instance_type = "t2.micro"
+ key_name = (known after apply)
+ private_ip = (known after apply)
+ public_ip = (known after apply)
+ security_groups = (known after apply)
+ tags = {
+ "Name" = "Web Server"
}
+ tenancy = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
O símbolo + indica criação, ~ indica mudança, - indica destruição e -/+ indica substituição (destruir e recriar). Valores marcados com (known after apply) não podem ser conhecidos até o recurso ser criado (como IPs públicos).
Plan com Variáveis e Targets
# variables.tf
variable "environment" {
type = string
default = "development"
}
variable "instance_count" {
type = number
default = 2
}
# main.tf
resource "aws_instance" "app_servers" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.environment == "production" ? "t2.large" : "t2.micro"
tags = {
Name = "app-server-${count.index + 1}"
}
}
# Plan apenas para produção com 4 instâncias
terraform plan -var="environment=production" -var="instance_count=4"
# Plan apenas para um recurso específico
terraform plan -target="aws_instance.app_servers[0]"
Variáveis permitem parametrizar sua configuração. Use -var para sobrescrever valores padrão. O count cria múltiplas instâncias do mesmo recurso. Usar -target é útil para debugging, mas não é recomendado no fluxo normal porque você não está vendo o impacto completo.
Apply e Execution
Executando o Plan com Apply
Após revisar o plan e estar confiante, aplique as mudanças:
# Aplicar o plan salvo anteriormente (seguro!)
terraform apply tfplan
# Ou criar um novo plan e aplicar em um passo (perigoso, pede confirmação)
terraform apply
Quando você executa terraform apply sem argumentos, o Terraform cria um novo plan, mostra para você e pede confirmação digitando yes. Usar um plan salvo (tfplan) é mais seguro porque garante que você está aplicando exatamente o que revisou.
Destruindo Infraestrutura
Para remover toda a infraestrutura:
terraform destroy
O Terraform calcula quais recursos deletar baseado no state, mostra um plan e pede confirmação. Use -auto-approve apenas em ambientes automatizados onde você confia no processo:
terraform destroy -auto-approve
Ciclo Prático Completo: Exemplo Real
Vamos ver um exemplo completo e realista de um projeto Terraform:
# providers.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "company-terraform-state"
key = "web-app/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
}
# variables.tf
variable "aws_region" {
type = string
default = "us-east-1"
description = "Região AWS"
}
variable "environment" {
type = string
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Environment deve ser development, staging ou production."
}
}
variable "instance_type" {
type = map(string)
default = {
development = "t2.micro"
staging = "t2.small"
production = "t2.medium"
}
}
# main.tf
resource "aws_vpc" "app" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.app.id
cidr_block = "10.0.1.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet"
}
}
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.app.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
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"]
}
tags = {
Name = "${var.environment}-web-sg"
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type[var.environment]
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
environment = var.environment
}))
tags = {
Name = "${var.environment}-web-server"
Environment = var.environment
}
lifecycle {
create_before_destroy = true
}
}
# data.tf
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# outputs.tf
output "instance_ip" {
value = aws_instance.web.public_ip
description = "IP público da instância"
}
output "vpc_id" {
value = aws_vpc.app.id
description = "ID do VPC criado"
}
Fluxo de execução:
# Inicializar (fazer download de providers, configurar backend)
terraform init
# Validar sintaxe
terraform validate
# Ver plan para desenvolvimento
terraform plan -var="environment=development"
# Aplicar para desenvolvimento
terraform apply -var="environment=development"
# Depois, para produção (com variáveis diferentes)
terraform plan -var="environment=production"
terraform apply -var="environment=production"
Este exemplo demonstra: providers, resources, state remoto, data sources, variáveis com validação, templates, lifecycle rules e outputs. É um projeto real que você pode adaptar para seus casos de uso.
Conclusão
Os quatro pilares do Terraform que aprendemos aqui trabalham juntos de forma coesa: Providers conectam você aos serviços reais, Resources definem o que você quer criar, State rastreia o que existe, e Plan permite visualizar mudanças antes de aplicá-las. Dominar esses conceitos é fundamental para usar Terraform efetivamente.
O grande aprendizado é entender que Terraform é declarativo, não imperativo — você declara o estado desejado, e o Terraform calcula as mudanças necessárias. Sempre execute plan antes de apply e mantenha seu state remoto, seguro e com locking habilitado. Com essa disciplina, você terá infraestrutura reproduzível, versionável e auditável.