Lab 56 — Um ambiente por conta, sem copiar e colar
O problema, e a empresa que o tem
A Cadência, do L03 e do L43, cresceu para seis desenvolvedores e ganhou um cliente grande o bastante para exigir um ambiente de homologação antes de qualquer mudança chegar a produção. A conta de Não-Produção do L43 virou duas: uma para desenvolvimento e uma nova, para homologação — a que o cliente às vezes acessa para validar antes de aprovar.
O time criou a segunda conta copiando a pasta Terraform que já existia. Funcionou no primeiro dia. Três meses depois, um teste de carga em homologação não reproduziu um incidente que aconteceu em produção na semana seguinte — e só ao comparar as duas pastas, linha por linha, alguém percebeu que o intervalo do health check era diferente havia dois meses. Ninguém decidiu isso. Alguém editou a pasta de dev para acelerar o próprio ciclo de teste, meses atrás, e a mudança nunca saiu dali.
O problema não é a pasta copiada em si — é o que ela permite sem avisar: cada cópia evolui pela mão de quem a edita naquele dia, e não existe mecanismo que force as três a concordarem. `dev` e `prod` não divergiram porque alguém errou um valor; divergiram porque existem três códigos-fonte fingindo ser um só.
O que este laboratório NÃO é
Não é onde a conta de homologação é criada — isso é o Account Factory do L43, ou a ampliação dele. Não é onde o pipeline nasce — o CodePipeline com credencial federada é o L54, e este laboratório o reaproveita sem reconstruir. E não é uma landing zone completa com dezenas de módulos compostos: é o mínimo que resolve "três pastas divergindo", que é o L98 quando a plataforma cresce para múltiplos times.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido a diferença entre workspace e diretório.
- Explicar por que copiar uma pasta Terraform produz drift estrutural, não só de valor.
- Justificar, com a citação da própria documentação da HashiCorp, por que workspace não isola ambientes com credenciais e contas separadas.
- Desenhar um módulo cuja única variação entre ambientes está em variáveis, nunca em blocos de recurso.
- Configurar backend remoto com estado isolado por ambiente, fora das contas que ele descreve.
- Explicar por que homologação precisa ter a MESMA forma de produção, e por que dev não precisa.
- Promover a mesma tag de imagem por três contas sem reconstruir em nenhuma.
- Provar diff estrutural zero entre duas chamadas do módulo com uma ferramenta automatizada.
- Diagnosticar um lock de estado preso e decidir quando é seguro forçar a liberação.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Ambiente por conta | SAP-C02, DOP-C02 | dev, homologação e produção como três contas dentro das OUs do L43 | por que conta é fronteira e diretório de código não é |
| Promoção de artefato | DOP-C02 | a mesma tag de imagem atravessando três estágios de pipeline | por que rebuild por ambiente invalida o que foi testado |
| Paridade de infraestrutura | DOP-C02, SAP-C02 | homologação com a mesma capacidade de produção, dev menor | que paridade importa entre o ÚLTIMO ambiente e produção, não entre todos |
| Estado remoto e lock | DOP-C02 | backend S3 com tabela de lock, um estado por ambiente | por que dois apply simultâneos sem lock corrompem o mesmo arquivo de estado |
| Workspace vs. diretório | DOP-C02 | diretório por ambiente escolhido de propósito, com o motivo escrito | a diferença entre isolamento LÓGICO (workspace) e isolamento ESTRUTURAL (diretório com backend próprio) |
| Módulo reutilizável | DOP-C02, SAP-C02 | um módulo, três chamadas, só variáveis diferindo | como uma correção de bug se propaga a todas as chamadas de uma vez |
| Assunção de role entre contas | SAP-C02 | pipeline na conta de Ferramentas assumindo role por ambiente | por que rodar Terraform de uma conta administrativa reduz o raio de alcance de um erro |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve um time que usa `terraform workspace` para dev, hml e prod com um único conjunto de credenciais, e pergunta o risco. A resposta não é "nenhum, workspace resolve isso" — é que workspace isola ESTADO, não CREDENCIAL nem CONTA. Se as três continuam na mesma conta com a mesma role, o erro de aplicar no ambiente errado continua tecnicamente possível; só o arquivo de estado mudou de nome.
Requisitos, e como cada um muda o desenho
Requisito que não aparece numa linha do Terraform é intenção, não requisito. A coluna da direita é onde cada um deixou marca concreta.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Divergência entre ambientes | estruturalmente impossível, não só desencorajada | módulo único chamado por conta — nunca pasta copiada |
| Isolamento de credencial por ambiente | obrigatório, herdado do L43 | diretório próprio por conta, cada um com sua role assumida — descarta workspace único |
| Homologação representativa de produção | mesma contagem, CPU e memória | hml.tfvars espelha prod.tfvars nos quatro parâmetros de capacidade |
| Custo de ambiente ocioso | dev deve custar menos que hml e prod | dev.tfvars com menor contagem e menor CPU/memória — paridade não vale para dev |
| Rastreabilidade de quem aplicou o quê | obrigatória para auditoria | só a role do pipeline pode escrever nos três estados; humano não tem credencial de apply |
| Promoção sem rebuild | a imagem testada é a imagem publicada | variável `tag_imagem` passada intacta pelos três estágios; nenhum estágio reconstrói |
| Estado não pode ficar em laptop | obrigatório, herdado do L55 | backend S3 remoto com lock por DynamoDB, numa conta separada das de ambiente |
| Produção só após aprovação humana | obrigatório | estágio de aprovação manual entre homologação e produção no pipeline |
Arquitetura mínima: três pastas, três históricos
Este é o desenho que a Cadência tem hoje, e é legítimo como ponto de partida: copiar uma pasta e trocar o nome não exige aprender módulo, backend remoto nem assunção de role entre contas. O laboratório começa por tornar o drift discutível com um diff, em vez de deixá-lo como "acho que são iguais".
- → terraform apply na pasta ambientes-copiados/dev
- → terraform apply na pasta ambientes-copiados/hml
- → terraform apply na pasta ambientes-copiados/prod
- → monitora esta cópia, e só esta
- Fora da AWS
- Compute
- Gestão e governança
Este desenho publica os três ambientes, e é por isso que ele sobrevive: copiar uma pasta e editar o nome é a forma mais rápida de ter "mais um ambiente" na tarde de sexta-feira. O defeito não é nenhuma linha errada — é que não existe mais UM código-fonte, existem três, e cada um evolui sem saber dos outros. Percorra os passos: a divergência não é hipotética, ela está no diff entre as pastas agora.
- Três pastas, três históricos de edição. A pasta de homologação nasceu de uma cópia da de dev; a de produção nasceu de uma cópia mais antiga, antes de um ajuste específico. Não existe mais um código com três configurações — existem três códigos que começaram iguais.
- O intervalo do health check já não é o mesmo número. Alguém encurtou o intervalo em dev, meses atrás, para não esperar 30 s a cada teste local. A mudança nunca foi replicada para produção, porque replicar significa lembrar que ela existe — e nada no repositório avisa disso.
- O alarme existe numa conta e não nas outras duas. O alarme de 5xx nasceu de um incidente em homologação e resolveu o problema daquele dia. Ele nunca foi promovido para produção — que é exatamente a conta onde a ausência dele custa mais.
- O diff entre as pastas é a prova, não a suspeita. Um `diff -ru` entre as três pastas não mostra "pequenas diferenças esperadas": mostra um bloco de recurso inteiro presente numa e ausente nas outras duas, e um valor que diverge sem que ninguém tenha decidido divergir.
- Ninguém decidiu a divergência; ela é subproduto de "copiar é mais rápido". Nenhuma reunião aprovou health check diferente entre dev e prod. Cada edição, isolada, foi razoável — resolver o problema do dia, na pasta que estava aberta. A soma delas é que não é razoável.
- A cópia funciona hoje, e é exatamente por isso que ela sobrevive. Os três ambientes estão no ar, respondendo. Drift não derruba nada até o dia em que a diferença importa — um teste de carga em homologação que não reproduz o comportamento de produção, ou um health check que reage rápido demais numa conta e devagar demais na outra durante um incidente real.
O diff abaixo não é ilustrativo — é o tipo exato de achado que aparece quando alguém finalmente compara as pastas depois de meses de edição independente.
$ diff -ru ambientes-copiados/dev ambientes-copiados/prod
diff -ru ambientes-copiados/dev/main.tf ambientes-copiados/prod/main.tf
--- ambientes-copiados/dev/main.tf
+++ ambientes-copiados/prod/main.tf
@@ -40,7 +40,7 @@
health_check {
path = "/health/ready"
- interval = 15
+ interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
Only in ambientes-copiados/hml: alarme-5xx.tf
# O segundo achado é o que mais importa, e um diff de VALOR não o mostra: um
# arquivo inteiro — o alarme de 5xx — só existe na pasta de homologação.
# "Only in" é a linha do diff que a maioria ignora ao procurar divergência,
# porque se procura valor diferente, não arquivo ausente.
A pasta copiada não erra "às vezes" — ela diverge sempre, cedo ou tarde
Não é um risco hipotético: é uma função do tempo. Cada edição feita numa pasta e não replicada nas outras duas é uma linha de drift a mais, e nada no fluxo de trabalho avisa quando isso acontece. Depois de meses, a pergunta deixa de ser "as pastas ainda são iguais?" e passa a ser "onde exatamente elas divergiram, e há quanto tempo?" — e essa segunda pergunta só tem resposta com um diff manual, linha por linha.
Arquitetura para produção: módulo único, três chamadas
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A pergunta que este desenho responde não é "qual pasta editar" — é "quais valores esta conta recebe".
- → lê e grava o estado, key por ambiente
- → trava o estado antes de aplicar
- → terraform apply -var-file dev.tfvars
- → terraform apply -var-file hml.tfvars
- → terraform apply -var-file prod.tfvars, após aprovação
- → pull da mesma tag, sem rebuild
- → a mesma tag promovida de dev
- → a mesma tag promovida de homologação
- Compute
- Gestão e governança
- Armazenamento
- Banco de dados
A pergunta deixa de ser "qual pasta editar" e passa a ser "quais valores passar". O que muda no desenho é que existe UM código-fonte, chamado uma vez por conta, com estado guardado fora das três contas que ele descreve. Percorra os passos: cada peça nova rastreia a um requisito da seção anterior.
- O módulo é chamado, não copiado. As três contas recebem a mesma origem de módulo Terraform. Uma correção de bug no módulo se propaga às três chamadas na próxima execução do pipeline — não existe um "esqueceram de aplicar em produção" possível para a lógica em si.
- O estado vive fora das contas que ele descreve. O bucket de estado mora na conta de Ferramentas, não em cada conta de ambiente. Isso reduz o raio de alcance de um erro: fechar a conta de homologação não leva junto o histórico de estado dela, e comprometer uma conta de ambiente não dá acesso ao estado das outras duas.
- O lock impede duas aplicações simultâneas no mesmo ambiente. Um item por ambiente na tabela trava o estado durante o apply. Duas execuções do pipeline disparadas ao mesmo tempo para o mesmo ambiente esperam a vez, em vez de escrever por cima uma da outra.
- A tag que chega em produção é a mesma que passou por homologação. Nenhum estágio reconstrói a imagem. O que muda de conta para conta é qual código de infraestrutura aponta para aquela tag — a tag em si é uma constante do início ao fim do pipeline.
- A única coisa que muda por chamada é o arquivo de variáveis. Contagem de tasks, CPU, memória e domínio vêm de `dev.tfvars`, `hml.tfvars` e `prod.tfvars`. Nenhum dos três arquivos contém um bloco de recurso — só valores, e é isso que torna o diff estrutural entre eles zero.
- Produção só recebe depois de homologação aprovar. A chamada 3 é condicionada ao sucesso da chamada 2 — e homologação, por ter a mesma forma de produção, é onde esse sucesso significa algo próximo do real.
- Ninguém aplica direto do laptop nestas três contas. A role que o pipeline assume em cada conta é distinta da role de operação humana do L43 — e é a única com permissão de escrita no estado. Um `terraform apply` local não encontra a trava nem a credencial para seguir adiante.
O que NÃO precisou mudar em relação ao L43
As duas OUs do L43 continuam as mesmas — Não-Produção agora com duas contas (dev e homologação) em vez de uma, Produção com a mesma conta de sempre. A SCP que impede exclusão destrutiva em produção não muda uma linha: ela protege a conta, e a conta continua sendo a mesma fronteira, não importa quantos módulos Terraform a apontem.
Como uma promoção acontece, ponta a ponta
Uma promoção completa passa por três aplicações do MESMO módulo, na ordem, com um portão de aprovação humana entre a penúltima e a última.
O evento que dispara a promoção não é o commit — é o estágio anterior ter terminado
A conta de Ferramentas escuta eventos de PIPELINE, não de repositório. Um commit novo inicia a execução inteira do zero (estágio 1); o evento abaixo é o que faz o estágio de produção começar depois de homologação — e só depois de homologação, nunca em paralelo.
Evento real emitido pelo EventBridge quando o estágio Homologação termina com sucesso — schema de "CodePipeline Stage Execution State Change" (Amazon CodePipeline User Guide, "Monitoring CodePipeline events"). A regra de promoção para o estágio Produção escuta exatamente este evento, filtrando por "stage": "Homologacao" e "state": "SUCCEEDED".
{
"version": "0",
"id": "5c2c7c39-2c8b-4a2e-9e2e-8f1e6b6f9a11",
"detail-type": "CodePipeline Stage Execution State Change",
"source": "aws.codepipeline",
"account": "555555555555",
"time": "2026-08-07T14:32:09Z",
"region": "us-east-1",
"resources": [
"arn:aws:codepipeline:us-east-1:555555555555:cadencia-promocao"
],
"detail": {
"pipeline": "cadencia-promocao",
"execution-id": "9f1c2e3a-71b4-4d5e-8c3a-1a2b3c4d5e6f",
"start-time": "2026-08-07T14:28:41.102Z",
"stage": "Homologacao",
"state": "SUCCEEDED",
"version": 12.0,
"pipeline-execution-attempt": 1.0
}
}
As decisões, e o que se perde em cada uma
📋 A Cadência, com seis desenvolvedores e três ambientes (dev, homologação, produção) em três contas AWS, precisa que uma alteração de infraestrutura seja aplicada nos três sem que uma pessoa precise lembrar de repetir o comando três vezes nem editar três pastas separadas.
Diretório por ambiente é mais arquivo, e é assim de propósito: cada diretório é uma declaração explícita de "isto é o ambiente X", com o próprio bloco de backend, a própria role e o próprio arquivo de variáveis. Um `terraform apply` rodado no diretório errado aponta para o backend errado e falha ao tentar assumir a role errada — o erro aparece antes de tocar em qualquer recurso. Isso resolve o requisito central deste laboratório, que é impedir divergência estrutural entre pastas, sem depender de disciplina humana.
Alt: Terraform workspace único — A documentação da própria HashiCorp desaconselha workspace para isolar ambientes que exigem credenciais e controle de acesso separados — que é exatamente o caso aqui, com uma conta AWS por ambiente. O risco concreto é esquecer `terraform workspace select prod` antes de um `apply`: o comando roda no workspace ativo, silenciosamente, sem perguntar "tem certeza".
Alt: Account Factory for Terraform (AFT) — É o caminho que a AWS oferece para provisionar CONTAS inteiras via Terraform, integrado ao Control Tower do L43. Resolve um problema anterior a este — criar a conta — não o deste laboratório, que é aplicar o MESMO módulo de aplicação dentro de contas que já existem. Vale a pena quando a Cadência estiver criando contas com frequência, não antes.
Alt: HCP Terraform (Terraform Cloud) com um workspace por ambiente — Resolve o mesmo problema com execução remota, aprovação por interface e variáveis por workspace geridas centralmente — e tira a trava e o backend das suas mãos. É um produto pago com uma conta a mais para administrar; legítimo quando o time cresce além do que três diretórios versionados aguentam sozinhos.
Alt: CloudFormation StackSets — Aplica o mesmo template em várias contas nativamente, sem backend próprio para gerenciar. Troca a linguagem inteira — YAML/JSON de CloudFormation em vez de HCL — e descartaria o módulo Terraform que a Cadência já tem do L55. Reescrever por reescrever não paga a conta aqui.
Dentro da decisão vencedora ainda existe uma escolha real, sem resposta universalmente certa: como o Terraform representa "três ambientes" dentro do próprio código. A tabela abaixo não tem coluna de vencedor — as duas linhas são válidas, e a Cadência escolheu a segunda porque credencial e conta já eram separadas pelo L43.
| Abordagem | Como funciona | O que ela isola | O que ela NÃO isola | Quando escolher |
|---|---|---|---|---|
| `terraform workspace` | um código-fonte, N estados sob o mesmo backend (`env:/<workspace>/<key>`) | o ARQUIVO de estado — cada workspace tem o seu | credencial: a MESMA role e a mesma conta aplicam em qualquer workspace ativo; esquecer `workspace select` aplica no ativo, sem aviso | ambientes de teste efêmeros, mesma conta, sem exigência de credencial separada |
| Diretório por ambiente (escolha deste laboratório) | um diretório por ambiente, cada um com seu próprio bloco de backend e provider | estado E credencial: cada diretório assume uma role diferente, aponta para uma conta diferente | nada de crítico — o custo é mais arquivo para manter em sincronia (mitigado pelo módulo único: só o diretório-raiz se repete, a lógica não) | ambientes que exigem conta e credencial separadas — é o caso de dev/hml/prod na AWS |
A citação que resolve a dúvida na hora da prova
A própria documentação da Terraform é explícita: workspaces "não são apropriados para decomposição de sistema ou para implantações que exigem credenciais e controles de acesso separados". Ambiente por CONTA AWS é exatamente essa exigência — por isso a resposta certa na certificação, quando o cenário envolve conta separada por ambiente, nunca é "use workspace".
Construir: o módulo, que não muda entre ambientes
O módulo abaixo é o único lugar onde a forma do serviço existe. Ele não sabe se está sendo chamado por dev, homologação ou produção — só recebe valores.
# modules/servico-cadencia/main.tf — chamado uma vez por conta, nunca copiado
#
# Este arquivo nao muda entre dev, homologacao e producao. O que muda entre os
# tres ambientes esta inteiro nos valores passados as variaveis abaixo — nunca
# num bloco de recurso a mais ou a menos.
resource "aws_ecs_task_definition" "servico" {
family = "cadencia-${var.nome_ambiente}"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memoria
execution_role_arn = var.execution_role_arn
task_role_arn = var.task_role_arn
container_definitions = jsonencode([{
name = "api"
image = "${var.repositorio_ecr}:${var.tag_imagem}" # a MESMA tag em todo ambiente
essential = true
portMappings = [{ containerPort = 8080, protocol = "tcp" }]
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval = 10
timeout = 5
retries = 3
startPeriod = 30
}
environment = [
{ name = "ASPNETCORE_URLS", value = "http://+:8080" },
{ name = "AMBIENTE", value = var.nome_ambiente },
]
secrets = [{
name = "ConnectionStrings__Padrao"
valueFrom = var.segredo_banco_arn
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/cadencia-${var.nome_ambiente}"
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "api"
}
}
}])
}
resource "aws_ecs_service" "servico" {
name = "cadencia-${var.nome_ambiente}"
cluster = var.cluster_arn
task_definition = aws_ecs_task_definition.servico.arn
desired_count = var.contagem_desejada # unica diferenca de capacidade entre ambientes
launch_type = "FARGATE"
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
deployment_circuit_breaker {
enable = true
rollback = true
}
network_configuration {
subnets = var.subnets_privadas
security_groups = [var.security_group_id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.servico.arn
container_name = "api"
container_port = 8080
}
}
resource "aws_lb_target_group" "servico" {
name = "cadencia-${var.nome_ambiente}"
port = 8080
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = "/health/ready"
interval = 15
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
matcher = "200"
}
deregistration_delay = 30
}
# O limiar do alarme MUDA por ambiente (menos ruido em dev, mais rigor em
# producao) — mas o BLOCO existe nos tres, e essa e a diferenca em relacao a
# arquitetura minima, onde o alarme so existia em uma pasta.
resource "aws_cloudwatch_metric_alarm" "erro_5xx_do_alvo" {
alarm_name = "cadencia-${var.nome_ambiente}-5xx-alvo"
namespace = "AWS/ApplicationELB"
metric_name = "HTTPCode_Target_5XX_Count"
statistic = "Sum"
period = 60
evaluation_periods = 2
threshold = var.alarme_5xx_limiar
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching"
dimensions = {
TargetGroup = aws_lb_target_group.servico.arn_suffix
}
alarm_actions = [var.topico_alerta_arn]
}
# modules/servico-cadencia/variables.tf — só o que realmente muda por conta
variable "nome_ambiente" {
type = string
description = "dev | hml | prod — entra no nome de todo recurso e na tag AMBIENTE"
}
variable "tag_imagem" {
type = string
description = "SHA do commit; a MESMA em dev, hml e prod dentro de uma promoção"
}
variable "contagem_desejada" {
type = number
description = "1 em dev; 2 em hml e prod — hml espelha prod, dev não precisa"
}
variable "cpu" {
type = number
}
variable "memoria" {
type = number
}
variable "alarme_5xx_limiar" {
type = number
description = "20 em dev (ruído tolerado), 10 em hml, 5 em prod (rigor máximo)"
}
# As quatro abaixo vêm do estado do L01/L43 daquela conta especifica — cada
# ambiente tem sua propria VPC, cluster e roles, porque cada um vive numa
# conta diferente. Nao ha valor compartilhado possivel aqui, so passado.
variable "cluster_arn" { type = string }
variable "vpc_id" { type = string }
variable "subnets_privadas" { type = list(string) }
variable "security_group_id" { type = string }
variable "execution_role_arn" { type = string }
variable "task_role_arn" { type = string }
variable "segredo_banco_arn" { type = string }
variable "topico_alerta_arn" { type = string }
variable "repositorio_ecr" { type = string }
variable "regiao" {
type = string
default = "us-east-1"
}
O alarme está no módulo, não numa pasta à parte
É a correção direta do achado da arquitetura mínima: lá, o alarme de 5xx só existia na pasta de homologação porque nasceu de um incidente ali. Aqui, o BLOCO do alarme está no módulo — existe nos três ambientes por construção. Só o `threshold` muda, via `alarme_5xx_limiar`, e mudar um limiar é decisão de ambiente; ter ou não ter o alarme nunca deveria ter sido.
Construir: a chamada por conta
Cada ambiente é um diretório com backend, provider e a chamada do módulo. O arquivo de dev abaixo está completo; o de homologação e o de produção têm a MESMA estrutura — só o diff importa, e ele está na sequência.
# ambientes/dev/main.tf — a UNICA coisa que muda entre este arquivo e o de
# producao sao os valores abaixo. Compare com ambientes/prod/main.tf: mesma
# forma, backend com key diferente, role diferente, variaveis diferentes.
terraform {
backend "s3" {
bucket = "cadencia-terraform-state-ferramentas" # conta 555555555555
key = "cadencia/dev/servico.tfstate"
region = "us-east-1"
dynamodb_table = "cadencia-terraform-lock"
encrypt = true
}
}
provider "aws" {
region = "us-east-1"
# O pipeline (rodando na conta de Ferramentas) assume esta role NA CONTA DE
# DEV para criar recursos la. O backend acima fica na propria conta de
# Ferramentas e nao precisa de assume_role — só os RECURSOS mudam de conta.
assume_role {
role_arn = "arn:aws:iam::222222222222:role/TerraformAplicaAmbiente"
session_name = "pipeline-dev"
}
}
module "servico" {
source = "../../modules/servico-cadencia"
nome_ambiente = "dev"
tag_imagem = var.tag_imagem
contagem_desejada = 1
cpu = 256
memoria = 512
alarme_5xx_limiar = 20
cluster_arn = data.aws_ecs_cluster.principal.arn
vpc_id = data.aws_vpc.principal.id
subnets_privadas = data.aws_subnets.privadas.ids
security_group_id = data.aws_security_group.tarefa.id
execution_role_arn = data.aws_iam_role.execucao.arn
task_role_arn = data.aws_iam_role.tarefa.arn
segredo_banco_arn = data.aws_secretsmanager_secret.banco.arn
topico_alerta_arn = data.aws_sns_topic.alertas.arn
repositorio_ecr = "555555555555.dkr.ecr.us-east-1.amazonaws.com/cadencia-api"
}
$ diff -u ambientes/dev/main.tf ambientes/prod/main.tf
--- ambientes/dev/main.tf
+++ ambientes/prod/main.tf
@@ -6,7 +6,7 @@
terraform {
backend "s3" {
bucket = "cadencia-terraform-state-ferramentas"
- key = "cadencia/dev/servico.tfstate"
+ key = "cadencia/prod/servico.tfstate"
region = "us-east-1"
dynamodb_table = "cadencia-terraform-lock"
encrypt = true
@@ -16,18 +16,18 @@
provider "aws" {
region = "us-east-1"
assume_role {
- role_arn = "arn:aws:iam::222222222222:role/TerraformAplicaAmbiente"
- session_name = "pipeline-dev"
+ role_arn = "arn:aws:iam::333333333333:role/TerraformAplicaAmbiente"
+ session_name = "pipeline-prod"
}
}
module "servico" {
source = "../../modules/servico-cadencia"
- nome_ambiente = "dev"
+ nome_ambiente = "prod"
tag_imagem = var.tag_imagem
- contagem_desejada = 1
- cpu = 256
- memoria = 512
- alarme_5xx_limiar = 20
+ contagem_desejada = 2
+ cpu = 512
+ memoria = 1024
+ alarme_5xx_limiar = 5
# Zero linhas de bloco de recurso adicionadas ou removidas. Toda a diferença
# está em VALOR — key do backend, conta assumida, nome do ambiente e as
# quatro variáveis de capacidade. É essa a prova de "diff estrutural zero"
# que a seção de implantação mede com a ferramenta em C# a seguir.
A role por conta é infraestrutura de conta, não do módulo de aplicação
A role `TerraformAplicaAmbiente` e a política de confiança dela vivem em `iam.tf`, dentro de cada diretório de ambiente — não em `modules/servico-cadencia`. Colocá-la no módulo criaria uma role nova a cada chamada, dentro da conta ERRADA: o módulo roda DEPOIS de a role já ter sido assumida, ele não pode criar a porta pela qual entrou.
# ambientes/dev/iam.tf — a role que a conta de Ferramentas pode assumir AQUI,
# e só aqui. Repetida (com o account_id do principal trocado) nas pastas de
# hml e prod — é infraestrutura de conta, não do módulo, por isso não entra
# em modules/servico-cadencia.
data "aws_iam_policy_document" "confianca_pipeline" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
# Só a role de execução do pipeline, na conta de Ferramentas — nunca a
# conta inteira. É a mesma lição de menor privilégio do L41, aplicada
# à fronteira entre contas em vez de à fronteira dentro de uma conta.
identifiers = ["arn:aws:iam::555555555555:role/PipelinePromocaoCadencia"]
}
# Sem isto, qualquer serviço com permissão de assumir role na conta de
# Ferramentas poderia se passar pelo pipeline. O external ID é o mesmo
# valor nos três ambientes — é o pipeline, não a distinção entre eles,
# que este condition confirma.
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.id_externo_pipeline]
}
}
}
resource "aws_iam_role" "terraform_aplica_ambiente" {
name = "TerraformAplicaAmbiente"
assume_role_policy = data.aws_iam_policy_document.confianca_pipeline.json
# 4 horas: o suficiente para o apply mais lento observado (RDS, quando este
# ambiente tiver um) mais folga; não o teto de 12h que a role permitiria.
max_session_duration = 14400
}
# Escopo por RECURSO com nome previsível, nunca por serviço inteiro. Uma
# policy "AmazonECS_FullAccess" nesta role tornaria o pipeline capaz de tocar
# clusters e serviços que este módulo nunca criou.
data "aws_iam_policy_document" "aplica_servico_cadencia" {
statement {
sid = "GerenciaRecursosDoServico"
effect = "Allow"
actions = [
"ecs:DescribeServices", "ecs:UpdateService", "ecs:RegisterTaskDefinition",
"elasticloadbalancing:DescribeTargetGroups", "elasticloadbalancing:ModifyTargetGroup",
"cloudwatch:PutMetricAlarm", "cloudwatch:DescribeAlarms",
]
resources = ["*"]
# As seis ações acima não têm variação de ARN que restrinja por "nome
# começa com cadencia-": describe/update de serviço ECS aceita o ARN do
# SERVIÇO, mas registrar uma nova revisão de task definition é, na API,
# uma ação de FAMÍLIA sem recurso anterior a apontar. Restringir por
# prefixo de nome aqui repetiria o erro exato que o L43 mediu — um
# wildcard de string que parece filtro e não é. A fronteira real está na
# SCP da OU (L43) e no fato de esta role só existir nesta conta.
}
}
resource "aws_iam_role_policy" "aplica_servico_cadencia" {
role = aws_iam_role.terraform_aplica_ambiente.id
policy = data.aws_iam_policy_document.aplica_servico_cadencia.json
}
Construir: a ferramenta que prova diff estrutural zero, em C#
Comparar dois arquivos `.tfvars` a olho prova que os VALORES diferem do jeito esperado. Não prova que a FORMA é igual — que nenhum bloco de recurso foi acrescentado numa cópia e esquecido na outra. Para isso, a comparação precisa ser sobre o plano, não sobre o código-fonte.
// VerificadorDrift/Program.cs — compara a FORMA de dois planos, não os valores
//
// Roda "terraform show -json" sobre um plano salvo de cada ambiente e extrai,
// por recurso, o tipo e o CONJUNTO DE CHAVES do bloco de valores — nunca o
// valor em si. Dois ambientes com contagem_desejada = 1 e = 2 têm o mesmo
// conjunto de chaves; só o valor mudou, e é exatamente isso que este
// verificador ACEITA. O que ele reprova é recurso presente num plano e
// ausente no outro, ou uma chave nova que só existe numa cópia.
using System.Text.Json;
if (args.Length != 2)
{
Console.Error.WriteLine("uso: VerificadorDrift <plano-a.json> <plano-b.json>");
return 2;
}
var chavesA = ExtrairAssinaturaEstrutural(args[0]);
var chavesB = ExtrairAssinaturaEstrutural(args[1]);
var somenteEmA = chavesA.Except(chavesB).OrderBy(x => x).ToList();
var somenteEmB = chavesB.Except(chavesA).OrderBy(x => x).ToList();
if (somenteEmA.Count == 0 && somenteEmB.Count == 0)
{
Console.WriteLine($"OK: diff estrutural zero entre {args[0]} e {args[1]} " +
$"({chavesA.Count} assinaturas comparadas)");
return 0;
}
Console.WriteLine($"DRIFT ESTRUTURAL: {somenteEmA.Count + somenteEmB.Count} diferença(s)");
foreach (var linha in somenteEmA) Console.WriteLine($" só em {args[0]}: {linha}");
foreach (var linha in somenteEmB) Console.WriteLine($" só em {args[1]}: {linha}");
return 1;
// Assinatura = "tipo_do_recurso.endereco#chave", uma por chave do bloco de
// valores planejados. Ignora os VALORES de propósito — só a forma importa.
static HashSet<string> ExtrairAssinaturaEstrutural(string caminhoPlano)
{
using var stream = File.OpenRead(caminhoPlano);
using var doc = JsonDocument.Parse(stream);
var assinaturas = new HashSet<string>();
var recursos = doc.RootElement
.GetProperty("planned_values")
.GetProperty("root_module")
.GetProperty("resources")
.EnumerateArray();
foreach (var recurso in recursos)
{
var tipo = recurso.GetProperty("type").GetString();
var endereco = recurso.GetProperty("address").GetString();
foreach (var chave in recurso.GetProperty("values").EnumerateObject())
{
assinaturas.Add($"{tipo}.{endereco}#{chave.Name}");
}
}
return assinaturas;
}
Por que comparar o PLANO, e não os arquivos `.tf`
Dois arquivos `.tf` podem ter texto diferente e gerar o MESMO plano — comentário a mais, ordem de argumento diferente, `variable` com `default` explícito ou implícito. E podem ter texto quase idêntico e gerar planos diferentes, se uma condicional depender de uma variável não visível no diff. O plano é a fonte da verdade sobre o que vai ser criado; o texto é só uma forma de chegar lá.
Implantar, e provar que as três chamadas concordam
Cinco medições. Nenhuma delas é "parece que os três ambientes estão parecidos" — cada uma tem comando e resultado que aprova ou reprova.
# provas.sh — cinco medições; nenhuma conclusão vem de "as três contas parecem iguais"
# ── Prova 1: o diff estrutural entre dev e prod é zero ───────────────────────
terraform -chdir=ambientes/dev plan -out=/tmp/plano-dev.tfplan
terraform -chdir=ambientes/dev show -json /tmp/plano-dev.tfplan > /tmp/plano-dev.json
terraform -chdir=ambientes/prod plan -out=/tmp/plano-prod.tfplan
terraform -chdir=ambientes/prod show -json /tmp/plano-prod.tfplan > /tmp/plano-prod.json
dotnet run --project VerificadorDrift -- /tmp/plano-dev.json /tmp/plano-prod.json
# Esperado: "OK: diff estrutural zero". Qualquer linha "só em" reprova a prova.
# ── Prova 2: a mesma tag de imagem está nas três contas ──────────────────────
for ambiente in dev hml prod; do
aws ecs describe-task-definition \
--task-definition "cadencia-${ambiente}" --profile "${ambiente}" \
--query 'taskDefinition.containerDefinitions[0].image' --output text
done
# Esperado: as três linhas terminam no MESMO SHA depois de uma promoção completa.
# ── Prova 3: aplicar em produção com a credencial de dev falha ───────────────
AWS_PROFILE=dev terraform -chdir=ambientes/prod plan
# Esperado: erro ao assumir arn:...:333333333333:role/TerraformAplicaAmbiente —
# a role de dev não está na lista de principals confiáveis da role de prod.
# ── Prova 4: o lock impede dois apply simultâneos no mesmo ambiente ──────────
( terraform -chdir=ambientes/hml apply -auto-approve & )
sleep 2
terraform -chdir=ambientes/hml apply -auto-approve
# Esperado: o segundo comando espera ou falha com "Error acquiring the state
# lock", citando o mesmo LockID — nunca os dois aplicam ao mesmo tempo.
# ── Prova 5: o estado de cada ambiente está isolado do das outras contas ─────
aws s3api get-object --bucket cadencia-terraform-state-ferramentas \
--key cadencia/dev/servico.tfstate /tmp/estado-dev.json --profile ferramentas
python3 -c "import json; d=json.load(open('/tmp/estado-dev.json')); \
print([r['type'] for r in d['resources'] if 'prod' in str(r.get('instances'))])"
# Esperado: lista vazia — nenhum recurso de produção aparece no estado de dev.
As instâncias descartáveis da prova 4 não devem apontar para dado real
O apply simultâneo da prova 4 é sobre `ambientes/hml`, de propósito — nunca rode esse teste contra produção. O objetivo é observar a mensagem de erro do lock, não testar concorrência contra o ambiente que o cliente usa.
Quebrar de propósito: três falhas e o diagnóstico
As três falhas abaixo foram provocadas de propósito, uma de cada vez, num ambiente de teste — nunca em homologação ou produção reais.
| Falha provocada | Sintoma | Onde olhar | Correção |
|---|---|---|---|
| Editar um recurso direto no diretório `ambientes/prod` em vez de mudar uma variável | a próxima chamada do `VerificadorDrift` acusa uma assinatura só em produção | o diff do commit que tocou `ambientes/prod/main.tf` fora de uma linha de variável | reverter o recurso solto; se o ambiente precisa de algo que os outros não têm, isso vira variável no módulo, com valor `null`/`false` nos demais — nunca bloco a mais numa cópia |
| Matar o processo do `terraform apply` no meio (Ctrl+C) e deixar o lock preso | próxima execução falha com "Error acquiring the state lock", citando um LockID específico | `aws dynamodb get-item` na tabela de lock, pelo LockID do erro | confirmar que ninguém mais está aplicando naquele ambiente, então `terraform force-unlock <LockID>` — nunca apagar o item direto na tabela |
| Editar `prod.tfvars` copiando de `hml.tfvars` e esquecer de trocar `tag_imagem` | o pipeline reporta sucesso, mas produção continua rodando a tag anterior | comparar `taskDefinition.containerDefinitions[0].image` entre hml e prod após a promoção | o pipeline deve LER a tag do artefato aprovado no estágio anterior, nunca de um valor editado à mão no `.tfvars` de produção |
O lock preso tenta o atalho mais perigoso deste laboratório
`terraform force-unlock` sem confirmar que o apply anterior realmente terminou pode liberar a trava enquanto outra execução ainda está escrevendo — e as duas gravações concorrentes corrompem o mesmo arquivo de estado. Confirme no console do CodePipeline ou no processo local que não há apply em andamento antes de forçar.
Um time usa `terraform workspace` para dev, hml e prod, todos sob a mesma conta AWS e a mesma credencial de apply. Um desenvolvedor esquece de trocar de workspace antes de rodar `terraform apply`. O que acontece?
Segurança: o que protege a promoção entre contas
Centralizar o estado e a role de apply numa conta de Ferramentas resolve o problema deste laboratório e abre um novo: quem compromete a conta de Ferramentas ganha um caminho de escrita para as três contas de ambiente.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Credencial do pipeline comprometida alcança produção diretamente | baixa | alto | role por ambiente com `ExternalId` e principal restrito à role do pipeline, nunca à conta inteira | CloudTrail em `AssumeRole` na conta de produção, fora da janela esperada do pipeline | revogar a role de confiança na conta afetada; rotacionar `ExternalId` |
| Estado com segredo em texto plano dentro do `.tfstate` | média | alto | segredo sempre por referência de ARN (`segredo_banco_arn`), nunca por valor em variável | busca por padrão de credencial nos objetos do bucket de estado | rotacionar o segredo exposto; o valor no state não é mais válido, mas precisa ser purgado das versões antigas do bucket |
| Bucket de estado exposto publicamente por engano | baixa | crítico | bloqueio de acesso público na conta, política de bucket restrita à role do pipeline | AWS Config: regra de bucket público; Access Analyzer | corrigir a política, considerar todo o histórico de estado como comprometido, rotacionar segredos referenciados |
| Tabela de lock apagada ou alterada por engano | baixa | médio | menor privilégio na role do pipeline: `dynamodb:PutItem`/`DeleteItem` restritos ao item por ambiente | CloudTrail em `DeleteTable`/`UpdateItem` fora do pipeline | recriar a tabela; um lock perdido não corrompe o state existente, só permite concorrência até ser recriada |
| Alguém aplica direto do laptop, contornando o pipeline | média (antes deste laboratório), baixa (depois) | alto | só a role do pipeline consta como principal confiável nas três roles `TerraformAplicaAmbiente` | CloudTrail em `AssumeRole` com principal humano nas contas de ambiente | revogar o acesso que permitiu o assume; investigar como a permissão apareceu |
O `*` que aparece na política do pipeline, e por que ele se justifica
`ecs:RegisterTaskDefinition` não aceita ARN de recurso específico — é uma ação de FAMÍLIA sem revisão anterior para apontar, então `Resource: "*"` é a única forma de concedê-la. A fronteira real não vem dessa linha: vem de esta role só existir dentro de uma conta, e a SCP do L43 continuar vetando as ações destrutivas mesmo que esta policy algum dia ficasse generosa demais.
Observabilidade: as perguntas que a promoção precisa responder
O painel de uma promoção multi-conta responde a uma pergunta que um painel de aplicação única não precisa responder: as três contas estão de fato de acordo?
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| A tag em produção é a mesma que passou por homologação? | comparar `image` das três task definitions ativas | promoção incompleta ou rebuild indevido em algum estágio | qualquer divergência é alarme, não é gradual |
| Há drift estrutural entre as chamadas do módulo? | saída do `VerificadorDrift` no CI | alguém editou um ambiente direto, fora do módulo | zero diferenças; qualquer uma reprova o pipeline |
| Quanto tempo uma promoção leva, de commit a produção? | duração total das execuções do pipeline | gargalo em algum estágio específico | acima do dobro da mediana das últimas 10 execuções |
| Alguém aplicou fora do pipeline? | CloudTrail: `AssumeRole` em `TerraformAplicaAmbiente` com principal humano | contorno do controle central | qualquer ocorrência |
| O lock ficou preso? | idade do item mais antigo na tabela de lock | apply interrompido sem liberar a trava | acima de 15 min, tempo maior que qualquer apply observado |
| Homologação continua com a mesma forma de produção? | diff automatizado entre `hml.tfvars` e `prod.tfvars` | alguém reduziu capacidade em homologação para economizar, sem perceber que isso invalida o teste | qualquer diferença fora dos quatro campos de capacidade documentados |
A ausência de alarme é o sinal, não o silêncio
Não existe métrica nativa da AWS chamada "drift estrutural" — ela só existe porque este laboratório construiu o `VerificadorDrift` e o rodou no pipeline. Sem essa etapa, o painel fica cego exatamente para o risco que o laboratório existe para resolver: tudo parece saudável até alguém comparar as pastas manualmente, como aconteceu com a Cadência antes deste módulo.
Escala: 3 contas, 10, 50, e a falha de uma conta
| Volume | O que acontece com a promoção | O que passa a doer | O que fazer |
|---|---|---|---|
| 3 contas (este laboratório) | um módulo, três diretórios, três roles | nada; é o cenário construído aqui | nada |
| 10 contas (múltiplos times, cada um com dev/hml/prod) | trinta diretórios, cada um ainda chamando o mesmo módulo | trinta arquivos de backend a manter em sincronia manualmente | gerar os diretórios a partir de um template (Terragrunt ou script próprio), nunca copiar à mão |
| 50 contas (plataforma multi-time, aproxima-se do L98) | centenas de chamadas do módulo | nenhum humano revisa todas as promoções; a aprovação manual do estágio 5 vira gargalo | aprovação automática por política (testes de aceite como portão, não pessoa) com auditoria pós-fato |
| Falha da conta de Ferramentas | nenhuma das três contas de ambiente é afetada diretamente — os serviços continuam no ar | nenhuma promoção NOVA pode acontecer até o estado voltar a ficar acessível | versionamento do bucket de estado e réplica entre regiões, se a janela de indisponibilidade aceitável for curta |
| Falha de uma conta de ambiente (ex.: homologação) | dev e produção continuam funcionando; só as promoções que passam por homologação ficam bloqueadas | o portão de qualidade fica indisponível, não só o ambiente | nenhuma ação imediata além de restaurar a conta — é o comportamento correto: o gate travando é melhor que o gate sendo pulado |
O gargalo que só aparece com muitos times
Com uma equipe, aprovação manual no estágio de produção custa segundos de atenção. Com dez times promovendo várias vezes por dia, a mesma aprovação manual vira fila — e a resposta sob pressão tende a ser "aprovar sem olhar", que é pior que não ter aprovação nenhuma, porque dá falsa sensação de revisão. É exatamente o problema que o L98 aborda com cota e chargeback por time.
Custo: o que este laboratório acrescenta à fatura
O que este laboratório introduz de infraestrutura nova — bucket de estado e tabela de lock — custa centavos por mês. O que ele multiplica é o que já existia: a aplicação, agora rodando em três contas em vez de uma.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo (só dev e prod, sem hml) | 2 contas de ambiente | estado e lock triviais; capacidade de aplicação em 2 contas | baixa | nenhuma; a fatura relevante é a do L01, duplicada uma vez |
| Este laboratório (dev, hml, prod) | 3 contas de ambiente | hml com a MESMA capacidade de prod — é o dobro da capacidade de aplicação paga, não o triplo | previsível | dev pode escalar a zero fora do horário de trabalho, sem afetar hml nem prod |
| Múltiplos times (aproxima-se do L98) | 10+ contas de ambiente | estado e lock continuam desprezíveis; a capacidade de aplicação multiplicada por time | linear com número de times | rateio por conta (tagging já resolvido pelo L43) para chargeback — a pergunta de custo muda de "quanto" para "de quem" |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Bucket de estado (S3) | GB-mês armazenado + requisições | versionamento ligado guarda todo histórico; sem ciclo de vida, cresce para sempre |
| Tabela de lock (DynamoDB) | capacidade provisionada ou sob demanda | sob demanda é a escolha certa aqui: o volume de locks é baixo e irregular |
| Capacidade de aplicação em homologação | igual à de produção, de propósito | é o único ambiente cujo custo NÃO deve ser otimizado para baixo — a paridade é o que compra o teste válido |
| Capacidade de aplicação em dev | menor CPU, memória e contagem | é o lugar certo para agendar desligamento fora do horário comercial |
| Transferência entre contas | geralmente não aplicável aqui | o pipeline aplica DENTRO de cada conta; não há tráfego de dado cruzando fronteira de conta neste desenho |
O ganho de custo que não aparece na fatura da AWS
O tempo gasto comparando pastas manualmente para achar onde elas divergiram — o que levou a Cadência horas na história de abertura — desaparece. O custo que este laboratório ataca não é o de infraestrutura: é o de um desenvolvedor sênior gastando uma tarde fazendo diff manual entre três diretórios.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | módulo único, diff estrutural provado automaticamente a cada promoção | geração dos diretórios de ambiente ainda é manual, não escala além de poucas dezenas de contas | template/gerador de diretório de ambiente (Terragrunt ou script próprio) | média |
| Segurança | role por conta com principal restrito e `ExternalId`, estado fora das contas que descreve | a conta de Ferramentas é ponto único de comprometimento com alcance às três contas | MFA/condição adicional na role de pipeline; alarme de `AssumeRole` fora da janela esperada | alta |
| Confiabilidade | lock por ambiente impede escrita concorrente no mesmo estado | falha da conta de Ferramentas bloqueia toda promoção nova, mesmo sem afetar o que já roda | versionamento e réplica do bucket de estado entre regiões | média |
| Eficiência de performance | homologação com a forma exata de produção torna o teste representativo | nenhuma automação hoje detecta se `hml.tfvars` deixou de espelhar `prod.tfvars` | checagem automatizada de paridade entre os dois arquivos, além do diff estrutural do módulo | média |
| Otimização de custos | dev dimensionado abaixo de hml/prod, sem perder a garantia estrutural | dev roda 24/7 mesmo fora do horário de uso do time | agendamento de `desired_count = 0` fora do horário comercial em dev | baixa |
| Sustentabilidade | capacidade de dev já reduzida ao mínimo viável | nenhuma | nenhuma ação adicional necessária neste estágio | baixa |
Evolução em níveis: de uma pasta copiada a uma plataforma
A terceira arquitetura não é um desenho: é a resposta a QUANDO três diretórios deixam de bastar. Cada nível resolve um risco e compra outro — e é a segunda coluna que raramente se escreve.
Um ambiente só, um diretório Terraform, aplicado à mão. É onde qualquer projeto começa, e continua legítimo enquanto não existir segundo ambiente.Dev, homologação e produção como cópias independentes do mesmo diretório inicial, cada uma editada separadamente. É onde a Cadência estava no início deste laboratório.Módulo único, um diretório por ambiente chamando o mesmo módulo, estado remoto isolado, promoção da mesma tag por um pipeline com aprovação manual antes de produção.Geração automatizada dos diretórios de ambiente (Terragrunt ou gerador próprio), múltiplos módulos compostos — rede, dado e aplicação versionados e aplicados separadamente — para dezenas de contas.Catálogo de módulos aprovados centralmente, provisionamento self-service via Service Catalog ou AFT, cota e chargeback por time — é o L98.O histórico de planos e aplies vira dado: cada `terraform plan` gerado no pipeline já é JSON estruturado, guardado ao lado do resultado (sucesso, rollback, incidente aberto depois). Um modelo treinado sobre esse histórico classifica o RISCO ESTRUTURAL de um plano novo — recursos destrutivos, mudança fora do padrão histórico daquele módulo — e sinaliza para revisão humana extra antes da aprovação automática, em vez de tratar toda mudança como igualmente arriscada.A ordem não é negociável, e o motivo é concreto
O nível 6 depende de um histórico de planos estruturados — que só existe porque o nível 3 já produz `terraform show -json` a cada execução do pipeline. Sem o módulo único e o estado centralizado, não haveria onde coletar esse dado de forma consistente entre as três contas.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. "Este plano tem a mesma forma que o outro?" é uma pergunta com resposta determinística — conjunto de chaves igual ou não — e é exatamente o que o `VerificadorDrift` já faz sem nenhum modelo.
Há um lugar onde IA acrescentaria valor real, e ele é o nível 6 da escada anterior: graduar o RISCO de uma mudança, não confirmar sua FORMA. São perguntas diferentes — a primeira é comparação exata, a segunda é julgamento sobre um padrão histórico.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | sinalizar plano com risco estrutural alto (recurso destrutivo, mudança fora do padrão) para revisão extra, em vez de aprovação manual genérica para toda mudança |
| Por que uma regra não bastaria? | uma regra simples cobre o caso óbvio: "todo plano com `destroy` de RDS ou de conta exige dois aprovadores". Isso já resolveria a maior parte do risco sem nenhum modelo — comece por ela |
| De onde viriam os dados? | o `terraform show -json` de cada plan, já produzido pelo pipeline deste laboratório, mais o resultado de cada promoção (sucesso, rollback, incidente aberto depois) do L54 |
| Qual o risco? | aprender de poucos incidentes e liberar uma mudança destrutiva por não reconhecer o padrão, ou represar mudanças seguras — exige avaliação com dado retido e um caminho manual sempre disponível |
| Por que não agora? | a Cadência tem três contas e um histórico de promoções que cabe numa planilha. Modelo sobre dezenas de exemplos é superstição com aparência de estatística — este é candidato ao L98, quando o número de times e de promoções crescer o bastante para o padrão existir |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "olhar o `.tfvars` de produção e dizer se está certo" substitui uma comparação EXATA — que é o que o `VerificadorDrift` faz — por um julgamento probabilístico, exatamente na camada em que exatidão é o requisito. Onde existe comparação determinística disponível, IA só acrescenta latência e a chance de aprovar com confiança algo estruturalmente diferente.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta |
|---|---|---|---|---|
| Copiar a pasta Terraform para "criar" um ambiente novo | é a forma mais rápida de ter algo funcionando na mesma tarde | os dois códigos começam iguais e divergem estruturalmente a cada edição futura | diff entre pastas revela bloco de recurso presente numa e ausente na outra | módulo único, chamado a partir de um diretório novo que só declara valores |
| Usar `terraform workspace` pensando que resolve isolamento de conta | parece mais simples que manter múltiplos diretórios e backends | isola só o arquivo de estado; a MESMA credencial e a mesma conta continuam alcançando todos os workspaces, então o erro de aplicar no ambiente errado continua possível | alguém aplica em `prod` estando, sem perceber, no workspace de produção com a credencial de dev | diretório por ambiente, cada um com backend e role próprios |
| Aplicar direto do laptop "só dessa vez" para adiantar uma promoção | o pipeline está lento ou travado e a mudança parece urgente | contorna a única barreira que impede escrita de fora da role auditada | CloudTrail mostra `AssumeRole` humano na role de produção, fora do padrão do pipeline | nenhuma role de ambiente confia em principal humano — só na role do pipeline |
| Deixar homologação com capacidade menor que produção para economizar | parece desperdício pagar duas contas do mesmo tamanho | o teste em homologação deixa de ser representativo; um problema de capacidade só aparece em produção, exatamente onde custa mais | incidente em produção que o teste de carga em homologação não reproduziu | homologação espelha produção em contagem, CPU e memória; só dev é dimensionado para baixo |
| Corrigir drift editando o estado manualmente (`terraform state rm`/`import` ad hoc) | parece mais rápido que investigar por que o plano e a realidade divergiram | resolve o SINTOMA no estado sem corrigir a CAUSA no código; o próximo apply pode reintroduzir a mesma divergência | o mesmo drift reaparece algumas execuções depois, e ninguém lembra por quê | investigar a causa (recurso criado fora do Terraform? módulo desatualizado?) antes de tocar no estado |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| `VerificadorDrift` acusa diferença entre dev e prod | edição direta num diretório de ambiente, fora do módulo | compare o `plan` reprovado com o commit mais recente naquele diretório | histórico de commits de `ambientes/<ambiente>/main.tf` | reverter a edição solta; se é intencional, vira variável nova no módulo, com valor padrão nos demais ambientes |
| `Error acquiring the state lock` | apply anterior travado ou interrompido sem liberar o lock | confirme se há execução em andamento no CodePipeline para aquele ambiente | console do CodePipeline; `aws dynamodb get-item` na tabela de lock | se não há execução ativa, `terraform force-unlock` com o LockID do erro |
| Apply em produção falha ao assumir a role | a credencial usada não é a role do pipeline na conta de Ferramentas | confira qual principal está tentando o `AssumeRole` no erro | mensagem de erro do provider; política de confiança da role de destino | rodar a partir do estágio de produção do pipeline, nunca com credencial local |
| Task definition em produção não reflete a tag esperada após promoção | `prod.tfvars` com `tag_imagem` desatualizada, editada manualmente | compare a tag do artefato aprovado no estágio de homologação com a aplicada em produção | `describe-task-definition` nas duas contas | pipeline deve propagar a tag automaticamente entre estágios, nunca por edição manual do `.tfvars` |
| Homologação passa nos testes e produção falha do mesmo jeito | `hml.tfvars` deixou de espelhar `prod.tfvars` em capacidade | diff entre os dois arquivos de variáveis, além do diff estrutural do módulo | os quatro campos de capacidade: contagem, CPU, memória, limiar de alarme | restaurar a paridade entre homologação e produção nos campos de capacidade |
A pergunta que resolve metade destes casos
Antes de tocar em qualquer arquivo, pergunte: isto é diferença de VALOR (esperada, está no `.tfvars`) ou diferença de FORMA (inesperada, é o que o `VerificadorDrift` reprova)? As duas produzem sintomas parecidos — "os ambientes não batem" — mas pedem correção em lugares diferentes.
Limpeza: o que desfazer três ambientes não leva junto
O `destroy` tem de rodar três vezes — uma por diretório — e mesmo assim alguns recursos sobrevivem, de propósito, porque não pertencem a nenhum dos três ambientes.
# limpar.sh — o destroy tem de rodar TRÊS vezes, uma por diretório
for ambiente in dev hml prod; do
echo "destruindo ${ambiente}..."
terraform -chdir="ambientes/${ambiente}" destroy -auto-approve
done
# 1. O BUCKET DE ESTADO sobrevive aos três destroy — ele não pertence a
# nenhum dos três, mora na conta de Ferramentas e guarda o HISTÓRICO.
# Versionamento ligado significa que apagar o objeto não apaga as versões
# antigas; elas continuam cobrando armazenamento até uma política de ciclo
# de vida do bucket as expirar.
aws s3api list-object-versions \
--bucket cadencia-terraform-state-ferramentas --prefix cadencia/ \
--query 'Versions[].Key' --output table --profile ferramentas
# 2. A TABELA DE LOCK fica vazia, mas continua existindo e cobrando por
# capacidade provisionada, se não estiver em modo sob demanda.
aws dynamodb describe-table --table-name cadencia-terraform-lock \
--profile ferramentas --query 'Table.BillingModeSummary.BillingMode'
# 3. AS ROLES TerraformAplicaAmbiente nas três contas não saem com o destroy
# do módulo de aplicação — elas foram criadas por um Terraform separado
# (iam.tf, por conta). Uma role órfã com confiança na conta de Ferramentas
# é superfície de ataque que ninguém está mais olhando.
for ambiente in dev hml prod; do
aws iam get-role --role-name TerraformAplicaAmbiente --profile "${ambiente}" \
--query 'Role.Arn' --output text
done
# 4. O REPOSITÓRIO ECR não pertence a nenhum dos três ambientes — ele é
# compartilhado (L54) e sobrevive de propósito, mesmo destruindo os três.
# 5. Prova final: nenhum objeto de estado sem o destroy correspondente.
aws s3 ls s3://cadencia-terraform-state-ferramentas/cadencia/ --recursive \
--profile ferramentas
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Serviço, task definition e alvo de cada ambiente | sim, por diretório | não | pertencem ao módulo, chamado uma vez por diretório |
| Bucket de estado (conta de Ferramentas) | não | sim, GB-mês por versão retida | não pertence a nenhum dos três ambientes; guarda o histórico dos três |
| Tabela de lock (DynamoDB) | não | sim, se capacidade provisionada | infraestrutura da conta de Ferramentas, fora do ciclo dos módulos de aplicação |
| Roles `TerraformAplicaAmbiente` nas três contas | não | não, mas é superfície de ataque | criadas por `iam.tf`, separado do destroy do módulo de aplicação |
| Repositório ECR e imagens | não | sim, GB-mês | compartilhado com o L54; sobrevive de propósito à destruição de qualquer ambiente |
| A própria conta de homologação (se for encerrada) | não | sim, até o fechamento completar | fechamento de conta-membro segue processo e prazo próprios da AWS — é o procedimento do L43, não deste módulo |
Versionamento do bucket de estado significa que "apagar" não é imediato
Com versionamento ligado, remover o objeto `cadencia/hml/servico.tfstate` cria um marcador de exclusão — as versões anteriores continuam armazenadas e cobrando até uma política de ciclo de vida do bucket as expirar, ou até alguém as apagar explicitamente com o ID de cada versão.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Pasta copiada diverge estruturalmente com o tempo | módulo único, chamado por conta | uma correção no módulo se propaga às três chamadas na próxima execução — não existe cópia para esquecer |
| Workspace não isola credencial nem conta | diretório por ambiente, com backend e role próprios | errar de ambiente falha ao assumir a role errada, antes de tocar em qualquer recurso |
| Estado em laptop, sem trava | backend S3 remoto com lock DynamoDB, numa conta separada | reduz o raio de alcance e impede escrita concorrente no mesmo arquivo de estado |
| Teste em homologação não reproduz produção | paridade de capacidade entre hml e prod | é a paridade entre o ÚLTIMO ambiente e produção que valida o teste, não a paridade entre todos |
| Rebuild por ambiente invalida o que foi testado | a mesma tag promovida pelos três estágios | o binário que passou pelos testes é, byte a byte, o que chega a produção |
| Ninguém percebe drift até comparar manualmente | `VerificadorDrift` no pipeline | compara a FORMA do plano, não o texto do código — pega divergência que um diff de arquivo não pega |
| Apply direto do laptop contorna o controle | role de ambiente só confia na role do pipeline | a barreira é técnica, não um pedido para "sempre usar o pipeline" |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Bloco de recurso esquecido numa cópia | módulo único chamado por conta | variável de capacidade mal preenchida — isso é diferença de VALOR, esperada |
| Aplicar no ambiente errado | diretório com backend e role próprios | aplicar o valor errado DENTRO do ambiente certo — isso ainda é erro humano no `.tfvars` |
| Dois apply concorrentes no mesmo estado | lock por ambiente (DynamoDB) | dois apply concorrentes em ambientes DIFERENTES — esses não competem pelo mesmo lock, e não deveriam |
| Teste em homologação não representativo | paridade de capacidade com produção | diferença de DADO entre os ambientes — capacidade igual não implica volume de tráfego igual |
| Rebuild indevido entre estágios | a mesma tag propagada pelo pipeline | imagem vulnerável que passou pelos três estágios antes de a CVE ser publicada — isso é o L03/L54 |
- O pipeline constrói a imagem uma vez e a envia ao ECR com tag = SHA do commit.
- O estágio de dev assume a role da conta 222222222222 e aplica dev.tfvars.
- Testes automatizados rodam contra dev; falha aqui interrompe tudo, barato.
- O estágio de homologação assume a role da conta 444444444444 e aplica hml.tfvars — mesma capacidade de produção.
- Um humano aprova, depois de conferir os testes de aceite em homologação.
- O EventBridge emite o evento de estágio concluído, que dispara o estágio seguinte.
- O estágio de produção assume a role da conta 333333333333 e aplica prod.tfvars — a mesma tag, sem rebuild.
- O pipeline observa o alarme de 5xx por 10 minutos antes de fechar como sucesso.
- O VerificadorDrift roda sobre os três planos e confirma diff estrutural zero entre eles.
Perguntas frequentes
❓ Por que copiar a pasta Terraform de dev para criar o ambiente de homologação causa drift?
❓ Terraform workspace resolve o isolamento entre ambientes com contas AWS separadas?
❓ Por que o ambiente de homologação precisa ter a mesma capacidade que produção?
❓ Onde deve morar o estado remoto do Terraform de cada ambiente?
❓ Como garantir que a imagem testada em homologação é exatamente a publicada em produção?
❓ O que fazer quando o terraform apply trava com "Error acquiring the state lock"?
❓ Por que a role que aplica infraestrutura em cada conta não deve confiar em usuário humano?
❓ Quando vale a pena trocar diretório manual por ambiente por um gerador como o Terragrunt?
Fixando
Por que o `VerificadorDrift` deste laboratório compara o `terraform show -json` do plano de cada ambiente, em vez de comparar o texto dos arquivos `.tf`?
A Cadência reduz a contagem de tasks de homologação de 2 para 1, para economizar, mantendo produção em 2. Duas semanas depois, um incidente de capacidade acontece em produção sob pico, sem ter aparecido em nenhum teste de homologação. Qual é a explicação mais provável?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L43 (Organizations, OUs, SCP, Identity Center) e L55 (módulo Terraform, estado remoto, drift) concluídos |
| Conhecimentos adquiridos | por que pasta copiada diverge estruturalmente; por que workspace não isola conta nem credencial; paridade entre o último ambiente e produção; promoção de artefato sem rebuild; diff estrutural provado por ferramenta, não por inspeção visual |
| Limitação que fica | a geração dos diretórios de ambiente ainda é manual — não escala sem esforço além de poucas dezenas de contas; e a aprovação de produção continua sendo um humano só |
| Próximo laboratório recomendado | L98 — plataforma de IA multi-time com cota e chargeback. Reaproveita as três contas e o módulo único deste laboratório, multiplicando por time em vez de por ambiente |
| Também habilitado por este módulo | qualquer laboratório que precise de ambiente representativo antes de produção herda a paridade hml↔prod construída aqui, sem reconstruir a fronteira de conta do L43 |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: Terraform — State: Workspaces — o aviso explícito de que workspaces não são apropriados para decomposição de sistema nem para implantações que exigem credenciais e controles de acesso separados, que é a base da decisão de usar diretório por ambiente em vez de workspace; Terraform — Backend Type: s3 — a estrutura de `key`, o mecanismo de lock (a documentação atual recomenda o lock nativo do S3 via `use_lockfile`, com a tabela DynamoDB tratada como caminho legado; este laboratório segue a tabela DynamoDB por ser o mecanismo que o L55 desta série ensina, com a alternativa nativa citada no callout da seção de decisões); e Amazon CodePipeline — Monitoring CodePipeline events — o schema exato do evento "CodePipeline Stage Execution State Change" usado na seção de fluxo ponta a ponta. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
Os nomes de conta, IDs de conta (222222222222, 333333333333, 444444444444, 555555555555) e a estrutura exata de OUs são os do exemplo didático deste laboratório e do L43 — substitua pelos IDs reais da sua organização. O texto exato da política de confiança e os limites de `max_session_duration` devem ser revisados contra a política de segurança da sua empresa, não copiados como estão. E o valor de `alarme_5xx_limiar` por ambiente é um ponto de partida razoável para o volume da Cadência — meça o seu antes de copiar os números.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…