DevOps

Introdução ao Terraform: Infraestrutura que Você Pode Versionar Já leu

11 min de leitura

Introdução ao Terraform: Infraestrutura que Você Pode Versionar
Imagine a seguinte situação, comum em empresas que não adotaram Infraestrutura como Código. Um servidor crítico de produção começa a apresentar problemas. A equipe decide provisionar um novo servidor idêntico para substi

Imagine a seguinte situação, comum em empresas que não adotaram Infraestrutura como Código. Um servidor crítico de produção começa a apresentar problemas. A equipe decide provisionar um novo servidor idêntico para substituí-lo. Ninguém sabe exatamente como o servidor original foi configurado — foi feito manualmente por um engenheiro que saiu da empresa há dois anos, com ajustes adicionais feitos por outras três pessoas ao longo do tempo, nenhum deles documentado. O processo de recriar o ambiente leva dias, envolve tentativa e erro, e o resultado final provavelmente tem diferenças sutis em relação ao original que só serão descobertas em produção.

Esse problema tem um nome: snowflake servers — servidores floco de neve, únicos e impossíveis de reproduzir. São o resultado inevitável de gerenciar infraestrutura manualmente ao longo do tempo.

A Infraestrutura como Código — IaC — resolve isso aplicando à infraestrutura os mesmos princípios que já se aplicam ao código da aplicação: tudo é descrito em arquivos de texto, versionado no Git, revisado em Pull Requests e aplicado de forma automatizada. O estado desejado da infraestrutura é declarado explicitamente, e a ferramenta garante que o estado real corresponde ao declarado.


O Que é o Terraform

O Terraform é a ferramenta de IaC mais amplamente adotada no mercado. Criado pela HashiCorp em 2014 e disponível como open source sob a licença MPL, ele permite descrever infraestrutura em uma linguagem declarativa chamada HCL — HashiCorp Configuration Language — e aplicar essa descrição em dezenas de provedores de nuvem e serviços.

A proposta central do Terraform é ser agnóstico de provedor: o mesmo fluxo de trabalho que provisiona recursos na AWS funciona para provisionar recursos no Azure, no GCP, no Cloudflare, no Datadog, no GitHub e em centenas de outros provedores. Cada provedor é um plugin que traduz os recursos declarados em HCL para as APIs específicas daquele serviço.

O Terraform opera em um modelo declarativo: o engenheiro descreve o que quer — "quero um servidor com 2 vCPUs, 4GB de RAM, rodando Ubuntu 22.04, na região us-east-1" — e o Terraform determina o que precisa ser feito para chegar a esse estado. Se o recurso não existe, ele cria. Se existe mas com configuração diferente, ele atualiza. Se foi removido da configuração, ele destrói.


Instalando o Terraform

# No Ubuntu/Debian — via repositório oficial HashiCorp
wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list

sudo apt update && sudo apt install terraform

# Verifica a instalação
terraform version

Para gerenciar múltiplas versões do Terraform em diferentes projetos, recomenda-se o tfenv — um gerenciador de versões análogo ao nvm para Node.js:

# Instala o tfenv
git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Instala uma versão específica do Terraform
tfenv install 1.7.0
tfenv use 1.7.0

# Lista versões disponíveis
tfenv list

Os Quatro Comandos Fundamentais

O fluxo de trabalho do Terraform gira em torno de quatro comandos que serão usados repetidamente ao longo de toda a carreira:

terraform init — inicializa o diretório de trabalho, baixando os plugins de provedor necessários e configurando o backend de estado. Deve ser executado sempre que um novo projeto é clonado ou quando provedores são adicionados ou atualizados.

terraform plan — compara o estado atual da infraestrutura com o estado declarado nos arquivos de configuração e exibe um plano de execução: o que será criado, modificado ou destruído. É essencialmente um dry run — nenhuma mudança real acontece.

terraform apply — aplica o plano, executando as mudanças necessárias para que a infraestrutura real corresponda ao estado declarado. Solicita confirmação interativa por padrão.

terraform destroy — destrói todos os recursos gerenciados pela configuração atual. Útil para ambientes temporários, mas deve ser usado com extrema cautela em produção.


A Linguagem HCL

A HCL foi projetada para ser legível por humanos sem ser um YAML ou JSON. É declarativa, tipada e suporta expressões, funções e referências entre recursos.

A estrutura básica de um arquivo Terraform:

# Bloco de configuração do Terraform — define versões e backend
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Bloco de provedor — configura as credenciais e região
provider "aws" {
  region = "us-east-1"
}

# Bloco de recurso — declara um recurso de infraestrutura
resource "aws_instance" "servidor_web" {
  ami           = "ami-0c55b159cbfafe1f0"  # Ubuntu 22.04
  instance_type = "t3.micro"

  tags = {
    Name        = "servidor-web"
    Environment = "producao"
    ManagedBy   = "terraform"
  }
}

A sintaxe resource "tipo_do_recurso" "nome_local" é o bloco mais fundamental do Terraform. O tipo do recurso (aws_instance) determina o que está sendo criado. O nome local (servidor_web) é usado para referenciar esse recurso em outras partes da configuração.


O Primeiro Projeto Terraform

Um projeto completo e funcional que cria uma instância EC2 com grupo de segurança e IP elástico na AWS. A estrutura de arquivos recomendada:

meu-projeto/
├── main.tf          # Recursos principais
├── variables.tf     # Declaração de variáveis
├── outputs.tf       # Outputs do projeto
├── versions.tf      # Versões do Terraform e provedores
└── terraform.tfvars # Valores das variáveis (não versionar se tiver secrets)

versions.tf — configuração do Terraform e provedores:

terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.31"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

variables.tf — declaração de variáveis com tipos e descrições:

variable "aws_region" {
  description = "Região AWS onde os recursos serão criados"
  type        = string
  default     = "us-east-1"
}

variable "project_name" {
  description = "Nome do projeto — usado em tags e nomes de recursos"
  type        = string
}

variable "environment" {
  description = "Ambiente de implantação"
  type        = string

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "O ambiente deve ser development, staging ou production."
  }
}

variable "instance_type" {
  description = "Tipo da instância EC2"
  type        = string
  default     = "t3.micro"
}

variable "allowed_ssh_cidr" {
  description = "CIDR permitido para acesso SSH"
  type        = string
  default     = "0.0.0.0/0"
}

main.tf — recursos da infraestrutura:

# Busca a AMI mais recente do Ubuntu 22.04
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# Grupo de segurança
resource "aws_security_group" "servidor_web" {
  name        = "${var.project_name}-${var.environment}-sg"
  description = "Security group do servidor web"

  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.allowed_ssh_cidr]
  }

  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Todo tráfego de saída"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-sg"
  }
}

# Par de chaves SSH
resource "aws_key_pair" "deploy" {
  key_name   = "${var.project_name}-${var.environment}-key"
  public_key = file("~/.ssh/id_ed25519.pub")
}

# Instância EC2
resource "aws_instance" "servidor_web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  key_name               = aws_key_pair.deploy.key_name
  vpc_security_group_ids = [aws_security_group.servidor_web.id]

  root_block_device {
    volume_size           = 20
    volume_type           = "gp3"
    delete_on_termination = true
    encrypted             = true
  }

  user_data = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y docker.io docker-compose-plugin
    systemctl enable docker
    systemctl start docker
    usermod -aG docker ubuntu
  EOF

  tags = {
    Name = "${var.project_name}-${var.environment}-web"
  }
}

# IP elástico
resource "aws_eip" "servidor_web" {
  instance = aws_instance.servidor_web.id
  domain   = "vpc"

  tags = {
    Name = "${var.project_name}-${var.environment}-eip"
  }
}

outputs.tf — valores exportados após o apply:

output "ip_publico" {
  description = "IP público da instância"
  value       = aws_eip.servidor_web.public_ip
}

output "dns_publico" {
  description = "DNS público da instância"
  value       = aws_instance.servidor_web.public_dns
}

output "comando_ssh" {
  description = "Comando SSH para acessar a instância"
  value       = "ssh -i ~/.ssh/id_ed25519 ubuntu@${aws_eip.servidor_web.public_ip}"
}

output "id_instancia" {
  description = "ID da instância EC2"
  value       = aws_instance.servidor_web.id
}

terraform.tfvars — valores concretos das variáveis:

project_name     = "minha-api"
environment      = "staging"
instance_type    = "t3.micro"
allowed_ssh_cidr = "203.0.113.0/24"  # IP do escritório

Executando o Projeto

# 1. Inicializa o projeto — baixa os plugins de provedor
terraform init

# Saída esperada:
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.31"...
# - Installing hashicorp/aws v5.31.0...
# Terraform has been successfully initialized!

# 2. Valida a sintaxe dos arquivos
terraform validate

# 3. Formata os arquivos seguindo o padrão oficial
terraform fmt

# 4. Gera e exibe o plano de execução
terraform plan

# Saída esperada:
# Plan: 4 to add, 0 to change, 0 to destroy.
# + aws_eip.servidor_web
# + aws_instance.servidor_web
# + aws_key_pair.deploy
# + aws_security_group.servidor_web

# 5. Aplica as mudanças
terraform apply

# Para aplicar sem confirmação interativa (útil em pipelines)
terraform apply -auto-approve

# 6. Exibe os outputs após o apply
terraform output

# 7. Quando não precisar mais do ambiente
terraform destroy

Entendendo o Grafo de Dependências

O Terraform constrói automaticamente um grafo de dependências entre os recursos com base nas referências entre eles. No exemplo acima, o Terraform sabe que:

  • aws_eip depende de aws_instance (referência aws_instance.servidor_web.id)
  • aws_instance depende de aws_security_group e aws_key_pair
  • aws_security_group e aws_key_pair não têm dependências entre si

Com esse grafo, o Terraform pode criar aws_security_group e aws_key_pair em paralelo, depois criar aws_instance quando ambos estiverem prontos, e finalmente criar aws_eip. Essa paralelização automática acelera significativamente a aplicação de configurações complexas.

Para visualizar o grafo:

# Gera o grafo em formato DOT
terraform graph | dot -Tpng > grafo.png

Blocos de Dados: Consultando Recursos Existentes

O bloco data permite consultar informações de recursos que já existem na infraestrutura sem gerenciá-los. É útil para referenciar recursos criados fora do Terraform ou compartilhados entre projetos:

# Busca uma VPC existente pelo nome
data "aws_vpc" "principal" {
  filter {
    name   = "tag:Name"
    values = ["vpc-principal"]
  }
}

# Usa a VPC encontrada em um novo recurso
resource "aws_subnet" "app" {
  vpc_id     = data.aws_vpc.principal.id
  cidr_block = "10.0.10.0/24"
}

# Busca o ID da conta AWS atual
data "aws_caller_identity" "atual" {}

output "account_id" {
  value = data.aws_caller_identity.atual.account_id
}

O Que Vem a Seguir

O próximo artigo aprofunda o conceito de state — o arquivo que o Terraform mantém para rastrear o estado atual da infraestrutura. Entender o state é fundamental para trabalhar em equipe, configurar backends remotos e evitar os problemas mais comuns que os engenheiros enfrentam ao usar Terraform em produção.


Referências para Aprofundamento

Documentação oficial - Terraform Documentation — developer.hashicorp.com — Documentação completa do Terraform, incluindo referência da linguagem HCL, guias de provedores e tutoriais interativos. - Terraform AWS Provider — registry.terraform.io — Documentação completa de todos os recursos e data sources do provedor AWS, com exemplos para cada recurso.

Aprendizado interativo - HashiCorp Learn — Terraform — Tutoriais oficiais interativos da HashiCorp cobrindo desde os primeiros passos até tópicos avançados como módulos e workspaces. Incluem ambientes de laboratório sem necessidade de conta na nuvem.

Boas práticas - Terraform Best Practices — terraform-best-practices.com — Guia independente e amplamente referenciado sobre boas práticas de estrutura de projetos, nomenclatura e organização de módulos Terraform. - tfenv — GitHub — Repositório oficial do gerenciador de versões do Terraform com instruções de instalação para diferentes sistemas operacionais.

Comentários

Mais em DevOps

Deploy Automático para Servidores com GitHub Actions
Deploy Automático para Servidores com GitHub Actions

Todo o trabalho construído nos artigos anteriores — testes, build de imagens,...

Protegendo Branches e Revisando Código com Pull Requests
Protegendo Branches e Revisando Código com Pull Requests

Em equipes sem processo de revis&atilde;o, &eacute; comum encontrar c&oacute;...

Semantic Versioning e Tags de Release
Semantic Versioning e Tags de Release

Imagine receber uma mensagem de um colega dizendo: "o bug est&aacute; na vers...