Última revisão: maio de 2026
Construa os serviços da AWS do exame TF-PRO 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á exercitado os quatro domínios mais importantes do exame Terraform Pro: autoria de HCL avançado, construção e composição de módulos, manipulação de estado e operação contra o HCP Terraform — tudo com os provedores random e local sem credenciais, para que não haja nada a pagar e nada a limpar além de uma pasta.
Você modelará entradas com restrições de tipo ricas e validação, transformará coleções com as expressões e funções que o exame valoriza, será autor de um módulo reutilizável e o distribuirá com for_each, para então refatorar o estado ativo com blocos import, moved e removed. A etapa final conecta tudo ao HCP Terraform para execução remota. Cada trecho é Terraform puro — idêntico no OpenTofu. Construa-o em um único diretório de trabalho; indicaremos os arquivos que ficam em ./modules.
>= 1.9 ou OpenTofu >= 1.8 (terraform version). Usamos blocos removed (1.7+) e import (1.5+), então uma versão atual é mais importante aqui do que no laboratório Associate.random e local são executados inteiramente em sua máquina.mkdir tf-pro-lab && cd tf-pro-lab).terraform fmt e terraform validate após cada edição — o exame Pro assume que você os trata como parte da autoria, não como um último pensamento.Este laboratório é completamente gratuito. Os provedores random e local criam apenas pequenos arquivos em seu próprio disco mais um terraform.tfstate local; nada é provisionado em qualquer nuvem e nada gera cobranças enquanto ocioso. A Etapa 7 (HCP Terraform) é executada na camada gratuita, que cobre um único workspace de laboratório e os recursos operacionais que o exame aborda.
Começamos o domínio HCL e Configuração fixando uma versão recente do Terraform — o exame Pro assume recursos de linguagem que só existem em versões mais recentes (atributos de objeto optional(), blocos removed). O bloco required_providers trava random e local, e terraform init escreve um .terraform.lock.hcl que você deve versionar para que os planos sejam reproduzíveis entre uma equipe.
Os dois hábitos a serem desenvolvidos agora se estendem por todo o laboratório: terraform fmt canonicaliza o espaçamento e o alinhamento (o exame testa que ele reescreve arquivos no local e sai com status diferente de zero no modo -check), e terraform validate verifica se sua configuração é internamente consistente antes que qualquer provedor seja contatado. Com o conjunto de ferramentas fixado e esses dois comandos ao seu alcance, podemos escrever HCL que vale a pena validar.
terraform {
required_version = ">= 1.9"
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
# Build the muscle memory:
# terraform fmt # rewrite files to canonical style
# terraform validate # check internal consistency, offlineO exame Pro vai muito além de type = string. Aqui declaramos um tipo object com atributos optional() que fornecem valores padrão quando um chamador os omite, então anexamos múltiplos blocos validation — o exame testa que uma variável pode conter mais de um, cada um com sua própria condition e error_message.
O try() em locals é o outro movimento de nível Pro: ele retorna a primeira expressão que avalia sem erro, então a leitura de um atributo talvez ausente se degrada graciosamente em vez de interromper o plano. Seu irmão can() (que retorna um booleano) aparece mais tarde na validação da Etapa 4. Esta variável platform se torna a única fonte de verdade para o restante do laboratório — cada etapa posterior lê dela, então acertar o tipo e suas garantias aqui compensa posteriormente.
variable "platform" {
description = "Platform configuration."
type = object({
name = string
replicas = optional(number, 2)
features = optional(set(string), [])
owners = list(string)
})
validation {
condition = var.platform.replicas >= 1 && var.platform.replicas <= 10
error_message = "replicas must be between 1 and 10."
}
validation {
condition = length(var.platform.owners) > 0
error_message = "At least one owner is required."
}
default = {
name = "lab"
owners = ["platform@example.com"]
}
}
locals {
# try() returns the first error-free expression - here it shields
# against features being unset and classifies the tier.
tier = length(try(var.platform.features, [])) > 0 ? "enhanced" : "standard"
}As 'ginásticas' de coleção são a parte mais densa do domínio HCL e Configuração. Usamos setproduct() para construir cada par região/serviço, e então uma expressão for para remodelar esses pares em um mapa com chaves — o padrão exato que posteriormente alimenta um for_each. Junto a isso, flatten() + distinct() colapsam uma lista de listas de proprietários em um conjunto único e limpo.
Estas são as funções que as perguntas do Pro abordam repetidamente: for com um => para produzir um mapa, setproduct para produtos cartesianos, flatten para remover um nível de aninhamento, merge para combinar mapas e jsonencode para serializar o resultado. Renderizamos o mapa deployments calculado para um arquivo para que você possa cat out/deployments.json e ver a forma que sua expressão produziu. Esse mapa com chaves é precisamente o tipo de valor que passaremos para o for_each de um módulo a seguir.
locals {
regions = ["us-east-1", "eu-west-1"]
services = ["api", "web"]
# setproduct builds every (region, service) pair; the for
# expression reshapes the pairs into a keyed map.
deployments = {
for pair in setproduct(local.regions, local.services) :
"${pair[0]}/${pair[1]}" => {
region = pair[0]
service = pair[1]
}
}
# flatten + distinct collapse nested owner lists into a unique set.
all_owners = distinct(flatten([
for owner in var.platform.owners : split(",", owner)
]))
}
resource "local_file" "matrix" {
filename = "${path.module}/out/deployments.json"
content = jsonencode(local.deployments)
}Agora o domínio Módulos (25%). Extraímos um módulo filho workload em ./modules/workload com seus próprios required_providers, entradas validadas e saídas publicadas. O can(regex(...)) na validação do nome é o idioma Pro: regex geraria um erro em caso de não correspondência, e can transforma esse erro em um false que a condition pode usar.
Um módulo bem construído esconde seus internos e expõe um contrato estável — os chamadores passam name e replicas, e recebem de volta um id e um manifest_path, nunca tocando no random_string interno. O módulo declarar seus próprios requisitos de provedor é um ponto Pro deliberado: a configuração do provedor é herdada da raiz por padrão, mas os requisitos do provedor são declarados por módulo. Com um módulo limpo em mãos, a questão interessante é como instanciá-lo muitas vezes e conectar as instâncias.
# modules/workload/main.tf
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
variable "name" {
type = string
validation {
# can() turns regex's raise-on-no-match into a usable bool.
condition = can(regex("^[a-z][a-z0-9-]{1,30}$", var.name))
error_message = "name must be 2-31 chars: lowercase letter, then letters/digits/hyphens."
}
}
variable "replicas" {
type = number
default = 1
}
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
resource "local_file" "manifest" {
filename = "${path.root}/out/${var.name}.json"
content = jsonencode({
name = var.name
replicas = var.replicas
id = "${var.name}-${random_string.suffix.result}"
})
}
output "id" {
description = "Stable identifier for this workload."
value = "${var.name}-${random_string.suffix.result}"
}
output "manifest_path" {
value = local_file.manifest.filename
}Ainda no domínio Módulos, agora chamamos module.workload uma vez por workload com for_each sobre um conjunto, definimos replicas condicionalmente por chave, e então consumimos a saída id de cada instância em um único registry.json agregado. Iterar sobre module.workload com uma expressão for é a maneira canônica que o exame conecta as saídas de um módulo às entradas de outro recurso.
O depends_on explícito no arquivo agregado é um ponto de ensino: dependências implícitas da referência module.workload já ordenam as coisas corretamente, mas depends_on torna a intenção inconfundível e é ocasionalmente necessário quando uma dependência não é expressa por meio de dados. Execute terraform init após adicionar a origem do módulo, depois apply. Com várias instâncias de módulo ativas e suas saídas agregadas, temos um estado real que vale a pena operar — que é o próximo domínio.
# main.tf (root) - fan the module out, then aggregate its outputs
module "workload" {
source = "./modules/workload"
for_each = toset(["api", "web", "worker"])
name = each.key
replicas = each.key == "api" ? 3 : 1
}
resource "local_file" "registry" {
filename = "${path.module}/out/registry.json"
content = jsonencode({
for k, m in module.workload : k => m.id
})
depends_on = [module.workload]
}
output "workload_ids" {
value = { for k, m in module.workload : k => m.id }
}O domínio Gerenciamento de CLI e Estado (25%) é onde o exame Pro se diferencia do Associate. Usamos três operações de estado impulsionadas por configuração. Um bloco import adota um objeto que já existe no estado — visamos um terraform_data embutido para que o exemplo permaneça sem credenciais. Um bloco removed remove um recurso do estado sem destruir o objeto real — exatamente o que você busca ao transferir a propriedade para outra configuração. E terraform_data com replace_triggered_by força uma substituição subsequente sempre que o registro da Etapa 5 muda, sem envolver nenhum provedor.
Conheça os equivalentes imperativos da CLI que o exame ainda testa: terraform state list e state show para inspecionar, terraform state mv para renomear, terraform state rm para esquecer, terraform plan -target=ADDR para restringir uma execução e terraform apply -replace=ADDR para forçar a recriação de um recurso. Com o estado firmemente sob seu controle localmente, o último domínio é fazer tudo isso no HCP Terraform.
# 1) Adopt an existing object into state (Terraform 1.5+),
# replacing the older imperative "terraform import" command.
import {
to = terraform_data.legacy
id = "existing-id"
}
resource "terraform_data" "legacy" {}
# 2) Stop managing a resource WITHOUT destroying it (Terraform 1.7+).
removed {
from = random_string.deprecated
lifecycle {
destroy = false
}
}
# 3) terraform_data + replace_triggered_by recreates a marker
# whenever the registry file content changes.
resource "terraform_data" "deploy_marker" {
input = local_file.registry.content
lifecycle {
replace_triggered_by = [local_file.registry]
}
}O domínio Operações do HCP Terraform (20%) encerra o exame. Um bloco cloud — aqui usando workspaces { tags = [...] } para que uma configuração possa mapear para múltiplos workspaces correspondentes a tags — move a execução para fora do seu laptop. As execuções ocorrem no HCP Terraform com estado remoto e bloqueado; o exame contrasta os modos de execução remoto, local e agente e os fluxos de trabalho de execução orientados por VCS, orientados por CLI e orientados por API.
É também aqui que os recursos operacionais que o exame Pro aborda se reúnem: conjuntos de variáveis para compartilhar entradas entre workspaces, gatilhos de execução para encadear a aplicação de um workspace ao plano de outro, tarefas de execução para integrações externas, conjuntos de políticas (Sentinel ou OPA) para controlar aplicações, e um registro de módulos privado para publicar o tipo de módulo que você construiu na Etapa 4. Troque my-org pela sua organização, execute terraform login e depois terraform init para migrar o estado. A configuração que você escreveu nas Etapas 1–6 é executada inalterada — apenas o local e as salvaguardas ao seu redor foram aprimorados, o que é a história completa do Pro.
terraform {
cloud {
organization = "my-org"
workspaces {
tags = ["terraform-pro-lab"]
}
}
}
# Authenticate once, then migrate state to the remote workspace:
# terraform login
# terraform initTudo é local, então a remoção é rápida:
terraform destroy para remover os arquivos gerados e limpá-los do estado. Por causa do bloco removed na Etapa 6, random_string.deprecated (se alguma vez existiu) é esquecido em vez de destruído — isso é intencional.terraform destroy contra o workspace remoto, depois exclua o workspace terraform-pro-lab na UI e remova o bloco cloud.cd .. && rm -rf tf-pro-lab. O cache .terraform/, .terraform.lock.hcl e qualquer estado local são removidos junto.O exame Pro testa a autoria e operações do Terraform, não uma nuvem específica, então este laboratório não provisiona infraestrutura de nuvem intencionalmente — isso o mantém livre de credenciais e permite que cada etapa se concentre em HCL, módulos, estado e HCP Terraform.
Alguns tópicos relevantes para o Pro são melhor estudados do que provisionados aqui: blocos dynamic (que precisam de um tipo de recurso com blocos aninhados repetíveis — um recurso de nuvem — para serem significativos), configuration_aliases de provedor e a passagem de provedores com alias para módulos, autoria de políticas Sentinel / OPA, e o fluxo de publicação do registro de módulos privado. As seções Navegar e Guia desta página de certificação cobrem esses conceitos. O valor prático aqui é o ciclo de autoria e operações em torno do qual o exame é construído: entradas complexas validadas, módulos compostos, 'cirurgia' de estado sem medo e execuções remotas.