Lab 10 — Voltar de um desastre, com ensaio
O problema, e a empresa que o tem
A Cadência é a mesma equipe de duas pessoas do L01 e do L03: uma API de pedidos em .NET 8, ECS Fargate, RDS PostgreSQL em duas zonas, e desde o L07 uma réplica de leitura com o pool ajustado. Nada nessa lista está errado. O que está errado é uma frase no documento de arquitetura: RTO 30 min · RPO 5 min. Ninguém nunca restaurou nada.
Numa terça, às 14:07:12, um script de correção de dados rodou sem a cláusula de filtro e apagou 41.318 linhas de itens de pedido. As duas peças em que a equipe confiava não ajudaram, e é importante entender por quê: a implantação em duas zonas aplicou o mesmo DELETE na cópia síncrona, no mesmo commit, e a réplica de leitura o repetiu segundos depois. As duas fazem exatamente o que prometem — elas protegem a disponibilidade do banco, não o conteúdo dele.
A operação das lojas descobriu às 14:41. Do dano à conclusão da recuperação passaram 77 minutos, e 69 minutos de escrita legítima se perderam. Os dois números do documento estavam errados por fatores de 2,5 e de 14 — e nenhum deles por má-fé. Este laboratório existe para produzir os números medidos uma vez, com cronômetro, e para mostrar que as duas maiores parcelas do RTO não estão dentro da AWS.
A frase que este laboratório existe para tornar impossível
"Temos backup." Backup que nunca foi restaurado não é backup: é uma suposição com custo de armazenamento. E a suposição costuma estar errada nos dois eixos — a janela de retenção é menor do que se pensa, porque ninguém a escolheu, e o RTO é maior, porque ninguém contou o tempo de repontar a aplicação para um endpoint que só existe depois da restauração.
O que este laboratório NÃO é
Não é recuperação de desastre entre regiões. Aqui o incidente é de dado, dentro de uma região, e a resposta é restauração. Perder uma região inteira é outro problema, com outra topologia e outro custo permanente, e é o L58 — que depende deste, porque não existe estratégia entre regiões antes de a restauração local ter um número.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, e não com a sensação de ter entendido.
- Explicar recuperação para um instante como snapshot mais log reaplicado, e derivar daí por que a janela de retenção limita quão atrás você chega.
- Distinguir o que a cadência de cinco minutos do log limita (a proximidade do presente) do que ela não limita (a precisão de um instante passado).
- Medir o piso do RPO na sua conta comparando o instante mais recente restaurável com o relógio, em três amostras.
- Provar com dois comandos que a restauração cria uma instância nova com endpoint novo, e que não existe restaurar sobre a instância atual.
- Cronometrar um corte completo e decompor o RTO em detectar, decidir, restaurar, repontar e validar.
- Calcular o RPO real a partir do instante alvo e do instante do corte, e mostrar qual das duas parcelas domina.
- Demonstrar que a implantação em duas zonas não desfaz um DELETE, provocando um failover e reencontrando a linha ausente.
- Diferenciar snapshot automático de snapshot manual pelo comportamento na exclusão da instância, com a prova executada.
- Configurar cofre do AWS Backup com trava, escolhendo entre governança e conformidade com consciência do que cada modo impede.
- Reduzir o RPO medido sem mudar nada na AWS, congelando a escrita no momento da detecção.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| RTO e RPO como grandezas distintas | SAA-C03, SOA-C02 | os dois são medidos e decompostos no ensaio | que RTO é tempo até voltar e RPO é dado perdido, e que reduzir um não reduz o outro |
| Mecanismo do PITR | SAA-C03, SOA-C02, DVA-C02 | snapshot diário mais log de cinco em cinco minutos | que não é backup contínuo por minuto, e que o teto recente é o instante mais recente restaurável |
| Janela de retenção | SOA-C02, SAA-C03 | 0 a 35 dias, com padrão de 1 pela API e 7 pelo console | que aumentar a retenção hoje não recupera o log de ontem, e que sair de zero causa parada |
| Restauração cria recurso novo | SAA-C03, SOA-C02 | endpoint novo, e o corte entra no RTO | que não há como restaurar sobre a instância existente, em nenhum dos dois caminhos |
| Snapshot automático versus manual | SOA-C02, SAA-C03 | a prova executada na exclusão de uma instância descartável | que o automático segue o ciclo da instância e o manual sobrevive, com limite de 100 por região |
| Multi-AZ versus backup | SAA-C03 | o failover provocado que não desfaz o DELETE | que duas zonas resolvem disponibilidade e que o endpoint permanece o mesmo no failover |
| Réplica de leitura versus restauração | SAA-C03 | a réplica do L07 replica o dano | que promover réplica cobre perda de instância, não corrupção de conteúdo |
| AWS Backup e cofre com trava | SAP-C02, SCS-C02 | cofre em outra conta, governança contra conformidade | que conformidade é irreversível após a carência, inclusive para o usuário raiz |
| Grupos padrão na restauração | SOA-C02, DVA-C02 | os três argumentos explícitos na chamada de restauração | que sem declarar, a instância nasce com VPC, grupo de segurança e parâmetros padrão |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve corrupção lógica de dado e oferece Multi-AZ, réplica de leitura, PITR e cópia de snapshot entre regiões. As três primeiras alternativas são plausíveis porque todas aparecem na mesma tela do console, e duas delas são inúteis para o cenário: replicação síncrona e assíncrona propagam a transação, porque não existe camada que julgue o conteúdo dela. A variante mais difícil troca o cenário para "o incidente foi há nove dias" e mantém a retenção em sete — aí a resposta correta deixa de ser PITR e passa a ser o snapshot manual mensal.
Requisitos, e como cada um muda o desenho
Requisito de recuperação que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca — e note que dois deles não são atendidos por nenhum recurso da AWS, e sim por código da aplicação.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Incidente mais antigo que se quer desfazer | 14 dias | retenção de PITR em 14, contra o padrão de 1 da API — e o snapshot mensal para o que é mais antigo que isso |
| Precisão do instante alvo | segundo | obriga auditoria de escrita no banco: sem ela, o alvo é chute e o chute vira RPO |
| Tempo de repontar | abaixo de 1 min | endereço em parâmetro externo e recarga de pool no processo, em vez de variável de ambiente da definição de task |
| Sobreviver a credencial comprometida | requisito de segurança | cofre do AWS Backup em outra conta, com trava e chave própria |
| Perda de dado durante o incidente | congelar na detecção | a aplicação precisa de um modo somente-leitura acionável por valor, não por deploy |
| Ensaio não pode envelhecer | mensal | regra agendada e máquina de estados; ensaio manual tem meia-vida de dois trimestres |
| O número precisa sobreviver ao terminal | auditável | os marcos viram métrica publicada, e o alarme compara com o valor acordado |
| Custo do ensaio | desprezível | instância de ensaio em zona única, apagada ao fim, com etiqueta de efêmero |
| Auditoria de fechamento contábil | 12 meses | segunda regra no plano de backup, com transição para armazenamento frio — o contínuo não serve, porque tem teto de 35 dias |
O requisito que quase ninguém escreve, e que decide a arquitetura
"Quanto tempo pode passar entre o dano e a descoberta?" Ele não é um requisito de banco de dados, e é o que domina o RTO e o RPO em incidente lógico. Na Cadência foram 34 minutos, e nenhum ajuste de RDS os encurta. Enquanto ele não estiver escrito, todo o esforço vai para a parcela de 19 minutos que a AWS executa, e a de 34 continua invisível.
Arquitetura mínima: o backup que nunca voltou
Este é o desenho que a Cadência tem depois do L07, e ele é legítimo: tem duas zonas, tem réplica, tem backup ativado. O laboratório começa medindo dois números dele — porque um número torna o defeito discutível, e "acho que a gente consegue restaurar" não.
- → DELETE que alcança 41 mil linhas de item de pedido
- → o mesmo DELETE, replicado com fidelidade
- → pool Npgsql para o host escrito na definição de task
- → HTTPS 443 na API de pedidos
- → cópia do volume na janela de backup
- → segmentos de log a cada cinco minutos
- → descarta o snapshot que passou de um dia
- → apaga o log com mais de 24 horas
- Fora da AWS
- Compute
- Banco de dados
- Conceito de arquitetura
- Gestão e governança
Nada aqui está desligado: existe backup, existe zona dupla, existe réplica. O que não existe é uma restauração já feita — e por isso o RTO do documento é uma estimativa que nunca encontrou a realidade. Percorra os passos e repare que três limites já estão fixados neste desenho, e nenhum deles foi escolhido por alguém.
- O dano nasce dentro do banco, e é uma transação válida. Nenhuma peça de infraestrutura tem como distinguir corrupção lógica de trabalho legítimo: um DELETE sem filtro é sintaticamente idêntico a um DELETE correto. Essa é a razão pela qual proteção de disponibilidade não cobre este risco — ela replica o que recebe, ela não avalia o que recebe.
- Zona dupla e réplica repetem o dano, não o desfazem. A implantação em duas zonas mantém uma cópia síncrona e troca o registro de DNS para ela quando a zona primária falha — o nome do banco continua o mesmo. É a resposta certa para perda de zona, de hardware ou de armazenamento, e é a resposta errada para este incidente. A réplica de leitura do L07 tem o mesmo destino, com alguns segundos de atraso.
- O que existe para voltar são DUAS peças, não uma. Recuperação para um instante não é "um backup por minuto". É o snapshot mais os segmentos de log reaplicados até o instante pedido. Entender isso muda a leitura de tudo o que vem depois: o snapshot decide a que ponto de partida se volta, e o log decide até onde se avança a partir dele.
- A cadência do log fixa quão PERTO do agora você chega. O RDS envia o log ao S3 a cada cinco minutos, e o campo `LatestRestorableTime` mostra o teto. Cuidado com a leitura mais comum e errada: isso limita a proximidade do presente, não a precisão de um instante passado. Para um incidente de ontem às 14:07:12 você pede exatamente 14:07:00, com precisão de segundo.
- A retenção fixa quão LONGE você chega, e ninguém a escolheu. Criado pela API ou pela CLI sem declarar nada, o banco nasce com um dia de retenção; pelo console, sete. O intervalo permitido é 0 a 35, e zero desliga a recuperação para um instante. Um incidente descoberto no fechamento da semana seguinte está fora da janela — e aumentar a retenção hoje não recupera nada de ontem, porque o log daquele dia já foi apagado.
- Restaurar não devolve este endereço. Não existe restaurar sobre uma instância que já existe: a operação cria uma instância NOVA, com endpoint novo. Como o endereço vive numa variável de ambiente da definição de task, apontar a aplicação para o banco restaurado passa a exigir um deploy — e o tempo desse deploy entra no RTO sem que ninguém o tenha previsto.
- Por que este desenho sobrevive por anos. Porque funciona. O console mostra backup ativado, a auditoria interna marca a caixa, e o documento de arquitetura tem uma linha dizendo RTO de 30 minutos e RPO de 5. Nenhum dos dois números foi medido. O laboratório existe para medi-los uma vez — e o desconforto de descobrir o valor real é o produto, não o efeito colateral.
Antes de mudar qualquer coisa, meça o piso do RPO. Ele não é uma opinião: é a distância entre o relógio e o instante mais recente restaurável, e sai de um comando.
# Rode ANTES de mudar nada. Tres amostras, dois minutos entre elas — porque uma
# amostra pode cair logo depois de um envio de log e mentir para baixo.
for i in 1 2 3; do
agora=$(date -u +%s)
ultimo=$(aws rds describe-db-instances --db-instance-identifier app-postgres \
--query "DBInstances[0].LatestRestorableTime" --output text)
eu=$(date -u -d "$ultimo" +%s 2>/dev/null || date -u -jf "%Y-%m-%dT%H:%M:%S%z" "$ultimo" +%s)
printf "amostra %d: defasagem de %d s\n" "$i" $(( agora - eu ))
sleep 120
done
# Na Cadencia: 214 s, 61 s e 288 s. Nenhuma passou de 300 — o que confirma a
# cadencia de cinco minutos do log e fixa o piso do RPO para perda de
# infraestrutura. Repare que a defasagem OSCILA: ela nao e um valor, e uma
# serra entre zero e cinco minutos. Prometer "RPO de 1 minuto" com este
# mecanismo e prometer o melhor caso de uma serra.
# E o segundo numero, que e o teto para tras:
aws rds describe-db-instance-automated-backups \
--db-instance-identifier app-postgres \
--query "DBInstanceAutomatedBackups[0].RestoreWindow" --output json
# { "EarliestTime": "...", "LatestTime": "..." }
# Na Cadencia, antes deste laboratorio, a janela tinha 24 h: a retencao era 1.O erro de leitura mais comum sobre os cinco minutos
A cadência de cinco minutos do log NÃO é a granularidade do que você pode pedir. Você pede um instante com precisão de segundo, e o RDS reaplica o log até ali. O que os cinco minutos limitam é quão PERTO do agora o teto está — o instante mais recente restaurável fica atrás do relógio. Para perda de infraestrutura com corte imediato, esse é o piso do RPO. Para o incidente lógico deste módulo, é irrelevante: você quer voltar a ontem às 14:07:00, e isso é permitido ao segundo.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Duas delas não são serviço da AWS, e isso é o achado do módulo: a maior redução de RTO veio de código de aplicação e de um parâmetro, não de configuração de banco.
- → qual host está ativo agora
- → usuário e senha do host ativo
- → pool Npgsql para o host que o parâmetro devolveu
- → log contínuo e snapshot diário, sob o plano de backup
- → ponto de recuperação guardado fora do alcance da produção
- → pedido de decifragem do ponto de recuperação
- → gatilho mensal com o instante alvo sorteado
- → restauração com sub-rede, grupo de segurança e parâmetros declarados
- → snapshot mais log reaplicado até o segundo pedido
- → troca o endereço ativo, e é este instante que o cronômetro registra
- → os marcos do ensaio, em segundos
- → limiar de RTO ou de RPO acordado estourado
- → registro da restauração e da identidade que a pediu
- Banco de dados
- Compute
- Gestão e governança
- Segurança e identidade
- Armazenamento
- Conceito de arquitetura
- Integração de apps
Duas mudanças estruturais, e cada uma ataca uma parcela medida do RTO: a indireção transforma repontar de deploy em troca de valor, e o cofre fora da conta de produção transforma "posso apagar?" em "consigo alcançar?". A terceira mudança é de hábito: o ensaio virou recurso, com gatilho e métrica. Percorra os passos e confira que cada peça nova tem um requisito escrito atrás dela.
- A indireção vem antes do cofre, porque paga mais. O requisito é "repontar sem deploy". A aplicação lê o host de um parâmetro e a credencial de um segredo, e recarrega o pool quando o valor muda. Isso tira do RTO a parcela de construir imagem, criar revisão de definição de task e esperar rollout — que no ensaio da Cadência foi de 11 minutos, mais que a restauração de metade do volume.
- O ponto de recuperação sai da conta que pode apagá-lo. O requisito é sobreviver a credencial comprometida. Quem tem permissão ampla na conta de produção apaga a instância e os backups automáticos dela junto. Com o cofre em outra conta e trava aplicada, a remoção do ponto de recuperação é negada até o fim do ciclo de vida — e a documentação é explícita em incluir o usuário raiz nessa negativa.
- A chave é um segundo raio, e é de propósito. O cofre é cifrado com uma chave da conta de backup. Sem acesso a ela, um ponto de recuperação copiado não é legível — o que fecha o caminho de "exfiltrar o backup" mesmo quando alguém consegue enumerá-lo. É também o motivo pelo qual restaurar entre contas exige política de chave, e não só permissão de backup.
- O ensaio deixa de ser voluntário. O requisito é "o número não pode envelhecer". Uma regra agendada dispara a máquina de estados uma vez por mês, com o instante alvo sorteado dentro da janela de retenção — sortear evita o ensaio que só testa o caminho fácil. Ensaio que depende de alguém lembrar tem meia-vida de dois trimestres.
- A restauração declara sub-rede, grupo de segurança e parâmetros. Dois comportamentos documentados que custam minutos quando surpreendem: a instância restaurada recebe a VPC, o grupo de sub-redes e o grupo de segurança PADRÃO se você não passar outros, e recebe o grupo de parâmetros padrão, que não tem nenhum ajuste seu. O primeiro faz a aplicação não conectar; o segundo faz ela conectar e ficar lenta.
- O corte é o passo cronometrado. Trocar o valor do parâmetro é o instante em que o dano para de crescer, e é ele que fecha a conta do RPO — não o instante em que a instância ficou disponível. Registrar este marco separado dos outros é o que revela que a maior parcela do RTO não estava na AWS.
- O número volta como métrica, não como sensação. A máquina publica os marcos em segundos, o alarme compara com o valor acordado e a trilha guarda quem pediu a restauração. Sem isso, o resultado do ensaio mora na memória de quem o fez, e a próxima conversa sobre RTO volta a ser opinião contra opinião.
A diferença estrutural em relação ao desenho anterior não é uma caixa a mais. São três mudanças de forma: o endereço do banco sai do artefato de implantação e vira um valor consultável; o ponto de recuperação sai da conta que tem permissão para apagá-lo; e o ensaio deixa de ser uma intenção de calendário e vira um recurso com gatilho, marcos e métrica.
A alavanca com maior efeito por linha alterada
Das cinco parcelas do RTO, duas somam 45 dos 77 minutos medidos: detectar (34) e repontar (11). Nenhuma das duas melhora com classe de instância maior, com IOPS provisionado ou com retenção mais longa. A de repontar cai para menos de um minuto com um parâmetro e um vigia de 15 segundos no processo. A de detectar cai com um sinal de escrita anômala — e é a única parte deste módulo em que IA teria um papel defensável.
O ensaio cronometrado, marco por marco
O que segue não é a narrativa de um incidente: é o roteiro que o laboratório executa, com um marco por etapa e um relógio comum. Marcar cada etapa separadamente é o que torna o resultado acionável — um RTO de 77 minutos sozinho não diz onde investir, e a decomposição diz.
A conta do RPO, feita devagar, com os números do ensaio
Instante alvo: 14:07:00. Instante do incidente: 14:07:12. Instante do corte: 15:16. A perda deliberada é de 12 segundos — a escrita legítima que existiu entre o alvo e o dano. A janela do banco danificado é de 68 minutos e 48 segundos, porque a aplicação continuou aceitando pedidos que o banco restaurado nunca viu. Total: 69 minutos. O documento prometia 5, e o 5 estava certo para OUTRO cenário — perder a instância e cortar imediatamente. Confundir os dois cenários é o defeito, não o número.
O payload abaixo é o que a máquina de estados grava por marco. Ele existe para que o resultado do ensaio não more na memória de quem o conduziu: com isso, comparar o ensaio de agosto com o de setembro é uma consulta, e a conversa sobre RTO deixa de ser opinião contra opinião.
{
"ensaio": "2026-08-11T17:07:00Z",
"instanteAlvo": "2026-08-11T17:07:00Z",
"instanteIncidente": "2026-08-11T17:07:12Z",
"instanciaOrigem": "app-postgres",
"instanciaRestaurada": "ffv-lab-ensaio-202608111752",
"enderecoAntigo": "app-postgres.abc123.us-east-1.rds.amazonaws.com",
"enderecoNovo": "ffv-lab-ensaio-202608111752.abc123.us-east-1.rds.amazonaws.com",
"marcos": {
"T0_incidente": 1786000032,
"T1_detectado": 1786002072,
"T2_alvo_decidido": 1786002372,
"T2b_escrita_congelada": 1786002432,
"T3_restauracao_disparada": 1786002732,
"T4_instancia_disponivel": 1786003872,
"T5_corte_efetivado": 1786004172,
"T6_validado": 1786004652
},
"medido": {
"rtoSegundos": 4620,
"rpoSegundos": 4140,
"rpoComCongelamentoSegundos": 2400,
"volumeAlocadoGb": 180,
"zonaDuplaNoEnsaio": false
},
"comparacaoComOAcordado": {
"rtoAcordadoSegundos": 1800,
"rpoAcordadoSegundos": 300,
"veredito": "REPROVADO nos dois eixos"
}
}Por que o ensaio registra a zona dupla no resultado
A instância do ensaio sobe em zona única, para economizar. Isso é legítimo, e é desonesto esconder: uma restauração com zona dupla alonga a etapa, porque a cópia síncrona tem de ser construída. Ensaiar em zona única e prometer o número para um corte com zona dupla é a forma mais fácil de produzir um RTO medido que continua sendo ficção. O campo no payload existe para que a comparação entre ensaios não misture peras com maçãs.
As decisões, e o que se perde em cada uma
📋 Terça, 14:41. Um DELETE sem filtro apagou 41 mil linhas de itens de pedido às 14:07:12. A aplicação continua no ar e continua gravando pedidos novos, que são legítimos. Retenção de PITR em 14 dias, banco de 180 GB alocados, equipe de duas pessoas e nenhuma restauração já feita nesta conta.
O dano é cirúrgico: uma tabela, um intervalo de chaves, nenhuma escrita posterior dependendo do que foi apagado. Nesse caso a instância restaurada é uma FONTE DE CONSULTA, não um destino de corte — e o RPO cai para zero em tudo que não foi tocado, porque nenhuma escrita das últimas horas é descartada. O RTO também encolhe, porque a parcela de repontar desaparece: ninguém troca de banco. O preço é que a reconciliação é código específico do incidente, escrito sob pressão, e precisa de conferência de chave estrangeira e de sequência. Este é o caminho para dano localizado — e o laboratório ensaia OS DOIS, porque o outro é o que ninguém conhece.
Alt: Corte completo para a instância restaurada — É obrigatório quando o dano é difuso — ransomware que cifra tudo, migração que reescreveu colunas em vinte tabelas, corrupção que você não consegue delimitar. Descarta toda escrita legítima entre o alvo e o corte, o que no ensaio foram 69 minutos de pedidos. É o caminho cujo RTO o time não conhece, e por isso é o que o ensaio cronometra.
Alt: Promover a réplica de leitura do L07 — Não serve para este incidente: a réplica aplicou o mesmo DELETE. Promover réplica resolve perda da instância primária, não corrupção de conteúdo. Confundir as duas é o erro que a seção de certificação cobra.
Alt: Aceitar o dano e reconstruir por lógica de negócio — Legítimo quando o dado é derivável de outra fonte — item de pedido que ainda existe no sistema fiscal, por exemplo. Custa dias de reconciliação e não tem garantia de completude, mas é a única saída quando o incidente é mais antigo que a janela de retenção.
Alt: Restaurar sobre a instância de produção — Não existe. A operação de restauração sempre cria instância nova, e não há parâmetro que a faça sobrescrever a atual. Quem planeja com essa suposição descobre no pior momento, e improvisa renomeação de instância na pressa.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Janela de retenção | 14 dias | 1 (padrão da API); 7 (padrão do console); 35 (máximo) | cobre duas rodadas de fechamento semanal, que é onde a inconsistência aparece | armazenamento de backup proporcional ao período, e mais log guardado |
| Onde o ponto de recuperação vive | cofre do AWS Backup em outra conta | só backup automático do RDS; cofre na mesma conta; cópia entre regiões | credencial comprometida na conta de produção não alcança o cofre | complexidade de duas contas e política de chave para restaurar de volta |
| Modo da trava do cofre | governança | conformidade com carência de 3 dias; sem trava | protege de erro operacional e de credencial de aplicação, e continua reversível | quem tem permissão de IAM suficiente ainda remove a trava — conformidade é o que fecha isso, ao preço de ser irreversível |
| Onde o endereço do banco vive | parâmetro consultado em execução | variável de ambiente na definição de task; nome de DNS próprio com CNAME | transforma repontar de deploy em troca de valor, tirando 11 min do RTO | uma chamada a mais na partida e um vigia de 15 s no processo |
| Como o pool troca de banco | nova fonte de dados e descarte da antiga em 10 s | reiniciar o processo; descartar imediatamente | não corta transação em voo e não deixa escrita indo para o banco antigo | uma janela de dez segundos em que as duas fontes existem — exige idempotência |
| Caminho do incidente localizado | restaurar em paralelo e extrair | corte completo para a instância restaurada | RPO de zero em tudo que não foi tocado, e nenhuma parcela de repontar | reconciliação escrita sob pressão, com risco em chave estrangeira e sequência |
| Frequência do ensaio | mensal, com instante alvo sorteado | trimestral; anual; sob demanda | sortear o alvo evita ensaiar só o caminho fácil, e um mês mantém o número fresco | doze instâncias de ensaio por ano cobrando por hora ligada durante o ensaio |
| Snapshot final na exclusão | obrigatório, com nome versionado | pular o snapshot final; confiar no backup retido | é a única cópia que não expira, e cobre o pedido de auditoria de 2027 | um snapshot que cobra GB-mês para sempre até alguém decidir apagá-lo |
A dívida que este módulo cria e não paga
Um corte completo descarta a escrita da janela danificada, e a Cadência aceita isso porque as escritas são idempotentes por chave de pedido — a hipótese está declarada na abertura. Num sistema em que a fila não é reprocessável, o corte precisa de um diário de escritas fora do banco para replicar o que se perdeu, e isso é arquitetura de evento, não de backup. É honesto dizer que este laboratório não resolve esse caso: ele mede o custo dele.
Construir: a retenção, e o que o terraform apply decidiu por você
Três argumentos deste arquivo têm padrão hostil, e nenhum dos três aparece em tutorial. A retenção, que a API põe em um dia quando você não declara. A exclusão dos backups automáticos, que o provider põe em true — de forma que apagar a instância leva o backup embora. E o snapshot final, que a maior parte dos exemplos desliga para o destroy não pedir nome.
# banco-backup.tf — a política de retenção, e por que ela não pode ficar em branco
#
# Quando o Terraform não declara `backup_retention_period`, a API aplica o padrão
# DELA: um dia. Pelo console seriam sete. Ninguém escolheu nenhum dos dois, e é
# esse número que decide quão atrás a recuperação para um instante consegue
# chegar. O intervalo permitido é 0 a 35 dias, e zero desliga o mecanismo.
variable "retencao_backup_dias" {
description = "Janela de PITR em dias. Derive do incidente mais ANTIGO que você quer poder desfazer."
type = number
default = 14
validation {
# Zero desligaria o PITR e 35 é o teto da API. Falhar no plano é mais barato
# que descobrir no incidente que a janela era de um dia.
condition = var.retencao_backup_dias >= 1 && var.retencao_backup_dias <= 35
error_message = "Use entre 1 e 35: zero desliga o PITR e 35 é o máximo que o RDS aceita."
}
}
variable "rotulo_snapshot_final" {
description = "Sufixo do snapshot final. Valor DECLARADO, versionado com o código."
type = string
default = "v1"
}
resource "aws_db_instance" "principal" {
# ── O que já vem do L01, sem alteração ──────────────────────────────────
identifier = "app-postgres"
engine = "postgres"
instance_class = var.classe_banco
allocated_storage = 180
multi_az = true # protege de perda de zona; NÃO protege de DELETE
db_subnet_group_name = aws_db_subnet_group.privadas.name
vpc_security_group_ids = [aws_security_group.banco.id]
manage_master_user_password = true
kms_key_id = aws_kms_key.dados.arn
# ── O que este laboratório acrescenta ──────────────────────────────────
backup_retention_period = var.retencao_backup_dias
# Trocar de zero para diferente de zero (ou o contrário) causa
# indisponibilidade — a documentação é explícita. Ir de 1 para 14, que é o caso
# aqui, é mudança entre dois valores não nulos e se aplica sem parada.
backup_window = "06:30-07:00" # UTC. Sem declarar, a AWS sorteia 30 min num bloco de 8 h.
# Sem isto o snapshot nasce sem etiqueta, e o rateio de custo do L09 não
# consegue atribuir o armazenamento de backup a ninguém.
copy_tags_to_snapshot = true
# O padrão do provider é `true`: apagar a instância levaria os backups
# automáticos com ela. Em `false`, eles sobrevivem pelo prazo que a retenção
# tinha no momento da exclusão — e depois expiram sozinhos.
delete_automated_backups = false
# O snapshot final é a única cópia que NÃO expira. Ele não substitui a
# retenção; ele cobre o caso "apaguei a instância e preciso do dado em 2027".
skip_final_snapshot = false
final_snapshot_identifier = "app-postgres-final-${var.rotulo_snapshot_final}"
# A trava mais barata contra `terraform destroy` disparado na conta errada.
# Ela obriga um `terraform apply` deliberado antes de qualquer exclusão.
deletion_protection = true
# ATENÇÃO ao que NÃO fazer aqui: `final_snapshot_identifier` com `timestamp()`
# muda em todo plano, e o Terraform passa a propor alteração a cada execução.
# Nome de snapshot é decisão versionada, não relógio.
tags = merge(local.tags, {
# A etiqueta que o AWS Backup usa para selecionar. Banco novo entra no plano
# por ser etiquetado, não por alguém lembrar de editar o plano.
Backup = "obrigatorio"
})
}
output "instante_mais_recente_restauravel" {
description = "Teto do PITR na última leitura de estado. A distância até o relógio é o piso do RPO."
# O provider expõe este atributo, mas ele é lido no refresh: não é relógio ao
# vivo. Para o valor do momento, use `describe-db-instances` — é o que o
# roteiro do ensaio faz.
value = aws_db_instance.principal.latest_restorable_time
}
output "endereco_banco_primario" {
value = aws_db_instance.principal.address
}
O par de argumentos que já apagou dado de gente competente
Com `delete_automated_backups = true` (o padrão do provider) e `skip_final_snapshot = true` (o que quase todo exemplo escreve para o `destroy` funcionar sem atrito), um `terraform destroy` na conta errada não deixa NADA para trás: nem a instância, nem os backups automáticos, nem um snapshot final. A documentação da AWS é explícita: se você não escolher retê-los, todos os backups automáticos são apagados junto com a instância. Snapshots manuais e o final são independentes disso e sobrevivem — e é exatamente por isso que este laboratório insiste no snapshot manual mensal.
O que "retido" significa, com precisão
Ao apagar a instância retendo os backups automáticos, eles duram o número de dias que a retenção tinha NO MOMENTO da exclusão, e expiram sozinhos depois disso: não são renovados, porque não há mais instância gerando snapshot novo. Eles também não guardam informação de grupo de parâmetros nem de grupo de opções, e não podem ser modificados. O limite é de 40 backups automáticos retidos por região, contado separado da cota de instâncias. Por isso a documentação recomenda tirar o snapshot final mesmo retendo os automáticos: o final não expira.
Construir: o cofre em outra conta, e a trava que você pode não conseguir desfazer
O cenário inclui credencial comprometida, e é isso que move o ponto de recuperação para fora da conta de produção. A trava é o segundo movimento, e ela tem uma armadilha de forma: não existe argumento chamado mode. É a presença de changeable_for_days que decide o modo, e no modo de conformidade a decisão é irreversível depois da carência.
# cofre-backup.tf — o ponto de recuperação fora do alcance de quem pode apagá-lo
#
# O cenário deste laboratório inclui credencial comprometida. Se o ponto de
# recuperação vive na mesma conta que o banco, quem tem permissão ampla apaga os
# dois. Cofre em outra conta troca a pergunta "posso apagar?" pela pergunta
# "consigo alcançar?", e a segunda é muito mais difícil.
resource "aws_backup_vault" "cofre" {
provider = aws.backup # alias apontando para a conta de backup
name = "${var.projeto}-cofre"
kms_key_arn = aws_kms_key.cofre.arn # chave da conta de backup, não da de produção
tags = local.tags
}
# ── A trava. Leia o modo duas vezes antes de aplicar. ───────────────────────
#
# Não existe argumento "mode": é a PRESENÇA de `changeable_for_days` que decide.
# Presente => modo de conformidade. Ausente => modo de governança.
#
# governança → a trava é removível por quem tem a permissão de IAM certa.
# Protege de erro operacional e de credencial de aplicação.
# conformidade → passado o período de carência (mínimo 3 dias, máximo 36.500),
# a trava é imutável: nem o usuário raiz nem a AWS a removem
# enquanto houver ponto de recuperação dentro.
#
# Num laboratório, conformidade significa pagar armazenamento até o ciclo de vida
# terminar, sem saída. Aqui, governança — e a linha do outro modo fica comentada
# de propósito, com o aviso ao lado.
resource "aws_backup_vault_lock_configuration" "trava" {
provider = aws.backup
backup_vault_name = aws_backup_vault.cofre.name
# Trabalho que entrar no cofre com ciclo de vida fora deste intervalo FALHA.
# Recuperação que já estava lá antes da trava mantém o ciclo antigo.
min_retention_days = 7
max_retention_days = 35
# changeable_for_days = 3 # NÃO descomente em laboratório. Ver o aviso acima.
}
resource "aws_backup_plan" "plano" {
name = "${var.projeto}-plano"
tags = local.tags
# ── Regra 1: o contínuo. É ele que dá "qualquer segundo dos últimos 35 dias".
#
# Ligar isto transfere ao AWS Backup a administração da janela de backup
# automático do RDS. Depois disso, mexer na retenção pelo RDS não é o caminho:
# é pelo plano. E a primeira ativação, quando a retenção sai de zero, é
# agendada para a próxima janela de manutenção — porque ela causa parada.
rule {
rule_name = "continuo"
target_vault_name = aws_backup_vault.cofre.name
schedule = "cron(30 6 * * ? *)"
enable_continuous_backup = true
start_window = 60
completion_window = 180
lifecycle {
# O teto do contínuo é 35 dias, e ele não transiciona para armazenamento
# frio: frio exige no mínimo 90 dias, que é mais que o próprio teto.
delete_after = var.retencao_backup_dias
}
}
# ── Regra 2: o periódico longo, que responde a OUTRA pergunta.
#
# O contínuo responde "desfazer o que aconteceu ontem". Este responde "provar
# como estava o dado no fechamento de março", que é auditoria e não
# recuperação. Um só não cobre os dois.
rule {
rule_name = "mensal_longo"
target_vault_name = aws_backup_vault.cofre.name
schedule = "cron(0 7 1 * ? *)"
lifecycle {
cold_storage_after = 90
delete_after = 365 # tem de ser ao menos 90 dias depois do frio
}
}
}
resource "aws_backup_selection" "bancos" {
iam_role_arn = aws_iam_role.backup.arn
name = "bancos-etiquetados"
plan_id = aws_backup_plan.plano.id
# Seleção por etiqueta e não por ARN: banco novo entra no plano por ser
# etiquetado. O risco simétrico é banco que nasce SEM a etiqueta — e é por isso
# que a seção de observabilidade liga a checagem do Backup Audit Manager.
selection_tag {
type = "STRINGEQUALS"
key = "Backup"
value = "obrigatorio"
}
}
# ── O papel que o AWS Backup assume ───────────────────────────────────────
resource "aws_iam_role" "backup" {
name = "${var.projeto}-backup"
assume_role_policy = data.aws_iam_policy_document.confia_backup.json
}
data "aws_iam_policy_document" "confia_backup" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["backup.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "backup_base" {
role = aws_iam_role.backup.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
}
resource "aws_iam_role_policy_attachment" "backup_restaura" {
role = aws_iam_role.backup.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores"
}
# A permissão que o guia do AWS Backup exige para o contínuo do RDS e que não
# está nas políticas gerenciadas: ligar o backup contínuo MODIFICA a instância,
# porque muda a retenção dela.
data "aws_iam_policy_document" "backup_extra" {
statement {
actions = ["rds:ModifyDBInstance"]
resources = [aws_db_instance.principal.arn]
}
}
| Aspecto | Modo de governança | Modo de conformidade |
|---|---|---|
| Como se declara | sem `changeable_for_days` | com `changeable_for_days` presente |
| Quem pode remover a trava | quem tem a permissão de IAM adequada | ninguém depois da carência — nem o usuário raiz, nem a AWS |
| Carência | não se aplica | mínimo 3 dias, máximo 36.500 |
| Durante a carência | não se aplica | a trava e a configuração ainda podem ser alteradas ou removidas |
| Protege de | erro operacional e credencial de aplicação | tudo o que a governança protege, mais administrador mal-intencionado |
| O que dá errado | quem comprometeu um papel administrativo remove a trava | ponto de recuperação com retenção longa fica cobrando até o fim do ciclo, sem saída |
| Em laboratório | este é o certo | não use: você paga armazenamento até o ciclo terminar |
A conta fechada não é uma saída
A documentação do AWS Backup registra o único caminho em que o conteúdo de um cofre travado desaparece: fechar a conta AWS. Nesse caso a AWS suspende a conta por 90 dias com os backups intactos e, se ela não for reaberta, apaga o conteúdo do cofre — mesmo com a trava aplicada. Isso não é uma forma de sair de uma trava em conformidade: é uma informação de continuidade de negócio, e vale conhecer antes de escolher o modo.
Construir: a aplicação que aceita ser repontada
Esta é a peça de maior retorno do módulo, e ela não é AWS: é código. Enquanto o endereço do banco viver numa variável de ambiente da definição de task, repontar é construir imagem, criar revisão e esperar rollout — 11 minutos no ensaio de comparação, mais que a restauração de metade do volume. Com o endereço num parâmetro consultado em execução, repontar é uma escrita de valor.
// ProvedorDeConexao.cs — a peça que transforma repontar em troca de valor
//
// O ponto central deste laboratório em uma linha de código: a aplicação NÃO
// guarda o endereço do banco. Ela o resolve, e aceita que ele mude sem reiniciar.
using System.Text.Json;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using Npgsql;
public sealed class ProvedorDeConexao : IAsyncDisposable
{
private readonly IAmazonSimpleSystemsManagement _ssm;
private readonly IAmazonSecretsManager _segredos;
private readonly string _nomeParametro;
private readonly string _arnSegredo;
private readonly ILogger<ProvedorDeConexao> _log;
private readonly SemaphoreSlim _trava = new(1, 1);
private NpgsqlDataSource? _fonte;
public string HostAtual { get; private set; } = "(nao resolvido)";
public ProvedorDeConexao(IAmazonSimpleSystemsManagement ssm,
IAmazonSecretsManager segredos,
IConfiguration cfg,
ILogger<ProvedorDeConexao> log)
{
_ssm = ssm;
_segredos = segredos;
// Nome do parâmetro e ARN do segredo por configuração. O que vem por
// variável de ambiente é a REFERÊNCIA, nunca o valor.
_nomeParametro = cfg["Banco:ParametroEndereco"]!;
_arnSegredo = cfg["Banco:ArnSegredo"]!;
_log = log;
}
public NpgsqlDataSource Fonte =>
_fonte ?? throw new InvalidOperationException("Chame ResolverAsync antes do primeiro uso.");
/// <summary>
/// Lê o endereço ativo e, se mudou, constrói um pool novo e descarta o antigo.
/// Devolve true quando houve troca — é o sinal que o roteiro do ensaio observa
/// para cronometrar o instante do corte.
/// </summary>
public async Task<bool> ResolverAsync(CancellationToken ct = default)
{
await _trava.WaitAsync(ct);
try
{
var par = await _ssm.GetParameterAsync(
new GetParameterRequest { Name = _nomeParametro }, ct);
var host = par.Parameter.Value.Trim();
if (host == HostAtual && _fonte is not null) return false;
var seg = await _segredos.GetSecretValueAsync(
new GetSecretValueRequest { SecretId = _arnSegredo }, ct);
var cred = JsonSerializer.Deserialize<Credencial>(seg.SecretString)!;
var construtor = new NpgsqlConnectionStringBuilder
{
Host = host,
Port = 5432,
Database = "appdb",
Username = cred.username,
Password = cred.password,
SslMode = SslMode.VerifyFull,
RootCertificate = "/etc/ssl/certs/rds-global-bundle.pem",
// O pool é o assunto do L07. Aqui ele importa por outro motivo:
// trocar de banco exige DESCARTAR o pool antigo, senão as
// conexões abertas continuam apontando para o banco danificado.
MaxPoolSize = 20,
MinPoolSize = 2,
Timeout = 5,
CommandTimeout = 15,
KeepAlive = 30,
ApplicationName = "api-pedidos",
};
var nova = new NpgsqlDataSourceBuilder(construtor.ConnectionString).Build();
var antiga = _fonte;
_fonte = nova;
var anterior = HostAtual;
HostAtual = host;
_log.LogWarning("Endereco do banco trocado de {Antes} para {Depois}", anterior, host);
if (antiga is not null)
{
// Espera curta para o comando em voo terminar, e só então fecha.
// Descartar imediatamente cortaria transação legítima; esperar
// para sempre deixaria escrita indo para o banco errado.
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(10), CancellationToken.None);
await antiga.DisposeAsync();
});
}
return true;
}
finally { _trava.Release(); }
}
public ValueTask DisposeAsync() => _fonte?.DisposeAsync() ?? ValueTask.CompletedTask;
private sealed record Credencial(string username, string password);
}
// ── O vigia que faz o corte acontecer sem deploy ───────────────────────────
// Quinze segundos é uma escolha, não um padrão: é o teto que a equipe aceitou
// para a parcela de repontar. Menos que isso multiplica chamadas ao Parameter
// Store sem ganho perceptível.
public sealed class VigiaDeEndereco(ProvedorDeConexao provedor, ILogger<VigiaDeEndereco> log)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try { await provedor.ResolverAsync(ct); }
catch (Exception e) { log.LogError(e, "Falha ao resolver o endereco ativo"); }
await Task.Delay(TimeSpan.FromSeconds(15), ct);
}
}
}
Por que descartar o pool antigo depois de dez segundos, e não na hora
Descartar imediatamente cortaria transação em voo, e transação cortada no meio de uma recuperação é a última coisa que você quer explicar depois. Não descartar deixaria conexões abertas escrevendo no banco DANIFICADO, o que aumenta o RPO em silêncio — e é o pior defeito possível nesta arquitetura, porque parece que o corte funcionou. Dez segundos é uma escolha derivada do tempo de comando mais longo da Cadência; meça o seu e ajuste, porque copiar este número sem medir é o antipadrão que a série critica.
A rota de prontidão ganha uma terceira pergunta, e é ela que fecha o laço do corte: não basta abrir conexão, é preciso confirmar que a conexão chegou ao host que o parâmetro manda. Sem essa comparação, "o pool recarregou" e "o pool continua falando com o banco antigo" são indistinguíveis de fora.
// Program.cs — as rotas de saúde, e a terceira pergunta que este módulo acrescenta
using Npgsql;
using Polly;
using Polly.Retry;
var construtor = WebApplication.CreateBuilder(args);
construtor.Services.AddAWSService<Amazon.SimpleSystemsManagement.IAmazonSimpleSystemsManagement>();
construtor.Services.AddAWSService<Amazon.SecretsManager.IAmazonSecretsManager>();
construtor.Services.AddSingleton<ProvedorDeConexao>();
construtor.Services.AddHostedService<VigiaDeEndereco>();
construtor.Services.AddOpenTelemetry().WithTracing(t => t.AddNpgsql().AddOtlpExporter());
// Repetição com espera crescente E dispersão. Sem dispersão, todas as tasks
// tentam no mesmo milissegundo e transformam oscilação em apagão (assunto do L36).
var repeticao = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(200),
ShouldHandle = new PredicateBuilder().Handle<NpgsqlException>(e => e.IsTransient),
})
.Build();
var app = construtor.Build();
var provedor = app.Services.GetRequiredService<ProvedorDeConexao>();
await provedor.ResolverAsync();
// ── Vivacidade: NÃO toca o banco ──────────────────────────────────────────
// Se esta rota consultasse o banco, uma restauração em andamento marcaria todas
// as tasks como mortas, o ECS as substituiria em laço, e as novas nasceriam
// igualmente mortas. Você teria transformado um incidente de dado num incidente
// de disponibilidade, no pior momento possível.
app.MapGet("/health/live", () => Results.Ok(new { vivo = true }));
// ── Prontidão: toca o banco E confere a identidade dele ───────────────────
// Três perguntas, e a terceira é a que este laboratório acrescenta:
// 1. o pool abre conexão? (rede e instância)
// 2. o schema esperado está aplicado? (migration)
// 3. estamos ligados ao host que o parâmetro manda? (o corte terminou)
app.MapGet("/health/ready", async (ProvedorDeConexao pr) =>
{
try
{
await using var conexao = await pr.Fonte.OpenConnectionAsync();
await using var cmd = new NpgsqlCommand(
"SELECT count(*) FROM information_schema.tables WHERE table_name = 'pedido_item'",
conexao);
var tabelas = (long)(await cmd.ExecuteScalarAsync())!;
if (tabelas == 0) return Results.StatusCode(503);
// A conexão real informa o host a que chegou. Comparar com o que o
// parâmetro pediu é o que distingue "o pool recarregou" de "o pool ainda
// está falando com o banco antigo" — e essa distinção é o corte.
var alinhado = string.Equals(conexao.Host, pr.HostAtual, StringComparison.OrdinalIgnoreCase);
return alinhado
? Results.Ok(new { pronto = true, host = conexao.Host })
: Results.StatusCode(503);
}
catch { return Results.StatusCode(503); }
});
// ── A rota que o roteiro do ensaio consulta para cronometrar o corte ──────
// Sem autenticação de usuário final: ela vive na rede privada, atrás do
// balanceador interno, e devolve só o host — nunca credencial.
app.MapGet("/interno/onde-estou", (ProvedorDeConexao pr) =>
Results.Ok(new { host = pr.HostAtual, quando = DateTimeOffset.UtcNow }));
app.Run();
A rota de vivacidade não pode tocar o banco — e aqui o motivo é agudo
No L01 essa regra é uma boa prática. Durante uma restauração ela é a diferença entre um incidente e dois: se a vivacidade consultar o banco, a indisponibilidade do banco marca todas as tasks como mortas, o ECS as substitui em laço, e as novas nascem igualmente mortas. Você acabou de transformar um incidente de dado num incidente de capacidade, no minuto em que menos tem gente sobrando.
Construir: o roteiro cronometrado
O roteiro é o laboratório. Ele não devolve "ok": devolve dois números em segundos, e grava um marco por etapa num arquivo separado — porque a decomposição é o que torna o resultado acionável.
#!/usr/bin/env bash
# ensaiar-restauracao.sh — o laboratorio inteiro cabe aqui, e o que ele produz
# nao e um "ok": sao dois numeros, em segundos.
set -euo pipefail
PROJETO="${PROJETO:-ffv-lab}"
ORIGEM="app-postgres"
ALVO="${PROJETO}-ensaio-$(date -u +%Y%m%d%H%M)"
PARAMETRO="/${PROJETO}/banco/endereco-ativo"
URL_INTERNA="${URL_INTERNA:?defina a URL interna da API}"
MARCOS="marcos-$(date -u +%Y%m%dT%H%M%SZ).tsv"
marcar() { printf '%s\t%s\t%s\n' "$(date -u +%s)" "$(date -u +%FT%TZ)" "$1" | tee -a "$MARCOS"; }
epoca_de() { awk -F'\t' -v m="$1" '$3==m {print $1; exit}' "$MARCOS"; }
# ── T0 · o incidente ───────────────────────────────────────────────────────
# No ensaio ele e provocado, e o instante e anotado com precisao de segundo,
# porque o RPO se calcula contra ele. Num incidente real este e o numero mais
# dificil de obter: ele vem da auditoria do banco, nao da memoria de ninguem.
marcar T0_incidente
psql "$(cadeia_de_conexao_producao)" <<'SQL'
BEGIN;
-- O comando do ensaio e deliberadamente parecido com o do incidente real:
-- uma unica linha, sintaticamente perfeita, semanticamente catastrofica.
DELETE FROM pedido_item WHERE criado_em > now() - interval '30 days';
COMMIT;
SQL
# ── T1 · detectar ──────────────────────────────────────────────────────────
# Aqui o ensaio pode trapacear de duas formas, e as duas invalidam o numero:
# marcar T1 imediatamente (porque voce sabia da hora) ou nao marcar. O honesto e
# esperar o SINAL que existe na sua conta. Se nao existe sinal, use o tempo
# medio de reclamacao da operacao e escreva ao lado que e uma estimativa.
read -r -p "Pressione ENTER quando o alarme (ou a ligacao da operacao) chegar: " _
marcar T1_detectado
# ── T2 · decidir o instante alvo ───────────────────────────────────────────
# A precisao desta decisao vira RPO direto. Com pgaudit ou com o registro de
# transacao, voce tem o segundo. Sem isso, sobra o chute, e o chute prudente
# custa minutos de dado que existiam.
ALVO_ISO="${ALVO_ISO:?defina o instante alvo em ISO-8601 UTC, por exemplo 2026-08-11T17:07:00Z}"
ALVO_EPOCA=$(date -u -d "$ALVO_ISO" +%s 2>/dev/null || date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$ALVO_ISO" +%s)
marcar T2_alvo_decidido
# A janela e um TETO. Pedir um instante fora dela falha, e falha aqui e barata.
aws rds describe-db-instance-automated-backups \
--db-instance-identifier "$ORIGEM" \
--query 'DBInstanceAutomatedBackups[0].RestoreWindow' --output json
# ── T2b · congelar a escrita ───────────────────────────────────────────────
# A alavanca que o primeiro ensaio revelou, e que nao esta em nenhum console:
# enquanto a aplicacao grava no banco danificado, o RPO cresce. Somente-leitura
# aqui congela a perda no valor que ela tem agora.
aws ssm put-parameter --name "/${PROJETO}/app/modo" --value "somente-leitura" --overwrite
marcar T2b_escrita_congelada
# ── T3 · disparar a restauracao ────────────────────────────────────────────
# Os tres argumentos que evitam o incidente dentro do incidente. Sem eles, a
# instancia nasce com a VPC, o grupo de seguranca e o grupo de parametros
# PADRAO da conta — e a aplicacao nao conecta, ou conecta e fica lenta.
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier "$ORIGEM" \
--target-db-instance-identifier "$ALVO" \
--restore-time "$ALVO_ISO" \
--db-subnet-group-name "$(terraform output -raw grupo_subrede_privada)" \
--vpc-security-group-ids "$(terraform output -raw sg_banco)" \
--db-parameter-group-name "$(terraform output -raw grupo_parametros)" \
--db-instance-class "$(terraform output -raw classe_banco)" \
--no-multi-az \
--no-publicly-accessible \
--tags Key=Projeto,Value="$PROJETO" Key=Efemero,Value=sim
marcar T3_restauracao_disparada
# Zona unica no ensaio economiza; num corte real use --multi-az, e cronometre de
# novo, porque a etapa alonga. O que voce NAO pode e ensaiar com zona unica e
# prometer o numero para um corte com zona dupla.
# ── T4 · disponivel ────────────────────────────────────────────────────────
aws rds wait db-instance-available --db-instance-identifier "$ALVO"
marcar T4_instancia_disponivel
# Disponivel nao e pronta: os blocos continuam vindo do S3 em segundo plano.
aws rds describe-db-instances --db-instance-identifier "$ALVO" \
--query 'DBInstances[0].[StorageOperationStatus,StorageOperationPercentProgress]' \
--output text
NOVO_HOST=$(aws rds describe-db-instances --db-instance-identifier "$ALVO" \
--query 'DBInstances[0].Endpoint.Address' --output text)
echo "endereco novo: $NOVO_HOST" # compare com o antigo: nunca sao iguais
# Aquecimento das tabelas do caminho critico. Uma varredura completa obriga o
# RDS a trazer os blocos, e e mais rapido pagar isso agora que na primeira
# consulta de um usuario.
psql "postgresql://$NOVO_HOST/appdb" -c "SELECT count(*) FROM pedido_item" \
-c "SELECT count(*) FROM pedido"
# ── T5 · o corte ───────────────────────────────────────────────────────────
# Uma escrita de valor, e o vigia da aplicacao faz o resto. O marco so e
# registrado quando a aplicacao CONFIRMA que trocou — nao quando o comando
# retorna. A diferenca entre os dois e o ciclo de recarga do pool.
aws ssm put-parameter --name "$PARAMETRO" --value "$NOVO_HOST" --overwrite
until [ "$(curl -sf "$URL_INTERNA/interno/onde-estou" | jq -r .host)" = "$NOVO_HOST" ]; do
sleep 1
done
marcar T5_corte_efetivado
aws ssm put-parameter --name "/${PROJETO}/app/modo" --value "normal" --overwrite
# ── T6 · validar ───────────────────────────────────────────────────────────
# A pergunta nao e "a instancia subiu". E "a invariante de negocio voltou".
psql "postgresql://$NOVO_HOST/appdb" <<'SQL'
\set ON_ERROR_STOP on
-- Nenhum pedido pode ter ficado sem item: e a invariante que o DELETE violou.
SELECT count(*) AS pedidos_orfaos FROM pedido p
WHERE NOT EXISTS (SELECT 1 FROM pedido_item i WHERE i.pedido_id = p.id);
-- E a contagem tem de voltar ao patamar de antes do incidente.
SELECT count(*) AS itens FROM pedido_item;
SQL
marcar T6_validado
# ── Os dois numeros ────────────────────────────────────────────────────────
T0=$(epoca_de T0_incidente); T5=$(epoca_de T5_corte_efetivado); T6=$(epoca_de T6_validado)
T2B=$(epoca_de T2b_escrita_congelada)
RTO=$(( T6 - T0 ))
RPO=$(( T5 - ALVO_EPOCA )) # o real, sem congelamento
RPO_CONGELADO=$(( T2B - ALVO_EPOCA )) # o real, com congelamento
printf '\nRTO medido ............... %d s (%d min)\n' "$RTO" $(( RTO / 60 ))
printf 'RPO medido ............... %d s (%d min)\n' "$RPO" $(( RPO / 60 ))
printf 'RPO com congelamento ..... %d s (%d min)\n' "$RPO_CONGELADO" $(( RPO_CONGELADO / 60 ))
# Publicar e o que impede o numero de morrer neste terminal.
aws cloudwatch put-metric-data --namespace "FFV/Desastre" \
--metric-name RtoMedidoSegundos --value "$RTO" --unit Seconds
aws cloudwatch put-metric-data --namespace "FFV/Desastre" \
--metric-name RpoMedidoSegundos --value "$RPO" --unit Seconds
# ── Limpeza do que o ensaio criou ─────────────────────────────────────────
# Sem isto, o ensaio mensal deixa doze instancias cobrando por hora ligada.
aws ssm put-parameter --name "$PARAMETRO" --value "$(terraform output -raw endereco_banco_primario)" --overwrite
aws rds delete-db-instance --db-instance-identifier "$ALVO" \
--skip-final-snapshot --delete-automated-backups
As duas formas de trapacear num ensaio, e como o roteiro as impede
A primeira é marcar a detecção imediatamente, porque você sabia a hora — isso apaga a maior parcela do RTO e produz um número bonito e falso. O roteiro espera uma confirmação humana do sinal, e se não existe sinal ele obriga a escrever ao lado que o valor é estimativa. A segunda é fechar a conta do RPO em T4, quando a instância ficou disponível, em vez de T5, quando a aplicação passou a escrever nela: isso subestima o RPO em exatamente o tempo de repontar, que é o que se quer medir.
A permissão que restaura, e o curinga que fica
O papel do operador de desastre é separado do papel da aplicação, porque restaurar não é uma capacidade que a API precise ter. Dentro dele há dois curingas com razão escrita: o do destino da restauração, que ainda não existe quando a política é escrita e por isso é limitado ao prefixo das instâncias de ensaio; e o das ações de descrição do RDS, que são de escopo de conta. O segundo é o tipo de curinga que você deve reconferir na referência de ações do IAM antes de copiar para outra conta — essa lista muda.
# ensaio.tf — o papel de quem restaura, e por que o curinga aqui é honesto
data "aws_caller_identity" "atual" {}
resource "aws_iam_role" "operador_de_desastre" {
name = "${var.projeto}-operador-desastre"
# Assumido por quem conduz o ensaio e pela máquina de estados. Papel separado
# do papel da aplicação: restaurar não é uma capacidade que a API precise ter.
assume_role_policy = data.aws_iam_policy_document.confia_operador.json
}
data "aws_iam_policy_document" "operador" {
# ── A restauração ─────────────────────────────────────────────────────────
statement {
actions = [
"rds:RestoreDBInstanceToPointInTime",
"rds:RestoreDBInstanceFromDBSnapshot",
]
# O DESTINO ainda não existe quando esta política é escrita: o ARN exato é
# desconhecido por construção, não por preguiça. O curinga cobre só o prefixo
# das instâncias de ensaio, e é o mais estreito possível aqui.
resources = [
"arn:aws:rds:${var.regiao}:${data.aws_caller_identity.atual.account_id}:db:${var.projeto}-ensaio-*",
aws_db_instance.principal.arn,
"arn:aws:rds:${var.regiao}:${data.aws_caller_identity.atual.account_id}:subgrp:*",
"arn:aws:rds:${var.regiao}:${data.aws_caller_identity.atual.account_id}:pg:*",
]
}
# ── A leitura de estado ───────────────────────────────────────────────────
statement {
actions = [
"rds:DescribeDBInstances",
"rds:DescribeDBInstanceAutomatedBackups",
"rds:DescribeDBSnapshots",
]
# As ações de descrição do RDS são de escopo de conta e não aceitam ARN de
# recurso. Confira na referência de ações do IAM antes de copiar para outra
# conta: essa lista muda, e o que hoje é curinga obrigatório pode virar
# opcional amanhã.
resources = ["*"]
}
# ── O corte, que é uma escrita de valor ───────────────────────────────────
statement {
actions = ["ssm:PutParameter", "ssm:GetParameter"]
resources = [aws_ssm_parameter.endereco_ativo.arn]
}
# ── A limpeza do que o ensaio criou ───────────────────────────────────────
statement {
actions = ["rds:DeleteDBInstance", "rds:AddTagsToResource"]
resources = [
"arn:aws:rds:${var.regiao}:${data.aws_caller_identity.atual.account_id}:db:${var.projeto}-ensaio-*",
]
# Deliberadamente SEM o banco de produção na lista. A instância de ensaio é
# descartável; a de produção tem proteção de exclusão ligada e não deve
# sequer aparecer no alcance deste papel.
}
}
# ── A indireção: o único valor que o corte altera ─────────────────────────
resource "aws_ssm_parameter" "endereco_ativo" {
name = "/${var.projeto}/banco/endereco-ativo"
description = "Host do banco que a aplicação deve usar. Trocar aqui é o corte."
type = "String"
value = aws_db_instance.principal.address
lifecycle {
# O corte de desastre escreve neste parâmetro FORA do Terraform. Sem isto, o
# próximo `apply` desfaria o corte e devolveria a aplicação ao banco
# danificado — que é o pior defeito possível nesta arquitetura.
ignore_changes = [value]
}
}
resource "aws_cloudwatch_log_group" "ensaio" {
name = "/aws/vendedlogs/states/${var.projeto}-ensaio"
retention_in_days = 90 # os marcos do ensaio são o histórico de RTO da equipe
tags = local.tags
}
Implantar, e provar com números
Cinco provas. Cada uma tem comando, resultado que aprova e o que a reprovação significa — e nenhuma delas depende de acreditar em nada.
| Prova | Comando central | Resultado que aprova | O que a reprovação significa |
|---|---|---|---|
| 1 · o piso do RPO é a serra de cinco minutos | `describe-db-instances` lendo `LatestRestorableTime`, três amostras | defasagem entre 0 e cerca de 300 s, oscilando | defasagem sempre acima de 300 s indica envio de log falhando; verifique o estado da instância, porque o backup não ocorre fora do estado disponível |
| 2 · a retenção é o teto para trás | `describe-db-instance-automated-backups` lendo `RestoreWindow` | `EarliestTime` a 14 dias do agora | janela de 24 h significa que a retenção continua em 1: o `apply` não passou, ou passou em outra instância |
| 3 · restaurar cria endpoint novo | comparar `Endpoint.Address` da origem e da restaurada | duas cadeias diferentes, e a origem intacta | se você esperava o mesmo endereço, o seu plano de recuperação tem um passo que não existe — e é o passo que ninguém cronometrou |
| 4 · disponível não é pronta | `describe-db-instances` lendo `StorageOperationPercentProgress` | valor abaixo de 100 nos primeiros minutos após `available` | ausência dos dois campos significa inicialização concluída; se o p99 está ruim mesmo assim, a causa é o grupo de parâmetros padrão, não a carga de blocos |
| 5 · o RTO e o RPO medidos | `ensaiar-restauracao.sh` do começo ao fim | dois números em segundos, publicados como métrica | se um marco não foi gravado, o número não existe — e um ensaio sem marco é uma história, não uma medição |
# ── Prova 3 · nao existe restaurar sobre a instancia atual ───────────────
# Nao ha argumento para isso em nenhuma das duas operacoes. A propria API
# recusa o destino que ja existe, e a recusa e a prova:
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier app-postgres \
--target-db-instance-identifier app-postgres \
--use-latest-restorable-time 2>&1 | tail -1
# An error occurred (DBInstanceAlreadyExists) ...
# E os dois enderecos, lado a lado:
aws rds describe-db-instances \
--query "DBInstances[?starts_with(DBInstanceIdentifier, 'app-postgres') || \
starts_with(DBInstanceIdentifier, 'ffv-lab-ensaio')]\
.[DBInstanceIdentifier,Endpoint.Address]" --output table
# ── Prova 4 · disponivel nao e pronta ───────────────────────────────────
for i in $(seq 1 10); do
aws rds describe-db-instances --db-instance-identifier "$ALVO" \
--query "DBInstances[0].[DBInstanceStatus,StorageOperationStatus,\
StorageOperationPercentProgress]" --output text
sleep 60
done
# Na Cadencia: available / Initializing / 34 ... 61 ... 88 ... e depois os dois
# campos desaparecem, que e como a API diz "terminou". A leitura de
# `pedido_item` no primeiro minuto levou 4,1 s; depois de aquecida, 180 ms.
# ── Prova 5 · o snapshot manual sobrevive e o automatico nao ────────────
# Faca numa instancia DESCARTAVEL, nunca na de producao.
aws rds create-db-snapshot --db-instance-identifier descartavel \
--db-snapshot-identifier manual-de-teste
aws rds wait db-snapshot-completed --db-snapshot-identifier manual-de-teste
aws rds delete-db-instance --db-instance-identifier descartavel \
--skip-final-snapshot --delete-automated-backups
aws rds wait db-instance-deleted --db-instance-identifier descartavel
# O manual continua la:
aws rds describe-db-snapshots --snapshot-type manual \
--query "DBSnapshots[?DBInstanceIdentifier=='descartavel'].DBSnapshotIdentifier"
# ["manual-de-teste"]
# O automatico, nao:
aws rds describe-db-instance-automated-backups \
--query "DBInstanceAutomatedBackups[?DBInstanceIdentifier=='descartavel']"
# [] <- e com --no-delete-automated-backups esta lista teria uma entrada,
# valida pelo prazo que a retencao tinha na hora da exclusao.O resultado que aprova o laboratório
Dois números publicados como métrica, com a data do ensaio, e a comparação com o que estava acordado. Na Cadência: RTO medido de 4.620 s contra 1.800 s acordados, e RPO medido de 4.140 s contra 300 s acordados. Reprovado nos dois eixos — e é esse resultado, não um verde, que torna o laboratório útil. Um ensaio que aprova de primeira quase sempre ensaiou o caminho fácil.
Quebrar de propósito: três falhas do dia da restauração
As três acontecem depois de a restauração ter dado certo, que é o que as torna caras: o time já respirou, e agora tem um banco perfeito ao qual ninguém consegue chegar.
| Falha provocada | Sintoma | Onde olhar | Causa | Correção |
|---|---|---|---|---|
| Restaurar sem passar o grupo de segurança | a aplicação registra tempo esgotado na conexão; nenhum erro no banco | `describe-db-instances` no campo `VpcSecurityGroups` da instância nova | a documentação é explícita: sem escolher outros, a instância restaurada recebe a VPC, o grupo de sub-redes e o grupo de segurança PADRÃO | passar `--vpc-security-group-ids` e `--db-subnet-group-name` na chamada; corrigir depois é `modify-db-instance`, e custa mais minutos de RTO |
| Restaurar para um instante anterior à última rotação da senha | a conexão abre e a autenticação falha; a senha do segredo está certa e não funciona | a data da última rotação no segredo, contra o instante alvo da restauração | a senha do usuário mestre vive DENTRO do banco. A instância restaurada tem a que valia no instante alvo; o segredo tem a atual. A rotação automática é de sete dias por padrão, então uma restauração de nove dias atrás cai nisso com frequência | na instância restaurada, `modify-db-instance --manage-master-user-password --apply-immediately` gera um segredo novo, e o parâmetro da aplicação passa a apontar para ele |
| Restaurar sem passar o grupo de parâmetros | a aplicação conecta, funciona, e o p99 quadruplica; o pool do L07 esgota | `describe-db-instances` no campo `DBParameterGroups`, e a contagem de conexões | a instância restaurada recebe o grupo de parâmetros PADRÃO, que não tem nenhum ajuste seu — nem `max_connections`, nem `work_mem`, nem o que o L07 calibrou | passar `--db-parameter-group-name`; se esqueceu, aplicar depois exige reinício, o que acrescenta uma janela nova ao RTO |
A senha que gira: um achado DERIVADO, não copiado da documentação
A documentação afirma duas coisas separadas: que o RDS gira o segredo do usuário mestre a cada sete dias por padrão, e que a opção de gerenciar a senha no Secrets Manager durante uma restauração para um instante está disponível apenas para RDS for Oracle. Somando as duas com o fato de que a senha de um papel do PostgreSQL vive nos arquivos de dados, segue que a instância restaurada carrega a senha do instante alvo e não recebe segredo gerenciado próprio. A conclusão é uma DERIVAÇÃO nossa, não uma frase da AWS: confirme no seu ambiente antes de escrever o roteiro em cima dela, restaurando para um instante anterior à última rotação e tentando autenticar.
A falha que este laboratório não consegue provocar, e por que ela importa
Não há como ensaiar o esgotamento da janela de retenção sem esperar dias. O sintoma dela é o mais definitivo de todos: o comando de restauração recusa o instante pedido, e não existe caminho de volta — o log daquele dia foi apagado, e aumentar a retenção agora só afeta o futuro. É por isso que a prova 2 existe: ler a janela é a única forma de saber, de véspera, o que você não vai conseguir desfazer.
Uma inconsistência de dado é descoberta no fechamento contábil e a auditoria mostra que ela começou 9 dias atrás. A instância RDS foi criada por Terraform sem declarar `backup_retention_period`. Qual é a situação?
Segurança: o backup é o alvo, não o refúgio
Num incidente de ransomware, a primeira coisa que se procura é o backup — porque quem apaga a cópia elimina a alternativa ao pagamento. Isso inverte a lógica de segurança desta arquitetura: o cofre não é onde o dado se esconde, é o que precisa de mais proteção que a produção.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Credencial com permissão ampla apaga instância e backups | média | perda total do dado recuperável | cofre em outra conta com trava; `delete_automated_backups = false`; proteção de exclusão | CloudTrail em `DeleteDBInstance` e `DeleteRecoveryPoint`, com alarme sobre a métrica de chamada da API — não confirme o nome de uma métrica de CloudWatch específica sem checar a referência atual do AWS Backup | restaurar do cofre da conta de backup, que a credencial comprometida não alcança |
| Ransomware cifra o conteúdo do banco | baixa | indisponibilidade prolongada e possível extorsão | trava do cofre; chave da conta de backup separada da de produção | escrita anômala em volume por transação; alarme de crescimento súbito de tamanho de tabela | corte completo para o instante anterior ao início da cifragem — é o caminho que este módulo cronometra |
| Snapshot manual compartilhado por engano | baixa | exposição de dado pessoal a outra conta | snapshot cifrado com chave gerenciada por você: snapshot cifrado e compartilhado não é restaurável sem a chave | AWS Config na regra de snapshot público; Security Hub | remover o compartilhamento e girar a chave; avaliar notificação conforme a LGPD |
| Instância restaurada nasce acessível publicamente | média | banco de produção exposto na internet por minutos | `--no-publicly-accessible` e grupo de segurança explícitos na chamada de restauração | AWS Config na regra de instância pública; varredura de porta 5432 | `modify-db-instance` retirando o acesso público, e revisar o roteiro que omitiu o argumento |
| Restauração feita numa conta de desenvolvimento para "investigar" | alta | cópia de dado pessoal de produção num ambiente sem controle | papel do operador de desastre restrito à conta de produção; política de chave que não autoriza a conta de desenvolvimento | CloudTrail em `RestoreDBInstance*` fora da conta esperada | apagar a cópia, registrar o incidente de privacidade e mascarar o dado antes de qualquer investigação futura |
| Trava em conformidade com retenção longa num cofre de laboratório | média | custo de armazenamento sem saída até o fim do ciclo | usar governança fora de ambiente regulado; nunca usar retenção do tipo "sempre" | `describe-backup-vault` no campo `LockDate` antes de a carência vencer | remover a trava DURANTE a carência; depois dela, não há remoção — nem por você, nem pela AWS |
A separação que faz a trava valer alguma coisa
Trava de cofre na mesma conta e sob os mesmos administradores reduz risco de erro operacional, e é honesto dizer que ela não resolve administrador comprometido em modo de governança — porque quem tem a permissão de IAM remove a trava. O que fecha essa porta é uma das duas: o modo de conformidade, com o preço de ser irreversível, ou a separação de conta, que troca "posso apagar?" por "consigo alcançar?". Este laboratório usa a segunda, porque ela é reversível e o efeito prático é próximo.
Observabilidade: as perguntas que o painel tem de responder
Duas das perguntas abaixo não têm métrica nativa, e essa é uma informação de projeto, não uma reclamação: o RPO atual e o tempo desde o último ensaio precisam ser calculados e publicados. Sem isso, as duas voltam a ser respondidas de memória.
- Qual é o meu RPO neste instante, em segundos?
- Quantos dias atrás eu ainda consigo chegar?
- O backup de hoje rodou, ou falhou em silêncio?
- Alguém removeu ponto de recuperação nas últimas 24 horas?
- Quanto tempo passou desde o último ensaio de restauração concluído?
- Existe banco de produção sem etiqueta de backup, portanto fora do plano?
- Há ponto de recuperação expirado que o AWS Backup não conseguiu apagar e continua cobrando?
| Pergunta | Sinal | Limiar inicial | Por que este sinal, e não outro |
|---|---|---|---|
| Qual é o meu RPO agora? | métrica publicada `FFV/Desastre RpoSegundos`, de um cálculo agendado que lê `LatestRestorableTime` | alarme acima de 900 s por 15 min | a AWS não expõe o atraso do log como métrica; o valor tem de ser calculado, e ele oscila entre zero e cerca de cinco minutos |
| Quantos dias atrás eu chego? | `EarliestTime` de `RestoreWindow`, publicado como dias | alarme abaixde 13 dias, com requisito de 14 | a retenção configurada pode divergir da janela real quando a instância passou tempo parada, porque backup não ocorre com a instância parada |
| O backup rodou? | `AWS/Backup NumberOfBackupJobsFailed` | qualquer valor diferente de zero em 1 h | a causa mais comum de falha é o trabalho colidir com a janela de manutenção do banco, e o erro diz isso explicitamente |
| Alguém apagou ponto de recuperação? | CloudTrail em `DeleteRecoveryPoint` e `DeleteDBSnapshot` | qualquer remoção não originada do ciclo de vida | a trilha diz quem pediu; alarme de EventBridge sobre o próprio evento do CloudTrail substitui a necessidade de uma métrica de contagem separada |
| Há quanto tempo não ensaiamos? | métrica publicada `FFV/Desastre DiasDesdeOEnsaio` | alarme acima de 45 dias, com ensaio mensal | ensaio que não gera métrica volta a ser lembrança, e lembrança não dispara alarme |
| Banco fora do plano? | controle do AWS Backup Audit Manager para recurso protegido por plano | qualquer recurso não conforme | a seleção por etiqueta é o que permite escalar o plano, e o risco simétrico dela é o banco que nasce sem etiqueta |
| Backup expirado cobrando? | `AWS/Backup NumberOfRecoveryPointsExpired` | qualquer valor diferente de zero | a documentação é direta: você é cobrado pelo armazenamento que o backup expirado consome, e ele precisa ser apagado à mão |
"""Publica o RPO e a janela de retencao como metrica, a cada 5 minutos.
Existe porque nenhuma das duas grandezas tem metrica nativa. O calculo e
trivial; o valor esta em ele acontecer sem ninguem pedir.
"""
import os
from datetime import datetime, timezone
import boto3
RDS = boto3.client("rds")
CW = boto3.client("cloudwatch")
INSTANCIA = os.environ["INSTANCIA"]
def handler(evento, contexto):
agora = datetime.now(timezone.utc)
inst = RDS.describe_db_instances(DBInstanceIdentifier=INSTANCIA)["DBInstances"][0]
# Ausente enquanto a instancia nao estiver disponivel: backup nao ocorre
# fora do estado `available`, e tratar a ausencia como zero mentiria.
ultimo = inst.get("LatestRestorableTime")
if ultimo is None:
return {"publicado": False, "motivo": inst["DBInstanceStatus"]}
rpo = (agora - ultimo).total_seconds()
janela = RDS.describe_db_instance_automated_backups(
DBInstanceIdentifier=INSTANCIA)["DBInstanceAutomatedBackups"][0]["RestoreWindow"]
dias = (agora - janela["EarliestTime"]).total_seconds() / 86400
CW.put_metric_data(Namespace="FFV/Desastre", MetricData=[
{"MetricName": "RpoSegundos", "Value": rpo, "Unit": "Seconds"},
{"MetricName": "DiasRecuperaveis", "Value": dias, "Unit": "Count"},
])
return {"publicado": True, "rpoSegundos": rpo, "diasRecuperaveis": dias}O alarme que não existe, e a razão de ele não existir
Não há como alarmar "o dado está corrompido", porque corrupção lógica é semanticamente idêntica a trabalho legítimo — é a mesma razão pela qual a replicação não a filtra. O mais próximo que se consegue é volume de escrita fora do padrão: linhas afetadas por transação, taxa de exclusão por tabela, crescimento súbito de uma tabela. É um sinal indireto, produz falso positivo em migração legítima, e ainda assim é o que separa 34 minutos de detecção de 4.
Escala: 10 GB, 500 GB, 5 TB e a falha de zona
O que escala mal aqui não é o backup: é a restauração. A retenção custa armazenamento e nada mais; o corte custa tempo, e o tempo cresce com o volume. Em alguma ordem de grandeza a resposta deixa de ser "restaurar" e passa a ser "ter capacidade em pé", e essa transição é a decisão de arquitetura desta seção.
| Ordem de grandeza | O que acontece com o RTO | O que muda no desenho | Quando trocar de abordagem |
|---|---|---|---|
| 10 GB de dado | restauração em poucos minutos; o RTO é dominado por detectar e repontar | nada: este desenho é o certo | nunca — otimizar aqui é otimizar a parcela pequena |
| 180 GB (o caso deste módulo) | restauração de dezenas de minutos, medida em 19 no ensaio; detectar e repontar somam mais | indireção no endereço e detecção de escrita anômala | quando o requisito de RTO ficar abaixo do tempo de restauração medido |
| 500 GB | a carga de blocos do S3 passa a doer depois do corte, não antes dele | aquecimento explícito das tabelas do caminho crítico antes do corte, e classe maior na instância restaurada | quando o p99 pós-corte quebrar o acordo de nível de serviço por mais de alguns minutos |
| 5 TB | a restauração deixa de caber numa janela aceitável de recuperação | réplica em pé promovida para o caminho de perda de instância, e restauração reservada ao incidente lógico; Aurora com retrocesso para o lógico rápido | quando manter capacidade parada custar menos que o tempo fora do ar — a conta é aritmética, não gosto |
| Falha de zona, em qualquer volume | a implantação em duas zonas troca o registro de DNS para a cópia síncrona; o nome do banco não muda, e a documentação registra failover tipicamente entre 60 e 120 s | nada aqui: é o mecanismo do L01/L07 | nunca confunda com este módulo — failover mantém o endpoint, restauração cria um novo |
| Perda da região inteira | este desenho não responde | cópia do ponto de recuperação para outra região e infraestrutura pronta lá | é o L58, e ele depende deste: sem RTO local medido, o número entre regiões é chute multiplicado |
O ajuste de tempo de vida de DNS que quase ninguém faz, e que custa minutos
Tanto o failover de zona quanto o corte para a instância restaurada dependem de a aplicação resolver um nome novo. A documentação da AWS traz esse aviso para ambiente Java, onde a máquina virtual pode cachear resolução de DNS por tempo indeterminado, e recomenda no máximo 60 segundos. Em .NET o efeito equivalente aparece no descritor de serviço reutilizado e no pool de conexões: é por isso que o corte deste módulo troca a fonte de dados inteira, e não confia em o nome resolver de novo.
Custo: o que este laboratório acrescenta à fatura
Três dimensões, e a primeira tem uma fórmula documentada que explica por que a fatura de backup de muita gente é zero — e por que ela deixa de ser no dia em que a retenção aumenta.
| Cenário | Retenção | O que cobra | Onde o dinheiro vai |
|---|---|---|---|
| Protótipo | 1 dia, sem AWS Backup, sem ensaio | praticamente nada de backup | a franquia do armazenamento alocado cobre tudo — e a sensação de que backup é grátis nasce aqui |
| Este laboratório | 14 dias, cofre em outra conta, ensaio mensal | armazenamento acima da franquia, cofre do AWS Backup, e a instância do ensaio por hora ligada | a maior linha nova é a instância do ensaio, e ela é proporcional a quanto tempo você esquece de apagá-la |
| Produção com auditoria | 35 dias, cópia entre regiões, snapshot mensal por 12 meses | armazenamento em duas regiões, transferência da cópia, armazenamento frio | a transferência da primeira cópia entre regiões surpreende, porque ela é completa; as seguintes são incrementais |
O custo oculto deste laboratório, em três formas
Primeiro: a instância do ensaio cobra por hora ligada, e um ensaio interrompido no meio deixa uma instância de produção inteira de pé — o roteiro apaga por isso, e a etiqueta de efêmero existe para a varredura encontrar as sobreviventes. Segundo: snapshot manual não expira, então o mensal de auditoria precisa de uma regra de ciclo de vida, senão ele acumula por anos. Terceiro, e o pior: cofre travado em modo de conformidade cobra armazenamento até o ciclo de vida terminar, e não há como interromper — nem apagando a trava, nem abrindo caso de suporte. Nenhum preço aparece 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.
Well-Architected nos seis pilares
| Pilar | Situação depois deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | roteiro cronometrado, agendado mensalmente, com marcos publicados como métrica | o roteiro pressupõe que quem o roda entende cada passo; sob pressão isso falha | transformar o roteiro em máquina de estados com aprovação humana só nos pontos de decisão | alta |
| Segurança | cofre em outra conta com trava de governança, chave separada, papel de desastre apartado do papel da aplicação | governança é removível por quem tem permissão de IAM suficiente | modo de conformidade em ambiente regulado, com o custo irreversível entendido e aceito | média |
| Confiabilidade | RTO e RPO medidos, e a distinção entre falha de zona e falha de conteúdo escrita e provada | o número medido vale para o volume de hoje; ele envelhece com o crescimento do banco | publicar o RTO medido junto do volume alocado, e alarmar quando o volume crescer além do que foi ensaiado | alta |
| Eficiência de desempenho | aquecimento explícito das tabelas do caminho crítico antes do corte | a carga de blocos do S3 continua degradando o p99 nos primeiros minutos | classe maior na instância restaurada durante a recuperação, reduzida depois | média |
| Otimização de custo | instância do ensaio efêmera e etiquetada, retenção derivada de requisito | snapshot manual mensal acumula, e cofre travado não libera | regra de ciclo de vida no snapshot manual, e revisão trimestral do que está no cofre | média |
| Sustentabilidade | nada de capacidade parada só para recuperação: o desenho troca tempo por custo de propósito | ensaio mensal consome uma instância inteira por algumas horas | ensaiar com classe menor quando o objetivo é validar o dado, e com a classe real só quando o objetivo é medir o RTO | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro, e é a segunda coluna que raramente se escreve.
Retenção de um dia porque ninguém declarou, nenhum snapshot manual, nenhuma restauração já feita. RTO e RPO escritos no documento por analogia.Retenção derivada de requisito, cofre em outra conta com trava, endereço em parâmetro, roteiro cronometrado mensal, RTO e RPO medidos e publicados.Sinal de escrita anômala por tabela, auditoria de banco datando cada comando de escrita, e modo somente-leitura acionável por valor.Capacidade em pé: réplica promovível para perda de instância. Migrar para Aurora ganha PITR mais rápido, porque a restauração aponta para o armazenamento compartilhado em vez de copiar disco — mas não ganha retrocesso: esse recurso existe só na variante compatível com MySQL, não em Aurora PostgreSQL, que é o motor deste laboratório.Política de backup na organização, cofre com trava em conformidade, Backup Audit Manager com controles, e recuperação entre regiões ensaiada (L58).O histórico de ensaios e de trabalhos de backup vira dado: prever o RTO em função do volume e da classe, e classificar escrita anômala com base no comportamento normal de cada tabela.A ordem não é negociável, e o motivo é aritmético
Investir no nível 4 antes do nível 3 é comprar capacidade parada para encurtar uma parcela de 19 minutos enquanto a de 34 continua intacta. O ensaio do nível 2 é o que revela essa proporção, e é por isso que ele vem antes de qualquer decisão de gasto. Quem pula direto para réplica promovível quase sempre está resolvendo a parcela errada, com a fatura maior.
Onde IA entra nesta arquitetura, e onde não entra
IA não decide RTO nem RPO. Os dois são medição e negócio: um cronômetro e uma conversa sobre quanto dado a empresa aceita perder. Um modelo não melhora nenhum dos dois, e colocá-lo aqui seria exatamente o hype que esta série existe para não vender.
Há um lugar onde IA acrescentaria valor real neste módulo, e ele é a maior parcela do RTO: a detecção. Trinta e quatro dos 77 minutos foram gastos esperando a operação ligar. Um detector de escrita anômala — linhas afetadas por transação, taxa de exclusão por tabela, desvio do comportamento normal de cada tabela ao longo do dia e da semana — encurta essa parcela, e é a única alavanca deste módulo que reduz o RTO e o RPO ao mesmo tempo.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | perceber escrita destrutiva anômala em minutos, em vez de esperar a reclamação da operação |
| Por que uma regra não bastaria? | uma regra bastaria para começar, e deve vir primeiro: "exclusão que afete mais de mil linhas numa transação em `pedido_item`" pega o incidente deste módulo inteiro. A regra falha no caso difuso — cifragem gradual, atualização em massa que parece migração — e é aí que aprender o comportamento normal de cada tabela passa a valer |
| De onde viriam os dados? | auditoria de banco por tabela, métricas de escrita do CloudWatch, e o histórico de migrações legítimas para o modelo saber o que NÃO é anomalia |
| Qual o risco? | falso positivo que congela a escrita durante uma migração planejada, o que é um incidente de disponibilidade criado pela proteção; exige caminho manual de dispensa e aviso ao plantão em vez de bloqueio automático |
| Por que não agora? | porque a Cadência tem um incidente de histórico. Modelo de anomalia treinado num exemplo é superstição com aparência de estatística — comece pela regra, meça quantos falsos positivos ela dá, e só então pergunte se um modelo faz melhor |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "analisar o log e escolher o instante de restauração" troca uma aritmética exata por um palpite confiante. O instante alvo vem da auditoria do banco, com precisão de segundo, e errá-lo custa minutos de RPO de forma irreversível. Onde existe registro determinístico, um modelo só acrescenta latência e a chance de errar com segurança. O mesmo vale para "resumir se a restauração deu certo": a validação é uma invariante de negócio em SQL, e ela responde sim ou não.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma real | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Retenção que ninguém escolheu | porque o exemplo mínimo de Terraform não menciona `backup_retention_period`, e o banco sobe funcionando | o padrão da API é um dia, e ele passa a ser o teto de quão atrás você chega — uma decisão de continuidade tomada por omissão | "achei que tínhamos 30 dias"; a janela real tem 24 horas | derivar dos dias do incidente mais antigo que se quer poder desfazer, e validar no plano | ambiente descartável de teste, onde perder o dado é indiferente |
| Tratar duas zonas como resposta a backup | porque as duas palavras aparecem na mesma tela do console e as duas prometem proteção | a cópia síncrona aplica a transação destrutiva no mesmo commit: proteção de disponibilidade não filtra conteúdo | failover provocado e a linha continua ausente | duas zonas para perda de zona, recuperação para um instante para perda de conteúdo — e as duas ao mesmo tempo | nunca como substituto; sempre como complemento |
| Endereço do banco em variável de ambiente da definição de task | porque é a menor quantidade de linhas que funciona, e no dia normal funciona bem | transforma repontar num deploy inteiro, e coloca 11 minutos no RTO num momento em que ninguém tem paciência para pipeline | a restauração terminou às 15:11 e a aplicação só usou o banco novo às 15:22 | parâmetro consultado em execução, com recarga de pool no processo | aplicação de lote que pode ser reiniciada, onde o deploy não custa nada |
| `skip_final_snapshot = true` no Terraform | porque sem isso o `destroy` pede um nome e falha, e a pessoa quer o `destroy` limpo | combinado com `delete_automated_backups = true`, que é o padrão do provider, um `destroy` na conta errada não deixa nada | a instância sumiu e não há de onde restaurar | snapshot final com nome versionado, e proteção de exclusão ligada em produção | ambiente efêmero criado e destruído no mesmo pipeline |
| Ensaiar na base de teste de 200 MB | porque é rápido, barato e não assusta ninguém | o número que sai não vale para 180 GB: a etapa de restauração e a carga de blocos escalam com o volume | "ensaiamos e deu 4 minutos"; no incidente real foram 19 só nessa etapa | ensaiar no volume de produção, com a classe de produção, e registrar as duas coisas junto do número | validar o roteiro pela primeira vez — e aí o objetivo é o roteiro, não o número |
| Fechar a conta do RPO no instante em que a instância ficou disponível | porque é o marco visível, o que o console mostra e o que a espera da CLI devolve | subestima o RPO em exatamente o tempo de repontar, que é a parcela que o ensaio existe para revelar | RPO relatado de 60 min quando o real foi 69 | fechar em T5, quando a aplicação confirma que passou a escrever no banco novo | nunca; é a diferença entre medir e se enganar com método |
| Trava de cofre em conformidade no ambiente de laboratório | porque parece a opção mais segura, e "mais seguro" soa sempre correto | passada a carência de três dias, nem o usuário raiz nem a AWS removem a trava enquanto houver ponto de recuperação, e você paga até o ciclo terminar | cofre de laboratório cobrando armazenamento por meses, sem forma de interromper | governança em laboratório e em produção não regulada; conformidade só onde a regulação exige e o custo foi aceito | ambiente sujeito a retenção obrigatória por lei, com o prazo dimensionado |
| Restaurar direto por cima, achando que sobrescreve | porque é o modelo mental de restaurar um arquivo, e ninguém lê a página antes do incidente | não existe: a operação sempre cria instância nova, e descobrir isso na hora leva a improvisar renomeação de instância sob pressão | "por que ainda tem o banco antigo aí?" no meio do incidente | planejar o corte como parte do roteiro, com o endereço externalizado | nunca |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| A restauração recusa o instante pedido | o instante está fora da janela de retenção, ou depois do instante mais recente restaurável | comparar o alvo com `EarliestTime` e `LatestTime` | `describe-db-instance-automated-backups` no campo `RestoreWindow` | escolher instante dentro da janela; se o incidente é mais antigo, o caminho passa a ser o snapshot manual mensal |
| A instância nova sobe e a aplicação registra tempo esgotado | grupo de segurança padrão, que não autoriza a porta 5432 vinda do grupo da task | ler os grupos associados e comparar com os da origem | `describe-db-instances` no campo `VpcSecurityGroups` | passar `--vpc-security-group-ids` na chamada; corrigir depois com `modify-db-instance` e contar os minutos extras no RTO |
| A conexão abre e a autenticação falha | a senha do usuário mestre girou depois do instante alvo, e a instância restaurada tem a antiga | comparar a data da última rotação do segredo com o instante alvo | metadados do segredo no Secrets Manager e o campo `MasterUserSecret` da origem | `modify-db-instance --manage-master-user-password --apply-immediately` na instância restaurada, e apontar o parâmetro da aplicação para o segredo novo |
| O p99 quadruplica depois do corte | grupo de parâmetros padrão, ou carga de blocos do S3 ainda em andamento | ler o grupo de parâmetros e o progresso de inicialização, nesta ordem | `DBParameterGroups` e `StorageOperationPercentProgress` | grupo de parâmetros explícito na restauração; aquecer com varredura completa das tabelas do caminho crítico antes do corte |
| O trabalho de backup falha todos os dias no mesmo horário | o trabalho colide com a janela de manutenção do banco ou está muito perto dela | ler a mensagem de erro, que nomeia a causa explicitamente | `AWS/Backup NumberOfBackupJobsFailed` e o detalhe do trabalho no console | mover a regra de snapshot para fora da janela de manutenção; a janela de manutenção permanece sob seu controle, o AWS Backup não a administra |
| A retenção mudou no console e volta ao valor antigo no dia seguinte | o AWS Backup passou a administrar o backup contínuo, e o plano é a fonte da verdade | verificar se existe plano com backup contínuo cobrindo a instância | planos do AWS Backup e o nome do ponto de recuperação, que termina em `awsbackup` | alterar a retenção no plano, não no RDS; devolver o controle ao RDS exige apagar o plano e o ponto de recuperação contínuo |
| Ligar o backup pela primeira vez causou parada | sair de retenção zero para diferente de zero causa indisponibilidade, e a documentação avisa | ler o histórico de eventos da instância | eventos do RDS na instância | planejar essa mudança específica para a janela de manutenção; mudanças entre dois valores não nulos se aplicam sem parada |
| O parâmetro do endereço voltou ao banco antigo sozinho | um `terraform apply` desfez o corte, porque o parâmetro é administrado pelo Terraform | ler o plano do último `apply` procurando alteração no parâmetro | estado do Terraform e histórico do parâmetro no Parameter Store | `ignore_changes = [value]` no recurso do parâmetro — sem isso, o próximo `apply` devolve a aplicação ao banco danificado, que é o pior defeito possível aqui |
Limpeza: o que o destroy não leva
Este laboratório é o que deixa mais rastro cobrando da série até aqui, e por um motivo estrutural: quase tudo que ele cria existe justamente para sobreviver à destruição do resto. Snapshot, backup retido e cofre travado são recursos cujo propósito é não sair no destroy.
# 0. A proteção de exclusão do banco impede o destroy. Isto e de proposito:
# desligue deliberadamente, e so quando tiver certeza da conta.
aws sts get-caller-identity --query Account --output text # confira ANTES
aws rds modify-db-instance --db-instance-identifier app-postgres \
--no-deletion-protection --apply-immediately
# 1. Derrube o que o Terraform administra. Ele vai criar o snapshot final,
# porque `skip_final_snapshot = false` — e o snapshot final NAO expira.
terraform destroy -auto-approve
# 2. INSTANCIAS DE ENSAIO orfas. Um ensaio interrompido no meio deixa uma
# instancia do tamanho da producao cobrando por hora ligada.
aws rds describe-db-instances \
--query "DBInstances[?starts_with(DBInstanceIdentifier, 'ffv-lab-ensaio')]\
.[DBInstanceIdentifier,DBInstanceStatus,AllocatedStorage]" --output table
aws rds delete-db-instance --db-instance-identifier ffv-lab-ensaio-XXXX \
--skip-final-snapshot --delete-automated-backups
# 3. SNAPSHOTS MANUAIS, incluindo o final. Sao os unicos que NAO expiram:
# ficam cobrando GB-mes ate alguem apagar, e o limite e 100 por regiao.
aws rds describe-db-snapshots --snapshot-type manual \
--query "DBSnapshots[].[DBSnapshotIdentifier,AllocatedStorage,SnapshotCreateTime]" \
--output table
aws rds delete-db-snapshot --db-snapshot-identifier app-postgres-final-v1
# 4. BACKUPS AUTOMATICOS RETIDOS. Com delete_automated_backups = false eles
# sobrevivem a instancia e expiram sozinhos — mas cobram ate expirar, e o
# limite e 40 por regiao.
aws rds describe-db-instance-automated-backups \
--query "DBInstanceAutomatedBackups[].[DBInstanceIdentifier,DbiResourceId,Status]" \
--output table
aws rds delete-db-instance-automated-backup --dbi-resource-id db-XXXXXXXX
# 5. PONTOS DE RECUPERACAO NO COFRE. Se a trava for de governanca, isto
# funciona com a permissao certa. Se for de CONFORMIDADE e a carencia
# venceu, nao funciona para ninguem — nem para voce, nem para a AWS.
aws backup describe-backup-vault --backup-vault-name ffv-lab-cofre \
--query "[Locked,LockDate,NumberOfRecoveryPoints]" --output text
aws backup list-recovery-points-by-backup-vault --backup-vault-name ffv-lab-cofre \
--query "RecoveryPoints[].RecoveryPointArn" --output text
aws backup delete-recovery-point --backup-vault-name ffv-lab-cofre \
--recovery-point-arn "arn:aws:backup:..."
aws backup delete-backup-vault-lock-configuration --backup-vault-name ffv-lab-cofre
aws backup delete-backup-vault --backup-vault-name ffv-lab-cofre # so se vazio
# 6. GRUPO DE LOGS do ensaio: tem retencao propria e nao pertence ao ciclo da
# maquina de estados. Continua cobrando ingestao retida depois de tudo apagado.
aws logs delete-log-group --log-group-name /aws/vendedlogs/states/ffv-lab-ensaio
# 7. E o que veio do L01 e do L02 e cobra por hora ligada: balanceador (ALB),
# endpoint de VPC, repositorio ECR com imagem, e — se voce optou por saida
# para a internet — NAT Gateway com o Elastic IP dele, que cobra justamente
# por NAO estar associado a nada.
aws ec2 describe-addresses --query "Addresses[?AssociationId==null].PublicIp" --output table
aws ec2 describe-nat-gateways --query "NatGateways[?State=='available'].NatGatewayId" --output table
aws ec2 describe-vpc-endpoints --query "VpcEndpoints[].VpcEndpointId" --output table
aws elbv2 describe-load-balancers --query "LoadBalancers[].LoadBalancerArn" --output table
aws ecr describe-repositories --query "repositories[].repositoryName" --output table
# 8. Prova final: nada com o nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Instância RDS de produção | só depois de desligar a proteção de exclusão | sim, por hora ligada | a proteção existe para isto: obrigar um passo deliberado |
| Snapshot final | não — o destroy o CRIA | sim, GB-mês | é o único que não expira; é o objetivo dele |
| Backups automáticos retidos | não, com `delete_automated_backups = false` | sim, GB-mês até expirar | duram o prazo que a retenção tinha na exclusão, e o limite é 40 por região |
| Snapshots manuais (mensais de auditoria) | não | sim, GB-mês | não expiram nunca; precisam de regra de ciclo de vida ou de faxina agendada |
| Cofre do AWS Backup | só se vazio | sim, GB-mês | apagar o cofre exige apagar cada ponto de recuperação antes |
| Trava do cofre em conformidade | NUNCA depois da carência | sim, até o ciclo terminar | nem o usuário raiz nem a AWS a removem enquanto houver ponto de recuperação dentro |
| Instância do ensaio | sim, se o roteiro terminou | sim, por hora ligada e do tamanho da produção | ensaio interrompido no meio deixa a instância de pé — procure pela etiqueta de efêmero |
| Grupo de logs da máquina de estados | depende de `skip_destroy` | sim, retenção | tem ciclo próprio e sobrevive ao recurso que o alimentava |
| Balanceador, endpoint de VPC e ECR (L01) | sim | sim, por hora e por GB-mês | vêm do laboratório anterior; se você não vai seguir, rode a limpeza do L01 também |
O único recurso desta série que você pode não conseguir apagar
Cofre travado em modo de conformidade, depois de a carência vencer e com ponto de recuperação dentro. A documentação da AWS é explícita: a trava é imutável e não pode ser alterada nem removida por nenhum usuário, incluindo o raiz, nem pela AWS. O armazenamento cobra até o ciclo de vida de cada ponto de recuperação terminar. É por isso que a linha de `changeable_for_days` está comentada no Terraform deste laboratório, e é a única linha da série que vem com um aviso desses.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Não sei se consigo restaurar | roteiro cronometrado com marcos | medir é a única forma de o RTO deixar de ser uma frase no documento |
| Não sei quanto dado perco | a conta do RPO fechada no corte, não na disponibilidade | a parcela grande é a janela do banco danificado, e só o corte a encerra |
| Duas zonas não desfizeram o DELETE | recuperação para um instante | replicação propaga transação; recuperação para um instante volta no tempo |
| O incidente é mais antigo que a janela | snapshot manual mensal no cofre | o contínuo tem teto de 35 dias, e auditoria é outra pergunta |
| Credencial comprometida apaga tudo | cofre em outra conta com trava | muda a pergunta de "posso apagar?" para "consigo alcançar?" |
| Repontar levou 11 minutos | endereço em parâmetro e recarga de pool | transforma um deploy inteiro numa escrita de valor |
| O RPO cresce enquanto ninguém percebe | modo somente-leitura na detecção | congela a janela danificada sem depender de nada na AWS |
| A instância nova não aceitou conexão | grupo de segurança e sub-rede explícitos | por omissão a restauração usa os padrões da conta, e isso é documentado |
| Disponível e ainda lenta | aquecimento das tabelas do caminho crítico | os blocos continuam vindo do S3 depois de a instância ficar disponível |
| O ensaio envelheceu | regra mensal e métrica publicada | ensaio voluntário tem meia-vida de dois trimestres |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Perda de zona de disponibilidade | implantação em duas zonas, com failover de 60 a 120 s | nada de corrupção de conteúdo: o DELETE é replicado no mesmo commit |
| Perda da instância primária | failover, ou promoção da réplica de leitura do L07 | o dado apagado, que a réplica também apagou |
| Corrupção lógica dos últimos 14 dias | recuperação para um instante | incidente mais antigo que a janela de retenção |
| Incidente mais antigo que 35 dias | snapshot manual mensal | o instante exato: você volta ao primeiro do mês, não às 14:07 daquele dia |
| Exclusão da instância por engano | proteção de exclusão e backup retido | exclusão do próprio backup por quem tem permissão ampla |
| Credencial comprometida apagando backup | cofre em outra conta com trava | conta AWS fechada: após 90 dias a AWS apaga o conteúdo mesmo com trava |
| Perda da região inteira | nada aqui | é o L58: cópia entre regiões e infraestrutura pronta do outro lado |
- O incidente acontece dentro do banco, como uma transação válida.
- A implantação em duas zonas e a réplica replicam o dano com fidelidade.
- Alguém detecta — e essa parcela é a maior do RTO, e não está na AWS.
- A escrita é congelada, o que impede o RPO de continuar crescendo.
- O instante alvo sai da auditoria do banco, com precisão de segundo.
- A restauração cria uma instância NOVA, com sub-rede, grupo de segurança e parâmetros declarados.
- A instância fica disponível, e os blocos continuam vindo do S3 em segundo plano.
- As tabelas do caminho crítico são aquecidas com uma varredura completa.
- O corte é uma troca de valor no parâmetro, e a aplicação confirma o host novo.
- A invariante de negócio é conferida, e os dois números vão para a métrica.
Perguntas frequentes
❓ Qual é o RPO real do point-in-time recovery no RDS?
❓ Restaurar um snapshot do RDS sobrescreve o banco atual?
❓ Multi-AZ do RDS serve como backup?
❓ O que acontece com os snapshots automáticos quando eu apago a instância RDS?
❓ Quanto tempo leva para restaurar um banco de dados RDS?
❓ Qual a diferença entre snapshot manual e snapshot automático no RDS?
❓ Como o AWS Backup Vault Lock protege backup de credencial comprometida?
❓ Por quantos dias o RDS deixa fazer point-in-time recovery?
Fixando
Num ensaio de restauração, a instância nova ficou disponível 19 minutos depois do comando, e o RTO total medido foi de 77 minutos. Onde estão os outros 58 minutos, na ordem em que mais pesam?
Uma credencial com permissão ampla na conta de produção é comprometida. O atacante apaga a instância RDS pedindo exclusão dos backups automáticos e sem snapshot final. O que ainda permite recuperar o dado?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar (ECS Fargate, RDS em duas zonas, ALB), L07 (réplica de leitura e pool Npgsql), AWS CLI v2, psql e Terraform básicos |
| Conhecimentos adquiridos | recuperação para um instante como snapshot mais log; a distinção entre a cadência de cinco minutos e a janela de retenção; que restaurar cria instância nova com endpoint novo e grupos padrão; a decomposição do RTO em cinco parcelas e a conta do RPO fechada no corte; snapshot automático contra manual na exclusão; cofre com trava em governança contra conformidade |
| Entregável verificável | uma restauração cronometrada com RTO e RPO medidos em segundos, publicados como métrica no CloudWatch, com o arquivo de marcos e a comparação contra o valor acordado |
| Limitação que fica | o número medido vale para o volume e a classe de hoje, e para zona única no ensaio; a detecção continua dependendo de alguém reclamar; e o corte descarta a escrita da janela danificada, o que só é aceitável porque as escritas da Cadência são idempotentes por chave de pedido |
| Próximo exemplo recomendado | L58 — recuperação entre regiões. Ele depende deste, porque estratégia entre regiões sem RTO local medido é chute multiplicado por dois |
| Também habilitado por este módulo | a indireção no endereço do banco é pré-requisito prático de qualquer troca de banco sem parada, incluindo migração de versão e mudança de classe |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Restoring a DB instance to a specified time for Amazon RDS — a cadência de cinco minutos do log, o campo do instante mais recente restaurável e a carga de blocos em segundo plano depois de a instância ficar disponível; Introduction to backups e Backup retention period — o intervalo de 0 a 35 dias, os padrões diferentes de API e console, a parada ao sair de zero e a fórmula do armazenamento cobrado; Retaining automated backups — o prazo dos backups retidos, o limite de 40 por região e a recomendação de tirar o snapshot final mesmo assim; Restoring to a DB instance — que não se restaura sobre instância existente, e que os grupos de segurança, de sub-redes e de parâmetros vêm nos valores padrão quando não declarados; Password management with Amazon RDS and AWS Secrets Manager — a rotação de sete dias e a restrição da opção de senha gerenciada na restauração; Failing over a Multi-AZ DB instance for Amazon RDS — a troca do registro de DNS, o failover entre 60 e 120 segundos e o aviso sobre tempo de vida de cache de DNS; e, no guia do AWS Backup, Continuous backups and point-in-time recovery, AWS Backup Vault Lock e AWS Backup metrics with Amazon CloudWatch — o teto de 35 dias do contínuo, os dois modos de trava com a carência mínima de três dias, e os nomes exatos das métricas. Nenhum preço aparece 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 tempos deste módulo são de uma medição de exemplo, não referências: 19 minutos até a instância ficar disponível para 180 GB alocados em zona única, 34 minutos de detecção, 11 minutos de repontamento no desenho sem indireção. A AWS não garante duração de restauração, e ela varia com volume, classe e tipo de armazenamento — o produto deste laboratório é o SEU número, não o nosso. Além disso, uma afirmação central da seção de falhas é DERIVAÇÃO e não citação: a conclusão de que a instância PostgreSQL restaurada carrega a senha do usuário mestre válida no instante alvo, e não recebe segredo gerenciado próprio, junta três fatos documentados separados. Confirme restaurando para um instante anterior à última rotação antes de escrever um roteiro em cima disso.
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…