← Voltar ao blog

Backup não significa Disaster Recovery

Backup existente não é Disaster Recovery: o que três restores reais revelaram sobre control plane, PersistentVolumes, imagens OCI e recuperação offline.

kuberneteshomelabdisaster-recovery

Eu tinha backups do meu cluster Kubernetes.

Então resolvi fazer a pergunta que normalmente é fácil adiar:

consigo realmente restaurá-los?

A resposta curta foi: não tão facilmente quanto parecia.

Essa talvez tenha sido a etapa mais valiosa do meu homelab K3s até agora, porque o exercício deixou de ser sobre criar backup e passou a ser sobre produzir um sistema executável em outra máquina.

Dois níveis de recuperação

Comecei separando o problema em duas partes.

O primeiro nível é o control plane: restaurar o datastore SQLite do K3s e o server token.

O segundo é o que passei a chamar de Full DR:

flowchart LR
    CP[Control plane] --> Full[Full DR]
    PV[Persistent Volumes] --> Full
    OCI[Imagens OCI offline] --> Full
    WL[Workloads executáveis] --> Full

Essa distinção parece óbvia depois de escrita. Antes do teste, é fácil pensar no backup do cluster como uma coisa única. Não é.

O alvo de DR precisa ser perigoso por design — e seguro por controle

Usei uma segunda máquina Debian 13, guionote-hp, como alvo de recuperação. A produção continuou funcionando no servidor guiosoft-info.

Restaurar o datastore de produção em outra máquina traz um risco importante: os workloads restaurados podem acreditar que estão em produção e tentar acessar serviços externos.

Por isso o alvo recebe uma barreira independente de nftables antes do restore. Ela permite loopback, LAN e redes internas do Kubernetes, mas rejeita o restante do tráfego IPv4/IPv6 de saída e forwarding.

flowchart LR
    DR[Cluster DR restaurado] --> LAN[LAN]
    DR --> Pods[10.42.0.0/16]
    DR --> Services[10.43.0.0/16]
    DR -. bloqueado .-> Internet((Internet))

Em outras palavras: eu quero que o cluster restaurado funcione o suficiente para ser examinado, mas não quero que ele converse com a Internet.

Além disso, os scripts destrutivos exigem marker de DR, recusam hostname/IP de produção e exigem confirmações explícitas.

Recentemente o reset do alvo ganhou ainda um modo de preflight não destrutivo. Eu validei o mesmo script contra produção e ele recusou a execução antes de qualquer alteração.

O primeiro problema: datastore não vive sozinho

Uma das primeiras descobertas foi que restaurar apenas banco e token enquanto se preservavam materiais TLS/credentials do cluster limpo podia produzir incompatibilidade de bootstrap.

A estratégia foi alterada: durante o restore, os materiais do target são preservados em uma área de segurança, o datastore é restaurado e o K3s reconstrói o bootstrap a partir do estado recuperado.

Esse é exatamente o tipo de detalhe que um arquivo de backup existente não revela. Só aparece quando alguém tenta inicializar o sistema restaurado.

O segundo problema: PersistentVolumes locais lembram do servidor antigo

Meu cluster usa local-path em single-node.

Depois de restaurar os dados físicos dos volumes no host DR, ainda havia um problema Kubernetes: os PVs recuperados possuíam node affinity para o hostname de produção e paths locais apontando para o storage da produção.

Esses campos não podem simplesmente ser corrigidos com um patch comum.

A transação que funcionou foi mais cuidadosa:

flowchart TD
    A[Writers em zero réplicas] --> B[ReclaimPolicy = Retain]
    B --> C[Preservar manifests]
    C --> D[Remover PVC/PV antigos]
    D --> E[Recriar PV com path e affinity do DR]
    E --> F[Recriar PVC pre-bound por volumeName]
    F --> G[Validar UID, claimRef e Bound]
    G --> H[Ativar node e workloads]

Também apareceu outro detalhe: Pods restaurados em Terminating ainda podiam referenciar PVCs protegidos. A solução segura não era arrancar finalizers indiscriminadamente, mas identificar e remover somente os Pods que bloqueavam a transação.

O terceiro problema: o cluster restaurado começa sem o node DR

Durante o restore isolado, o K3s precisa iniciar em modo agentless (disable-agent:true). Nesse momento, a API pode conter apenas o Node restaurado da produção. O novo node ainda não existe.

Isso significa que o remapeamento de PV precisa aceitar node affinity para um hostname futuro. O objeto Node será registrado apenas quando o agent for ativado depois.

Um dos meus scripts inicialmente assumia o contrário. O rehearsal encontrou a suposição e ela virou um novo invariante da automação.

O quarto problema: imagem listada não é necessariamente imagem recuperável

Eu também descobri que não deveria tratar o content store do containerd da produção como backup de imagens.

Durante o rehearsal, algumas imagens que apareciam como disponíveis/running não puderam ser exportadas porque blobs referenciados não estavam presentes da forma esperada.

A solução foi criar previamente um kit OCI específico para a plataforma linux/amd64, contendo as imagens necessárias e checksums gerados na origem.

No DR, essas imagens são colocadas no diretório nativo de preload do K3s:

/var/lib/rancher/k3s/agent/images/

E a validação é feita pelo CRI, não apenas pela listagem do ctr.

Isso tornou o recovery independente de registries externos — algo obrigatório porque o alvo permanece sem Internet.

GitHub também desaparece durante o desastre

Outro aprendizado simples: se a estratégia de segurança do DR bloqueia a Internet, o runbook não pode dizer “agora faça git pull”.

O recovery kit passou a carregar uma cópia autocontida e versionada dos scripts necessários, checksums, imagens OCI e material cifrado de recuperação.

A chave privada age e demais credenciais continuam fora do Git e precisam de recuperação independente.

Essa restrição mudou a maneira como passei a pensar sobre GitOps:

Git é uma excelente fonte de verdade, mas não deve ser a única mídia necessária para recuperar o sistema.

E os workloads?

No rehearsal completo, quatro workloads persistentes de observabilidade foram recuperados.

Grafana abriu seu SQLite restaurado. Loki recuperou checkpoint/WAL e índices locais. Prometheus abriu os blocos TSDB, fez replay de WAL/checkpoint e voltou a escrever novos blocos. Tempo também voltou a ficar Ready.

Em um dos rehearsals, Tempo descartou um bloco WAL incompleto que não possuía meta.json.

Isso foi outro alerta: uma cópia de filesystem pode ser recuperável sem ser necessariamente um snapshot consistente da aplicação.

Essa observação acabaria levando a uma etapa posterior do projeto: criar um recovery set com consistência limitada e mensurada.

O principal aprendizado

Antes do teste, eu tinha backups.

Depois do teste, eu tinha uma lista de suposições que estavam erradas:

Suposição Realidade encontrada
Backup do datastore não garante cluster executável
Dados do PV não garantem PV utilizável em outro node
Imagem no containerd não garante backup OCI confiável
Git disponível hoje não garante Git disponível durante DR
Script terminou com 0 não garante pós-condição satisfeita

Essa é a diferença que passei a enxergar entre backup e Disaster Recovery.

Backup é um artefato.

Disaster Recovery é uma capacidade que precisa ser exercitada.

Depois que consegui recuperar o ambiente, surgiu a pergunta seguinte: quanto tempo isso leva?

No próximo artigo vou mostrar como comecei a medir RTO de verdade, incluindo bootstrap, intervenção manual e os próprios erros encontrados durante o rehearsal — e como o tempo caiu de 1h07m09s para 47m03s.


Projeto: guionardo/guiosoft-k3s-lab.