sluice

Migrating from Azure Database for PostgreSQL (Flexible Server) with sluice

The self-grantable REPLICATION attribute (ALTER ROLE ... WITH REPLICATION works as the admin user), the best TLS story in the series (verify-full with zero setup against public roots), and the wal_level flip that needs an explicit restart. The built-in PgBouncer is General-Purpose-tier-plus only.

Azure Database for PostgreSQL (Flexible Server) works with sluice's vanilla postgres engine — live-validated 2026-07-17 (PG 16.14) as a migration and slot-based CDC source: byte-identical bulk migrate on md5 ground truth (NaN in numeric[], ±Infinity, denormal floats, 2-D arrays with NULL elements) and exact CDC convergence with a clean snapshot → CDC handoff.

Enabling logical replication (three self-service steps) #

No ticket, all self-service — but mind the explicit restart in step 2:

  1. Set wal_level=logical — Azure exposes the GUC directly (no provider-specific alias). It is static, so the command returns with the change pending:
    az postgres flexible-server parameter set --resource-group <rg> --server-name <server> \
      --name wal_level --value logical
  2. Restart explicitly — the parameter does NOT take effect until you restart (~1 minute; contrast Cloud SQL, whose patch restarts for you). Verify afterward with SHOW wal_level;:
    az postgres flexible-server restart --resource-group <rg> --name <server>
  3. Grant the connecting role the REPLICATION attribute — this works as the (non-superuser) admin user, because Azure patches the grant for azure_pg_admin members. It is exactly recovery path (a) in sluice's replication-capability refusal:
    ALTER ROLE <role> WITH REPLICATION;

The platform replication role is grant-restricted and irrelevant here — there is no RDS-style membership model; the attribute is the mechanism. Baseline before the flip: wal_level=replica regardless of backup settings (no RDS-style retention-0 ⇒ minimal trap), max_replication_slots=10 / max_wal_senders=10 (unchanged by the flip), and zero platform slots.

TLS: the best story in the series #

TLS is mandatory (plaintext refused at pg_hba) and the certificate chain is public (Microsoft/DigiCert roots). So sslmode=verify-full works with no CA download and no sslrootcert — use it on every Azure DSN; it is both the strictest and the zero-config mode (better than the per-instance-CA fetch that DigitalOcean, Cloud SQL, and Vultr require). If a client stack with its own bundled CA fails verification, it's missing an OS trust store, not an Azure quirk.

sluice sync start \
    --source-driver postgres \
    --source 'postgres://myadmin:[email protected]:5432/app?sslmode=verify-full' \
    --target-driver postgres --target 'postgres://user:pass@target-host:5432/app?sslmode=require' \
    --stream-id azure-pg-app

The built-in PgBouncer (General Purpose tier and up) #

Azure's built-in PgBouncer requires the General Purpose tier or higher (port 6432 on the same hostname when enabled; it cannot be enabled at all on Burstable) and is expected to strip replication like the Supavisor class — untested. Connect sluice to port 5432.

Provisioning friction #

Microsoft.DBforPostgreSQL provider registration is a one-time subscription step, and region availability differs per subscription even from the MySQL flexible service — a region that provisions MySQL can refuse PG with “The location is restricted.” Plan a region fallback.

Decommissioning #

A cleanly stopped sluice stream leaves its (resumable) replication slot in place; when you're done for good, drop it — an abandoned slot retains WAL and will eventually fill the instance disk:

sluice slot drop --yes <slot>

What sluice checks for you #

Next steps #