Doc sections: “Enabling the backup and restore operator”, “Creating a DataProtectionApplication”, “Scheduling a cluster backup”, “Automatically connecting clusters by using a Managed Service Account”.
cluster-backup componentAcronyms: OADP = OpenShift API for Data Protection, STS = Security Token Service, AWS = Amazon Web Services.
This installs the backup controller and OADP in open-cluster-management-backup.
MCH=$(oc --context $HUB1 get multiclusterhub -n $ACM_NS -o jsonpath='{.items[0].metadata.name}')
IDX=$(oc --context $HUB1 get multiclusterhub $MCH -n $ACM_NS -o json \
| jq '.spec.overrides.components | map(.name) | index("cluster-backup")')
echo "MCH=$MCH cluster-backup index=$IDX"
if [ "$IDX" = "null" ]; then
oc --context $HUB1 patch multiclusterhub $MCH -n $ACM_NS --type json \
-p '[{"op":"add","path":"/spec/overrides/components/-","value":{"name":"cluster-backup","enabled":true}}]'
else
oc --context $HUB1 patch multiclusterhub $MCH -n $ACM_NS --type json \
-p "[{\"op\":\"replace\",\"path\":\"/spec/overrides/components/$IDX/enabled\",\"value\":true}]"
fi
Wait for OADP and the backup controller:
oc --context $HUB1 get csv -n $BKP_NS -w
# oadp-operator... Succeeded (Ctrl+C)
oc --context $HUB1 get pods -n $BKP_NS
# cluster-backup-chart-clusterbackup-* and openshift-adp-controller-manager-* Running
AWS STS clusters: OADP is not installed automatically. Install it manually first (doc 1.1.9).
Needed for useManagedServiceAccount: true. It is a MultiClusterEngine component:
oc --context $HUB1 get multiclusterengine \
-o jsonpath='{.items[0].spec.overrides.components[?(@.name=="managedserviceaccount")].enabled}{"\n"}'
If it prints false:
MCE=$(oc --context $HUB1 get multiclusterengine -o jsonpath='{.items[0].metadata.name}')
IDX=$(oc --context $HUB1 get multiclusterengine $MCE -o json \
| jq '.spec.overrides.components | map(.name) | index("managedserviceaccount")')
oc --context $HUB1 patch multiclusterengine $MCE --type json \
-p "[{\"op\":\"replace\",\"path\":\"/spec/overrides/components/$IDX/enabled\",\"value\":true}]"
Acronyms: S3 = Amazon Simple Storage Service (object storage protocol), DPA = DataProtectionApplication, TLS = Transport Layer Security, AWS = Amazon Web Services.
cat > /tmp/credentials-velero <<EOF
[default]
aws_access_key_id=${S3_ACCESS_KEY}
aws_secret_access_key=${S3_SECRET_KEY}
EOF
oc --context $HUB1 create secret generic cloud-credentials -n $BKP_NS \
--from-file=cloud=/tmp/credentials-velero
envsubst < manifests/common/dpa.yaml.tpl | oc --context $HUB1 apply -f -
AWS S3 (not MinIO): remove the
s3Url,s3ForcePathStyleandinsecureSkipTLSVerifylines from the rendered DPA.
Wait for velero and the storage location:
oc --context $HUB1 get pods -n $BKP_NS -l app.kubernetes.io/name=velero
# velero-* 1/1 Running
oc --context $HUB1 get backupstoragelocation.velero.io -n $BKP_NS
# NAME PHASE ...
# dpa-acm-1 Available
Unavailable here means endpoint, credentials, bucket or TLS. See
troubleshooting. Do not continue until Available.
envsubst < manifests/common/backupschedule.yaml.tpl | oc --context $HUB1 apply -f -
oc --context $HUB1 get backupschedule.cluster.open-cluster-management.io -n $BKP_NS
# schedule-acm-msa Enabled Velero schedules are enabled
The controller creates velero Schedule objects, which run a first backup immediately:
oc --context $HUB1 get schedule.velero.io -n $BKP_NS
# acm-credentials-schedule, acm-managed-clusters-schedule, acm-resources-schedule,
# acm-resources-generic-schedule, acm-validation-policy-schedule
oc --context $HUB1 get backup.velero.io -n $BKP_NS -w
# one backup per schedule, PHASE Completed (Ctrl+C)
Acronyms: MSA = ManagedServiceAccount.
Needed for the active scenario (cluster import). The passive scenario does not use the token, but checking it now costs nothing.
The MSA token for the spoke is created after the schedule is enabled, asynchronously. A backup taken before the token exists cannot auto-import that cluster (doc: “A backup taken without a token cannot auto-import that cluster”).
The MSA exists:
oc --context $HUB1 get managedserviceaccount -n $SPOKE_NAME auto-import-account
# auto-import-account ...
Its token secret exists and carries the backup label:
oc --context $HUB1 get secret -n $SPOKE_NAME auto-import-account \
--show-labels | grep cluster.open-cluster-management.io/backup
# auto-import-account ... cluster.open-cluster-management.io/backup=...
Compare the token creation time with the latest managed-clusters backup. Token time:
oc --context $HUB1 get secret -n $SPOKE_NAME auto-import-account -o jsonpath='{.metadata.creationTimestamp}{"\n"}'
# <token time>, e.g. 2026-10-15T20:26:10Z
Latest managed-clusters backup start time:
oc --context $HUB1 get backup.velero.io -n $BKP_NS -l velero.io/schedule-name=acm-managed-clusters-schedule \
--sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,STARTED:.status.startTimestamp | tail -n1
# acm-managed-clusters-schedule-<ts> <must be later than the token time>
If the backup is older than the token, either wait for the next hourly run, or force one by switching the schedule to every 5 minutes:
oc --context $HUB1 patch backupschedule.cluster.open-cluster-management.io schedule-acm-msa -n $BKP_NS \
--type merge -p '{"spec":{"veleroSchedule":"*/5 * * * *"}}'
# re-run the backup start time command above until it is later than the token time
Then put the cron back:
oc --context $HUB1 patch backupschedule.cluster.open-cluster-management.io schedule-acm-msa -n $BKP_NS \
--type merge -p "{\"spec\":{\"veleroSchedule\":\"$BACKUP_CRON\"}}"
scripts/hub-status.sh $HUB1
Look at the bucket too, it shows what HUB2 will see:
labs3 ls s3://$S3_BUCKET/$S3_PREFIX/backups/
# PRE acm-credentials-schedule-20261015100000/
# PRE acm-managed-clusters-schedule-20261015100000/
# ... one folder per velero backup
Acronyms: BSL = BackupStorageLocation.
dpa-acm-1 Availableschedule-acm-msa phase Enabledacm-* schedule Completedauto-import-account secret exists for the spoke and is older than the latest acm-managed-clusters-schedule backupbackup-restore-enabled Compliant (give it a few minutes; any violation explains what is wrong)