Skip to content

Commit 82b3055

Browse files
committed
feat: major in-place upgrades
Signed-off-by: Gabriele Bartolini <[email protected]>
1 parent 75570ce commit 82b3055

File tree

5 files changed

+340
-0
lines changed

5 files changed

+340
-0
lines changed
227 KB
Loading
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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+
98.4 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: pg
5+
spec:
6+
imageName: ghcr.io/cloudnative-pg/postgresql:13.20-minimal-bullseye
7+
instances: 3
8+
9+
storage:
10+
size: 1Gi
11+
walStorage:
12+
size: 1Gi
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: pg
5+
spec:
6+
imageName: ghcr.io/cloudnative-pg/postgresql:17.4-minimal-bullseye
7+
instances: 3
8+
9+
storage:
10+
size: 1Gi
11+
walStorage:
12+
size: 1Gi

0 commit comments

Comments
 (0)