Última revisão: maio de 2026
Construa os serviços da AWS do exame 004 com Terraform puro — um bloco de cada vez, cada um vinculado a um domínio do exame. O mesmo código funciona no OpenTofu.
Ao final deste laboratório, você terá executado o fluxo de trabalho Terraform completo de ponta a ponta e abordado todos os domínios do exame Associate — sem uma conta de nuvem ou um único centavo de gasto. Usamos os provedores random e local, que não precisam de credenciais, então o foco permanece no que o exame realmente testa: como o próprio Terraform se comporta.
Você escreverá seu primeiro recurso, o parametrizará com variáveis e saídas, gerará muitos recursos com for_each, refatorará a repetição em um módulo reutilizável, inspecionará e migrará o estado e, finalmente, apontará a configuração para o HCP Terraform para execuções remotas. Cada trecho é Terraform puro — o mesmo código funciona sem modificações no OpenTofu. Coloque os blocos em um único main.tf (mencionaremos os poucos que vivem em seus próprios arquivos), execute terraform init uma vez e, em seguida, terraform apply passo a passo.
>= 1.5 ou OpenTofu >= 1.6 em seu PATH (terraform version). Usamos blocos import orientados por configuração na Etapa 6, que precisam da versão 1.5+.random e local são executados inteiramente em sua máquina.mkdir tf-associate-lab && cd tf-associate-lab).Este laboratório é completamente gratuito. Os provedores random e local não criam recursos na nuvem — apenas alguns arquivos pequenos em seu próprio disco e entradas em um terraform.tfstate local. A Etapa 7 (HCP Terraform) usa o nível gratuito, que é suficiente para um único workspace de laboratório. Não há nada aqui que gere cobrança enquanto ocioso.
Antes que algo seja executado, declaramos qual versão do Terraform esperamos e de quais provedores dependemos. A fixação é um tópico favorito do exame — os domínios Terraform Basics e Core Workflow testam se você entende required_version, o bloco required_providers e o papel de terraform init no download de plugins de provedor para .terraform/.
Escolhemos deliberadamente random e local. Nenhum deles precisa de um login na nuvem, então todo o laboratório permanece gratuito e reproduzível, e o exame nunca pergunta sobre uma nuvem específica de qualquer forma — ele pergunta sobre o Terraform. Coloque isso em um main.tf novo e execute terraform init; você verá o Terraform resolver e bloquear ambos os provedores em um arquivo .terraform.lock.hcl, que é em si um ponto de discussão do domínio de Noções Básicas (confirme o arquivo de bloqueio para que cada execução use versões de plugin idênticas).
terraform {
required_version = ">= 1.5"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
# Both providers run locally and need no "provider" configuration
# block at all - a useful reminder that providers are just plugins.Agora exercitamos o coração do domínio Usar o Fluxo de Trabalho Principal do Terraform (18% do exame). Declaramos um random_pet que inventa um nome amigável, então um local_file que grava esse nome no disco. O recurso local_file.greeting referencia random_pet.name.id, e essa referência é o que informa ao Terraform que o arquivo depende do pet — ordenação implícita de dependência, sem necessidade de depends_on.
Execute o fluxo de trabalho em ordem: terraform plan mostra um diff do que será alterado antes que algo aconteça, e terraform apply o torna real e registra o resultado em terraform.tfstate. Execute terraform apply uma segunda vez sem alterar nada e você verá Nenhuma alteração — isso é idempotência, e o exame adora perguntar por que um segundo apply é uma operação nula. Com um recurso funcionando e um arquivo de estado novo em mãos, podemos começar a tornar a configuração flexível.
resource "random_pet" "name" {
length = 2
separator = "-"
}
resource "local_file" "greeting" {
filename = "${path.module}/hello.txt"
content = "Hello from ${random_pet.name.id}!\n"
}Valores codificados são bons para uma demonstração, mas o domínio Ler, Gerar e Modificar Configuração (19%) espera que você parametrize. Adicionamos uma variable de entrada com um type, um default e um bloco validation que rejeita qualquer coisa fora dos nossos ambientes permitidos — a validação é executada no tempo de plan e é uma pergunta frequente do exame. Calculamos um mapa locals uma vez e o reutilizamos, e expomos os resultados com blocos output para que outras configurações (e terraform output) possam lê-los.
Observe a camada que o exame testa: variable é a entrada, locals é um valor derivado/intermediário, e output é o resultado publicado. Aplique novamente e tente terraform output pet_name para ler um único valor, ou terraform output -json para scriptar contra ele. Com entradas e saídas configuradas, estamos prontos para parar de escrever um recurso por vez e gerar um conjunto inteiro.
variable "environment" {
description = "Deployment environment label."
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
locals {
common_tags = {
environment = var.environment
managed_by = "terraform"
}
}
output "pet_name" {
description = "The generated pet name."
value = random_pet.name.id
}
output "tags" {
value = local.common_tags
}Configurações reais raramente declaram uma de cada coisa. Aqui nos aprofundamos ainda mais no domínio Ler, Gerar e Modificar Configuração ao impulsionar a criação de recursos a partir de uma coleção. Uma variável set(string) lista serviços lógicos; for_each então cria uma random_string e um local_file para cada serviço, endereçáveis como random_string.suffix["api"] e assim por diante.
Esta etapa também mostra expressões e funções integradas que o exame espera que você reconheça: each.key para o elemento atual, interpolação de string para construir um nome de bucket por serviço e jsonencode() para transformar um objeto HCL em um arquivo JSON no disco. Reutilizamos var.environment e local.common_tags da Etapa 3 para que cada arquivo gerado carregue metadados consistentes. A próxima pergunta óbvia — isso está ficando repetitivo, como faço para empacotá-lo? — é exatamente o que os módulos respondem.
variable "services" {
description = "Logical services to generate a config file for."
type = set(string)
default = ["api", "web", "worker"]
}
resource "random_string" "suffix" {
for_each = var.services
length = 6
special = false
upper = false
}
resource "local_file" "service_config" {
for_each = var.services
filename = "${path.module}/config/${each.key}.json"
content = jsonencode({
service = each.key
environment = var.environment
bucket = "${each.key}-${random_string.suffix[each.key].result}"
tags = local.common_tags
})
}O domínio Interagir com Módulos Terraform exige que você crie um módulo, o chame e passe valores de entrada e saída. Movemos a lógica por serviço para um módulo filho em ./modules/service, damos a ele suas próprias entradas variable e uma output, e então o chamamos da raiz com for_each — uma instância de módulo por serviço.
Os dois arquivos abaixo mostram claramente o limite: o módulo filho não sabe nada sobre quais serviços existem (isso é trabalho do chamador via var.name), e a raiz não sabe nada sobre como um serviço é construído (isso está encapsulado no módulo). Execute terraform init novamente após adicionar um módulo — o exame testa que novas fontes de módulo exigem um re-init para serem instaladas. Com nossa configuração agora modular, o último grande tópico do Associate é o que o Terraform tem rastreado silenciosamente o tempo todo: o estado.
# modules/service/main.tf
variable "name" {
type = string
}
variable "environment" {
type = string
}
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
resource "local_file" "config" {
filename = "${path.root}/config/${var.name}.json"
content = jsonencode({
service = var.name
environment = var.environment
bucket = "${var.name}-${random_string.suffix.result}"
})
}
output "bucket_name" {
value = "${var.name}-${random_string.suffix.result}"
}
# main.tf (root) - call the module once per service
module "service" {
source = "./modules/service"
for_each = var.services
name = each.key
environment = var.environment
}
output "service_buckets" {
value = { for k, m in module.service : k => m.bucket_name }
}Os domínios Implementar e Manter o Estado (19%) e Usar Terraform Fora do Fluxo de Trabalho Principal (9%) residem aqui. O estado é o livro-razão JSON que mapeia seus endereços de configuração para objetos reais; terraform state list o enumera e terraform state show <addr> imprime uma entrada. O exame espera que você saiba que renomear um recurso na configuração normalmente o destruiria e recriaria — a menos que você diga ao Terraform que o endereço foi movido.
Um bloco moved faz exatamente isso declarativamente: renomeie local_file.greeting (da Etapa 2) para local_file.welcome e o bloco moved migra o estado no local, então plan mostra um movimento, não uma destruição e recriação. (O equivalente imperativo é terraform state mv local_file.greeting local_file.welcome.) Também mostramos um bloco import orientado por configuração — a maneira 1.5+ de adotar um objeto pré-existente no estado sem o comando CLI terraform import mais antigo. Com o estado sob controle, resta uma capacidade a ser atendida: executar tudo isso remotamente.
# Renaming a resource? A "moved" block migrates state in place
# instead of destroying and recreating the object. Replace the
# Step 2 "greeting" resource with this renamed "welcome" one.
moved {
from = local_file.greeting
to = local_file.welcome
}
resource "local_file" "welcome" {
filename = "${path.module}/hello.txt"
content = "Hello from ${random_pet.name.id}!\n"
}
# Config-driven import (Terraform 1.5+): adopt an object that
# already exists into state, no "terraform import" CLI command.
# terraform_data is a built-in resource - nothing to provision.
import {
to = terraform_data.tracked
id = "existing-id"
}
resource "terraform_data" "tracked" {}O domínio final, Compreender as Capacidades do HCP Terraform (5%), completa o exame. Um único bloco cloud dentro de terraform {} troca a execução local pelo HCP Terraform: o estado reside remotamente e é bloqueado durante as execuções, terraform plan/apply são executados nos runners da HashiCorp, e a saída da execução (junto com um plano armazenado) aparece na interface web. É também aqui que o exame contrasta as funcionalidades do HCP Terraform — estado remoto, histórico de execuções, aplicação de políticas com Sentinel/OPA e um registro de módulos privado — em relação ao fluxo de trabalho puramente local que usamos nas Etapas 1-6.
Substitua my-org pela sua própria organização, execute terraform login uma vez para armazenar um token de API e, em seguida, terraform init para migrar o estado para o workspace remoto. Tudo o que você escreveu neste laboratório agora é executado sem alterações — apenas onde é executado mudou. Essa viagem de ida e volta, de um main.tf vazio para um workspace com suporte remoto, é todo o arco do Associate em uma única sessão.
terraform {
cloud {
organization = "my-org"
workspaces {
name = "terraform-associate-lab"
}
}
}
# Run once to authenticate, then re-init to migrate state:
# terraform login
# terraform initTudo está em sua máquina, então a limpeza é rápida:
terraform destroy para remover os arquivos gerados e limpá-los do estado.terraform-associate-lab da UI do HCP Terraform (ou execute terraform destroy contra ele primeiro), depois remova o bloco cloud.cd .. && rm -rf tf-associate-lab. O cache de plugins local .terraform/, .terraform.lock.hcl e terraform.tfstate serão excluídos junto.O exame Associate é sobre o Terraform como ferramenta, não sobre uma nuvem específica, então este laboratório intencionalmente não provisiona nenhuma infraestrutura de nuvem. Ignoramos recursos AWS / Azure / GCP propositalmente: eles exigem credenciais, podem gerar custos e distrairiam da mecânica que o exame realmente testa — o fluxo de trabalho, o estado, a linguagem de configuração e os módulos.
Alguns tópicos do Associate são melhor lidos do que executados: backends diferentes do HCP Terraform (S3, Azure Blob, GCS, Consul), provisioners como remote-exec (que a HashiCorp lista como último recurso) e workspaces para gerenciar múltiplas instâncias de estado. Para esses, as seções Navegar e Guia desta página de certificação têm a cobertura conceitual. O valor prático aqui é a memória muscular de init → plan → apply, ler um arquivo de estado e refatorar com segurança com moved.