# Backup diario do Postgres do tenant `plataforma` (pg_dump cifrado com age, ver P2.4) apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pg-backups namespace: plataforma-prod spec: accessModes: [ReadWriteOnce] storageClassName: local-path resources: requests: storage: 5Gi --- # ConfigMap com o DESTINATARIO age (chave PUBLICA). # # Publica pode ser versionada: ela so cifra. Quem tem a publica NAO abre o # backup. A privada e gerada por scripts/backup/gerar_chave_backup.sh na maquina # de quem restaura, e NUNCA chega neste repositorio nem no VPS. # # O valor abaixo e um marcador: substitua pelo destinatario real antes de # aplicar. Com o marcador, o backup_tenant.sh recusa rodar em vez de gravar em # claro — e o comportamento certo. apiVersion: v1 kind: ConfigMap metadata: name: pg-backup-chave namespace: plataforma-prod data: # Chave PUBLICA do age. Versionar isto e seguro e proposital: ela so serve # para CIFRAR. Abrir um backup exige a privada, que por desenho nao esta # neste repositorio nem no servidor — se estivesse no VPS, quem invadisse a # maquina levaria a chave junto com o arquivo cifrado, e o backup cifrado # nao teria servido para nada. # # Gerada em 2026-09-17 por `scripts/backup/gerar_chave_backup.sh`, na maquina # do operador. ANOTE A DATA: rotacionar exige manter a chave antiga enquanto # houver backup cifrado com ela dentro da retencao de 7 dias. destinatario: "age1g37gc3gvq0zn7802pqh5dd0m3pkvc4z3g2hqk5w3dgvnaz3hwsrq7ps04y" --- apiVersion: batch/v1 kind: CronJob metadata: name: pg-backup namespace: plataforma-prod spec: schedule: "0 2 * * *" # diariamente 02:00 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 concurrencyPolicy: Forbid jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: # Imagem propria: postgres:16 + age. O dump precisa ser cifrado # ANTES de sair do processo, entao as duas ferramentas tem de estar # no mesmo container. Construida de scripts/backup/Containerfile. - name: pg-backup image: docker.io/library/atm-pg-backup:1.3 env: - name: PGPASSWORD valueFrom: secretKeyRef: { name: db-credentials, key: password } - name: CHAVE_PUBLICA valueFrom: configMapKeyRef: { name: pg-backup-chave, key: destinatario } # ── QUANDO ESTE TENANT MIGRAR PARA O BANCO COMPARTILHADO (P4.7) # # Estas tres linhas mudam JUNTO com a migracao, e nao antes: # # PGHOST postgres -> pg-compartilhado # PGUSER atm -> r_ # DATABASES athleticmap ... -> athleticmap_ # # O `keycloak` sai da lista se o Keycloak tambem for compartilhado # (P4.8, card 156) — ate la ele continua no banco do tenant. # # E `PGHOST` aponta para o POSTGRES, e nao para o PgBouncer: o # `pg_dump` abre uma transacao unica e longa, que e exatamente o # que transaction pooling nao serve. Pelo bouncer, um dump grande # seguraria uma conexao do pool pelo tempo inteiro do backup. - { name: PGHOST, value: "postgres" } - { name: PGUSER, value: "atm" } - { name: DATABASES, value: "athleticmap keycloak" } - { name: DESTINO, value: "/backups" } - { name: RETENCAO_DIAS, value: "7" } # O envio para fora. O script se cala quando o Secret nao existe, e # falha ALTO quando existe e nao funciona — sao coisas diferentes, # e um CronJob que falha toda madrugada por falta de credencial # ensina todo mundo a ignorar o unico alerta que nao se pode ignorar. - { name: COMANDO_ENVIO, value: "enviar_offsite.sh {}" } # Um prefixo POR ORIGEM no bucket: sem ele todos gravavam em `athleticmap/` com o # mesmo nome de arquivo — nao se sabia de quem era o dump, e dois no mesmo segundo # se sobrescreveriam (visto em 01/10/2026). - { name: OFFSITE_PREFIXO, value: "athleticmap/plataforma" } # `optional: true`: enquanto o Secret `backup-offsite` nao existir, o # CronJob roda igual e guarda local. No dia em que ele existir, o # envio liga sozinho — sem tocar neste arquivo. envFrom: - secretRef: { name: backup-offsite, optional: true } command: ["backup_tenant.sh"] volumeMounts: - { name: backups, mountPath: /backups } volumes: - name: backups persistentVolumeClaim: claimName: pg-backups