feat(backup): envio para fora do servidor (atm-pg-backup:1.2 com rclone)

This commit is contained in:
deploy
2026-09-21 23:13:35 +00:00
parent 07682c2859
commit 5cae175d97
5 changed files with 165 additions and 68 deletions
+43 -20
View File
@@ -11,13 +11,30 @@ spec:
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: acme-prod
data:
destinatario: "SUBSTITUA_PELO_DESTINATARIO_age1"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-backup
namespace: acme-prod
spec:
schedule: "0 2 * * *"
schedule: "0 2 * * *" # diariamente 02:00
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid
@@ -27,30 +44,36 @@ spec:
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: postgres:16
image: docker.io/library/atm-pg-backup:1.0
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
command: ["/bin/sh", "-c"]
args:
- |
set -eo pipefail
echo "aguardando postgres ficar acessivel..."
for i in $(seq 1 30); do pg_isready -h postgres -U atm -t 3 >/dev/null 2>&1 && break; sleep 2; done
TS=$(date +%Y%m%d-%H%M%S)
for DB in athleticmap keycloak; do
echo "dump $DB ..."
pg_dump -h postgres -U atm -d "$DB" | gzip > "/backups/${DB}-${TS}.sql.gz"
done
find /backups -name '*.sql.gz' -mtime +7 -delete
echo "backups atuais:"; ls -lh /backups | tail -20
secretKeyRef: { name: db-credentials, key: password }
- name: CHAVE_PUBLICA
valueFrom:
configMapKeyRef: { name: pg-backup-chave, key: destinatario }
- { 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 {}" }
# `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
- { name: backups, mountPath: /backups }
volumes:
- name: backups
persistentVolumeClaim:
+43 -20
View File
@@ -11,13 +11,30 @@ spec:
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: demo-prod
data:
destinatario: "SUBSTITUA_PELO_DESTINATARIO_age1"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-backup
namespace: demo-prod
spec:
schedule: "0 2 * * *"
schedule: "0 2 * * *" # diariamente 02:00
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid
@@ -27,30 +44,36 @@ spec:
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: postgres:16
image: docker.io/library/atm-pg-backup:1.0
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
command: ["/bin/sh", "-c"]
args:
- |
set -eo pipefail
echo "aguardando postgres ficar acessivel..."
for i in $(seq 1 30); do pg_isready -h postgres -U atm -t 3 >/dev/null 2>&1 && break; sleep 2; done
TS=$(date +%Y%m%d-%H%M%S)
for DB in athleticmap keycloak; do
echo "dump $DB ..."
pg_dump -h postgres -U atm -d "$DB" | gzip > "/backups/${DB}-${TS}.sql.gz"
done
find /backups -name '*.sql.gz' -mtime +7 -delete
echo "backups atuais:"; ls -lh /backups | tail -20
secretKeyRef: { name: db-credentials, key: password }
- name: CHAVE_PUBLICA
valueFrom:
configMapKeyRef: { name: pg-backup-chave, key: destinatario }
- { 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 {}" }
# `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
- { name: backups, mountPath: /backups }
volumes:
- name: backups
persistentVolumeClaim:
+42 -19
View File
@@ -11,6 +11,23 @@ spec:
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: piloto-prod
data:
destinatario: "SUBSTITUA_PELO_DESTINATARIO_age1"
---
apiVersion: batch/v1
kind: CronJob
metadata:
@@ -27,30 +44,36 @@ spec:
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: postgres:16
image: docker.io/library/atm-pg-backup:1.0
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
command: ["/bin/sh", "-c"]
args:
- |
set -eo pipefail
echo "aguardando postgres ficar acessivel..."
for i in $(seq 1 30); do pg_isready -h postgres -U atm -t 3 >/dev/null 2>&1 && break; sleep 2; done
TS=$(date +%Y%m%d-%H%M%S)
for DB in athleticmap keycloak; do
echo "dump $DB ..."
pg_dump -h postgres -U atm -d "$DB" | gzip > "/backups/${DB}-${TS}.sql.gz"
done
find /backups -name '*.sql.gz' -mtime +7 -delete
echo "backups atuais:"; ls -lh /backups | tail -20
secretKeyRef: { name: db-credentials, key: password }
- name: CHAVE_PUBLICA
valueFrom:
configMapKeyRef: { name: pg-backup-chave, key: destinatario }
- { 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 {}" }
# `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
- { name: backups, mountPath: /backups }
volumes:
- name: backups
persistentVolumeClaim:
+25 -4
View File
@@ -65,15 +65,36 @@ spec:
- 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_<tenant>
# DATABASES athleticmap ... -> athleticmap_<tenant>
#
# 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" }
# COMANDO_ENVIO fica vazio ate haver credencial de armazenamento
# externo. Sem ele o backup fica cifrado, mas NO MESMO SERVIDOR —
# e o proprio script avisa isso no fim da execucao.
- { name: COMANDO_ENVIO, value: "" }
# 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 {}" }
# `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 }
@@ -21,7 +21,7 @@
#
# ── ANTES DE APLICAR ─────────────────────────────────────────────────────────
#
# 1. A imagem `atm-pg-backup:1.1` tem o `backup_tenant.sh` com `auto:` e ainda
# 1. A imagem `atm-pg-backup:1.2` tem o `backup_tenant.sh` com `auto:` e ainda
# NAO FOI CONSTRUIDA. Construir de scripts/backup/Containerfile e importar no
# k3s, como a 1.0. Com a 1.0, o CronJob falharia: ela nao entende `auto:`.
# 2. Zero tenants no compartilhado faz o job terminar com ERRO, de proposito
@@ -61,7 +61,7 @@ spec:
restartPolicy: OnFailure
containers:
- name: pg-backup
image: docker.io/library/atm-pg-backup:1.1
image: docker.io/library/atm-pg-backup:1.2
env:
# O admin do compartilhado: e o unico papel que le TODOS os
# databases. O `r_<tenant>` de cada cliente so ve o proprio.
@@ -77,9 +77,16 @@ spec:
- { name: DATABASES, value: "auto:athleticmap_" }
- { name: DESTINO, value: "/backups" }
- { name: RETENCAO_DIAS, value: "7" }
# Vazio pelo mesmo motivo do 50-backup: falta credencial de
# armazenamento externo. O script avisa no fim de cada rodada.
- { name: COMANDO_ENVIO, value: "" }
# 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 {}" }
# `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 }