Skip to content

Backup & Restore di un namespace OCP

Due strade, da scegliere in base a cosa deve sopravvivere al disastro.

kube-dumpOADP / Velero
Cosa salvasolo manifest YAMLmanifest + PV + snapshot CSI
Dovefilesystem locale / gitbucket S3 (ODF NooBaa o RGW)
Schedulazionecron sul bastionSchedule CR nativo
Restore selettivooc apply a mano--include-resources, --namespace-mappings
Prerequisitibinario + kubeconfigoperator + ObjectBucketClaim
Caso d’usonamespace stateless, snapshot pre-changeDR applicativo, namespace con PVC

Parte 1 — kube-dump

Installazione

Terminal window
curl -sLo /usr/local/bin/kube-dump https://raw.githubusercontent.com/WoozyMasta/kube-dump/master/kube-dump && chmod +x /usr/local/bin/kube-dump
Terminal window
kube-dump --help | head -30

Dipendenze: oc/kubectl, jq, yq. Opzionali ma consigliati fzf e git.

Dump base

Terminal window
kube-dump ns -n hn-apphub-be -d /tmp/backup-hn-apphub-be-$(date +%F)

Il risultato è <dest>/<namespace>/<resource-kind>/<name>.yaml, già ripulito da uid, resourceVersion, creationTimestamp, generation, managedFields, selfLink e status: gli YAML sono riapplicabili così come sono.

Escludere risorse

kube-dump non ha un flag di esclusione: -r / --kube-objects è inclusivo e per default enumera tutte le risorse namespaced restituite da api-resources. La lista va quindi costruita per sottrazione.

Solo gli events (il rumore peggiore, spesso centinaia di oggetti effimeri):

Terminal window
res=$(oc api-resources --verbs=list --namespaced -o name | grep -vE '^events(\.events\.k8s\.io)?$' | tr '\n' ' ')

Events + oggetti runtime che il controller rigenera da solo:

Terminal window
res=$(oc api-resources --verbs=list --namespaced -o name | grep -vE '^(events(\.events\.k8s\.io)?|pods|replicasets\.apps|endpoints|endpointslices\.discovery\.k8s\.io|controllerrevisions\.apps|podmetrics\.metrics\.k8s\.io)$' | tr '\n' ' ')
Terminal window
kube-dump ns -n hn-apphub-be -d /tmp/backup-hn-apphub-be-$(date +%F) -r "$res"

Flag utili

FlagEffetto
-n, --namespaceslista namespace (spazio o virgola)
-d, --destination-dirdirectory di output
-r, --kube-objectslista risorse namespaced da esportare
-k, --kube-cluster-objectsrisorse cluster-scoped (modalità cluster)
-f, --force-removesvuota la destination prima del dump
-s, --silentnessun output su stdout
-c, --detailedlog verboso

Script interattivo

Wrapper con menu di selezione namespace (multi-select via fzf, fallback su lista numerata), scelta dello scope risorse e destination con default /tmp/backup-$ns-$(date +%F).

#!/usr/bin/env bash
# kube-dump-menu.sh - interactive namespace backup wrapper around kube-dump
set -euo pipefail
TODAY="$(date +%F)"
EXCL_EVENTS='^events(\.events\.k8s\.io)?$'
EXCL_NOISE='^(events(\.events\.k8s\.io)?|pods|replicasets\.apps|endpoints|endpointslices\.discovery\.k8s\.io|controllerrevisions\.apps|podmetrics\.metrics\.k8s\.io|packagemanifests\.packages\.operators\.coreos\.com|bindings)$'
die() { printf '\e[31m[ERR]\e[0m %s\n' "$*" >&2; exit 1; }
info() { printf '\e[36m[*]\e[0m %s\n' "$*"; }
ok() { printf '\e[32m[OK]\e[0m %s\n' "$*"; }
CLI="$(command -v oc || command -v kubectl || true)"
[[ -n "$CLI" ]] || die "oc/kubectl not found in PATH"
command -v kube-dump >/dev/null || die "kube-dump not found in PATH"
"$CLI" auth can-i get namespaces >/dev/null 2>&1 || die "not logged in / no permission"
CTX="$("$CLI" config current-context 2>/dev/null || echo unknown)"
SRV="$("$CLI" whoami --show-server 2>/dev/null || echo unknown)"
list_resources() {
"$CLI" api-resources --verbs=list --namespaced -o name 2>/dev/null \
| grep -vE "$1" | sort -u | tr '\n' ' '
}
pick_namespaces() {
local all sel
mapfile -t all < <("$CLI" get ns -o custom-columns=NAME:.metadata.name --no-headers | sort)
[[ ${#all[@]} -gt 0 ]] || die "no namespaces returned"
if command -v fzf >/dev/null; then
mapfile -t NAMESPACES < <(printf '%s\n' "${all[@]}" | fzf --multi --height=70% --border \
--prompt='namespace> ' --header='TAB = multi-select, ENTER = confirm')
else
echo; printf '%s\n' "${all[@]}" | nl -w4 -s') '; echo
read -rp "Namespace: numbers (es. 3 7 12) or names: " -a sel
NAMESPACES=()
for s in "${sel[@]}"; do
if [[ "$s" =~ ^[0-9]+$ ]]; then NAMESPACES+=("${all[$((s-1))]}"); else NAMESPACES+=("$s"); fi
done
fi
[[ ${#NAMESPACES[@]} -gt 0 ]] || die "no namespace selected"
for ns in "${NAMESPACES[@]}"; do
"$CLI" get ns "$ns" >/dev/null 2>&1 || die "namespace not found: $ns"
done
}
pick_resources() {
echo
cat <<'EOF'
Resource scope:
1) skip events only
2) skip events + runtime noise (pods, rs, endpoints, endpointslices) [default]
3) everything (kube-dump default)
4) custom list (es. "deployments services configmaps secrets")
EOF
read -rp "> " choice
case "${choice:-2}" in
1) RESOURCES="$(list_resources "$EXCL_EVENTS")" ;;
2) RESOURCES="$(list_resources "$EXCL_NOISE")" ;;
3) RESOURCES="" ;;
4) read -rp "Resources: " RESOURCES ;;
*) die "invalid choice" ;;
esac
}
pick_destination() {
local def
if [[ ${#NAMESPACES[@]} -eq 1 ]]; then def="/tmp/backup-${NAMESPACES[0]}-${TODAY}"
else def="/tmp/backup-multi-${TODAY}"; fi
echo
read -rep "Destination dir [$def]: " DEST
DEST="${DEST:-$def}"
if [[ -d "$DEST" && -n "$(ls -A "$DEST" 2>/dev/null)" ]]; then
read -rp "$DEST exists and is not empty - wipe it? [y/N] " a
[[ "$a" =~ ^[Yy]$ ]] && FORCE=1 || FORCE=0
else FORCE=0; fi
mkdir -p "$DEST"
}
main() {
echo; info "cluster : $SRV"; info "context : $CTX"
pick_namespaces; pick_resources; pick_destination
echo "-------------------------------------------------"
echo " namespaces : ${NAMESPACES[*]}"
echo " destination: $DEST"
echo " resources : $([[ -n "$RESOURCES" ]] && echo "$(wc -w <<<"$RESOURCES") kinds" || echo 'all')"
echo " wipe first : $([[ $FORCE -eq 1 ]] && echo yes || echo no)"
echo "-------------------------------------------------"
read -rp "Proceed? [Y/n] " go
[[ "${go:-Y}" =~ ^[Nn]$ ]] && { echo "aborted"; exit 0; }
local args=(ns -n "${NAMESPACES[*]}" -d "$DEST")
[[ -n "$RESOURCES" ]] && args+=(-r "$RESOURCES")
[[ $FORCE -eq 1 ]] && args+=(-f)
info "kube-dump ${args[*]}"
kube-dump "${args[@]}" 2>&1 | tee "$DEST/kube-dump.log"
ok "files: $(find "$DEST" -name '*.yaml' | wc -l) yaml - size: $(du -sh "$DEST" | cut -f1)"
read -rp "Create tarball ${DEST}.tgz? [y/N] " t
if [[ "$t" =~ ^[Yy]$ ]]; then
tar czf "${DEST}.tgz" -C "$(dirname "$DEST")" "$(basename "$DEST")"
ok "$(ls -lh "${DEST}.tgz" | awk '{print $9, $5}')"
fi
}
main "$@"
Terminal window
install -m 0755 kube-dump-menu.sh /usr/local/bin/kube-dump-menu && kube-dump-menu

Restore da dump

Terminal window
oc new-project hn-apphub-be
Terminal window
oc apply -n hn-apphub-be -f /tmp/backup-hn-apphub-be-2026-09-02/hn-apphub-be/ --recursive

Pulizia rapida prima dell’apply:

Terminal window
find /tmp/backup-hn-apphub-be-2026-09-02 -type f \( -name 'builder.yaml' -o -name 'deployer.yaml' -o -name 'default.yaml' -o -name '*-dockercfg-*' -o -name '*-token-*' -o -name 'kube-root-ca.crt.yaml' -o -name 'openshift-service-ca.crt.yaml' \) -delete

Versionamento su git

Terminal window
kube-dump ns -n hn-apphub-be -d /var/lib/kube-dump --git-branch main --git-commit-user "svc-backup" --git-commit-email "middlewareadmins.it@accenture.com" --git-push

Cron sul bastion:

Terminal window
echo '0 3 * * * root /usr/local/bin/kube-dump ns -n hn-apphub-be -d /var/lib/kube-dump -s' > /etc/cron.d/kube-dump

Parte 2 — OADP / Velero con backend ODF

Prerequisiti

Terminal window
oc get clusterversion && oc get storagecluster -n openshift-storage
Terminal window
oc get sc

1. Install operator

Da GUI: Operators → OperatorHub → “OADP” → OADP Operator (Red Hat) → Install → namespace openshift-adp → channel stable-1.5 → Automatic.

Canali disponibili sul catalog:

Terminal window
oc get packagemanifest redhat-oadp-operator -n openshift-marketplace -o jsonpath='{range .status.channels[*]}{.name}{"\n"}{end}'

Da CLI:

apiVersion: v1
kind: Namespace
metadata:
name: openshift-adp
---
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: oadp
namespace: openshift-adp
spec:
targetNamespaces: [openshift-adp]
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: redhat-oadp-operator
namespace: openshift-adp
spec:
channel: stable-1.5
name: redhat-oadp-operator
source: redhat-operators
sourceNamespace: openshift-marketplace
installPlanApproval: Automatic
Terminal window
oc -n openshift-adp get csv -w

2. Bucket S3 via ObjectBucketClaim

Con ODF il target S3 te lo generi sul cluster: niente storage esterno da far provisionare.

apiVersion: objectbucket.io/v1alpha1
kind: ObjectBucketClaim
metadata:
name: oadp-bucket
namespace: openshift-adp
spec:
generateBucketName: oadp
storageClassName: openshift-storage.noobaa.io
Terminal window
oc -n openshift-adp get obc oadp-bucket -o jsonpath='{.status.phase}{"\n"}'
Terminal window
BUCKET=$(oc -n openshift-adp get cm oadp-bucket -o jsonpath='{.data.BUCKET_NAME}'); echo "$BUCKET"

L’OBC crea in automatico due oggetti omonimi: un ConfigMap con nome bucket ed endpoint, e un Secret con le credenziali S3.

3. Credenziali

Terminal window
oc -n openshift-adp get secret oadp-bucket -o go-template='[default]
aws_access_key_id={{.data.AWS_ACCESS_KEY_ID|base64decode}}
aws_secret_access_key={{.data.AWS_SECRET_ACCESS_KEY|base64decode}}
' > /tmp/credentials-velero
Terminal window
oc -n openshift-adp create secret generic cloud-credentials --from-file cloud=/tmp/credentials-velero && shred -u /tmp/credentials-velero

Nome secret e key devono combaciare con spec.backupLocations[].velero.credential nella DPA.

4. DataProtectionApplication

Terminal window
CACERT=$(oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0)
apiVersion: oadp.openshift.io/v1alpha1
kind: DataProtectionApplication
metadata:
name: dpa
namespace: openshift-adp
spec:
configuration:
velero:
defaultPlugins: [openshift, aws, csi]
resourceTimeout: 10m
podConfig:
resourceAllocations:
requests: {cpu: 500m, memory: 512Mi}
limits: {cpu: "2", memory: 2Gi}
nodeAgent:
enable: true
uploaderType: kopia
podConfig:
resourceAllocations:
requests: {cpu: 500m, memory: 1Gi}
limits: {cpu: "2", memory: 4Gi}
backupLocations:
- velero:
provider: aws
default: true
credential: {name: cloud-credentials, key: cloud}
objectStorage:
bucket: <BUCKET>
prefix: velero
caCert: <CACERT>
config:
profile: default
region: noobaa
s3ForcePathStyle: "true"
s3Url: https://s3.openshift-storage.svc
Terminal window
oc -n openshift-adp get bsl,pod
Terminal window
oc -n openshift-adp get dpa dpa -o jsonpath='{.status.conditions}' | jq .

La BackupStorageLocation deve risultare Available. Se resta Unavailable vai alla sezione troubleshooting.

5. VolumeSnapshotClass

Terminal window
oc get volumesnapshotclass

Velero considera solo le snapclass etichettate:

Terminal window
oc label volumesnapshotclass ocs-storagecluster-rbdplugin-snapclass ocs-storagecluster-cephfsplugin-snapclass velero.io/csi-volumesnapshot-class=true --overwrite

6. CLI velero

Terminal window
alias velero='oc -n openshift-adp exec deployment/velero -c velero -it -- ./velero'

Binario locale estratto dal pod:

Terminal window
oc -n openshift-adp cp $(oc -n openshift-adp get pod -l app.kubernetes.io/name=velero -o name | head -1 | cut -d/ -f2):velero /usr/local/bin/velero -c velero && chmod +x /usr/local/bin/velero

Backup

Namespace stateless:

Terminal window
velero backup create hn-apphub-be-$(date +%F) --include-namespaces hn-apphub-be --ttl 720h --wait

PVC su Ceph — snapshot CSI con copia dei dati nel bucket:

Terminal window
velero backup create app-$(date +%F) --include-namespaces myns --snapshot-move-data --ttl 720h --wait

PVC su thin-csi — file-system backup:

Terminal window
velero backup create app-$(date +%F) --include-namespaces myns --default-volumes-to-fs-backup --ttl 720h --wait

Solo configurazione, senza volumi:

Terminal window
velero backup create myns-cfg --include-namespaces myns --snapshot-volumes=false --exclude-resources events,events.events.k8s.io,pods,replicasets,endpointslices

Per label selector (utile per isolare una singola applicazione dentro un namespace condiviso):

Terminal window
velero backup create bff-only --include-namespaces hn-apphub-be --selector app.kubernetes.io/instance=bff-gateway-helm-deploy

Multi-namespace:

Terminal window
velero backup create farm-$(date +%F) --include-namespaces 'hn-*,t3-*' --ttl 720h

Schedule

Terminal window
velero schedule create daily-hn --schedule="0 2 * * *" --include-namespaces hn-apphub-be --ttl 168h
Terminal window
velero schedule get && velero backup get
Terminal window
velero schedule pause daily-hn

Hook applicativi

Le annotation vanno sul pod template, non sul Deployment.

spec:
template:
metadata:
annotations:
pre.hook.backup.velero.io/container: db
pre.hook.backup.velero.io/command: '["/bin/sh","-c","psql -c \"SELECT pg_backup_start(''velero'')\""]'
pre.hook.backup.velero.io/timeout: 5m
post.hook.backup.velero.io/command: '["/bin/sh","-c","psql -c \"SELECT pg_backup_stop()\""]'

Restore

Stesso namespace:

Terminal window
velero restore create --from-backup hn-apphub-be-2026-09-02 --wait

Namespace alternativo — modalità raccomandata per i test di DR, non tocca la produzione:

Terminal window
velero restore create dr-test --from-backup hn-apphub-be-2026-09-02 --namespace-mappings hn-apphub-be:hn-apphub-be-dr

Solo alcune risorse:

Terminal window
velero restore create cfg-only --from-backup hn-apphub-be-2026-09-02 --include-resources configmaps,secrets,deployments,services

Escludendo risorse problematiche:

Terminal window
velero restore create rst --from-backup hn-apphub-be-2026-09-02 --exclude-resources nodes,events,events.events.k8s.io,backups.velero.io,restores.velero.io
Terminal window
velero restore describe dr-test --details
Terminal window
velero restore logs dr-test

OADP setup — Velero + BackupStorageLocation (prerequisite)

Prima di qualsiasi backup servono:

  1. OADP operator installato (ns openshift-adp, es. oadp-operator.v1.4.11)
  2. Un object storage S3-compatible (NooBaa, MinIO, AWS S3, …) per i backup
  3. Una DataProtectionApplication (DPA) che spawna i pod velero + node-agent e genera una BackupStorageLocation collegata al bucket

Legenda CRD Velero

CRDCosa descriveEsempio
BackupStorageLocation (BSL)dove scrivere i backup (object storage)NooBaa, MinIO, AWS S3
VolumeSnapshotLocation (VSL)dove scrivere gli snapshot CSI (solo cloud nativi)AWS region per EBS
Backupcosa backuppare (ns/label/resource)db-manual, db-consistent
Restorecosa restorare e dove (mapping ns)restore in mydatabase-restored

La BSL deve essere in PHASE: Available prima di qualsiasi Backup — altrimenti i Backup falliscono subito con Failed.

Setup NooBaa + DPA

1 · ObjectBucketClaim → genera bucket NooBaa

Terminal window
oc apply -f - <<'EOF'
apiVersion: objectbucket.io/v1alpha1
kind: ObjectBucketClaim
metadata:
name: oadp-bucket
namespace: openshift-adp
spec:
generateBucketName: oadp
storageClassName: openshift-storage.noobaa.io
EOF
oc wait --for=jsonpath='{.status.phase}'=Bound obc/oadp-bucket -n openshift-adp --timeout=120s

2 · Estrai bucket name + credenziali

Terminal window
BUCKET=$(oc get obc oadp-bucket -n openshift-adp -o jsonpath='{.spec.bucketName}')
AK=$(oc get secret oadp-bucket -n openshift-adp -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d)
SK=$(oc get secret oadp-bucket -n openshift-adp -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d)
echo "bucket=$BUCKET"

3 · Secret cloud-credentials (formato AWS)

Terminal window
cat > /tmp/cc <<EOF
[default]
aws_access_key_id=$AK
aws_secret_access_key=$SK
EOF
oc create secret generic cloud-credentials -n openshift-adp --from-file=cloud=/tmp/cc
rm /tmp/cc

4 · Endpoint S3 interno NooBaa

Terminal window
oc get svc -n openshift-storage s3
# endpoint tipico: https://s3.openshift-storage.svc

5 · DataProtectionApplication (DPA)

Terminal window
oc apply -f - <<EOF
apiVersion: oadp.openshift.io/v1alpha1
kind: DataProtectionApplication
metadata:
name: dpa-noobaa
namespace: openshift-adp
spec:
configuration:
velero:
defaultPlugins:
- openshift
- aws
- csi
- kubevirt
nodeAgent:
enable: true
uploaderType: kopia
backupLocations:
- velero:
provider: aws
default: true
objectStorage:
bucket: \$BUCKET
prefix: oadp
credential:
name: cloud-credentials
key: cloud
config:
region: noobaa
s3ForcePathStyle: "true"
s3Url: https://s3.openshift-storage.svc
caCert: <base64 CA NooBaa — vedi passo 7>
EOF

6 · Verifica Velero + BSL

Terminal window
oc get pods -n openshift-adp -w
# attendi: velero-xxx Running + node-agent-xxx Running su ogni worker
oc get bsl -n openshift-adp # PHASE deve essere Available

Debug se la BSL resta Unavailable:

Terminal window
oc describe bsl -n openshift-adp
oc logs -n openshift-adp deploy/velero -c velero --tail=50 | grep -iE 'bsl|storage|credential'

Quando bsl è Available, procedi al drill end-to-end qui sotto.

End-to-end validation workflow — create → backup → delete → restore → verify

Questa sezione è il “disaster recovery drill” completo: si crea un namespace con app + database stateful, si fa il backup (sia crash-consistent che application-consistent via hooks), si simula il disaster eliminando il progetto, si fa il restore in un nuovo namespace, e si valida l’integrità dei dati.

Flusso ispirato al pattern Red Hat DO380 “Backup and Restore with OADP”. Lo trovi utile come drill mensile per verificare che la pipeline OADP sia davvero funzionante.


1 · Create — namespace, app, DB con dati

Deploy di MariaDB con PVC su thin-csi (vSphere CSI; snapshot CSI non disponibile → Velero userà FSB/Kopia, vedi caveat sopra) + db-loader che inserisce righe ogni 10s per simulare attività.

Terminal window
oc new-project mydatabase

MariaDB deployment con PVC + Secret + Service:

Terminal window
oc apply -f - <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: mariadb
namespace: mydatabase
type: Opaque
stringData:
MYSQL_ROOT_PASSWORD: redhatocp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
MYSQL_DATABASE: application
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mariadb
namespace: mydatabase
labels:
app: mariadb
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
storageClassName: thin-csi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mariadb
namespace: mydatabase
labels:
app: mariadb
spec:
replicas: 1
selector:
matchLabels:
app: mariadb
template:
metadata:
labels:
app: mariadb
spec:
containers:
- name: mariadb
image: registry.redhat.io/rhel9/mariadb-105:latest
envFrom:
- secretRef:
name: mariadb
ports:
- containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: mariadb
---
apiVersion: v1
kind: Service
metadata:
name: mariadb
namespace: mydatabase
labels:
app: mariadb
spec:
ports:
- port: 3306
selector:
app: mariadb
EOF

Attendi che il pod sia Running:

Terminal window
oc wait --for=condition=Ready pod -l app=mariadb -n mydatabase --timeout=180s

Crea la tabella + inserisci dati iniziali:

Terminal window
oc exec -c mariadb deploy/mariadb -n mydatabase -- bash -c \
'mariadb -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" -e "
CREATE TABLE IF NOT EXISTS application_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
time DATETIME DEFAULT CURRENT_TIMESTAMP,
app VARCHAR(50),
message VARCHAR(255)
);
INSERT INTO application_logs (app, message) VALUES
(\"seed\", \"initial record 1\"),
(\"seed\", \"initial record 2\"),
(\"seed\", \"initial record 3\");
SELECT COUNT(*) AS total_rows FROM application_logs;"'

Simula attività continua con un db-loader (inserisce una riga ogni 10s):

Terminal window
oc apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: db-loader
namespace: mydatabase
spec:
replicas: 1
selector:
matchLabels:
app: db-loader
template:
metadata:
labels:
app: db-loader
spec:
containers:
- name: loader
image: registry.redhat.io/rhel9/mariadb-105:latest
envFrom:
- secretRef:
name: mariadb
command:
- /bin/bash
- -c
- |
while true; do
mariadb -h mariadb -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" \
-e "INSERT INTO application_logs (app, message) VALUES ('db-loader', MD5(RAND()));"
sleep 10
done
EOF

Snapshot pre-backup — annota il valore:

Terminal window
oc exec -c mariadb deploy/mariadb -n mydatabase -- bash -c \
'mariadb -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" -te \
"SELECT COUNT(*) AS rows_before_backup, MAX(id) AS max_id FROM application_logs;"'

Salva rows_before_backup e max_id — li confronterai dopo il restore.


2 · Backup — crash-consistent (FSB/Kopia)

Backup via Velero File System Backup (uploader Kopia) — su thin-csi niente snapshot CSI atomico. Velero legge i file del PVC mentre il pod gira: InnoDB con doublewrite buffer + redo log sa fare crash recovery al restore, ma non è un punto nel tempo atomico. Per garanzie forti → pattern 3 (hooks pre/post).

Terminal window
oc apply -f - <<'EOF'
apiVersion: velero.io/v1
kind: Backup
metadata:
name: db-manual
namespace: openshift-adp
spec:
includedNamespaces:
- mydatabase
orLabelSelectors:
- matchLabels:
app: mariadb
- matchLabels:
kubernetes.io/metadata.name: mydatabase
includedResources:
- namespace
- deployments
- configmaps
- secrets
- pvc
- pv
- services
defaultVolumesToFsBackup: true # thin-csi: no VolumeSnapshotClass → FSB (Kopia)
ttl: 720h0m0s
EOF

Attendi completion:

Terminal window
alias velero='oc -n openshift-adp exec deployment/velero -c velero -it -- ./velero'
velero get backup db-manual
# attendi che STATUS sia "Completed"

Verifica dettaglio:

Terminal window
velero describe backup db-manual --details

Checklist:

  • Phase: Completed (non PartiallyFailed o Failed)
  • Items Backed Up ≥ numero resources atteso
  • Nel bucket S3 trovi oadp/backups/db-manual/ + oadp/kopia/mydatabase/ (Data Mover)

3 · Backup — application-consistent (hooks pre/post)

Backup con hooks che bloccano il DB in modalità read-only durante il backup. Application-consistent: nessuna transazione in flight. Attenzione: con FSB (no snapshot CSI atomico) il DB resta read-only per TUTTA la durata del backup — non pochi secondi come con snapshot. Su PVC grandi, pianifica in finestra di manutenzione.

Terminal window
oc apply -f - <<'EOF'
apiVersion: velero.io/v1
kind: Backup
metadata:
name: db-consistent
namespace: openshift-adp
spec:
includedNamespaces:
- mydatabase
orLabelSelectors:
- matchLabels:
app: mariadb
- matchLabels:
kubernetes.io/metadata.name: mydatabase
includedResources:
- namespace
- deployments
- configmaps
- secrets
- pvc
- pv
- services
- pods
defaultVolumesToFsBackup: true # thin-csi: no VolumeSnapshotClass → FSB (Kopia)
hooks:
resources:
- name: mariadb-readonly
labelSelector:
matchLabels:
app: mariadb
pre:
- exec:
container: mariadb
command:
- /bin/bash
- -c
- |
mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" -e \
"set global read_only=1; BACKUP STAGE START; BACKUP STAGE BLOCK_COMMIT;"
sync
post:
- exec:
container: mariadb
command:
- /bin/bash
- -c
- |
mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" -e \
"set global read_only=0;"
EOF

Attendi completion:

Terminal window
velero get backup db-consistent
velero describe backup db-consistent --details | grep -A2 hookPhase

Cerca hookPhase=pre e hookPhase=post con exit code 0.


4 · Delete — simula il disaster

Terminal window
# Snapshot finale pre-disaster per riferimento
oc exec -c mariadb deploy/mariadb -n mydatabase -- bash -c \
'mariadb -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" -te \
"SELECT COUNT(*) AS rows_at_disaster, MAX(id) AS max_id_at_disaster FROM application_logs;"'
# Boom
oc delete project mydatabase
# Attendi che il namespace sia davvero sparito (può prendere 30-60s)
oc wait --for=delete namespace/database --timeout=180s
# Verifica che anche il PV sia andato (reclaimPolicy: Delete di default su Ceph RBD)
oc get pv | grep database || echo "✓ Nessun PV residuo"

5 · Restore — in nuovo namespace per isolamento

Best practice: restore in un namespace diverso da quello originale. Così puoi validare senza interferire con eventuali recovery paralleli, e confrontare mydatabase-restored vs il db originale (se re-ricreato).

Terminal window
oc apply -f - <<'EOF'
apiVersion: velero.io/v1
kind: Restore
metadata:
name: db-restore-consistent
namespace: openshift-adp
spec:
backupName: db-consistent
restorePVs: true
namespaceMapping:
mydatabase: mydatabase-restored
EOF

Attendi:

Terminal window
velero get restore db-restore-consistent
# attendi STATUS = Completed
velero describe restore db-restore-consistent --details

Checklist completion:

  • Phase: Completed
  • Warnings: 0 (o solo warning cosmetici su endpoint/pod già esistenti)
  • Items Restored ≥ numero resources atteso

Verifica che il pod MariaDB sia tornato up:

Terminal window
oc get all,pvc -n mydatabase-restored
oc wait --for=condition=Ready pod -l app=mariadb -n mydatabase-restored --timeout=180s

6 · Validate — integrità DB

Step 6.1 — Integrity check su MariaDB:

Terminal window
oc exec -c mariadb deploy/mariadb -n mydatabase-restored -- bash -c \
'mariadb-check -u root -p"${MYSQL_ROOT_PASSWORD}" -A --auto-repair'

Aspetta output OK su ogni tabella. Se vedi error : Corrupt o error : Wrong bytesec, hai fatto un backup crash-consistent su un DB sotto carico pesante — ecco perché gli hooks application-consistent sono preferibili per DB transazionali.

Step 6.2 — Confronta il row count col pre-backup:

Terminal window
oc exec -c mariadb deploy/mariadb -n mydatabase-restored -- bash -c \
'mariadb -u "${MYSQL_USER}" -p"${MYSQL_PASSWORD}" "${MYSQL_DATABASE}" -te \
"SELECT COUNT(*) AS rows_after_restore, MAX(id) AS max_id_after_restore FROM application_logs;
SELECT * FROM application_logs ORDER BY id DESC LIMIT 10;"'

Interpretazione:

  • rows_after_restore ≥ rows_before_backup ✓ → tutte le righe pre-backup sono state recuperate
  • rows_after_restore < rows_before_backup ✗ → corruzione o backup incompleto, indaga nei velero logs
  • Alcune righe inserite dal db-loader dopo il backup non ci sono — normale, il backup è un punto nel tempo

Step 6.3 — Verifica UID/GID preservati (critico per PVC Ceph):

Terminal window
oc get namespace mydatabase-restored -o yaml | grep -E 'uid-range|supplemental-groups'

Devono matchare openshift.io/sa.scc.uid-range e openshift.io/sa.scc.supplemental-groups del namespace originale. Se diversi, il DB può crashare al primo write per permessi sui file del PV.

Step 6.4 — Test endpoint applicativo (se hai Route):

Terminal window
oc get route -n mydatabase-restored
# curl -k https://<route-url>/healthz

7 · Cleanup drill

Quando il drill è finito, pulisci sempre per non lasciare backup orfani che consumano spazio nel bucket.

Terminal window
# Elimina il namespace di restore
oc delete project mydatabase-restored
# Elimina i backup (sia S3 che OpenShift CRD)
velero delete backup db-manual
velero delete backup db-consistent
# Rispondi 'y' quando chiesto
# Attendi che i backup siano effettivamente spariti (può prendere 1-2 min)
velero get backup
# atteso: no resources found
# Elimina i restore CRD
velero delete restore db-restore-consistent
# Elimina il BackupRepository Kopia (viene ricreato automaticamente al prossimo backup del namespace)
oc get backuprepository -n openshift-adp -l velero.io/repository-type=kopia \
-o jsonpath='{.items[?(@.spec.volumeNamespace=="database")].metadata.name}' | \
xargs -r oc delete backuprepository -n openshift-adp
# Verifica che il bucket sia pulito (opzionale, usa s3cmd)
s3cmd la -r s3://<your-backup-bucket>/oadp/backups/

Troubleshooting rapido

SintomoCausa probabileFix
Backup PartiallyFailed + nei log error executing hookHook pre fallisce (password errata, container name sbagliato)velero backup logs <name> | grep hookPhase → trova errore
Restore Completed ma MariaDB CrashLoopBackOffUID/GID namespace diversi → permessi su /var/lib/mysql/dataIncludi namespace in includedResources del backup
Restore pod stuck in PendingPVC non si bind, il PV restore è pendingControlla oc describe pvc + oc get events nel ns restore; se CSI timeout verifica Data Mover
Backup Finalizing da oreKopia stuck sul transferoc logs -n openshift-adp -l app.kubernetes.io/name=node-agent
mariadb-check dice CorruptBackup crash-consistent su DB in write pesanteRifare backup con hooks (step 3)
rows_after_restore molto < rows_before_backupBackup PartiallyFailed ma riportato Completed, PV non snapshottatovelero backup describe --details → sezione Backup Volumes

Pattern operativo per DR drill mensile

Script scheletro per automatizzare il drill come CronJob:

#!/bin/bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d-%H%M)
BACKUP="dr-drill-${TIMESTAMP}"
NS_SRC="database"
NS_DST="database-dr-${TIMESTAMP}"
# 1. Backup
oc apply -f - <<EOF
apiVersion: velero.io/v1
kind: Backup
metadata:
name: ${BACKUP}
namespace: openshift-adp
spec:
includedNamespaces: [${NS_SRC}]
defaultVolumesToFsBackup: true # thin-csi: no VolumeSnapshotClass → FSB (Kopia)
ttl: 24h0m0s
EOF
# 2. Wait completion
until velero get backup ${BACKUP} | grep -q Completed; do sleep 30; done
# 3. Restore in nuovo ns
oc apply -f - <<EOF
apiVersion: velero.io/v1
kind: Restore
metadata:
name: ${BACKUP}-restore
namespace: openshift-adp
spec:
backupName: ${BACKUP}
namespaceMapping: {${NS_SRC}: ${NS_DST}}
EOF
# 4. Validate
until velero get restore ${BACKUP}-restore | grep -q Completed; do sleep 30; done
oc exec -n ${NS_DST} -c mariadb deploy/mariadb -- mariadb-check -u root -p"${PWD}" -A
# 5. Cleanup
oc delete project ${NS_DST}
velero delete backup ${BACKUP} --confirm

Mettilo in un CronJob mensile con un ServiceAccount dedicato con RBAC su openshift-adp + database + delete project.

Troubleshooting

Terminal window
oc -n openshift-adp logs deployment/velero -c velero --tail=200
Terminal window
oc -n openshift-adp logs ds/node-agent --tail=200
Terminal window
velero backup describe <name> --details && velero backup logs <name>
Terminal window
oc -n openshift-adp get backup,restore,podvolumebackup,podvolumerestore,datauploads,datadownloads -o wide

BSL Unavailable

Nella grande maggioranza dei casi è una di tre cose: caCert errato o mancante, s3Url non risolvibile dai pod velero, nome bucket sbagliato.

Terminal window
oc -n openshift-adp exec deployment/velero -c velero -- curl -sS -o /dev/null -w '%{http_code}\n' https://s3.openshift-storage.svc --cacert /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
Terminal window
oc -n openshift-adp get bsl -o jsonpath='{.items[*].status.message}{"\n"}'

Backup PartiallyFailed

Terminal window
velero backup describe <name> --details | sed -n '/Errors/,/^$/p'

Cause tipiche: hook in timeout, PVC su storage class senza snapclass e senza --default-volumes-to-fs-backup, oppure risorse admission-rejected in restore.

Must-gather

Terminal window
oc adm must-gather --image=registry.redhat.io/oadp/oadp-mustgather-rhel9:v1.5

Perimetro: cosa NON copre

  • etcd — il backup del control plane è separato: /usr/local/bin/cluster-backup.sh sui master.
  • Cluster-scoped object non referenziati dai namespace inclusi (ClusterRole, SCC, MachineConfig, CRD custom, oggetti di rete cluster-wide).
  • Immagini container — Velero salva i riferimenti, non i layer. Se il registry (DTR/MSR, Quay) si perde, il restore fallisce in ImagePullBackOff: il registry ha il suo backup dedicato.
  • Configurazione esterna al cluster — VIP F5, iRule, regole firewall, record DNS, configurazioni gateway API.

Un piano di DR completo mette insieme i quattro pezzi: etcd, OADP, backup registry, e la configurazione di rete/perimetro versionata a parte.

Cos’è la DataProtectionApplication

La DataProtectionApplication (DPA) è la CR di configurazione dell’operator OADP: è l’unico oggetto che scrivi a mano. L’operator la riconcilia e da lì deriva tutto il resto.

DataProtectionApplication (tu scrivi questa)
│
│ reconcile dell'operator OADP
▼
├── Deployment/velero ← controller che esegue backup e restore
├── DaemonSet/node-agent ← pod per nodo, copia i dati dei volumi (kopia/restic)
├── BackupStorageLocation "dpa-1" ← dove finiscono gli archivi (il bucket S3)
└── VolumeSnapshotLocation ← solo per snapshot cloud-native, non serve con CSI

Conseguenza pratica: non fai mai oc edit deployment/velero. Se modifichi il deployment a mano, l’operator lo riporta allo stato descritto dalla DPA al successivo reconcile. Ogni cambio (plugin, risorse, credenziali, bucket) si fa modificando la DPA.

Il nome della BackupStorageLocation generata è <nome-dpa>-<indice>: con una DPA chiamata dpa e un solo elemento in backupLocations, la BSL è dpa-1. È quel nome che va referenziato in storageLocation nelle CR Backup e Schedule.

Campi principali

CampoCosa fa
configuration.velero.defaultPluginsPlugin caricati come init container. openshift gestisce le specificità OCP (SCC, image stream, route), aws parla S3 col backend, csi abilita gli snapshot CSI
configuration.velero.resourceTimeoutTimeout di attesa sulle risorse durante backup/restore
configuration.velero.podConfignodeSelector, tolerations, request/limit del pod velero
configuration.nodeAgent.enableDeploya il DaemonSet che copia i dati dei volumi. Senza, niente file-system backup
configuration.nodeAgent.uploaderTypekopia (default moderno, deduplica) o restic (legacy)
backupLocations[].velero.provideraws anche per storage S3-compatibili come ODF/NooBaa, MinIO, Ceph RGW
backupLocations[].velero.defaultMarca la BSL come default, usata se non specifichi storageLocation
backupLocations[].velero.credentialNome e key del Secret con le credenziali S3
objectStorage.bucketNome del bucket
objectStorage.prefixSottocartella dentro il bucket: permette di condividere un bucket tra più cluster
objectStorage.caCertCA che firma il certificato dell’endpoint S3, in base64
config.s3UrlEndpoint S3. Obbligatorio per storage non-AWS
config.s3ForcePathStyle"true" per gli S3-compatibili, che non supportano il virtual-host style
config.regionRichiesto dal client AWS anche quando è irrilevante: un valore qualsiasi va bene
backupImagesSe false, non salva le immagini del registry interno

Verifica dello stato

Terminal window
oc -n openshift-adp get dpa dpa -o jsonpath='{.status.conditions[?(@.type=="Reconciled")]}' | jq .
Terminal window
oc -n openshift-adp get bsl,deployment,daemonset
Terminal window
oc -n openshift-adp get pod -o wide

Situazione sana: pod velero-* in Running, un pod node-agent-* per ogni worker, BSL in Available.


I due placeholder

Nel manifest della DPA compaiono <BUCKET> e <CACERT>: sono valori che estrai dal cluster, non stringhe da inventare.

<BUCKET> è il nome reale del bucket generato dall’ObjectBucketClaim. La OBC non usa il nome che le dai tu (oadp-bucket è il nome della claim), ma genera un nome univoco con suffisso random, tipo oadp-8f3a1c2e-.... Lo trovi nel ConfigMap omonimo creato dalla OBC:

Terminal window
oc -n openshift-adp get cm oadp-bucket -o jsonpath='{.data.BUCKET_NAME}{"\n"}'

<CACERT> è la CA che firma il certificato TLS dell’endpoint S3, in base64 su una riga sola. Puntando a https://s3.openshift-storage.svc il certificato è emesso dal service-ca interno di OpenShift, quindi la CA da usare è quella iniettata nel ConfigMap openshift-service-ca.crt presente in ogni namespace:

Terminal window
oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0

Su macOS base64 -w0 non esiste, usa:

Terminal window
oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 | tr -d '\n'

Per evitare copia-incolla, sostituisci le variabili al volo con un heredoc non quotato (<<EOF, non <<'EOF'):

Terminal window
BUCKET=$(oc -n openshift-adp get cm oadp-bucket -o jsonpath='{.data.BUCKET_NAME}')
Terminal window
CACERT=$(oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0)
Terminal window
echo "bucket=$BUCKET"; echo "cacert bytes=${#CACERT}"

Se CACERT è vuoto o corto (poche decine di byte) qualcosa è andato storto e la BSL resterà Unavailable. Un valore plausibile sono alcune migliaia di caratteri.

Alternativa senza CA, sconsigliata fuori dai lab:

config:
insecureSkipTLSVerify: "true"

Reference comandi velero

Presuppone l’alias:

Terminal window
alias velero='oc -n openshift-adp exec deployment/velero -c velero -it -- ./velero'

Ispezione

ComandoUso
velero versionVersione client e server
velero backup getElenco backup con fase, errori, scadenza
velero backup describe <name>Dettaglio: namespace inclusi, policy volumi, hook
velero backup describe <name> --detailsAggiunge l’elenco per risorsa e lo stato di ogni volume
velero backup logs <name>Log del job di backup (scaricati dal bucket)
velero restore getElenco restore
velero restore describe <name> --detailsDettaglio con warning per risorsa
velero restore logs <name>Log del restore: qui trovi gli already exists
velero schedule getSchedulazioni attive e ultimo trigger
velero backup-location getStato delle BSL, equivalente di oc get bsl
velero snapshot-location getStato delle VSL
velero plugin getPlugin caricati nel pod velero

Operazioni

ComandoUso
velero backup create <name> --include-namespaces <ns>Backup on-demand
velero backup create <name> --from-schedule <sched>Backup immediato usando il template di una schedule
velero backup delete <name>Rimuove backup e relativi dati dal bucket
velero restore create --from-backup <backup>Restore
velero schedule pause <name> / unpause <name>Sospende o riattiva una schedulazione
velero schedule delete <name>Elimina la schedulazione (non i backup prodotti)

velero backup delete cancella anche i dati nel bucket. Per togliere solo il riferimento locale mantenendo l’archivio, si elimina la CR con oc delete backup.velero.io <name> -n openshift-adp, ma alla successiva sincronizzazione della BSL Velero la ricrea leggendo il bucket.

Filtri utili

Terminal window
velero backup get --selector velero.io/schedule-name=hn-apphub-be-daily
Terminal window
velero backup describe hn-apphub-be-daily-20260902070000 --details | sed -n '/Resource List/,$p'
Terminal window
velero backup describe <name> --details | sed -n '/Errors/,/^$/p'

Equivalenti via oc

Utili in pipeline e quando il pod velero non risponde:

Terminal window
oc -n openshift-adp get backups.velero.io -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,ERRORS:.status.errors,START:.status.startTimestamp
Terminal window
oc -n openshift-adp get restores.velero.io -o custom-columns=NAME:.metadata.name,BACKUP:.spec.backupName,PHASE:.status.phase,WARN:.status.warnings,ERR:.status.errors
Terminal window
oc -n openshift-adp get datauploads,datadownloads -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,PROGRESS:.status.progress
Terminal window
oc -n openshift-adp get podvolumebackups -o custom-columns=NAME:.metadata.name,POD:.spec.pod.name,VOL:.spec.volume,PHASE:.status.phase

Fasi possibili

New → InProgress → Completed. Le altre vanno lette così:

FaseSignificato
FailedValidationErrore nella spec: BSL inesistente, namespace non valido. Nulla è stato eseguito
PartiallyFailedAlcune risorse o volumi non sono stati salvati. L’archivio esiste ma è incompleto
FailedBackup non utilizzabile
DeletingCancellazione in corso dei dati nel bucket

Su PartiallyFailed il backup non va considerato valido per un restore: va sempre aperto --details per capire cosa manca prima di dichiararlo buono.


Approccio dichiarativo (CR) — allineato alle doc Red Hat

La sezione precedente usa la CLI velero, comoda in interattivo. In produzione conviene la forma dichiarativa: le CR sono versionabili in git, applicabili da pipeline e sono la modalità documentata da Red Hat.

Backup CR

apiVersion: velero.io/v1
kind: Backup
metadata:
name: hn-apphub-be-manual
namespace: openshift-adp
labels:
velero.io/storage-location: default
spec:
includedNamespaces:
- hn-apphub-be
excludedResources:
- events
- events.events.k8s.io
storageLocation: dpa-1
defaultVolumesToFsBackup: false
snapshotVolumes: true
ttl: 720h0m0s
Terminal window
oc -n openshift-adp get backup hn-apphub-be-manual -o jsonpath='{.status.phase}{"\n"}'

Schedule CR

La modalità documentata per le schedulazioni ricorrenti: si crea una Schedule e il controller genera una Backup a ogni trigger. Prerequisito: operator installato e DPA in stato Ready.

apiVersion: velero.io/v1
kind: Schedule
metadata:
name: hn-apphub-be-daily
namespace: openshift-adp
spec:
schedule: 0 7 * * *
template:
hooks: {}
includedNamespaces:
- hn-apphub-be
storageLocation: dpa-1
defaultVolumesToFsBackup: true
ttl: 720h0m0s

Campi principali: schedule è l’espressione cron (0 7 * * * = ogni giorno alle 07:00), includedNamespaces l’array dei namespace, storageLocation il nome della BackupStorageLocation. defaultVolumesToFsBackup: true è la chiave da usare per il backup dei volumi via file system (OADP 1.2+).

Terminal window
oc -n openshift-adp get schedule,backup

Il nome della BSL generata dalla DPA si ricava così — non è velero-sample-1 come negli esempi generici della doc:

Terminal window
oc -n openshift-adp get bsl -o name

Restore CR

apiVersion: velero.io/v1
kind: Restore
metadata:
name: hn-apphub-be-restore
namespace: openshift-adp
spec:
backupName: hn-apphub-be-daily-20260902070000
includedResources: []
excludedResources:
- nodes
- events
- events.events.k8s.io
- backups.velero.io
- restores.velero.io
- resticrepositories.velero.io
- csinodes.storage.k8s.io
- volumeattachments.storage.k8s.io
- backuprepositories.velero.io
restorePVs: true

includedResources vuoto significa tutte le risorse presenti nel backup, escluse quelle in excludedResources. L’elenco di esclusioni qui sopra è quello raccomandato: sono oggetti cluster-scoped o interni a Velero che, riapplicati, rompono il restore.

Terminal window
oc -n openshift-adp get restore hn-apphub-be-restore -o jsonpath='{.status}' | jq .

Backup etcd

Perimetro diverso da OADP: qui si salva lo stato del cluster, non le applicazioni. Sono due piani distinti e servono entrambi.

Metodo 1 — script Red Hat sul control plane

Il metodo storico e supportato su tutte le versioni.

Terminal window
oc debug node/<master-node> -- chroot /host /usr/local/bin/cluster-backup.sh /home/core/assets/backup

Produce due file: lo snapshot del database e le risorse statiche del control plane. Vanno subito portati fuori dal nodo.

Metodo 2 — backup etcd automatici (Tech Preview)

Feature nativa a partire da OCP 4.14, che supporta backup singoli e ricorrenti. I backup ricorrenti creano un cron job che avvia un backup singolo a ogni trigger.

apiVersion: config.openshift.io/v1
kind: FeatureGate
metadata:
name: cluster
spec:
featureSet: TechPreviewNoUpgrade
Terminal window
oc get crd | grep backup

Devono comparire backups.config.openshift.io e etcdbackups.operator.openshift.io.

PVC di destinazione:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: etcd-backup-pvc
namespace: openshift-etcd
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
volumeMode: Filesystem

Backup singolo:

apiVersion: operator.openshift.io/v1alpha1
kind: EtcdBackup
metadata:
name: etcd-single-backup
namespace: openshift-etcd
spec:
pvcName: etcd-backup-pvc

Backup ricorrente con retention:

apiVersion: config.openshift.io/v1alpha1
kind: Backup
metadata:
name: etcd-recurring-backup
spec:
etcd:
schedule: "20 4 * * *"
timeZone: "Europe/Rome"
retentionPolicy:
retentionType: RetentionNumber
retentionNumber:
maxNumberOfBackups: 5
pvcName: etcd-backup-pvc
Terminal window
oc -n openshift-etcd get cronjob,job,pvc

Metodo 3 — CronJob custom

Approccio descritto sul blog Red Hat per chi non vuole abilitare il Tech Preview: un namespace dedicato, un ServiceAccount con ClusterRole per fare oc debug node, SCC privileged, e un CronJob che lancia cluster-backup.sh sui master.

Terminal window
oc new-project ocp-etcd-backup --display-name "Backup ETCD Automation"
Terminal window
oc -n ocp-etcd-backup create sa openshift-backup
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: openshift-backup
subjects:
- kind: ServiceAccount
name: openshift-backup
namespace: ocp-etcd-backup
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-etcd-backup
Terminal window
oc adm policy add-scc-to-user privileged -z openshift-backup -n ocp-etcd-backup

Trigger manuale per test, senza aspettare lo schedule:

Terminal window
oc -n ocp-etcd-backup create job backup-test --from=cronjob/openshift-backup

Il SA richiede privilegi elevati (privileged SCC, esecuzione di chroot sul nodo): va isolato in un namespace dedicato e con RBAC stretto.

Retention e destinazione

Qualunque metodo si usi, il file di backup non deve restare sul nodo o su un PVC dello stesso cluster. Rsync verso il bastion o verso storage esterno, con rotazione:

Terminal window
find /backup/etcd -name 'snapshot_*.db' -mtime +14 -delete

Quadro d’insieme

LivelloStrumentoFrequenza tipicaDestinazione
Manifest namespacekube-dump + crongiornalieragit / filesystem bastion
Applicazione + PVOADP Schedule CRgiornaliera, TTL 30ggbucket ODF
Stato clustercluster-backup.sh / Backup CRgiornalierafuori cluster
Immaginibackup registry DTR/MSRsecondo policy registrystorage registry
Perimetro retegit (IaC)a ogni changerepo

Nessuno dei quattro copre gli altri. Un test di restore periodico su namespace mappato (--namespace-mappings) è l’unico modo per sapere se il piano funziona davvero.