{{- if .Values.backups.enabled -}} # Daily dump → S3. # # Architecture: pg_dump in postgres:alpine, pipe through gzip, then # `aws s3 cp -` to push the stream straight to MEGA S4. We use a # single multi-stage shell command (no init container) so the dump # never lands on the customer-server's local disk — the instance # data and the backup destination are deliberately separated. # # AWS credentials come from a K8s Secret (default `s3-backup-creds`) # provisioned out-of-band by Tower's bootstrap. Endpoint + bucket + # prefix are committed in this file's values; only the access/secret # pair lives in the Secret. apiVersion: batch/v1 kind: CronJob metadata: name: {{ include "instance.fullname" . }}-backup labels: {{- include "instance.labels" . | nindent 4 }} odoosky.io/role: backup spec: schedule: {{ .Values.backups.schedule | quote }} concurrencyPolicy: Forbid successfulJobsHistoryLimit: 5 failedJobsHistoryLimit: 3 jobTemplate: metadata: labels: {{- include "instance.labels" . | nindent 8 }} odoosky.io/role: backup spec: backoffLimit: 1 template: metadata: labels: {{- include "instance.labels" . | nindent 12 }} odoosky.io/role: backup spec: restartPolicy: Never containers: - name: pgdump-s3 # postgres:16-alpine + `apk add aws-cli` — alpine's # aws-cli package is ~30 MB and adds ~5 s to the first # job run on each node. Subsequent runs reuse the # already-installed binary because we keep the same # image (containerd's layer cache covers the apk index # download). This matches the postgres version of the # cluster's actual database container, so pg_dump's # client/server protocol always lines up. image: "{{ .Values.postgres.image }}:{{ .Values.postgres.tag }}" imagePullPolicy: IfNotPresent env: - name: PGHOST value: {{ include "instance.fullname" . }}-pg - name: PGUSER valueFrom: secretKeyRef: name: {{ include "instance.fullname" . }}-pg key: POSTGRES_USER - name: PGPASSWORD valueFrom: secretKeyRef: name: {{ include "instance.fullname" . }}-pg key: POSTGRES_PASSWORD - name: PGDATABASE valueFrom: secretKeyRef: name: {{ include "instance.fullname" . }}-pg key: POSTGRES_DB - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: {{ .Values.backups.credentialsSecret }} key: AWS_ACCESS_KEY_ID - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: {{ .Values.backups.credentialsSecret }} key: AWS_SECRET_ACCESS_KEY - name: S3_ENDPOINT value: {{ .Values.backups.s3.endpoint | quote }} - name: AWS_DEFAULT_REGION value: {{ .Values.backups.s3.region | quote }} - name: S3_BUCKET value: {{ .Values.backups.s3.bucket | quote }} - name: S3_PREFIX value: {{ .Values.instance.code | quote }} - name: RETAIN value: {{ .Values.backups.retain | quote }} command: - /bin/sh - -c - | # `pipefail` is critical: without it, a failed # pg_dump piped into `aws s3 cp` would still produce # a 0-exit "successful" job (the cp succeeded # uploading empty/garbage data). With pipefail any # element of the pipe failing fails the whole thing. set -euo pipefail TS=$(date -u +%Y%m%dT%H%M%SZ) KEY="${S3_PREFIX}/${TS}.sql.gz" echo ">>> dumping to s3://${S3_BUCKET}/${KEY}" if ! command -v aws >/dev/null 2>&1; then apk add --no-cache aws-cli >/dev/null fi pg_dump --format=plain --clean --if-exists --no-owner --no-acl \ | gzip -9 \ | aws --endpoint-url "$S3_ENDPOINT" s3 cp - "s3://${S3_BUCKET}/${KEY}" echo ">>> uploaded" echo ">>> rotating: keep last $RETAIN under ${S3_PREFIX}/" aws --endpoint-url "$S3_ENDPOINT" s3api list-objects-v2 \ --bucket "$S3_BUCKET" --prefix "${S3_PREFIX}/" \ --query 'Contents[].Key' --output text 2>/dev/null \ | tr '\t' '\n' | sort -r | tail -n +$((RETAIN + 1)) \ | while read OLDKEY; do [ -n "$OLDKEY" ] || continue echo ">>> deleting old: $OLDKEY" aws --endpoint-url "$S3_ENDPOINT" s3 rm "s3://${S3_BUCKET}/${OLDKEY}" done echo ">>> done" resources: requests: cpu: 100m memory: 256Mi limits: cpu: "1" memory: 1Gi {{- end }}