Backup CronJob: also archive filestore (/var/lib/odoo) to S3 alongside the SQL dump
Pairs each <TS>.sql.gz with a <TS>.filestore.tar.gz under the same prefix; rotation prunes both together. Backup pod runs on the same node as Odoo (podAffinity) and mounts the filestore PVC read-only — RWO permits multiple pods on the same node, so this is safe. Restore (Tower-side) reads the companion key from S3, scales Odoo to 0, restores DB + filestore, and scales Odoo back up.
This commit is contained in:
@@ -37,6 +37,24 @@ spec:
|
|||||||
odoosky.io/role: backup
|
odoosky.io/role: backup
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
|
# Land on the same node as the running Odoo pod so the backup
|
||||||
|
# container can mount the filestore PVC. The PVC is RWO,
|
||||||
|
# which K8s reads as "one pod per node" — multiple pods on
|
||||||
|
# the SAME node can mount the same volume simultaneously,
|
||||||
|
# so this is safe and gives the backup direct read access
|
||||||
|
# to /var/lib/odoo without disturbing Odoo.
|
||||||
|
affinity:
|
||||||
|
podAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/instance: {{ .Values.instance.code | quote }}
|
||||||
|
odoosky.io/role: odoo
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
volumes:
|
||||||
|
- name: filestore
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "instance.fullname" . }}-odoo
|
||||||
containers:
|
containers:
|
||||||
- name: pgdump-s3
|
- name: pgdump-s3
|
||||||
# postgres:16-alpine + `apk add aws-cli` — alpine's
|
# postgres:16-alpine + `apk add aws-cli` — alpine's
|
||||||
@@ -49,6 +67,10 @@ spec:
|
|||||||
# client/server protocol always lines up.
|
# client/server protocol always lines up.
|
||||||
image: "{{ .Values.postgres.image }}:{{ .Values.postgres.tag }}"
|
image: "{{ .Values.postgres.image }}:{{ .Values.postgres.tag }}"
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
|
volumeMounts:
|
||||||
|
- name: filestore
|
||||||
|
mountPath: /var/lib/odoo
|
||||||
|
readOnly: true
|
||||||
env:
|
env:
|
||||||
- name: PGHOST
|
- name: PGHOST
|
||||||
value: {{ include "instance.fullname" . }}-pg
|
value: {{ include "instance.fullname" . }}-pg
|
||||||
@@ -98,24 +120,44 @@ spec:
|
|||||||
# element of the pipe failing fails the whole thing.
|
# element of the pipe failing fails the whole thing.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
KEY="${S3_PREFIX}/${TS}.sql.gz"
|
SQL_KEY="${S3_PREFIX}/${TS}.sql.gz"
|
||||||
echo ">>> dumping to s3://${S3_BUCKET}/${KEY}"
|
FS_KEY="${S3_PREFIX}/${TS}.filestore.tar.gz"
|
||||||
if ! command -v aws >/dev/null 2>&1; then
|
if ! command -v aws >/dev/null 2>&1; then
|
||||||
apk add --no-cache aws-cli >/dev/null
|
apk add --no-cache aws-cli tar >/dev/null
|
||||||
fi
|
fi
|
||||||
|
echo ">>> dumping DB to s3://${S3_BUCKET}/${SQL_KEY}"
|
||||||
pg_dump --format=plain --clean --if-exists --no-owner --no-acl \
|
pg_dump --format=plain --clean --if-exists --no-owner --no-acl \
|
||||||
| gzip -9 \
|
| gzip -9 \
|
||||||
| aws --endpoint-url "$S3_ENDPOINT" s3 cp - "s3://${S3_BUCKET}/${KEY}"
|
| aws --endpoint-url "$S3_ENDPOINT" s3 cp - "s3://${S3_BUCKET}/${SQL_KEY}"
|
||||||
echo ">>> uploaded"
|
echo ">>> archiving filestore to s3://${S3_BUCKET}/${FS_KEY}"
|
||||||
echo ">>> rotating: keep last $RETAIN under ${S3_PREFIX}/"
|
# Tar the filestore tree from /var/lib/odoo. If the
|
||||||
|
# dir is empty (fresh instance) we still upload an
|
||||||
|
# empty tar so the snapshot is paired — restore code
|
||||||
|
# treats absent filestore object as "no filestore
|
||||||
|
# captured for this snapshot" (older backups).
|
||||||
|
if [ -d /var/lib/odoo ] && [ -n "$(ls -A /var/lib/odoo 2>/dev/null)" ]; then
|
||||||
|
tar -czf - -C /var/lib/odoo . \
|
||||||
|
| aws --endpoint-url "$S3_ENDPOINT" s3 cp - "s3://${S3_BUCKET}/${FS_KEY}"
|
||||||
|
else
|
||||||
|
echo "(filestore empty; skipping archive)"
|
||||||
|
fi
|
||||||
|
echo ">>> rotating: keep last $RETAIN snapshots under ${S3_PREFIX}/"
|
||||||
|
# Group keys by timestamp prefix (everything before
|
||||||
|
# the first dot after the date) and prune the oldest
|
||||||
|
# groups. Both .sql.gz and .filestore.tar.gz share
|
||||||
|
# the same timestamp prefix, so groups stay paired.
|
||||||
aws --endpoint-url "$S3_ENDPOINT" s3api list-objects-v2 \
|
aws --endpoint-url "$S3_ENDPOINT" s3api list-objects-v2 \
|
||||||
--bucket "$S3_BUCKET" --prefix "${S3_PREFIX}/" \
|
--bucket "$S3_BUCKET" --prefix "${S3_PREFIX}/" \
|
||||||
--query 'Contents[].Key' --output text 2>/dev/null \
|
--query 'Contents[].Key' --output text 2>/dev/null \
|
||||||
| tr '\t' '\n' | sort -r | tail -n +$((RETAIN + 1)) \
|
| tr '\t' '\n' \
|
||||||
| while read OLDKEY; do
|
| grep -E '\.sql\.gz$' \
|
||||||
[ -n "$OLDKEY" ] || continue
|
| sort -r | tail -n +$((RETAIN + 1)) \
|
||||||
echo ">>> deleting old: $OLDKEY"
|
| while read OLDSQL; do
|
||||||
aws --endpoint-url "$S3_ENDPOINT" s3 rm "s3://${S3_BUCKET}/${OLDKEY}"
|
[ -n "$OLDSQL" ] || continue
|
||||||
|
OLDFS="${OLDSQL%.sql.gz}.filestore.tar.gz"
|
||||||
|
echo ">>> deleting: $OLDSQL + $OLDFS (if present)"
|
||||||
|
aws --endpoint-url "$S3_ENDPOINT" s3 rm "s3://${S3_BUCKET}/${OLDSQL}" || true
|
||||||
|
aws --endpoint-url "$S3_ENDPOINT" s3 rm "s3://${S3_BUCKET}/${OLDFS}" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
echo ">>> done"
|
echo ">>> done"
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
Reference in New Issue
Block a user