|
| 1 | +--- |
| 2 | +title: "CNPG Recipe 17 - PostgreSQL In-Place Major Upgrades" |
| 3 | +date: 2025-04-03T09:31:13+01:00 |
| 4 | +description: "How CloudNativePG implements offline in-place major upgrades of PostgreSQL" |
| 5 | +tags: ["postgresql", "postgres", "kubernetes", "k8s", "cloudnativepg", "cnpg", "postgresql", "postgres", "dok", "data on kubernetes"] |
| 6 | +cover: cover.jpg |
| 7 | +thumb: thumb.jpg |
| 8 | +draft: false |
| 9 | +--- |
| 10 | + |
| 11 | +CloudNativePG 1.26 introduces one of its most anticipated features: |
| 12 | +**declarative in-place major upgrades** for PostgreSQL using `pg_upgrade`. This |
| 13 | +new approach allows you to upgrade PostgreSQL clusters by simply modifying the |
| 14 | +`imageName` in their configuration—just like a minor version update. While it |
| 15 | +requires brief downtime, it significantly reduces operational overhead, making |
| 16 | +it ideal for managing **large fleets of PostgreSQL databases** in Kubernetes. |
| 17 | +In this article, I will explore how it works, its benefits and limitations, |
| 18 | +and cover an upgrade of a 2.2TB database. |
| 19 | + |
| 20 | +<!--more--> |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +CloudNativePG 1.26, expected at the end of this month, introduces one of the most highly anticipated features in |
| 25 | +the project's history: in-place major version upgrades of PostgreSQL using |
| 26 | +`pg_upgrade`. |
| 27 | + |
| 28 | +Unlike minor upgrades, which primarily involve applying patches, major upgrades |
| 29 | +require handling changes to the internal storage format introduced by the new |
| 30 | +PostgreSQL version. |
| 31 | + |
| 32 | +This feature is now available for public testing through the preview |
| 33 | +[1.26.0-rc1 release](https://cloudnative-pg.io/releases/cloudnative-pg-1-26.0-rc1-released/). |
| 34 | + |
| 35 | +## An Overview of the Existing Methods |
| 36 | + |
| 37 | +CloudNativePG now provides three declarative (yes, declarative!) methods for |
| 38 | +performing major version upgrades. Two of these require a new cluster and are |
| 39 | +classified as **blue/green deployment strategies**. |
| 40 | + |
| 41 | +The first approach leverages the `import` capability with `pg_dump` and |
| 42 | +`pg_restore`. While practical for small databases and useful for testing new |
| 43 | +versions, the final cutover requires downtime, making it an offline upgrade. |
| 44 | + |
| 45 | +The second method takes advantage of PostgreSQL’s native logical replication, |
| 46 | +enabling zero-downtime upgrades—hence, an online upgrade—regardless of database |
| 47 | +size. This remains my preferred approach for upgrading business-critical |
| 48 | +PostgreSQL databases. It can also be used for migrations from external |
| 49 | +environments into Kubernetes (e.g., from Amazon RDS to CloudNativePG). |
| 50 | +For more details, see ["CloudNativePG Recipe 15 - PostgreSQL Major Online Upgrades with Logical Replication"]({{< relref "../20241210-major-online-upgrades/index.md" >}}). |
| 51 | + |
| 52 | +The third method, and the focus of this article, is offline in-place upgrades |
| 53 | +using `pg_upgrade`, PostgreSQL's official tool for this kind of operations. |
| 54 | + |
| 55 | +## The Use Case for In-Place Major Upgrades |
| 56 | + |
| 57 | +The primary motivation for introducing this feature in Kubernetes is to |
| 58 | +eliminate the operational difference between minor and major PostgreSQL |
| 59 | +upgrades for GitOps users. With this approach, upgrading simply requires |
| 60 | +modifying the cluster configuration's `spec` and updating the image for all |
| 61 | +cluster components (primary and standby servers). This is particularly |
| 62 | +beneficial at scale—when managing dozens or even hundreds of PostgreSQL |
| 63 | +clusters within the same Kubernetes cluster—where blue/green upgrades pose |
| 64 | +operational challenges. |
| 65 | + |
| 66 | +## Before You Start |
| 67 | + |
| 68 | +In-place major upgrades are currently available for preview and testing in |
| 69 | +[CloudNativePG 1.26.0-RC1](https://cloudnative-pg.io/documentation/preview/installation_upgrade/#directly-using-the-operator-manifest). |
| 70 | +You can test this feature on any Kubernetes cluster, including a local setup |
| 71 | +using `kind`, as explained in ["CloudNativePG Recipe 1 - Setting Up Your Local Playground in Minutes"]({{< relref "../20240303-recipe-local-setup/index.md" >}}). |
| 72 | + |
| 73 | +To deploy CloudNativePG 1.26.0-RC1, run: |
| 74 | + |
| 75 | +```sh |
| 76 | +kubectl apply --server-side -f \ |
| 77 | + https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.26.0-rc1.yaml |
| 78 | +``` |
| 79 | + |
| 80 | +## How It Works |
| 81 | + |
| 82 | +CloudNativePG allows you to specify the PostgreSQL operand image in two ways: |
| 83 | + |
| 84 | +- Using the `.spec.imageName` option |
| 85 | +- Using image catalogs (`ImageCatalog` and `ClusterImageCatalog` resources) |
| 86 | + |
| 87 | +This article focuses on the `imageName` method, though the same principles |
| 88 | +apply to the image catalog approach. |
| 89 | + |
| 90 | +Let’s assume you have a PostgreSQL cluster running with: |
| 91 | + |
| 92 | +```yaml |
| 93 | +imageName: ghcr.io/cloudnative-pg/postgresql:13.20-minimal-bullseye |
| 94 | +``` |
| 95 | +
|
| 96 | +This means your cluster is using the latest available container image for |
| 97 | +PostgreSQL 13 (minor version 20). Since PostgreSQL 13 reaches end-of-life in |
| 98 | +November this year, you decide to upgrade to PostgreSQL 17 using the |
| 99 | +`ghcr.io/cloudnative-pg/postgresql:17.4-minimal-bullseye` image. |
| 100 | + |
| 101 | +By updating the `imageName` field in the cluster configuration, CloudNativePG |
| 102 | +automatically initiates a major version upgrade. |
| 103 | + |
| 104 | +### The Upgrade Process |
| 105 | + |
| 106 | +The first step is safely shutting down the PostgreSQL cluster to ensure data |
| 107 | +consistency before upgrading. This is an offline operation that incurs |
| 108 | +downtime, but it allows modifications to static data files with full integrity. |
| 109 | + |
| 110 | +CloudNativePG then updates the `Cluster` resource status to record the |
| 111 | +currently running image before initiating the upgrade. This is essential for |
| 112 | +rollback in case of failure (discussed later in the article). |
| 113 | + |
| 114 | +After that, CloudNativePG starts a Kubernetes job responsible for preparing the |
| 115 | +PostgreSQL data files on the Persistent Volume Claims (PVC) for the new major |
| 116 | +version using `pg_upgrade`: |
| 117 | + |
| 118 | +- The job creates a temporary copy of the old PostgreSQL binaries. |
| 119 | +- It initializes a new `PGDATA` directory using `initdb` for the target |
| 120 | + PostgreSQL version. |
| 121 | +- It verifies the upgrade requirement by comparing the on-disk PostgreSQL |
| 122 | + versions, preventing unintended upgrades based on image tags. |
| 123 | +- It automatically remaps WAL and tablespace volumes as needed. |
| 124 | + |
| 125 | +At this point, it runs the actual upgrade process with `pg_upgrade` and the |
| 126 | +`--link` option to leverage hard links, significantly speeding up data |
| 127 | +migration while minimizing storage overhead and disk I/0. |
| 128 | + |
| 129 | +If the upgrade completes successfully, CloudNativePG replaces the original |
| 130 | +PostgreSQL data directories with the upgraded versions, destroys the persistent |
| 131 | +volume claims of the replicas, and restarts the cluster. |
| 132 | + |
| 133 | +However, if `pg_upgrade` encounters an error, you will need to manually revert |
| 134 | +to the previous PostgreSQL major version by updating the `Cluster` |
| 135 | +specification and deleting the upgrade job. Like any in-place upgrade, there is |
| 136 | +always a risk of failure. To mitigate this, it is crucial to maintain |
| 137 | +continuous base backups. If your storage class supports volume snapshots, |
| 138 | +consider taking one before initiating the upgrade—it’s a simple precaution that |
| 139 | +could save you from unexpected issues. |
| 140 | + |
| 141 | +Overall, this streamlined approach enhances the efficiency and reliability of |
| 142 | +in-place major upgrades, making PostgreSQL version transitions more manageable |
| 143 | +in Kubernetes environments. |
| 144 | + |
| 145 | +## Example |
| 146 | + |
| 147 | +The best way to understand this feature is to test it in practice. Let’s start |
| 148 | +with a basic PostgreSQL 13 cluster named `pg`, defined in the following |
| 149 | +`pg.yaml`: |
| 150 | + |
| 151 | +```yaml |
| 152 | +{{< include "yaml/pg-13.yaml" >}} |
| 153 | +``` |
| 154 | + |
| 155 | +After creating the cluster, check its status with: |
| 156 | + |
| 157 | +```sh |
| 158 | +kubectl cnpg status pg |
| 159 | +``` |
| 160 | + |
| 161 | +You can also verify the version with `psql`: |
| 162 | + |
| 163 | +```sh |
| 164 | +kubectl cnpg psql pg -- -qAt -c 'SELECT version()' |
| 165 | +``` |
| 166 | + |
| 167 | +Returning something similar to this: |
| 168 | + |
| 169 | +```console |
| 170 | +PostgreSQL 13.20 (Debian 13.20-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit |
| 171 | +``` |
| 172 | + |
| 173 | +Now, let’s upgrade from PostgreSQL 13, which is nearing end-of-life, to the |
| 174 | +latest minor release of the most recent major version. To do this, simply |
| 175 | +update the `imageName` field in your configuration: |
| 176 | + |
| 177 | +```yaml |
| 178 | +{{< include "yaml/pg-17.yaml" >}} |
| 179 | +``` |
| 180 | + |
| 181 | +Apply the changes to trigger the major upgrade procedure: |
| 182 | + |
| 183 | +```sh |
| 184 | +kubectl apply -f pg.yaml |
| 185 | +``` |
| 186 | + |
| 187 | +Once the process is complete, verify the upgrade by checking the cluster status |
| 188 | +again. Your database should now be running PostgreSQL 17. |
| 189 | + |
| 190 | +If you check again the version, you should now get a similar output: |
| 191 | + |
| 192 | +```console |
| 193 | +PostgreSQL 17.4 (Debian 17.4-1.pgdg110+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit |
| 194 | +``` |
| 195 | + |
| 196 | +If you type `kubectl get pods` now, you will see that pods and PVCs named |
| 197 | +`pg-2` and `pg-3` don't exist anymore, as the scale up operation replaced them |
| 198 | +with sequence numbers 4 and 5: |
| 199 | + |
| 200 | +```console |
| 201 | +NAME READY STATUS RESTARTS AGE |
| 202 | +pg-1 1/1 Running 0 62s |
| 203 | +pg-4 1/1 Running 0 36s |
| 204 | +pg-5 1/1 Running 0 15s |
| 205 | +``` |
| 206 | + |
| 207 | +## Limitations and Caveats |
| 208 | + |
| 209 | +As you have just experienced, one limitation of this implementation—though it |
| 210 | +does not affect database access—is the need to recreate **replicas**, which is |
| 211 | +currently supported only via `pg_basebackup`. This means that until a new |
| 212 | +replica is available, if the primary node fails, you will need to restore from |
| 213 | +the most recent backup. In most cases, this backup will be from the previous |
| 214 | +PostgreSQL version, requiring you to repeat the major upgrade process. |
| 215 | + |
| 216 | +While this scenario is unlikely, it is important to acknowledge the risk. |
| 217 | +However, in most cases, replication completes within minutes, depending |
| 218 | +on database complexity (primarily number of tables). |
| 219 | + |
| 220 | +For significantly larger databases, be aware that the cluster will remain in a |
| 221 | +degraded state for high availability until replication is fully restored. To |
| 222 | +mitigate risk, I strongly recommend taking a physical backup as soon as |
| 223 | +possible after the upgrade completes. |
| 224 | + |
| 225 | +Another key consideration is **extensions**. They are an integral part of the |
| 226 | +upgrade process. Ensure that all required extensions—and their respective |
| 227 | +versions—are available in the target PostgreSQL version's operand image. If any |
| 228 | +are missing, the upgrade will fail. Always validate extension compatibility |
| 229 | +before proceeding. |
| 230 | + |
| 231 | +## Testing a Large Database Upgrade |
| 232 | + |
| 233 | +As part of my testing efforts, I wanted to evaluate how a major PostgreSQL |
| 234 | +upgrade handles a large database. To do this, I created a **2.2TB** PostgreSQL |
| 235 | +16 database using `pgbench` with a scale of **150,000**. Below is an excerpt |
| 236 | +from the `cnpg status` command: |
| 237 | + |
| 238 | +```console |
| 239 | +Cluster Summary |
| 240 | +Name default/pg |
| 241 | +System ID: 7487705689911701534 |
| 242 | +PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:16 |
| 243 | +Primary instance: pg-1 |
| 244 | +Primary start time: 2025-03-30 20:42:26 +0000 UTC (uptime 72h32m31s) |
| 245 | +Status: Cluster in healthy state |
| 246 | +Instances: 1 |
| 247 | +Ready instances: 1 |
| 248 | +Size: 2.2T |
| 249 | +Current Write LSN: 1D0/8000000 (Timeline: 1 - WAL File: 00000001000001D000000001) |
| 250 | +<snip> |
| 251 | +``` |
| 252 | + |
| 253 | +I then triggered an upgrade to **PostgreSQL 17**, which completed in just **33 |
| 254 | +seconds**, restoring the cluster to full operation in under a minute. Below is |
| 255 | +the updated `cnpg status` output: |
| 256 | + |
| 257 | +```console |
| 258 | +Cluster Summary |
| 259 | +Name default/pg |
| 260 | +System ID: 7488830276033003555 |
| 261 | +PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:17 |
| 262 | +Primary instance: pg-1 |
| 263 | +Primary start time: 2025-03-30 20:42:26 +0000 UTC (uptime 72h44m45s) |
| 264 | +Status: Cluster in healthy state |
| 265 | +Instances: 1 |
| 266 | +Ready instances: 1 |
| 267 | +Size: 2.2T |
| 268 | +Current Write LSN: 1D0/F404F9E0 (Timeline: 1 - WAL File: 00000001000001D00000003D) |
| 269 | +``` |
| 270 | + |
| 271 | +Since CloudNativePG leverages PostgreSQL’s `--link` option (which uses hard |
| 272 | +links), **upgrade time primarily depends on the number of tables rather than |
| 273 | +database size**. |
| 274 | + |
| 275 | +## Conclusions |
| 276 | + |
| 277 | +In-place major upgrades with `pg_upgrade` bring PostgreSQL’s traditional upgrade |
| 278 | +path into Kubernetes, giving users a declarative way to transition between |
| 279 | +major versions with minimal operational overhead. While this method does |
| 280 | +involve downtime, it eliminates the need for blue/green clusters, making it |
| 281 | +particularly well-suited for environments managing a **large fleet of small to |
| 282 | +medium-sized PostgreSQL instances**. |
| 283 | + |
| 284 | +If the upgrade succeeds, you have a fully functional PostgreSQL cluster, just |
| 285 | +as if you had run `pg_upgrade` on a traditional VM or bare metal instance. If it |
| 286 | +fails, rollback options are available—including reverting to the original |
| 287 | +manifest and deleting the upgrade job. If necessary, continuous backups provide |
| 288 | +an additional safety net. |
| 289 | + |
| 290 | +Although in-place upgrades may not be my preferred method for mission-critical |
| 291 | +databases, they provide an important option for teams that prioritise |
| 292 | +**operational simplicity and scalability** over achieving zero-downtime |
| 293 | +upgrades. As demonstrated in testing, upgrade times primarily depend on the |
| 294 | +number of tables rather than database size, making this approach efficient even |
| 295 | +for large datasets. |
| 296 | + |
| 297 | +The **success of this feature relies on real-world feedback**. We encourage you |
| 298 | +to test and validate it during the release candidate phase to ensure |
| 299 | +CloudNativePG 1.26.0 is robust and production-ready—especially when using |
| 300 | +extensions. Your insights will directly influence its future, so let us know |
| 301 | +what you think! |
| 302 | + |
| 303 | +--- |
| 304 | + |
| 305 | +Stay tuned for the upcoming recipes! For the latest updates, consider |
| 306 | +subscribing to my [LinkedIn](https://www.linkedin.com/in/gbartolini/) and |
| 307 | +[Twitter](https://twitter.com/_GBartolini_) channels. |
| 308 | + |
| 309 | +If you found this article informative, feel free to share it within your |
| 310 | +network on social media using the provided links below. Your support is |
| 311 | +immensely appreciated! |
| 312 | + |
| 313 | +<!-- |
| 314 | +_Cover Picture: [“Indian Elephant Photo - Kalyan Varma“](https://animalia.bio/indian-elephant)._ |
| 315 | +--> |
| 316 | + |
0 commit comments