DevOps Admin

Boas Práticas de Terraform Fundamentos: Providers, Resources, State e Plan para Times Ágeis Já leu

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

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.

Referências


Artigos relacionados