<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Viacheslav(Slava) Sarzhan</title>
    <description>The latest articles on DEV Community by Viacheslav(Slava) Sarzhan (@vyacheslav_sarzhan_3c1767).</description>
    <link>https://dev.to/vyacheslav_sarzhan_3c1767</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951251%2Ffb0304d0-d0a6-4ea8-824d-679d79b8a4a0.jpg</url>
      <title>DEV Community: Viacheslav(Slava) Sarzhan</title>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vyacheslav_sarzhan_3c1767"/>
    <language>en</language>
    <item>
      <title>Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator: The Standby Cluster Method</title>
      <dc:creator>Viacheslav(Slava) Sarzhan</dc:creator>
      <pubDate>Wed, 27 May 2026 12:38:21 +0000</pubDate>
      <link>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-the-standby-cluster-2k5h</link>
      <guid>https://dev.to/vyacheslav_sarzhan_3c1767/migrate-from-crunchy-data-postgresql-operator-to-percona-postgresql-operator-the-standby-cluster-2k5h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuem9x3be7wsdf21sv83f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuem9x3be7wsdf21sv83f.png" alt="Migrate from Crunchy Data PostgreSQL Operator to Percona PostgreSQL Operator" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Crunchy to Percona PostgreSQL migration is more straightforward than most cross-operator moves on Kubernetes, because the Percona PostgreSQL Operator is a hard fork of the Crunchy Data PostgreSQL Operator. Same Patroni HA, same pgBackRest backups, same overall CRD shape. This post walks through the safest of the three migration paths: a standby cluster method with near-zero downtime.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;part 2 of a 3-part series&lt;/strong&gt; on running PostgreSQL on Kubernetes with a fully open-source operator. &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; walked through the changing open-source landscape and announced the hard fork of the Crunchy Data PostgreSQL Operator into the fully independent Percona PostgreSQL Operator &lt;a href="https://github.com/percona/percona-postgresql-operator/tree/v3.0.0" rel="noopener noreferrer"&gt;v3.0.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is the first practical playbook of the series. It covers the &lt;strong&gt;standby cluster method&lt;/strong&gt;, the safest migration path when downtime budget is tight. &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt; covers two simpler paths: backup-and-restore and persistent-volume reuse.&lt;/p&gt;

&lt;p&gt;If you are landing here without context on why you might want to migrate at all, &lt;a href="https://www.percona.com/blog/not-all-open-source-is-equal-choosing-postgresql-operator-kubernetes-2026/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;start with part 1&lt;/a&gt;. The rest of this post assumes you have already decided to move and want a tested playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration approach in one paragraph
&lt;/h2&gt;

&lt;p&gt;The Percona PostgreSQL Kubernetes Operator is a hard fork of the Crunchy Data PostgreSQL Kubernetes Operator, which simplifies the migration paths considerably: the same underlying tools (Patroni, pgBackRest, PgBouncer) and the same overall design are used in both operators. All three migration paths in this series are reversible: because Percona's operator is fully open source and remains compatible with the same backup format, the move back to Crunchy is also possible if your team decides to walk it back.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on the storage layer
&lt;/h3&gt;

&lt;p&gt;All examples in this guide use an in-cluster &lt;a href="https://github.com/seaweedfs/seaweedfs" rel="noopener noreferrer"&gt;SeaweedFS&lt;/a&gt; instance as the pgBackRest S3 repository. SeaweedFS is Apache-2.0 licensed, actively maintained, and a clean drop-in replacement for the role MinIO used to fill in this stack. Any other S3-compatible storage works just as well: AWS S3, Google Cloud Storage (via HMAC keys), Ceph RadosGW, Cloudflare R2, and so on. For non-SeaweedFS endpoints, remove &lt;code&gt;repo1-s3-uri-style: path&lt;/code&gt; and &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt; from the pgBackRest configuration and replace the endpoint with your provider's URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this series does NOT cover
&lt;/h3&gt;

&lt;p&gt;To keep scope honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application-side connection-string changes beyond updating to the new pgBouncer service. If your app uses connection-pool tuning, custom auth, or a service mesh, that work stays with you.&lt;/li&gt;
&lt;li&gt;Schema-changing upgrades, major PostgreSQL version upgrades, or extension migrations. The PostgreSQL major version must match between source and target.&lt;/li&gt;
&lt;li&gt;Crunchy enterprise-only features like TDE, Crunchy Postgres for Kubernetes specific operators, or pgBackRest custom encryption. If your environment uses these, contact the Percona team for a tailored plan.&lt;/li&gt;
&lt;li&gt;Operating two operators against the same namespace before the hard fork. Use Percona PostgreSQL Operator v3.0.0 or higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tested with
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crunchy Data PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v5.8.x (tested on v5.8.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percona PostgreSQL Kubernetes Operator&lt;/td&gt;
&lt;td&gt;v3.x.x (tested on v3.0.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;18 (must match between source and target)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;SeaweedFS (Apache-2.0), or any other S3-compatible service accessible from all cluster pods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt; (v3), &lt;code&gt;yq&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Different versions may have slight differences in CR fields or behavior. Always consult the official documentation for the operator and PostgreSQL version you are running.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration using a standby cluster
&lt;/h2&gt;

&lt;p&gt;This is the safest method when downtime budget is tight. The Percona cluster is brought up as a standby of the Crunchy primary, catches up via pgBackRest plus streaming replication, and is promoted at cutover. The only downtime is the cutover step itself.&lt;/p&gt;

&lt;p&gt;You can wire the standby in two ways, and combining both gives you maximum safety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pgBackRest repo-based standby&lt;/strong&gt; seeds the standby from the latest base backup and replays archived WAL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming replication&lt;/strong&gt; keeps the standby in sync with the live primary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F350v7cc0v3ecb22uugwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F350v7cc0v3ecb22uugwu.png" alt="Standby cluster migration overview" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Before you begin
&lt;/h3&gt;

&lt;p&gt;Set the target namespace once. Every command in this guide reads from this variable so you can change it in a single place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres-migration
kubectl create namespace &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy SeaweedFS
&lt;/h3&gt;

&lt;p&gt;Skip this step if you already have an S3-compatible repository (AWS S3, GCS, Ceph). Update the endpoint and credentials in the YAML examples accordingly.&lt;/p&gt;

&lt;p&gt;SeaweedFS provides an S3-compatible object store that runs inside Kubernetes. Both operators will use it as the shared pgBackRest WAL archive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS is required.&lt;/strong&gt; pgBackRest always connects to S3 endpoints over HTTPS, even when &lt;code&gt;repo1-s3-verify-tls: "n"&lt;/code&gt; is set (that flag skips certificate verification, it does not fall back to HTTP). The steps below generate a self-signed certificate and pass it to SeaweedFS via Helm values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a self-signed TLS certificate for SeaweedFS S3&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /tmp/seaweedfs.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=seaweedfs-all-in-one"&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; create secret tls seaweedfs-s3-tls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/seaweedfs.key

helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;seaweedfs seaweedfs/seaweedfs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 4.23.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/seaweedfs-values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Helm values file in the repo creates the &lt;code&gt;pg-migration&lt;/code&gt; bucket on first start, so no separate &lt;code&gt;aws s3 mb&lt;/code&gt; step is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 0. Create pgBackRest secrets
&lt;/h3&gt;

&lt;p&gt;Both operators need credentials to read and write the shared SeaweedFS bucket. Apply the secrets from &lt;code&gt;examples/01-pgbackrest-secret.yaml&lt;/code&gt; after filling in your access key and secret key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy and edit the file first to set your credentials.&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/01-pgbackrest-secret.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both secrets contain the same SeaweedFS credentials (&lt;code&gt;pgmigration&lt;/code&gt; / &lt;code&gt;pgmigration123&lt;/code&gt;). For AWS S3, replace those with your IAM access key ID and secret access key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Start with your existing Crunchy Data cluster
&lt;/h3&gt;

&lt;p&gt;If you already have a running Crunchy cluster, ensure its pgBackRest &lt;code&gt;repo1&lt;/code&gt; points at the shared bucket and path. The &lt;code&gt;repo1-path&lt;/code&gt; value must be identical in both cluster specs. Mismatched paths will prevent the Percona standby from finding the WAL archive.&lt;/p&gt;

&lt;p&gt;Optional: deploy a Crunchy operator to test the migration end to end. &lt;strong&gt;The Helm install below is shown only as a quick way to reproduce this blog post's example. If you are running Crunchy PGO in production, follow &lt;a href="https://access.crunchydata.com/documentation/postgres-operator/latest/installation/" rel="noopener noreferrer"&gt;the official Crunchy Data documentation&lt;/a&gt; for installation. The migration steps in the rest of this post do not depend on how you deployed the source operator.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;pgo &lt;span class="se"&gt;\&lt;/span&gt;
  oci://registry.developers.crunchydata.com/crunchydata/pgo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 5.8.7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;singleNamespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply &lt;code&gt;examples/02-crunchy-source-cluster.yaml&lt;/code&gt; (or adapt your existing cluster's pgBackRest config):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/02-crunchy-source-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key pgBackRest settings in the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repo1-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/crunchy-to-percona/repo1&lt;/span&gt;   &lt;span class="c1"&gt;# shared path, must match Percona side&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-uri-style&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;                &lt;span class="c1"&gt;# required for path-style S3 endpoints (SeaweedFS, MinIO)&lt;/span&gt;
  &lt;span class="na"&gt;repo1-s3-verify-tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n"&lt;/span&gt;                &lt;span class="c1"&gt;# skip TLS verification for self-signed cert; remove for AWS S3&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;
    &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-migration&lt;/span&gt;
      &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seaweedfs-all-in-one.postgres-migration.svc.cluster.local:8443&lt;/span&gt;
      &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the cluster to be ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--selector&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Trigger a full backup on the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;Wait for the pgBackRest stanza to be created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;postgrescluster/crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a full backup before creating the Percona standby. This gives the standby a recent base to restore from, so it only needs to replay a small amount of WAL to catch up. This matches the realistic production migration pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl annotate postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the backup job to complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;job &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/pgbackrest-backup&lt;span class="o"&gt;=&lt;/span&gt;manual,postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3. Copy TLS certificates (cross-namespace only)
&lt;/h3&gt;

&lt;p&gt;If the Percona cluster is in a different namespace from the Crunchy cluster, copy the Crunchy TLS secrets to the Percona namespace. These allow mutual TLS authentication during streaming replication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;secret &lt;span class="k"&gt;in &lt;/span&gt;crunchy-source-cluster-cert crunchy-source-replication-cert&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;kubectl get secret &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;secret&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;CRUNCHY_NS&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json | &lt;span class="se"&gt;\&lt;/span&gt;
    yq &lt;span class="s1"&gt;'{"apiVersion": .apiVersion, "kind": .kind, "data": .data,
         "metadata": {"name": .metadata.name}, "type": .type}'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="se"&gt;\&lt;/span&gt;
    kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both clusters are in the same namespace, skip this step. The secrets are already accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4. Deploy the Percona PG Operator
&lt;/h3&gt;

&lt;p&gt;The Crunchy PGO operator can stay in the same or a different namespace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;kubectl apply&lt;/code&gt; below pulls the operator manifest from the &lt;code&gt;migration-from-crunchy-guide&lt;/code&gt; branch of the operator repo, which is the source for this guide's examples. For production deployments, follow &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/installation.html" rel="noopener noreferrer"&gt;the official Percona Operator for PostgreSQL installation documentation&lt;/a&gt; and pin to a released version tag rather than a feature branch.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/tags/v3.0.0/deploy/bundle.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the operator deployment is ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;deployment percona-postgresql-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5. Create the Percona cluster in standby mode
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/03-percona-standby-cluster.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/03-percona-standby-cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key settings that wire the Percona cluster to the Crunchy source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;standby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;repoName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repo1&lt;/span&gt;                                         &lt;span class="c1"&gt;# restore initial base backup from this repo&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-ha.postgres-migration.svc.cluster.local&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;

&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;customTLSSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-cluster-cert&lt;/span&gt;                     &lt;span class="c1"&gt;# Crunchy CA for mutual TLS&lt;/span&gt;
  &lt;span class="na"&gt;customReplicationTLSSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crunchy-source-replication-cert&lt;/span&gt;                 &lt;span class="c1"&gt;# cert for _crunchyreplication user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Percona operator will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restore the base backup from the SeaweedFS bucket.&lt;/li&gt;
&lt;li&gt;Replay WAL from SeaweedFS until it catches up with the live Crunchy cluster.&lt;/li&gt;
&lt;li&gt;Switch to streaming replication from &lt;code&gt;crunchy-source-ha&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wait for the cluster to reach ready state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that data is replicating to the standby:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;t&lt;/code&gt; (in recovery) and a non-null LSN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6. Verify replication lag before cutover
&lt;/h3&gt;

&lt;p&gt;Query the Crunchy primary to confirm the Percona standby has caught up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;master &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    SELECT
        client_addr,
        state,
        pg_wal_lsn_diff(sent_lsn, replay_lsn) AS byte_lag,
        write_lag,
        flush_lag,
        replay_lag
    FROM pg_stat_replication;
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proceed to the next step only when &lt;code&gt;write_lag&lt;/code&gt; and &lt;code&gt;replay_lag&lt;/code&gt; are NULL or under a few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7. Cutover the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;This is the only step that causes downtime. Stop accepting writes on the application side, then patch the Crunchy cluster into standby mode. Patroni steps down and archives the final WAL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"standby": {"enabled": true, "repoName": "repo1"}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify demotion (poll until &lt;code&gt;pg_is_in_recovery()&lt;/code&gt; returns &lt;code&gt;t&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CRUNCHY_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 8. (Optional) Shut down the Crunchy cluster
&lt;/h3&gt;

&lt;p&gt;Once the Percona standby has replayed all WAL, shut down the Crunchy cluster to prevent split-brain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch postgrescluster crunchy-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"shutdown": true}}'&lt;/span&gt;

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;crunchy-source,postgres-operator.crunchydata.com/data&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9. Promote the Percona cluster
&lt;/h3&gt;

&lt;p&gt;Confirm that the Percona standby has finished replaying all WAL (the LSN stops advancing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_last_wal_replay_lsn();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this a few times. When the LSN is stable, replay is complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch perconapgcluster percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"spec": {"standby": {"enabled": false}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the cluster to become ready and confirm it is writable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ready &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;480s

&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;primary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  psql &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT pg_is_in_recovery();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;f&lt;/code&gt; (the cluster is now the primary and accepts writes).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10. Verify stanza creation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgcluster/percona-standby &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.pgbackrest.repos[0].stanzaCreated}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 11. Take a post-migration backup
&lt;/h3&gt;

&lt;p&gt;Apply &lt;code&gt;examples/04-post-migration-backup.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/percona/percona-postgresql-operator/refs/heads/migration-from-crunchy-guide/e2e-tests/tests/migration-from-crunchy-standby/examples/04-post-migration-backup.yaml

kubectl &lt;span class="nb"&gt;wait &lt;/span&gt;perconapgbackup/post-migration-backup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.state}'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Succeeded &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clean recovery point on the new timeline. All future PITR restores will use this backup as their starting point, independent of the old Crunchy WAL archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconnecting your application
&lt;/h3&gt;

&lt;p&gt;Update your application's connection string to point at the Percona cluster's pgBouncer service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get service &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; postgres-operator.crunchydata.com/cluster&lt;span class="o"&gt;=&lt;/span&gt;percona-standby,postgres-operator.crunchydata.com/role&lt;span class="o"&gt;=&lt;/span&gt;pgbouncer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This migration path works almost entirely out of the box. For users coming from the Crunchy Data PostgreSQL Operator, this method feels familiar because it leverages the same standby/replica mechanisms used for HA and disaster recovery. The key difference is that you can now use this familiar mechanism to migrate safely to the Percona PostgreSQL Operator, a fully open-source alternative running on a fully open-source storage layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback
&lt;/h3&gt;

&lt;p&gt;The standby method is the most rollback-friendly of the three. Until you take the post-migration backup, the Crunchy cluster still holds the original timeline. To roll back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop writes on the Percona side and patch the Percona cluster back into standby mode (&lt;code&gt;spec.standby.enabled: true&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Patch the Crunchy cluster out of standby mode and let Patroni promote it.&lt;/li&gt;
&lt;li&gt;Verify with &lt;code&gt;pg_is_in_recovery()&lt;/code&gt; on both sides.&lt;/li&gt;
&lt;li&gt;Switch the application connection string back to the Crunchy pgBouncer service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After Step 11 (post-migration backup), the timelines have diverged. From that point, the rollback story is the same as a fresh restore, and you should treat the Crunchy cluster as a historical reference, not a live target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Percona standby not connecting to the Crunchy primary.&lt;/strong&gt; Verify the &lt;code&gt;crunchy-source-ha&lt;/code&gt; service resolves from within the Percona pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STANDBY_POD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"getent hosts crunchy-source-ha.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.cluster.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replication authentication errors.&lt;/strong&gt; The Percona standby authenticates as the &lt;code&gt;_crunchyreplication&lt;/code&gt; PostgreSQL user using the certificate in &lt;code&gt;crunchy-source-replication-cert&lt;/code&gt;. Verify the secret exists and matches what the Crunchy operator generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secret crunchy-source-replication-cert &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pgBackRest restore fails.&lt;/strong&gt; Confirm both secrets contain identical credentials and that &lt;code&gt;repo1-path&lt;/code&gt; is the same in both cluster specs (&lt;code&gt;/crunchy-to-percona/repo1&lt;/code&gt; in this guide). Mismatched paths cause an &lt;code&gt;archive.info missing&lt;/code&gt; error. Verify the bucket is reachable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; s3-check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;perconalab/awscli &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    AWS_ACCESS_KEY_ID=pgmigration &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_SECRET_ACCESS_KEY=pgmigration123 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    AWS_DEFAULT_REGION=us-east-1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    aws --endpoint-url https://seaweedfs-all-in-one.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MIGRATION_NS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.cluster.local:8443 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        --no-verify-ssl &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
        s3 ls s3://pg-migration
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Timeline history file (&lt;code&gt;00000002.history&lt;/code&gt;) missing after promotion.&lt;/strong&gt; This is a known &lt;a href="https://github.com/CrunchyData/postgres-operator/issues/4472" rel="noopener noreferrer"&gt;issue&lt;/a&gt; with Crunchy PGO's async archive mode. After promotion, push the history file synchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$MIGRATION_NS&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PERCONA_PRIMARY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; database &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
    pgbackrest --stanza=db --no-archive-async &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
      archive-push &lt;/span&gt;&lt;span class="se"&gt;\"\$&lt;/span&gt;&lt;span class="s2"&gt;{PGDATA}/pg_wal/00000002.history&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; || true
  "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This was the safest migration path. &lt;a href="https://www.percona.com/blog/migrate-from-crunchy-data-to-percona-postgresql-operator-backup-restore-pv-reuse/?utm_source=devto&amp;amp;utm_medium=cross-post&amp;amp;utm_campaign=pg-migration-series" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt; covers two simpler options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backup and restore&lt;/strong&gt;. The simplest path. You take a Crunchy pgBackRest backup and the Percona cluster bootstraps from it. Cutover is the time between the final backup and pointing the application at the new cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent volume reuse&lt;/strong&gt;. For when you want to skip the data copy entirely. The Percona cluster takes over the existing PGDATA volume, no restore step required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick the method that fits your downtime budget, data size, and storage layout.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post covers basic deployment patterns and simplified configuration examples. If your environment is more complex, uses custom images, includes Crunchy enterprise features like TDE, or otherwise needs tailored migration steps, contact the Percona team and we will help you plan and execute the move.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Percona Operator for PostgreSQL docs&lt;/strong&gt;: &lt;a href="https://docs.percona.com/percona-operator-for-postgresql/latest/" rel="noopener noreferrer"&gt;https://docs.percona.com/percona-operator-for-postgresql/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/percona/percona-postgresql-operator" rel="noopener noreferrer"&gt;https://github.com/percona/percona-postgresql-operator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue tracker&lt;/strong&gt;: &lt;a href="https://github.com/percona/percona-postgresql-operator/issues" rel="noopener noreferrer"&gt;https://github.com/percona/percona-postgresql-operator/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public roadmap&lt;/strong&gt;: &lt;a href="https://github.com/orgs/percona/projects/10/views/6" rel="noopener noreferrer"&gt;https://github.com/orgs/percona/projects/10/views/6&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Forum&lt;/strong&gt;: &lt;a href="https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68" rel="noopener noreferrer"&gt;https://forums.percona.com/c/postgresql/percona-kubernetes-operator-for-postgresql/68&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>kubernetes</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
