Operator-driven disaster recovery for Hybrid Manager Innovation Release

From HM 2026.11, the HM operator can recover a Hybrid Manager (HM) installation for you. Instead of installing an empty HM and then restoring the control plane into it yourself with Velero, as in Disaster recovery for a single Hybrid Manager installation or Disaster recovery for a multi-DC Hybrid Manager deployment, you install the new HM with one extra field, spec.recoveryFrom, in its HybridControlPlane resource. The operator installs it against the object storage the old HM left behind and restores the control plane from it.

Warning

Check the known limitations before you start. The operator recovers the control plane and the Postgres management resources, but the Postgres clusters themselves are restored from the HM console, as in Restoring the Postgres clusters. Where a limitation applies, such as recovering into an environment with its own bucket, follow the manual procedure instead.

What the operator recovers

The operator restores the HM control plane and the Postgres management resources from the old installation's backups. It doesn't restore your Postgres clusters. The following table shows what the operator recovers and what you still restore yourself.

WhatRecovered by the operator
Control-plane secrets, such as database credentials and service secretsYes, before the installation starts
HM internal databasesYes, created already recovered from their backups
Read-only access to the old Velero backupsYes
Location resourcesYes, with streaming disabled on the locations that mode selects
Postgres management resources: what HM records about your Postgres clusters and their backupsYes, after the Location resources, from the same backup
Your Postgres clustersNo. Restore each one from its backups, as in Restoring the Postgres clusters

The internal databases are what make operator-driven recovery different from the manual procedure: they bootstrap from the old backups and come up holding the old data, instead of being initialized empty and restored into afterwards.

Note

A Location whose site is gone is restored with spec.stream.remoteResources.disabled: true, so the recovered control plane doesn't stream cluster resources, backups or apps to a location that will never answer. mode decides which locations that applies to. Re-enable streaming deliberately, as part of Restoring the Postgres clusters.

Prerequisites

  • The same HM version on both sides. Install the new HM at the version the old one was running. The operator compares spec.version with the version recorded in the old backup folder and stops the installation if they differ: a recovery restores a version, it doesn't upgrade one.

  • HM 2026.11 or later. Earlier versions don't label the Postgres management resources the operator restores, so a backup taken by an earlier HM brings none of them back.

  • HM operator 2.3.0 or later. Earlier operators skip the stages that run after the installation, so a recovery can reach Completed without restoring the Location resources or the resources of your Postgres clusters. See the operator compatibility matrix.

  • Both kinds of backup in the old object storage. See Checking the old object storage.

  • Read access to the old bucket from the new environment. If object storage is reached through workload identity, the identity of the new environment needs access to the old bucket as well as its own.

  • The image pull secret on the new cluster, created as for any installation. See Preparing your environment. It's environment setup, not restored state, so the recovery never restores it.

  • edbctl version 1.11.0 or later. See EDB Hybrid Manager CLI.

Gathering the values of the old installation

Collect four values that describe the installation you're recovering:

export OLD_BUCKET=<old_bucket>
export OLD_BACKUP_ID=<old_backup_folder>
export OLD_LOCATION_ID=<old_location_id>
export NEW_BACKUP_ID=<new_backup_folder>

Where:

  • OLD_BUCKET is the object storage bucket the old HM wrote its backups to.

  • OLD_BACKUP_ID is the spec.globalParameters.internal_backup_folder of the old HM.

  • OLD_LOCATION_ID is the spec.componentsParameters.upm-beacon.beacon_location_id of the old HM. Take it from the old HybridControlPlane resource, for example from your GitOps repository: nothing in the bucket records it, and if the old cluster is gone you can't look it up. If you no longer have it, use mode: full, where locationID is optional. full disables streaming on every restored location, so re-enable it by hand on any location that survived, as in Restoring the Postgres clusters. See mode.

  • NEW_BACKUP_ID is where the recovered HM writes its own backups. Use a new, unused folder name of 12 lowercase letters and digits. It must differ from OLD_BACKUP_ID. See Backup folders.

Checking the old object storage

Confirm that the old object storage holds what the recovery needs. A recovery reads one folder of one bucket, edb-internal-backups/${OLD_BACKUP_ID}/:

edb-internal-backups/<folder>/
├── databases/<db>/          internal database backups: base/ and wals/
├── velero/backups/<name>/   Velero backup of the Kubernetes state
└── hm-metadata.json         the HM version that wrote this folder

Both databases/ and velero/backups/ must be populated: Velero holds the Kubernetes state, and the internal database backups hold the control-plane data. At least one internal database needs a base backup in databases/<db>/base/. The recovery stops without one, and warns, naming them, about any database that has none: the installation brings those up empty.

To check that the internal databases have base backups, list their backups. This example uses the AWS CLI; use the equivalent for your object storage:

aws s3 ls --recursive s3://${OLD_BUCKET}/edb-internal-backups/${OLD_BACKUP_ID}/databases/

If the old HM is still reachable, take a fresh set of backups first. Take the list of internal databases to back up from the ScheduledBackup resources, not from the list of Postgres clusters: an internal cluster without backups configured is deliberately never backed up.

kubectl get scheduledbackup -A \
  -o jsonpath='{range .items[*]}{.metadata.namespace}/{.spec.cluster.name}{"\n"}{end}'

Writing the recovery resource

Take the HybridControlPlane resource of the HM you're recovering, give it a new internal_backup_folder, and add spec.recoveryFrom. Edit the existing resource and substitute the values from Gathering the values of the old installation:

apiVersion: edbpgai.edb.com/v1alpha1
kind: HybridControlPlane
metadata:
  name: edbpgai
spec:
  version: <the version of the old HM>
  globalParameters:
    internal_backup_folder: <NEW_BACKUP_ID>
  recoveryFrom:
    mode: full            # or primary-only: see mode
    target:
      objectStoreSecretRef:
        name: edb-object-storage
        namespace: edb-system
      internalBackupFolder: <OLD_BACKUP_ID>
      locationID: <OLD_LOCATION_ID>
    reuse:
      objectStore: true
    preflight:
      enforce: true
    claim: false          # true only for a real DR: see claim

Backup folders

The backup folder of the recovered HM must differ from the old one. The recovered HM reads the old folder and writes its own backups to its own folder, so a target.internalBackupFolder equal to globalParameters.internal_backup_folder is rejected.

Reusing the old folder is the most common mistake when you build the resource from the old one, because the old resource already carries the old folder in globalParameters. Replace that value with the new folder before you apply the resource.

mode

Set primary-only when only the location that hosted the control plane is gone and the other locations survive and keep running their Postgres clusters, and full when other locations are gone too. primary-only requires target.locationID.

The mode decides which restored Location resources come back with streaming disabled:

Mode valueStreaming disabled on
fullEvery restored location
primary-onlyOnly target.locationID

primary-only leaves the surviving locations streaming, which is what lets the recovered control plane take back the Postgres clusters still running there. With full, re-enable streaming by hand on the locations that survived, as in Restoring the Postgres clusters. A wrong mode is recoverable but not silent: primary-only where other locations were lost too leaves the recovered control plane streaming to locations that are gone, until you disable it on them as in Disable resource streaming on an affected location.

target

Identify the old installation in target: where its backups are stored, which folder holds them, and which location the old control plane ran in. It has three fields:

  • objectStoreSecretRef: the secret that describes the old object storage, its provider, bucket and credentials, in the same format as the edb-object-storage secret of a running HM. The bucket is read in place and read-only, so read access is enough. The name and the namespace are both required.

  • internalBackupFolder: the internal_backup_folder of the old HM, OLD_BACKUP_ID.

  • locationID: the location the old control plane ran in, OLD_LOCATION_ID. It must be a valid DNS label. It's required for primary-only and optional for full, but worth setting either way when several HM installations share a bucket.

reuse.objectStore

Set it to true. It decides whether the object storage credentials come back from the backup, and its default, false, isn't what a recovery into the same bucket needs.

The new HM usesSet it to
The same bucket as the old onetrue
A bucket of its ownfalse (the default)

Only the first case is supported. See Known limitations.

With true, the operator restores the object storage credentials from the backup. These are the edb-object-storage secret, the per-project edb-object-storage-prj-* secrets, and the slo-kv-store config map that records which object storage each project is bound to. They come back together on purpose. If the recovery used the old bucket but bound the projects again from scratch, the projects would end up bound to the default object storage, permanently.

Regardless of this setting, the operator keeps a renamed copy of every object storage credential in the hm-recovery namespace, because restoring your Postgres clusters later reads their backups from the old bucket.

The image pull secrets are never restored. They must exist before the installation can start, so they're always the new environment's own.

preflight.enforce

Defaults to true, which makes a failed preflight check stop the installation rather than letting it build a control plane on data that can't be restored. Set it to false only to turn the failures into warnings, for example when the old HM ran an operator earlier than 2.2.0, whose backup folder has no hm-metadata.json to check the version against. Confirm the versions match yourself first. The failures it lets through are recorded in status.recovery.ignoredPreflightFailures.

claim

Defaults to false. Leave it off to rehearse a recovery, and set it to true only for a real disaster recovery, when the old HM is gone.

With claim: true, the recovery records in the old backup folder that this HM has taken it over. If the old HM comes back, it reads that record and fences itself: it pauses its backups, fences its internal databases and stops the server that manages its Postgres clusters. The claim can't be taken back.

Once the control plane is recovered, the claim also removes the old HM from the federation of this one. If you rebuild it later, it can only rejoin through a new federation join.

Warning

Leave claim off for a drill. A drill recovers from the folder of an HM that's still running, and claim: true would fence it.

If another HM has already claimed the same folder, the recovery fails and stays failed, and you must rebuild the environment.

Warning

You can't change spec.recoveryFrom after the installation starts, because it drives what the installation does. Check every value before you apply the resource. If a value is wrong, rebuild the environment.

Installing the new Hybrid Manager

Apply the resource and let the operator install the new HM. Follow Install Hybrid Manager on Kubernetes, except for the step that creates the install secrets.

Warning

Don't run edbctl hybrid-manager create-install-secrets. The recovery restores those secrets from the backup, and any that already exist are kept instead, which leaves the platform with new credentials that don't match the restored databases.

Monitoring the recovery progress

Read the current phase, stage and message from the status of the HybridControlPlane resource:

kubectl get hcp <name> -o jsonpath='{.status.recovery.phase}/{.status.recovery.stage}{"\n"}'
kubectl get hcp <name> -o jsonpath='{.status.recovery.message}{"\n"}'

The recovery runs in stages, before and after the installation:

PhaseStageWhat it does
PreparingPreflightChecks the old object storage and gates the installation
PreparingClaimOnly with claim: true: records the takeover in the old backup folder
PreparingFenceHolds the new HM back until it's prepared
PreparingRemoveInternalDatabasesNothing to do on a new installation
PreparingRestoreSecretsReads the control-plane secrets from the backup
PreparedThe installation goes ahead, with the internal databases created already recovered
RestoringAcquireSourceGives Velero read-only access to the old backups
RestoringRestoreLocationsRestores the Location resources, with streaming disabled on the locations that mode selects
RestoringRestorePGManagementRestores the Postgres management resources, from the same backup
RestoringFlipTopologyOnly with claim: true: removes the old HM from the federation
CompletedThe control plane is recovered

Failed isn't final. A failed stage is run again a minute later, and the installation waits meanwhile. The cause is usually outside the resource, such as object storage that isn't reachable yet, so a recovery can show Failed and still complete once the cause clears. Read status.recovery.message before treating it as the outcome.

Warning

The Velero stages don't retry on their own. A Velero Restore keeps the result it was given, so once RestoreLocations or RestorePGManagement fails, it reports the same failure every minute, even after you fix the cause. To retry a failed stage:

  1. Find out why it failed. Velero records the reason for each item that failed, so run velero restore describe <name> --details.

  2. Fix the cause that Velero reports.

  3. Delete the failed Restore that status.recovery.message names. The next pass creates a new one under the same name.

kubectl delete restore recovery-0-restorelocations -n velero

RestoreLocations creates recovery-<epoch>-restorelocations, and RestorePGManagement creates recovery-<epoch>-pg-status, recovery-<epoch>-pg-management and recovery-<epoch>-pg-backups, one after the other. The epoch is 0 unless the recovery sets claim: true.

If the stage still can't complete after you delete its Restore, restore what it covers by hand, as in Restore resources.

If the installed HM can't support RestoreLocations or RestorePGManagement, the stage is skipped rather than run, and the recovery goes on without it. The skipped stages and the reason are listed in status.recovery.skippedStages, and the Completed message names them. Restore what a skipped stage left out by hand, as in Restore resources. Velero can already read the old backups through the recovery-source backup storage location.

Troubleshooting preflight check failures

If a check fails, status.recovery.message names it. If the old bucket can't be read, the checks after that one are skipped.

CheckFails when
RecoveryTakeoverCheckerThe HM is a secondary. Only a primary or standalone HM can be recovered this way
RecoveryBucketReachableCheckerThe old object storage can't be read with the given credentials
RecoveryPrefixesCheckerNo internal database has a base backup, or velero/backups/ is empty under the folder
RecoveryBackupCheckerNo completed Velero backup exists
RecoveryBackupSecretsCheckerThe backup to restore holds no secrets
RecoveryVersionCheckerhm-metadata.json records a different HM version than the one being installed, or the version can't be established
RecoveryEpochCheckerThe takeover record in the old folder, hm-ownership.json, can't be read

Validating the recovery

Confirm that the recovery succeeded by checking what it restored, as well as its phase. Start with the phase of the HybridControlPlane resource and of the recovery:

kubectl get hcp <name> -o jsonpath='phase={.status.phase} recovery={.status.recovery.phase}{"\n"}'
# phase=deployed recovery=Completed

Both backup storage locations should be Available, which means Velero reached the object storage through both folders:

kubectl get backupstoragelocation -n velero \
  -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,ACCESS:.spec.accessMode,PREFIX:.spec.objectStorage.prefix'
# default          Available  ReadWrite  edb-internal-backups/<new>/velero
# recovery-source  Available  ReadOnly   edb-internal-backups/<old>/velero

If the recovery reports that it kept the existing edb-object-storage secret, that's the expected outcome, because that secret already describes the object storage the new HM can reach.

The Location resources of the old HM should be back. List every location and check the Streaming column:

edbctl location list

Streaming is disabled on the locations that mode selects: every restored location with full, or only target.locationID with primary-only. That follows the mode you chose, not which locations were actually lost, so it may not match them yet. Before you remove any Postgres resources, make sure streaming is disabled on every affected location and on no other, as in Restoring the Postgres clusters.

Note

If the new HM reuses the location ID of the old one, both are the same Location, so streaming is disabled on the location the recovered HM itself runs in, and it doesn't manage clusters until you turn it back on. Do that as part of Restoring the Postgres clusters, not before.

The Postgres management resources should be back too, in the same namespace as the Location resources:

kubectl get clusterwrappers,remotepgs,backupwrappers -A

Once the recovery is Completed, the internal databases look like new ones in their specification, with bootstrap set to initdb. That's expected, because their recovered bootstrap is only read when they're created, and the operator then puts back their normal specification. The data is the recovered one.

Restoring the Postgres clusters

Restore the Postgres clusters yourself, because the operator brings back the Postgres management resources but not the clusters themselves. Whether a location streams decides what HM does with the clusters recorded there.

Note

With primary-only, a cluster that ran only in the surviving locations needs none of this. It keeps running, and the recovered control plane takes it back.

Go through the locations in this order:

  1. Re-enable streaming on every location that survived, if the recovery disabled it, as full does. The clusters, replicas and groups still running there need it, to be taken back and repaired in the next steps.

    To re-enable streaming on a location, run:

    edbctl dr set-location-streaming --id <location-id> --enable

    Where <location-id> is the ID of the location, as shown in the Location ID column of edbctl location list. Then check the Streaming column of the same output to confirm it took effect.

  2. Before you remove anything, confirm that streaming is disabled on every affected location and on no other. An affected location is one that was lost, or the one the recovered HM runs in, when it reuses the old location ID. If a lost location still streams, for example because primary-only was used when other locations were lost too, disable it as in Disable resource streaming on an affected location. If a surviving location is left disabled, the clusters with replicas or groups there can't be repaired in the next step, because a forced failover or dropping a lost group needs streaming.

  3. Remove the Postgres clusters, replicas and groups that ran in the lost locations, as in Remove Postgres clusters, replicas and groups from an affected location. Delete each cluster that ran only in lost locations, and repair in place each one that still has replicas or groups in a surviving location.

  4. Only then re-enable streaming on the location the recovered HM runs in, with the command in step 1, if it reuses the old location ID. If you re-enable it before the removal, HM creates the clusters that ran there again from scratch, rather than from their backups.

  5. Restore each deleted cluster from the HM console, from its most recent backup, as in Restore Postgres clusters.

Warning

Don't re-enable streaming on a location that is still lost. HM then lets you create clusters there, but the location is unreachable and can't act on the requests, so the clusters stay pending. Re-enable streaming only once the location is back.

Known limitations

  • Your Postgres clusters are restored by hand, one at a time, from the HM console. See Restoring the Postgres clusters.

  • Custom managed storage locations aren't restored. If the old HM had any, restore them as in the first step of Restore resources before you restore the Postgres clusters that back up to them.

  • The recovered HM keeps using the bucket of the old one. Recovering into an environment with its own bucket isn't supported: the restored resources still point at the old bucket, and the backups of your Postgres clusters stay there. Use the manual procedure instead.

  • Identity provider (IdP) settings aren't restored. Configure your IdP on the recovered HM again.