Summary
CNPG's default backup target is prefer-standby, so by default CNPG dispatches scheduled backups to a replica to keep the datafile scan and object-store upload IO off the primary. With this plugin, a backup that lands on a standby fails at stanza-create with pgBackRest exit 56 (unable to find primary cluster - cannot proceed). On a cluster with two or more instances, the only way to get backups working today is to pin spec.target: primary, which puts the full backup IO back on the primary. That defeats the purpose of prefer-standby.
This is a feature request for backups physically taken from a standby, coordinated with the primary the way pgBackRest supports natively via multi-host plus --backup-standby. It comes with a concrete, SSH-less design and an offer to implement it.
I've read the two earlier issues on this (#79 and #83). Both are closed, and neither landed a fix on main. How this proposal relates to them, and why it isn't a re-file, is below.
Current behavior (main at 025fd12, functionally identical to the latest release v0.6.0)
The plugin configures pgBackRest for exactly one local PostgreSQL instance and never generates a second (primary) host:
-
AppendStanzaOptionsFromConfiguration calls appendStanzaOptions(ctx, options, 0, …) with a hardcoded db index 0. The maintainer TODO right above it already raises the same question:
// TODO: There probably should be more entries for replicas. Is it even doable
// to backup from a replica without SSH access or pgbackrest server running on the other node?
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/command/commandbuilder.go#L199-L203
-
That single instance is wired to the local unix socket /controller/run/. No pgN-host is emitted for a peer:
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/command/commandbuilder.go#L214-L227
-
The Backup RPC runs CreatePgbackrestStanza unconditionally, before Take, with no check for whether the pod is a standby or in recovery:
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/cnpgi/instance/backup.go#L121
-
The public API type PgbackrestConfiguration (and the archives.pgbackrest.cnpg.opera.com CRD) has no topology, host, or standby fields. It only carries repositories, wal, data, restore, compression, and stanza:
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/api/config.go#L324-L353
-
The project's own e2e backup fixtures pin target: primary with // TODO: Implement support for standby backups. in two places:
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/test/e2e/internal/tests/backup/fixtures.go#L153
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/test/e2e/internal/tests/backup/fixtures.go#L176
Reproduction: any CNPG cluster with two or more instances plus this plugin. Create a Backup or ScheduledBackup with spec.target: prefer-standby (or leave it at the CNPG default). CNPG places the backup on a replica, the phase goes to failed, and the sidecar shows stanza-create exit 56. Setting spec.target: primary makes the same backup succeed.
Relationship to #79 and #83
This issue picks up from that comment. It is a concrete design for the coordinated case plus an offer to build it, rather than a re-open of the "just skip stanza-create" idea.
Why the minimal "skip stanza-create on a standby" idea isn't enough
One natural first instinct is to guard CreatePgbackrestStanza with pg_is_in_recovery() and skip it on a standby. That does not produce a working standby backup. pgBackRest's backup command itself requires a primary among its configured pgN hosts. It brackets the backup with pg_backup_start() and pg_backup_stop(), and PostgreSQL refuses those during recovery. With only the local standby configured, backup fails with the same unable to find primary cluster (exit 56) that stanza-create hits. So removing the stanza-create step does not get you there. The plugin has to give pgBackRest a connection to the current primary, which means a multi-host setup. This matches @Agalin's point above.
Proposed design (SSH-less, TLS-server multi-host)
pgBackRest's supported path for a standby backup is a two-host config. It talks to both primary and standby, runs start/stop backup on the primary, and copies data from the standby. A distroless sidecar has no SSH, so this uses pgBackRest's TLS server transport, which is the modern alternative to SSH. Sketch:
- Emit a second pg-host in the config of the pod running the backup (the standby):
pg2-host=<current primary>, pg2-host-type=tls, pg2-host-port, pg2-path, pg2-user, and pg2-host-cert-file / pg2-host-key-file / pg2-host-ca-file. pg1 stays the local instance as today.
- Run a pgBackRest TLS server in each instance sidecar (
pgbackrest server plus tls-server-address / -port / -cert-file / -key-file / -ca-file / -auth), so a peer sidecar can drive pg_backup_start and pg_backup_stop on it. Today the sidecar only runs client commands against the local socket.
- Pass
--backup-standby, either as y or mapped from the CNPG target so prefer-standby becomes --backup-standby=prefer, so pgBackRest offloads the data copy to the standby while coordinating control on the primary.
- Failover-aware primary discovery. The primary is dynamic. The plugin already reads this from CNPG:
configuration.Cluster.Status.CurrentPrimary and Cluster.IsReplica() are used in the WAL path (internal/cnpgi/common/wal.go). The backup config-build path can reuse the same signal to fill pg2-host (the current-primary pod, or the -rw Service) and reconcile it when the primary changes.
- TLS material for the pgBackRest server. This is a separate cert chain from the plugin's existing CNPG-I gRPC client and server secrets. The plugin would need to provision (or deliberately reuse) certs for the pgBackRest server, with client-cert CNs that
tls-server-auth accepts.
This also implies a small API addition, for example an opt-in backupStandby knob on the archive config, since PgbackrestConfiguration has nowhere to express any of this today.
Open questions for maintainers
- Is a PR along these lines something you'd accept, or do you already have a direction in mind?
- TLS server lifecycle: a long-running
pgbackrest server alongside the existing sidecar process, or one started on demand per backup. Any preference?
- Certs: provision a dedicated pgBackRest-server chain, or reuse and derive from the existing plugin TLS secrets? Any constraint I should respect?
- Primary discovery: the
-rw Service (k8s handles the failover repointing, though TLS CN verification against a Service name needs some thought), or resolving the current-primary pod from Cluster.Status.CurrentPrimary directly?
- Anything in the CNPG-I sidecar model that makes a peer-to-peer TLS server between instance pods harder than I've assumed?
Happy to prototype this and open a draft PR to make the discussion concrete. I wanted to align on direction, and on the questions above, before writing much code.
References: pgBackRest user guide sections on backup from a standby and TLS server setup; the pgstef writeup linked on #83; pgBackRest #1553 and the Crunchy Data writeup linked on #79.
Summary
CNPG's default backup target is
prefer-standby, so by default CNPG dispatches scheduled backups to a replica to keep the datafile scan and object-store upload IO off the primary. With this plugin, a backup that lands on a standby fails atstanza-createwith pgBackRest exit 56 (unable to find primary cluster - cannot proceed). On a cluster with two or more instances, the only way to get backups working today is to pinspec.target: primary, which puts the full backup IO back on the primary. That defeats the purpose ofprefer-standby.This is a feature request for backups physically taken from a standby, coordinated with the primary the way pgBackRest supports natively via multi-host plus
--backup-standby. It comes with a concrete, SSH-less design and an offer to implement it.I've read the two earlier issues on this (#79 and #83). Both are closed, and neither landed a fix on
main. How this proposal relates to them, and why it isn't a re-file, is below.Current behavior (
mainat025fd12, functionally identical to the latest releasev0.6.0)The plugin configures pgBackRest for exactly one local PostgreSQL instance and never generates a second (primary) host:
AppendStanzaOptionsFromConfigurationcallsappendStanzaOptions(ctx, options, 0, …)with a hardcoded db index0. The maintainer TODO right above it already raises the same question:https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/command/commandbuilder.go#L199-L203
That single instance is wired to the local unix socket
/controller/run/. NopgN-hostis emitted for a peer:https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/command/commandbuilder.go#L214-L227
The
BackupRPC runsCreatePgbackrestStanzaunconditionally, beforeTake, with no check for whether the pod is a standby or in recovery:https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/cnpgi/instance/backup.go#L121
The public API type
PgbackrestConfiguration(and thearchives.pgbackrest.cnpg.opera.comCRD) has no topology, host, or standby fields. It only carries repositories, wal, data, restore, compression, and stanza:https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/internal/pgbackrest/api/config.go#L324-L353
The project's own e2e backup fixtures pin
target: primarywith// TODO: Implement support for standby backups.in two places:https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/test/e2e/internal/tests/backup/fixtures.go#L153
https://github.com/operasoftware/cnpg-plugin-pgbackrest/blob/v0.6.0/test/e2e/internal/tests/backup/fixtures.go#L176
Reproduction: any CNPG cluster with two or more instances plus this plugin. Create a
BackuporScheduledBackupwithspec.target: prefer-standby(or leave it at the CNPG default). CNPG places the backup on a replica, the phase goes tofailed, and the sidecar showsstanza-createexit 56. Settingspec.target: primarymakes the same backup succeed.Relationship to #79 and #83
backup.gostill callsCreatePgbackrestStanzaunconditionally onmain. On that thread @Agalin wrote: "while I agree that standby backups should be added, it's not as simple as executing pgbackrest on the standby … backup process must be coordinated across primary and standby node." That is correct, and it is the reason for the design below.This issue picks up from that comment. It is a concrete design for the coordinated case plus an offer to build it, rather than a re-open of the "just skip stanza-create" idea.
Why the minimal "skip stanza-create on a standby" idea isn't enough
One natural first instinct is to guard
CreatePgbackrestStanzawithpg_is_in_recovery()and skip it on a standby. That does not produce a working standby backup. pgBackRest'sbackupcommand itself requires a primary among its configuredpgNhosts. It brackets the backup withpg_backup_start()andpg_backup_stop(), and PostgreSQL refuses those during recovery. With only the local standby configured,backupfails with the sameunable to find primary cluster(exit 56) that stanza-create hits. So removing the stanza-create step does not get you there. The plugin has to give pgBackRest a connection to the current primary, which means a multi-host setup. This matches @Agalin's point above.Proposed design (SSH-less, TLS-server multi-host)
pgBackRest's supported path for a standby backup is a two-host config. It talks to both primary and standby, runs start/stop backup on the primary, and copies data from the standby. A distroless sidecar has no SSH, so this uses pgBackRest's TLS server transport, which is the modern alternative to SSH. Sketch:
pg2-host=<current primary>,pg2-host-type=tls,pg2-host-port,pg2-path,pg2-user, andpg2-host-cert-file/pg2-host-key-file/pg2-host-ca-file.pg1stays the local instance as today.pgbackrest serverplustls-server-address/-port/-cert-file/-key-file/-ca-file/-auth), so a peer sidecar can drivepg_backup_startandpg_backup_stopon it. Today the sidecar only runs client commands against the local socket.--backup-standby, either asyor mapped from the CNPG target soprefer-standbybecomes--backup-standby=prefer, so pgBackRest offloads the data copy to the standby while coordinating control on the primary.configuration.Cluster.Status.CurrentPrimaryandCluster.IsReplica()are used in the WAL path (internal/cnpgi/common/wal.go). The backup config-build path can reuse the same signal to fillpg2-host(the current-primary pod, or the-rwService) and reconcile it when the primary changes.tls-server-authaccepts.This also implies a small API addition, for example an opt-in
backupStandbyknob on the archive config, sincePgbackrestConfigurationhas nowhere to express any of this today.Open questions for maintainers
pgbackrest serveralongside the existing sidecar process, or one started on demand per backup. Any preference?-rwService (k8s handles the failover repointing, though TLS CN verification against a Service name needs some thought), or resolving the current-primary pod fromCluster.Status.CurrentPrimarydirectly?Happy to prototype this and open a draft PR to make the discussion concrete. I wanted to align on direction, and on the questions above, before writing much code.
References: pgBackRest user guide sections on backup from a standby and TLS server setup; the pgstef writeup linked on #83; pgBackRest #1553 and the Crunchy Data writeup linked on #79.