Backup & Restore di un namespace OCP
Due strade, da scegliere in base a cosa deve sopravvivere al disastro.
kube-dump | OADP / Velero | |
|---|---|---|
| Cosa salva | solo manifest YAML | manifest + PV + snapshot CSI |
| Dove | filesystem locale / git | bucket S3 (ODF NooBaa o RGW) |
| Schedulazione | cron sul bastion | Schedule CR nativo |
| Restore selettivo | oc apply a mano | --include-resources, --namespace-mappings |
| Prerequisiti | binario + kubeconfig | operator + ObjectBucketClaim |
| Caso d’uso | namespace stateless, snapshot pre-change | DR applicativo, namespace con PVC |
Parte 1 — kube-dump
Installazione
curl -sLo /usr/local/bin/kube-dump https://raw.githubusercontent.com/WoozyMasta/kube-dump/master/kube-dump && chmod +x /usr/local/bin/kube-dumpkube-dump --help | head -30Dipendenze: oc/kubectl, jq, yq. Opzionali ma consigliati fzf e git.
Dump base
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):
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:
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' ' ')kube-dump ns -n hn-apphub-be -d /tmp/backup-hn-apphub-be-$(date +%F) -r "$res"Flag utili
| Flag | Effetto |
|---|---|
-n, --namespaces | lista namespace (spazio o virgola) |
-d, --destination-dir | directory di output |
-r, --kube-objects | lista risorse namespaced da esportare |
-k, --kube-cluster-objects | risorse cluster-scoped (modalità cluster) |
-f, --force-remove | svuota la destination prima del dump |
-s, --silent | nessun output su stdout |
-c, --detailed | log 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-dumpset -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 "$@"install -m 0755 kube-dump-menu.sh /usr/local/bin/kube-dump-menu && kube-dump-menuRestore da dump
oc new-project hn-apphub-beoc apply -n hn-apphub-be -f /tmp/backup-hn-apphub-be-2026-09-02/hn-apphub-be/ --recursivePulizia rapida prima dell’apply:
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' \) -deleteVersionamento su git
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-pushCron sul bastion:
echo '0 3 * * * root /usr/local/bin/kube-dump ns -n hn-apphub-be -d /var/lib/kube-dump -s' > /etc/cron.d/kube-dumpParte 2 — OADP / Velero con backend ODF
Prerequisiti
oc get clusterversion && oc get storagecluster -n openshift-storageoc get sc1. Install operator
Da GUI: Operators → OperatorHub → “OADP” → OADP Operator (Red Hat) → Install → namespace openshift-adp → channel stable-1.5 → Automatic.
Canali disponibili sul catalog:
oc get packagemanifest redhat-oadp-operator -n openshift-marketplace -o jsonpath='{range .status.channels[*]}{.name}{"\n"}{end}'Da CLI:
apiVersion: v1kind: Namespacemetadata: name: openshift-adp---apiVersion: operators.coreos.com/v1kind: OperatorGroupmetadata: name: oadp namespace: openshift-adpspec: targetNamespaces: [openshift-adp]---apiVersion: operators.coreos.com/v1alpha1kind: Subscriptionmetadata: name: redhat-oadp-operator namespace: openshift-adpspec: channel: stable-1.5 name: redhat-oadp-operator source: redhat-operators sourceNamespace: openshift-marketplace installPlanApproval: Automaticoc -n openshift-adp get csv -w2. Bucket S3 via ObjectBucketClaim
Con ODF il target S3 te lo generi sul cluster: niente storage esterno da far provisionare.
apiVersion: objectbucket.io/v1alpha1kind: ObjectBucketClaimmetadata: name: oadp-bucket namespace: openshift-adpspec: generateBucketName: oadp storageClassName: openshift-storage.noobaa.iooc -n openshift-adp get obc oadp-bucket -o jsonpath='{.status.phase}{"\n"}'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
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-velerooc -n openshift-adp create secret generic cloud-credentials --from-file cloud=/tmp/credentials-velero && shred -u /tmp/credentials-veleroNome secret e key devono combaciare con spec.backupLocations[].velero.credential nella DPA.
4. DataProtectionApplication
CACERT=$(oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0)apiVersion: oadp.openshift.io/v1alpha1kind: DataProtectionApplicationmetadata: name: dpa namespace: openshift-adpspec: 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.svcoc -n openshift-adp get bsl,podoc -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
oc get volumesnapshotclassVelero considera solo le snapclass etichettate:
oc label volumesnapshotclass ocs-storagecluster-rbdplugin-snapclass ocs-storagecluster-cephfsplugin-snapclass velero.io/csi-volumesnapshot-class=true --overwrite6. CLI velero
alias velero='oc -n openshift-adp exec deployment/velero -c velero -it -- ./velero'Binario locale estratto dal pod:
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/veleroBackup
Namespace stateless:
velero backup create hn-apphub-be-$(date +%F) --include-namespaces hn-apphub-be --ttl 720h --waitPVC su Ceph — snapshot CSI con copia dei dati nel bucket:
velero backup create app-$(date +%F) --include-namespaces myns --snapshot-move-data --ttl 720h --waitPVC su thin-csi — file-system backup:
velero backup create app-$(date +%F) --include-namespaces myns --default-volumes-to-fs-backup --ttl 720h --waitSolo configurazione, senza volumi:
velero backup create myns-cfg --include-namespaces myns --snapshot-volumes=false --exclude-resources events,events.events.k8s.io,pods,replicasets,endpointslicesPer label selector (utile per isolare una singola applicazione dentro un namespace condiviso):
velero backup create bff-only --include-namespaces hn-apphub-be --selector app.kubernetes.io/instance=bff-gateway-helm-deployMulti-namespace:
velero backup create farm-$(date +%F) --include-namespaces 'hn-*,t3-*' --ttl 720hSchedule
velero schedule create daily-hn --schedule="0 2 * * *" --include-namespaces hn-apphub-be --ttl 168hvelero schedule get && velero backup getvelero schedule pause daily-hnHook 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:
velero restore create --from-backup hn-apphub-be-2026-09-02 --waitNamespace alternativo — modalità raccomandata per i test di DR, non tocca la produzione:
velero restore create dr-test --from-backup hn-apphub-be-2026-09-02 --namespace-mappings hn-apphub-be:hn-apphub-be-drSolo alcune risorse:
velero restore create cfg-only --from-backup hn-apphub-be-2026-09-02 --include-resources configmaps,secrets,deployments,servicesEscludendo risorse problematiche:
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.iovelero restore describe dr-test --detailsvelero restore logs dr-testOADP setup — Velero + BackupStorageLocation (prerequisite)
Prima di qualsiasi backup servono:
- OADP operator installato (ns
openshift-adp, es.oadp-operator.v1.4.11) - Un object storage S3-compatible (NooBaa, MinIO, AWS S3, …) per i backup
- Una DataProtectionApplication (DPA) che spawna i pod
velero+node-agente genera unaBackupStorageLocationcollegata al bucket
Legenda CRD Velero
| CRD | Cosa descrive | Esempio |
|---|---|---|
| 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 |
| Backup | cosa backuppare (ns/label/resource) | db-manual, db-consistent |
| Restore | cosa 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
oc apply -f - <<'EOF'apiVersion: objectbucket.io/v1alpha1kind: ObjectBucketClaimmetadata: name: oadp-bucket namespace: openshift-adpspec: generateBucketName: oadp storageClassName: openshift-storage.noobaa.ioEOF
oc wait --for=jsonpath='{.status.phase}'=Bound obc/oadp-bucket -n openshift-adp --timeout=120s2 · Estrai bucket name + credenziali
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)
cat > /tmp/cc <<EOF[default]aws_access_key_id=$AKaws_secret_access_key=$SKEOF
oc create secret generic cloud-credentials -n openshift-adp --from-file=cloud=/tmp/ccrm /tmp/cc4 · Endpoint S3 interno NooBaa
oc get svc -n openshift-storage s3# endpoint tipico: https://s3.openshift-storage.svc5 · DataProtectionApplication (DPA)
oc apply -f - <<EOFapiVersion: oadp.openshift.io/v1alpha1kind: DataProtectionApplicationmetadata: name: dpa-noobaa namespace: openshift-adpspec: 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>EOF6 · Verifica Velero + BSL
oc get pods -n openshift-adp -w# attendi: velero-xxx Running + node-agent-xxx Running su ogni workeroc get bsl -n openshift-adp # PHASE deve essere AvailableDebug se la BSL resta Unavailable:
oc describe bsl -n openshift-adpoc 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à.
oc new-project mydatabaseMariaDB deployment con PVC + Secret + Service:
oc apply -f - <<'EOF'apiVersion: v1kind: Secretmetadata: name: mariadb namespace: mydatabasetype: OpaquestringData: MYSQL_ROOT_PASSWORD: redhatocp MYSQL_USER: appuser MYSQL_PASSWORD: apppass MYSQL_DATABASE: application---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: mariadb namespace: mydatabase labels: app: mariadbspec: accessModes: [ReadWriteOnce] resources: requests: storage: 2Gi storageClassName: thin-csi---apiVersion: apps/v1kind: Deploymentmetadata: name: mariadb namespace: mydatabase labels: app: mariadbspec: 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: v1kind: Servicemetadata: name: mariadb namespace: mydatabase labels: app: mariadbspec: ports: - port: 3306 selector: app: mariadbEOFAttendi che il pod sia Running:
oc wait --for=condition=Ready pod -l app=mariadb -n mydatabase --timeout=180sCrea la tabella + inserisci dati iniziali:
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):
oc apply -f - <<'EOF'apiVersion: apps/v1kind: Deploymentmetadata: name: db-loader namespace: mydatabasespec: 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 doneEOFSnapshot pre-backup — annota il valore:
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).
oc apply -f - <<'EOF'apiVersion: velero.io/v1kind: Backupmetadata: name: db-manual namespace: openshift-adpspec: 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: 720h0m0sEOFAttendi completion:
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:
velero describe backup db-manual --detailsChecklist:
Phase: Completed(nonPartiallyFailedoFailed)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.
oc apply -f - <<'EOF'apiVersion: velero.io/v1kind: Backupmetadata: name: db-consistent namespace: openshift-adpspec: 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;"EOFAttendi completion:
velero get backup db-consistentvelero describe backup db-consistent --details | grep -A2 hookPhaseCerca hookPhase=pre e hookPhase=post con exit code 0.
4 · Delete — simula il disaster
# Snapshot finale pre-disaster per riferimentooc 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;"'
# Boomoc 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).
oc apply -f - <<'EOF'apiVersion: velero.io/v1kind: Restoremetadata: name: db-restore-consistent namespace: openshift-adpspec: backupName: db-consistent restorePVs: true namespaceMapping: mydatabase: mydatabase-restoredEOFAttendi:
velero get restore db-restore-consistent# attendi STATUS = Completed
velero describe restore db-restore-consistent --detailsChecklist completion:
Phase: CompletedWarnings: 0(o solo warning cosmetici su endpoint/pod già esistenti)Items Restored≥ numero resources atteso
Verifica che il pod MariaDB sia tornato up:
oc get all,pvc -n mydatabase-restoredoc wait --for=condition=Ready pod -l app=mariadb -n mydatabase-restored --timeout=180s6 · Validate — integrità DB
Step 6.1 — Integrity check su MariaDB:
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:
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 recuperaterows_after_restore < rows_before_backup✗ → corruzione o backup incompleto, indaga nei velero logs- Alcune righe inserite dal
db-loaderdopo il backup non ci sono — normale, il backup è un punto nel tempo
Step 6.3 — Verifica UID/GID preservati (critico per PVC Ceph):
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):
oc get route -n mydatabase-restored# curl -k https://<route-url>/healthz7 · Cleanup drill
Quando il drill è finito, pulisci sempre per non lasciare backup orfani che consumano spazio nel bucket.
# Elimina il namespace di restoreoc delete project mydatabase-restored
# Elimina i backup (sia S3 che OpenShift CRD)velero delete backup db-manualvelero 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 CRDvelero 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
| Sintomo | Causa probabile | Fix |
|---|---|---|
Backup PartiallyFailed + nei log error executing hook | Hook pre fallisce (password errata, container name sbagliato) | velero backup logs <name> | grep hookPhase → trova errore |
Restore Completed ma MariaDB CrashLoopBackOff | UID/GID namespace diversi → permessi su /var/lib/mysql/data | Includi namespace in includedResources del backup |
Restore pod stuck in Pending | PVC non si bind, il PV restore è pending | Controlla oc describe pvc + oc get events nel ns restore; se CSI timeout verifica Data Mover |
Backup Finalizing da ore | Kopia stuck sul transfer | oc logs -n openshift-adp -l app.kubernetes.io/name=node-agent |
mariadb-check dice Corrupt | Backup crash-consistent su DB in write pesante | Rifare backup con hooks (step 3) |
rows_after_restore molto < rows_before_backup | Backup PartiallyFailed ma riportato Completed, PV non snapshottato | velero backup describe --details → sezione Backup Volumes |
Pattern operativo per DR drill mensile
Script scheletro per automatizzare il drill come CronJob:
#!/bin/bashset -euo pipefail
TIMESTAMP=$(date +%Y%m%d-%H%M)BACKUP="dr-drill-${TIMESTAMP}"NS_SRC="database"NS_DST="database-dr-${TIMESTAMP}"
# 1. Backupoc apply -f - <<EOFapiVersion: velero.io/v1kind: Backupmetadata: name: ${BACKUP} namespace: openshift-adpspec: includedNamespaces: [${NS_SRC}] defaultVolumesToFsBackup: true # thin-csi: no VolumeSnapshotClass → FSB (Kopia) ttl: 24h0m0sEOF
# 2. Wait completionuntil velero get backup ${BACKUP} | grep -q Completed; do sleep 30; done
# 3. Restore in nuovo nsoc apply -f - <<EOFapiVersion: velero.io/v1kind: Restoremetadata: name: ${BACKUP}-restore namespace: openshift-adpspec: backupName: ${BACKUP} namespaceMapping: {${NS_SRC}: ${NS_DST}}EOF
# 4. Validateuntil velero get restore ${BACKUP}-restore | grep -q Completed; do sleep 30; doneoc exec -n ${NS_DST} -c mariadb deploy/mariadb -- mariadb-check -u root -p"${PWD}" -A
# 5. Cleanupoc delete project ${NS_DST}velero delete backup ${BACKUP} --confirmMettilo in un CronJob mensile con un ServiceAccount dedicato con RBAC su openshift-adp + database + delete project.
Troubleshooting
oc -n openshift-adp logs deployment/velero -c velero --tail=200oc -n openshift-adp logs ds/node-agent --tail=200velero backup describe <name> --details && velero backup logs <name>oc -n openshift-adp get backup,restore,podvolumebackup,podvolumerestore,datauploads,datadownloads -o wideBSL Unavailable
Nella grande maggioranza dei casi è una di tre cose: caCert errato o mancante, s3Url non risolvibile dai pod velero, nome bucket sbagliato.
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.crtoc -n openshift-adp get bsl -o jsonpath='{.items[*].status.message}{"\n"}'Backup PartiallyFailed
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
oc adm must-gather --image=registry.redhat.io/oadp/oadp-mustgather-rhel9:v1.5Perimetro: cosa NON copre
- etcd — il backup del control plane è separato:
/usr/local/bin/cluster-backup.shsui 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 CSIConseguenza 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
| Campo | Cosa fa |
|---|---|
configuration.velero.defaultPlugins | Plugin 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.resourceTimeout | Timeout di attesa sulle risorse durante backup/restore |
configuration.velero.podConfig | nodeSelector, tolerations, request/limit del pod velero |
configuration.nodeAgent.enable | Deploya il DaemonSet che copia i dati dei volumi. Senza, niente file-system backup |
configuration.nodeAgent.uploaderType | kopia (default moderno, deduplica) o restic (legacy) |
backupLocations[].velero.provider | aws anche per storage S3-compatibili come ODF/NooBaa, MinIO, Ceph RGW |
backupLocations[].velero.default | Marca la BSL come default, usata se non specifichi storageLocation |
backupLocations[].velero.credential | Nome e key del Secret con le credenziali S3 |
objectStorage.bucket | Nome del bucket |
objectStorage.prefix | Sottocartella dentro il bucket: permette di condividere un bucket tra più cluster |
objectStorage.caCert | CA che firma il certificato dell’endpoint S3, in base64 |
config.s3Url | Endpoint S3. Obbligatorio per storage non-AWS |
config.s3ForcePathStyle | "true" per gli S3-compatibili, che non supportano il virtual-host style |
config.region | Richiesto dal client AWS anche quando è irrilevante: un valore qualsiasi va bene |
backupImages | Se false, non salva le immagini del registry interno |
Verifica dello stato
oc -n openshift-adp get dpa dpa -o jsonpath='{.status.conditions[?(@.type=="Reconciled")]}' | jq .oc -n openshift-adp get bsl,deployment,daemonsetoc -n openshift-adp get pod -o wideSituazione 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:
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:
oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0Su macOS base64 -w0 non esiste, usa:
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'):
BUCKET=$(oc -n openshift-adp get cm oadp-bucket -o jsonpath='{.data.BUCKET_NAME}')CACERT=$(oc -n openshift-adp get cm openshift-service-ca.crt -o jsonpath='{.data.service-ca\.crt}' | base64 -w0)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:
alias velero='oc -n openshift-adp exec deployment/velero -c velero -it -- ./velero'Ispezione
| Comando | Uso |
|---|---|
velero version | Versione client e server |
velero backup get | Elenco backup con fase, errori, scadenza |
velero backup describe <name> | Dettaglio: namespace inclusi, policy volumi, hook |
velero backup describe <name> --details | Aggiunge l’elenco per risorsa e lo stato di ogni volume |
velero backup logs <name> | Log del job di backup (scaricati dal bucket) |
velero restore get | Elenco restore |
velero restore describe <name> --details | Dettaglio con warning per risorsa |
velero restore logs <name> | Log del restore: qui trovi gli already exists |
velero schedule get | Schedulazioni attive e ultimo trigger |
velero backup-location get | Stato delle BSL, equivalente di oc get bsl |
velero snapshot-location get | Stato delle VSL |
velero plugin get | Plugin caricati nel pod velero |
Operazioni
| Comando | Uso |
|---|---|
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
velero backup get --selector velero.io/schedule-name=hn-apphub-be-dailyvelero backup describe hn-apphub-be-daily-20260902070000 --details | sed -n '/Resource List/,$p'velero backup describe <name> --details | sed -n '/Errors/,/^$/p'Equivalenti via oc
Utili in pipeline e quando il pod velero non risponde:
oc -n openshift-adp get backups.velero.io -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,ERRORS:.status.errors,START:.status.startTimestampoc -n openshift-adp get restores.velero.io -o custom-columns=NAME:.metadata.name,BACKUP:.spec.backupName,PHASE:.status.phase,WARN:.status.warnings,ERR:.status.errorsoc -n openshift-adp get datauploads,datadownloads -o custom-columns=NAME:.metadata.name,PHASE:.status.phase,PROGRESS:.status.progressoc -n openshift-adp get podvolumebackups -o custom-columns=NAME:.metadata.name,POD:.spec.pod.name,VOL:.spec.volume,PHASE:.status.phaseFasi possibili
New → InProgress → Completed. Le altre vanno lette così:
| Fase | Significato |
|---|---|
FailedValidation | Errore nella spec: BSL inesistente, namespace non valido. Nulla è stato eseguito |
PartiallyFailed | Alcune risorse o volumi non sono stati salvati. L’archivio esiste ma è incompleto |
Failed | Backup non utilizzabile |
Deleting | Cancellazione 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/v1kind: Backupmetadata: name: hn-apphub-be-manual namespace: openshift-adp labels: velero.io/storage-location: defaultspec: includedNamespaces: - hn-apphub-be excludedResources: - events - events.events.k8s.io storageLocation: dpa-1 defaultVolumesToFsBackup: false snapshotVolumes: true ttl: 720h0m0soc -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/v1kind: Schedulemetadata: name: hn-apphub-be-daily namespace: openshift-adpspec: schedule: 0 7 * * * template: hooks: {} includedNamespaces: - hn-apphub-be storageLocation: dpa-1 defaultVolumesToFsBackup: true ttl: 720h0m0sCampi 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+).
oc -n openshift-adp get schedule,backupIl nome della BSL generata dalla DPA si ricava così — non è velero-sample-1 come negli esempi generici della doc:
oc -n openshift-adp get bsl -o nameRestore CR
apiVersion: velero.io/v1kind: Restoremetadata: name: hn-apphub-be-restore namespace: openshift-adpspec: 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: trueincludedResources 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.
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.
oc debug node/<master-node> -- chroot /host /usr/local/bin/cluster-backup.sh /home/core/assets/backupProduce 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/v1kind: FeatureGatemetadata: name: clusterspec: featureSet: TechPreviewNoUpgradeoc get crd | grep backupDevono comparire backups.config.openshift.io e etcdbackups.operator.openshift.io.
PVC di destinazione:
kind: PersistentVolumeClaimapiVersion: v1metadata: name: etcd-backup-pvc namespace: openshift-etcdspec: accessModes: - ReadWriteOnce resources: requests: storage: 200Gi volumeMode: FilesystemBackup singolo:
apiVersion: operator.openshift.io/v1alpha1kind: EtcdBackupmetadata: name: etcd-single-backup namespace: openshift-etcdspec: pvcName: etcd-backup-pvcBackup ricorrente con retention:
apiVersion: config.openshift.io/v1alpha1kind: Backupmetadata: name: etcd-recurring-backupspec: etcd: schedule: "20 4 * * *" timeZone: "Europe/Rome" retentionPolicy: retentionType: RetentionNumber retentionNumber: maxNumberOfBackups: 5 pvcName: etcd-backup-pvcoc -n openshift-etcd get cronjob,job,pvcMetodo 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.
oc new-project ocp-etcd-backup --display-name "Backup ETCD Automation"oc -n ocp-etcd-backup create sa openshift-backupkind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata: name: openshift-backupsubjects: - kind: ServiceAccount name: openshift-backup namespace: ocp-etcd-backuproleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-etcd-backupoc adm policy add-scc-to-user privileged -z openshift-backup -n ocp-etcd-backupTrigger manuale per test, senza aspettare lo schedule:
oc -n ocp-etcd-backup create job backup-test --from=cronjob/openshift-backupIl 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:
find /backup/etcd -name 'snapshot_*.db' -mtime +14 -deleteQuadro d’insieme
| Livello | Strumento | Frequenza tipica | Destinazione |
|---|---|---|---|
| Manifest namespace | kube-dump + cron | giornaliera | git / filesystem bastion |
| Applicazione + PV | OADP Schedule CR | giornaliera, TTL 30gg | bucket ODF |
| Stato cluster | cluster-backup.sh / Backup CR | giornaliera | fuori cluster |
| Immagini | backup registry DTR/MSR | secondo policy registry | storage registry |
| Perimetro rete | git (IaC) | a ogni change | repo |
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.