Skip to content

Feature: support backups taken from a standby (pgBackRest TLS multi-host + --backup-standby) #103

Description

@melancholictheory

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:

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Is a PR along these lines something you'd accept, or do you already have a direction in mind?
  2. TLS server lifecycle: a long-running pgbackrest server alongside the existing sidecar process, or one started on demand per backup. Any preference?
  3. Certs: provision a dedicated pgBackRest-server chain, or reuse and derive from the existing plugin TLS secrets? Any constraint I should respect?
  4. 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?
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions