DevOps & CI/CD

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

19 min de leitura

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

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

<h2>Introdução ao Terraform</h2>

<p>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 <em>como</em> fazer algo, Terraform permite descrever <em>o que</em> você quer que exista. Isso traz reproducibilidade, versionamento e automação para suas operações de infraestrutura.</p>

<p>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.</p>

<h2>Providers: A Ponte Entre Você e a Infraestrutura</h2>

<h3>O que é um Provider?</h3>

<p>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.</p>

<p>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 <code>terraform init</code>.</p>

<h3>Configurando Providers</h3>

<p>A configuração de providers é feita no bloco <code>terraform</code> ou diretamente no arquivo de configuração. Veja um exemplo prático com AWS:</p>

<pre><code class="language-hcl">terraform {

required_providers {

aws = {

source = &quot;hashicorp/aws&quot;

version = &quot;~&gt; 5.0&quot;

}

}

}

provider &quot;aws&quot; {

region = &quot;us-east-1&quot;

default_tags {

tags = {

Environment = &quot;development&quot;

ManagedBy = &quot;terraform&quot;

}

}

}</code></pre>

<p>Neste exemplo, estamos dizendo explicitamente que queremos a versão 5.x do provider AWS. O bloco <code>default_tags</code> é 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 (<code>AWS_ACCESS_KEY_ID</code> e <code>AWS_SECRET_ACCESS_KEY</code>) ou arquivo de credenciais.</p>

<p>Um exemplo com múltiplos providers (cenário multi-cloud):</p>

<pre><code class="language-hcl">terraform {

required_providers {

aws = {

source = &quot;hashicorp/aws&quot;

version = &quot;~&gt; 5.0&quot;

}

azurerm = {

source = &quot;hashicorp/azurerm&quot;

version = &quot;~&gt; 3.0&quot;

}

}

}

provider &quot;aws&quot; {

region = &quot;us-east-1&quot;

}

provider &quot;azurerm&quot; {

features {}

subscription_id = var.azure_subscription_id

}</code></pre>

<p>Nesse cenário, você pode criar recursos tanto em AWS quanto em Azure no mesmo projeto, mantendo o código organizado e centralizado.</p>

<h2>Resources: Definindo Sua Infraestrutura</h2>

<h3>Estrutura Fundamental de um Resource</h3>

<p>Um resource no Terraform é a unidade básica de infraestrutura que você deseja criar e gerenciar. Cada resource tem um tipo (como <code>aws_instance</code>, <code>aws_s3_bucket</code>, etc.) e um nome local que você define. O formato é <code>resource &quot;tipo&quot; &quot;nome_local&quot;</code>.</p>

<pre><code class="language-hcl">resource &quot;aws_instance&quot; &quot;web_server&quot; {

ami = &quot;ami-0c55b159cbfafe1f0&quot;

instance_type = &quot;t2.micro&quot;

tags = {

Name = &quot;Production Web Server&quot;

}

}</code></pre>

<p>Neste exemplo, criamos uma instância EC2 da AWS. O tipo é <code>aws_instance</code> e o nome local é <code>web_server</code>. Esse nome local é usado para referenciar este recurso em outras partes do seu código Terraform. Os argumentos (<code>ami</code>, <code>instance_type</code>, etc.) são específicos de cada tipo de resource e definidos pelo provider.</p>

<h3>Referências Entre Resources</h3>

<p>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:</p>

<pre><code class="language-hcl">resource &quot;aws_vpc&quot; &quot;main&quot; {

cidr_block = &quot;10.0.0.0/16&quot;

tags = {

Name = &quot;main-vpc&quot;

}

}

resource &quot;aws_subnet&quot; &quot;public&quot; {

vpc_id = aws_vpc.main.id

cidr_block = &quot;10.0.1.0/24&quot;

availability_zone = &quot;us-east-1a&quot;

}

resource &quot;aws_internet_gateway&quot; &quot;main&quot; {

vpc_id = aws_vpc.main.id

tags = {

Name = &quot;main-igw&quot;

}

}</code></pre>

<p>Repare que em <code>aws_subnet</code> usamos <code>aws_vpc.main.id</code> — isso referencia o atributo <code>id</code> 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.</p>

<h3>Argumentos Opcionais e Computed</h3>

<p>Nem todos os argumentos de um resource são obrigatórios. Alguns são opcionais e outros são &quot;computed&quot; (calculados pelo provider). Argumentos computed são gerados pelo provedor após a criação do recurso:</p>

<pre><code class="language-hcl">resource &quot;aws_s3_bucket&quot; &quot;data_storage&quot; {

bucket = &quot;my-unique-bucket-name-${data.aws_caller_identity.current.account_id}&quot;

tags = {

Name = &quot;Data Storage&quot;

}

}

&#039;arn&#039;, &#039;region&#039; e &#039;hosted_zone_id&#039; são computed

Você não pode definir, apenas ler após criação

data &quot;aws_caller_identity&quot; &quot;current&quot; {}

output &quot;bucket_arn&quot; {

value = aws_s3_bucket.data_storage.arn

description = &quot;ARN do bucket S3 criado&quot;

}</code></pre>

<p>Neste exemplo, <code>arn</code> é 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 <code>aws_caller_identity</code> para obter o ID da conta AWS atual, demonstrando como combinar dados e recursos.</p>

<h2>State: O Coração do Terraform</h2>

<h3>Entendendo o State File</h3>

<p>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.</p>

<pre><code class="language-json">{

&quot;version&quot;: 4,

&quot;terraform_version&quot;: &quot;1.5.0&quot;,

&quot;serial&quot;: 3,

&quot;lineage&quot;: &quot;abc123def456&quot;,

&quot;outputs&quot;: {},

&quot;resources&quot;: [

{

&quot;mode&quot;: &quot;managed&quot;,

&quot;type&quot;: &quot;aws_vpc&quot;,

&quot;name&quot;: &quot;main&quot;,

&quot;instances&quot;: [

{

&quot;schema_version&quot;: 1,

&quot;attributes&quot;: {

&quot;id&quot;: &quot;vpc-0123456789abcdef&quot;,

&quot;cidr_block&quot;: &quot;10.0.0.0/16&quot;,

&quot;enable_dns_hostnames&quot;: false

}

}

]

}

]

}</code></pre>

<p>Este é um exemplo simplificado de um state file. Cada recurso criado tem uma entrada aqui com seus atributos atuais. Quando você executa <code>terraform plan</code>, o Terraform compara seu código com este state para determinar o que precisa mudar.</p>

<h3>State Local vs. Remoto</h3>

<p>Por padrão, o Terraform armazena o state localmente em <code>terraform.tfstate</code>. Para ambientes de equipe ou produção, você <strong>nunca</strong> deve usar state local — é inseguro e causa problemas de concorrência. Use state remoto:</p>

<pre><code class="language-hcl">terraform {

backend &quot;s3&quot; {

bucket = &quot;meu-terraform-state&quot;

key = &quot;prod/terraform.tfstate&quot;

region = &quot;us-east-1&quot;

encrypt = true

dynamodb_table = &quot;terraform-locks&quot;

}

}</code></pre>

<p>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 <code>terraform apply</code> simultaneamente e corromper o state.</p>

<h3>Manipulando State com Cautela</h3>

<p>Às vezes, você precisa modificar o state manualmente. O Terraform fornece comandos para isso, mas use com cuidado:</p>

<pre><code class="language-bash"># 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</code></pre>

<p>Um cenário comum: você criou uma instância EC2 manualmente (fora do Terraform) e agora quer que o Terraform a gerencie. Use <code>terraform import</code> 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.</p>

<h2>Plan: Visualizando Mudanças Antes de Aplicar</h2>

<h3>O Comando Plan</h3>

<p>Antes de fazer qualquer mudança real, execute <code>terraform plan</code>. 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.</p>

<pre><code class="language-bash">terraform plan -out=tfplan</code></pre>

<p>O parâmetro <code>-out</code> salva o plano em arquivo binário <code>tfplan</code>, que pode ser reutilizado exatamente no <code>apply</code>. Isto garante que você aplica exatamente o que revisou.</p>

<h3>Interpretando a Saída do Plan</h3>

<pre><code>Terraform will perform the following actions:

aws_instance.web_server will be created

+ resource &quot;aws_instance&quot; &quot;web_server&quot; {

+ ami = &quot;ami-0c55b159cbfafe1f0&quot;

+ availability_zone = (known after apply)

+ instance_type = &quot;t2.micro&quot;

+ key_name = (known after apply)

+ private_ip = (known after apply)

+ public_ip = (known after apply)

+ security_groups = (known after apply)

+ tags = {

+ &quot;Name&quot; = &quot;Web Server&quot;

}

+ tenancy = (known after apply)

}

Plan: 1 to add, 0 to change, 0 to destroy.</code></pre>

<p>O símbolo <code>+</code> indica criação, <code>~</code> indica mudança, <code>-</code> indica destruição e <code>-/+</code> indica substituição (destruir e recriar). Valores marcados com <code>(known after apply)</code> não podem ser conhecidos até o recurso ser criado (como IPs públicos).</p>

<h3>Plan com Variáveis e Targets</h3>

<pre><code class="language-hcl"># variables.tf

variable &quot;environment&quot; {

type = string

default = &quot;development&quot;

}

variable &quot;instance_count&quot; {

type = number

default = 2

}

main.tf

resource &quot;aws_instance&quot; &quot;app_servers&quot; {

count = var.instance_count

ami = &quot;ami-0c55b159cbfafe1f0&quot;

instance_type = var.environment == &quot;production&quot; ? &quot;t2.large&quot; : &quot;t2.micro&quot;

tags = {

Name = &quot;app-server-${count.index + 1}&quot;

}

}</code></pre>

<pre><code class="language-bash"># Plan apenas para produção com 4 instâncias

terraform plan -var=&quot;environment=production&quot; -var=&quot;instance_count=4&quot;

Plan apenas para um recurso específico

terraform plan -target=&quot;aws_instance.app_servers[0]&quot;</code></pre>

<p>Variáveis permitem parametrizar sua configuração. Use <code>-var</code> para sobrescrever valores padrão. O <code>count</code> cria múltiplas instâncias do mesmo recurso. Usar <code>-target</code> é útil para debugging, mas não é recomendado no fluxo normal porque você não está vendo o impacto completo.</p>

<h2>Apply e Execution</h2>

<h3>Executando o Plan com Apply</h3>

<p>Após revisar o plan e estar confiante, aplique as mudanças:</p>

<pre><code class="language-bash"># 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</code></pre>

<p>Quando você executa <code>terraform apply</code> sem argumentos, o Terraform cria um novo plan, mostra para você e pede confirmação digitando <code>yes</code>. Usar um plan salvo (<code>tfplan</code>) é mais seguro porque garante que você está aplicando exatamente o que revisou.</p>

<h3>Destruindo Infraestrutura</h3>

<p>Para remover toda a infraestrutura:</p>

<pre><code class="language-bash">terraform destroy</code></pre>

<p>O Terraform calcula quais recursos deletar baseado no state, mostra um plan e pede confirmação. Use <code>-auto-approve</code> apenas em ambientes automatizados onde você confia no processo:</p>

<pre><code class="language-bash">terraform destroy -auto-approve</code></pre>

<h2>Ciclo Prático Completo: Exemplo Real</h2>

<p>Vamos ver um exemplo completo e realista de um projeto Terraform:</p>

<pre><code class="language-hcl"># providers.tf

terraform {

required_version = &quot;&gt;= 1.5&quot;

required_providers {

aws = {

source = &quot;hashicorp/aws&quot;

version = &quot;~&gt; 5.0&quot;

}

}

backend &quot;s3&quot; {

bucket = &quot;company-terraform-state&quot;

key = &quot;web-app/terraform.tfstate&quot;

region = &quot;us-east-1&quot;

encrypt = true

dynamodb_table = &quot;terraform-locks&quot;

}

}

provider &quot;aws&quot; {

region = var.aws_region

}

variables.tf

variable &quot;aws_region&quot; {

type = string

default = &quot;us-east-1&quot;

description = &quot;Região AWS&quot;

}

variable &quot;environment&quot; {

type = string

validation {

condition = contains([&quot;development&quot;, &quot;staging&quot;, &quot;production&quot;], var.environment)

error_message = &quot;Environment deve ser development, staging ou production.&quot;

}

}

variable &quot;instance_type&quot; {

type = map(string)

default = {

development = &quot;t2.micro&quot;

staging = &quot;t2.small&quot;

production = &quot;t2.medium&quot;

}

}

main.tf

resource &quot;aws_vpc&quot; &quot;app&quot; {

cidr_block = &quot;10.0.0.0/16&quot;

enable_dns_hostnames = true

tags = {

Name = &quot;${var.environment}-vpc&quot;

Environment = var.environment

}

}

resource &quot;aws_subnet&quot; &quot;public&quot; {

vpc_id = aws_vpc.app.id

cidr_block = &quot;10.0.1.0/24&quot;

availability_zone = data.aws_availability_zones.available.names[0]

map_public_ip_on_launch = true

tags = {

Name = &quot;${var.environment}-public-subnet&quot;

}

}

resource &quot;aws_security_group&quot; &quot;web&quot; {

name = &quot;${var.environment}-web-sg&quot;

description = &quot;Security group for web servers&quot;

vpc_id = aws_vpc.app.id

ingress {

from_port = 80

to_port = 80

protocol = &quot;tcp&quot;

cidr_blocks = [&quot;0.0.0.0/0&quot;]

}

ingress {

from_port = 443

to_port = 443

protocol = &quot;tcp&quot;

cidr_blocks = [&quot;0.0.0.0/0&quot;]

}

egress {

from_port = 0

to_port = 0

protocol = &quot;-1&quot;

cidr_blocks = [&quot;0.0.0.0/0&quot;]

}

tags = {

Name = &quot;${var.environment}-web-sg&quot;

}

}

resource &quot;aws_instance&quot; &quot;web&quot; {

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(&quot;${path.module}/user_data.sh&quot;, {

environment = var.environment

}))

tags = {

Name = &quot;${var.environment}-web-server&quot;

Environment = var.environment

}

lifecycle {

create_before_destroy = true

}

}

data.tf

data &quot;aws_availability_zones&quot; &quot;available&quot; {

state = &quot;available&quot;

}

data &quot;aws_ami&quot; &quot;ubuntu&quot; {

most_recent = true

owners = [&quot;099720109477&quot;] # Canonical

filter {

name = &quot;name&quot;

values = [&quot;ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*&quot;]

}

}

outputs.tf

output &quot;instance_ip&quot; {

value = aws_instance.web.public_ip

description = &quot;IP público da instância&quot;

}

output &quot;vpc_id&quot; {

value = aws_vpc.app.id

description = &quot;ID do VPC criado&quot;

}</code></pre>

<p>Fluxo de execução:</p>

<pre><code class="language-bash"># Inicializar (fazer download de providers, configurar backend)

terraform init

Validar sintaxe

terraform validate

Ver plan para desenvolvimento

terraform plan -var=&quot;environment=development&quot;

Aplicar para desenvolvimento

terraform apply -var=&quot;environment=development&quot;

Depois, para produção (com variáveis diferentes)

terraform plan -var=&quot;environment=production&quot;

terraform apply -var=&quot;environment=production&quot;</code></pre>

<p>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.</p>

<h2>Conclusão</h2>

<p>Os quatro pilares do Terraform que aprendemos aqui trabalham juntos de forma coesa: <strong>Providers</strong> conectam você aos serviços reais, <strong>Resources</strong> definem o que você quer criar, <strong>State</strong> rastreia o que existe, e <strong>Plan</strong> permite visualizar mudanças antes de aplicá-las. Dominar esses conceitos é fundamental para usar Terraform efetivamente.</p>

<p>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 <code>plan</code> antes de <code>apply</code> e mantenha seu state remoto, seguro e com locking habilitado. Com essa disciplina, você terá infraestrutura reproduzível, versionável e auditável.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.terraform.io/docs" target="_blank" rel="noopener noreferrer">Terraform Official Documentation</a></li>

<li><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" target="_blank" rel="noopener noreferrer">Terraform AWS Provider Documentation</a></li>

<li><a href="https://www.terraform.io/language/state" target="_blank" rel="noopener noreferrer">Terraform State Management</a></li>

<li><a href="https://github.com/antonbabenko/terraform-best-practices" target="_blank" rel="noopener noreferrer">Terraform Best Practices Guide - Anton Babenko</a></li>

<li><a href="https://www.oreilly.com/library/view/terraform-in-action/9781617296895/" target="_blank" rel="noopener noreferrer">Terraform in Action - Scott Winkler (O&#039;Reilly)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Kubernetes Fundamentos: Arquitetura, Componentes e kubectl na Prática: Do Básico ao Avançado
Kubernetes Fundamentos: Arquitetura, Componentes e kubectl na Prática: Do Básico ao Avançado

Entendendo Kubernetes: O Orquestrador de Contêineres Kubernetes é uma platafo...

Como Usar Azure para DevOps: AKS, Azure Pipelines e Resource Groups em Produção
Como Usar Azure para DevOps: AKS, Azure Pipelines e Resource Groups em Produção

Azure Resource Groups: Fundação da Organização em Nuvem Um Resource Group (Gr...

Dominando GitHub Actions Avançado: Matrix Builds, Environments e OIDC em Projetos Reais
Dominando GitHub Actions Avançado: Matrix Builds, Environments e OIDC em Projetos Reais

Matrix Builds: Executando Testes em Múltiplas Configurações A funcionalidade...