Skip to content

Commit 98d1d15

Browse files
committed
feat: recipe 23 - pgvector and postgis extension
Signed-off-by: Gabriele Bartolini <[email protected]>
1 parent b672d53 commit 98d1d15

File tree

6 files changed

+331
-0
lines changed

6 files changed

+331
-0
lines changed
114 KB
Loading
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
---
2+
title: "CNPG Recipe 23 - Managing extensions with ImageVolume in CloudNativePG"
3+
date: 2025-12-01T21:35:11+01:00
4+
description: "Leveraging the Kubernetes `ImageVolume` feature and PostgreSQL 18's `extension_control_path`, this article demonstrates the CloudNativePG method for declaratively managing extensions like `pgvector` and PostGIS from separate container images, marking the culmination of a multi-year vision to decouple the database core from extension distribution."
5+
tags: [ "postgresql", "kubernetes", "cloudnativepg", "pgvector", "postgis", "imagevolume", "declarative", "extensions", "immutability", "postgres18", "operator", "database", "cnpg", "dok", "extension_control_path" ]
6+
cover: cover.jpg
7+
thumb: thumb.jpg
8+
draft: false
9+
---
10+
11+
_Say goodbye to the old way of distributing Postgres extensions as part of the
12+
main pre-built operand image. Leveraging the Kubernetes `ImageVolume` feature,
13+
CloudNativePG now allows you to mount extensions like `pgvector` and
14+
PostGIS from separate, dedicated images.
15+
This new declarative method completely decouples the PostgreSQL core from the
16+
extension binaries, enabling dynamic addition, easier evaluation, and
17+
simplified updates without ever having to build or manage monolithic custom
18+
container images._
19+
20+
<!--more-->
21+
22+
---
23+
24+
In a [previous post]({{< relref
25+
"../20250303-volume-source-extension-control-path/index.md" >}}), I made the
26+
case for the **immutable future of PostgreSQL extensions** on Kubernetes.
27+
Traditionally, achieving immutability meant building large, custom container
28+
images for the Postgres operand with every extension needed.
29+
30+
Before the method I am about to show you, the common approach for extensions like
31+
`pgvector` was to use the `standard` CNPG image, which already came
32+
pre-packaged with only a few extensions—a method I detailed in my article on
33+
[getting started with `pgvector` on Kubernetes]({{< relref "../20250926-postgresql-18/index.md" >}}).
34+
35+
Now, I want to show you a groundbreaking, more flexible approach that I believe
36+
represents the true future of extension management: using CloudNativePG (CNPG) to
37+
mount extensions from dedicated container images via the Kubernetes
38+
`ImageVolume` feature.
39+
40+
Now, I want to show you a groundbreaking, more flexible approach that I believe
41+
represents the *true* future of extension management: a moment that marks the
42+
beginning of the end of a multi-year vision we've had at EDB and, previously,
43+
at 2ndQuadrant.
44+
45+
This new flexibility is unlocked by the latest crucial steps: the combination
46+
of PostgreSQL 18's `extension_control_path` GUC (Grand Unified Configuration
47+
variable) and the Kubernetes `ImageVolume` feature. We're leveraging this with
48+
CloudNativePG to mount extensions from dedicated container images. Notably,
49+
`pgvector` and PostGIS are the first set of extension images officially
50+
released by the community through the [`postgres-extensions-containers` project](https://github.com/cloudnative-pg/postgres-extensions-containers).
51+
52+
This allows us to use the small, official `minimal` PostgreSQL images while
53+
seamlessly integrating complex extensions like `pgvector` and PostGIS.
54+
55+
## Prerequisites: PostgreSQL 18 and Kubernetes' `ImageVolume`
56+
57+
Before diving into the manifests, I must stress the technical requirements for
58+
this approach. It relies on Kubernetes' capability to expose an entire
59+
container image as a volume, which CNPG then mounts into the PostgreSQL pod.
60+
61+
This functionality requires Kubernetes 1.33 or later because it depends on the
62+
`ImageVolume` feature. This feature is not yet enabled by default, but it is
63+
expected to be generally available and enabled by default in Kubernetes 1.35
64+
(available this month, December 2025).
65+
66+
If you are using Kind for your local development environment, you must
67+
explicitly enable this feature gate when creating your cluster, like in the
68+
example below:
69+
70+
```bash
71+
(cat << EOF
72+
kind: Cluster
73+
apiVersion: kind.x-k8s.io/v1alpha4
74+
featureGates:
75+
ImageVolume: true
76+
EOF
77+
) | kind create cluster --config -
78+
```
79+
80+
For our demonstration, I will be using **PostgreSQL 18**.
81+
Note that to run these manifests, you must also have the latest stable version
82+
of CloudNativePG installed in your Kubernetes cluster; instructions can be
83+
found in the official [CloudNativePG documentation](https://cloudnative-pg.io/documentation/preview/installation_upgrade/).
84+
85+
86+
## Starting with the minimal CNPG cluster
87+
88+
I always recommend starting simple. We begin with the most basic, lightweight
89+
CNPG Cluster definition. I am using the official CNPG `minimal` image for
90+
PostgreSQL (which I have written about previously in my piece on
91+
[leveraging the new CNPG supply chain and image catalogs]({{< relref "../20251006-image-catalog/index.md" >}}))
92+
for PostgreSQL 18 (which you can learn more about
93+
[running PostgreSQL 18 today on Kubernetes]({{< relref "../20250605-pgvector/index.md" >}})).
94+
95+
This image only contains the core database binaries, with no unnecessary
96+
extensions or dependencies.
97+
98+
```yaml
99+
{{< include "yaml/01.yaml" >}}
100+
```
101+
102+
This cluster is perfectly ready to serve basic PostgreSQL operations.
103+
However, if you attempt to run `CREATE EXTENSION vector;` right now, the
104+
command will fail because the required extension control and shared object
105+
(`.so`) files are not present in the `minimal` base image.
106+
107+
## Introducing `pgvector` via a dedicated image
108+
109+
Instead of switching to a heavy, custom `imageName`, I introduce the new
110+
`postgresql.extensions` block. This is where the magic happens. I am
111+
instructing CloudNativePG to find a separate image containing the compiled
112+
`pgvector` binaries and mount it into the PostgreSQL container using
113+
`ImageVolume`.
114+
115+
### The manifest
116+
117+
```yaml
118+
{{< include "yaml/02.yaml" >}}
119+
```
120+
121+
### Verification: the extension is ready
122+
123+
Once you apply the manifests above, CloudNativePG handles both mounting the
124+
binary files via `ImageVolume` and running the necessary `CREATE EXTENSION`
125+
SQL.
126+
127+
First, let's confirm the `pgvector` binaries have been successfully mounted
128+
into the pod's filesystem. I'm targeting the primary pod (assuming the name
129+
follows the pattern `angus-1`) and listing the contents of the
130+
mounted `/extensions/` directory:
131+
132+
```bash
133+
kubectl exec -ti angus-1 -c postgres -- ls /extensions/
134+
```
135+
136+
The output confirms the extension directory is present, mounted from the
137+
separate extension image:
138+
139+
```
140+
pgvector
141+
```
142+
143+
Next, we verify that the extension is active and ready in the `app` database:
144+
145+
```bash
146+
kubectl cnpg psql angus -- app -c '\dx'
147+
```
148+
149+
The output confirms the declarative process was successful:
150+
151+
```
152+
List of installed extensions
153+
Name | Version | Default version | Schema | Description
154+
---------+---------+-----------------+------------+------------------------------------------------------
155+
plpgsql | 1.0 | 1.0 | pg_catalog | PL/pgSQL procedural language
156+
vector | 0.8.1 | 0.8.1 | public | vector data type and ivfflat and hnsw access methods
157+
(2 rows)
158+
```
159+
160+
### What I have achieved here
161+
162+
1. `Cluster.spec.postgresql.extensions`: I have registered the `pgvector`
163+
extension by referencing a specific extension image. This image holds only
164+
the compiled binaries for `pgvector`.
165+
166+
2. `ImageVolume` in action: CloudNativePG intelligently mounts the contents of
167+
`ghcr.io/cloudnative-pg/pgvector:0.8.1-18-trixie` directly into the
168+
PostgreSQL pod's file system, ensuring the necessary binaries are available
169+
and read-only. Underneath, this is achieved by leveraging the
170+
`extension_control_path` GUC, a key feature introduced in PostgreSQL 18,
171+
which allows the database to locate the necessary extension control files
172+
outside of the traditional installation directories.
173+
174+
3. `Database.spec.extensions`: This resource handles the final, declarative
175+
activation. I instruct PostgreSQL to run `CREATE EXTENSION vector VERSION '0.8.1';`.
176+
This is key: CloudNativePG manages this command declaratively,
177+
meaning I never have to connect and run SQL myself. Furthermore, the
178+
`version` field is crucial; by simply applying a new image and changing this
179+
value, CNPG orchestrates the PostgreSQL update path (provided the extension
180+
itself supports the upgrade).
181+
182+
This cleanly decouples the PostgreSQL core from the extension binaries,
183+
providing immutability without the custom image maintenance headache.
184+
185+
## Scaling up: integrating multiple complex extensions
186+
187+
The real power of this method becomes apparent when dealing with complex,
188+
interdependent extensions like PostGIS. PostGIS requires several companion
189+
extensions and shared library dependencies, which are notoriously difficult to
190+
manage manually.
191+
192+
I can easily add PostGIS by simply listing it under `extensions`. Note the
193+
inclusion of the `ld_library_path` configuration for PostGIS; this is a vital
194+
element that ensures its dynamic linker paths are correctly configured for
195+
maximum reliability. Finally, I define all the necessary PostGIS-related
196+
companion extensions in the `Database` resource:
197+
198+
```yaml
199+
{{< include "yaml/03.yaml" >}}
200+
```
201+
202+
After applying this full manifest, I encourage you to check the validation
203+
commands from the previous section again. You'll find that the `/extensions/`
204+
directory now contains both `pgvector` and the PostGIS files, and the `\dx`
205+
output confirms that all **PostGIS** dependencies, including
206+
`postgis_topology`, have been successfully created in the database.
207+
208+
## Summary
209+
210+
The ability to mount extensions from separate images using `ImageVolume`
211+
with CloudNativePG 1.27+ is a game-changer. It allows us to:
212+
213+
- **Decouple:** upgrade the core PostgreSQL image independently of the
214+
extension images. This also applies to the build project (which is very
215+
important for us maintainers and contributors of CloudNativePG).
216+
- **Dynamic and easy evaluation:** extensions can be added dynamically to an
217+
existing cluster, making the evaluation of new features fast and
218+
frictionless.
219+
- **Maintain small images:** my base `imageName` remains small, secure, and
220+
simple.
221+
- **Ensure consistency:** CloudNativePG handles all the complex volume mounting
222+
and dependency mapping, guaranteeing a consistent, immutable environment
223+
across the entire cluster without needing custom Dockerfile builds.
224+
225+
Finally, I want to mention that we are currently working to standardise the way
226+
we create these extension images in the [`postgres-extensions-containers` repository on GitHub](https://github.com/cloudnative-pg/postgres-extensions-containers).
227+
228+
The goal of this project (see [issue #15](https://github.com/cloudnative-pg/postgres-extensions-containers/issues/15))
229+
is to scale up the number of supported extensions by providing a framework
230+
that can be used by more contributors to add extensions they like, as long as
231+
they become component owners/maintainers for that extension in the
232+
CloudNativePG community.
233+
I will cover our progress on this project in a future post.
234+
235+
---
236+
237+
Stay tuned for the upcoming recipes! For the latest updates, consider
238+
subscribing to my [LinkedIn](https://www.linkedin.com/in/gbartolini/) and
239+
[Twitter](https://twitter.com/_GBartolini_) channels.
240+
241+
If you found this article informative, feel free to share it within your
242+
network on social media using the provided links below. Your support is
243+
immensely appreciated!
244+
245+
<!--
246+
_Cover Picture: [“TITLE“](URL)._
247+
-->
248+
61.6 KB
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: angus
5+
spec:
6+
# the small, official `minimal` CNPG base image for Postgres 18
7+
imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
8+
instances: 1
9+
10+
storage:
11+
size: 1Gi
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: angus
5+
spec:
6+
imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
7+
instances: 1
8+
9+
storage:
10+
size: 1Gi
11+
12+
postgresql:
13+
extensions:
14+
- name: pgvector
15+
image:
16+
reference: ghcr.io/cloudnative-pg/pgvector:0.8.1-18-trixie
17+
---
18+
apiVersion: postgresql.cnpg.io/v1
19+
kind: Database
20+
metadata:
21+
name: angus-app
22+
spec:
23+
name: app
24+
owner: app
25+
cluster:
26+
name: angus
27+
extensions:
28+
- name: vector
29+
version: '0.8.1'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: angus
5+
spec:
6+
imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
7+
instances: 1
8+
9+
storage:
10+
size: 1Gi
11+
12+
postgresql:
13+
extensions:
14+
- name: pgvector
15+
image:
16+
reference: ghcr.io/cloudnative-pg/pgvector:0.8.1-18-trixie
17+
- name: postgis
18+
image:
19+
reference: ghcr.io/cloudnative-pg/postgis-extension:3.6.1-18-trixie
20+
ld_library_path:
21+
- system
22+
---
23+
apiVersion: postgresql.cnpg.io/v1
24+
kind: Database
25+
metadata:
26+
name: angus-app
27+
spec:
28+
name: app
29+
owner: app
30+
cluster:
31+
name: angus
32+
extensions:
33+
- name: vector
34+
version: '0.8.1'
35+
- name: postgis
36+
version: '3.6.1'
37+
- name: postgis_raster
38+
- name: postgis_sfcgal
39+
- name: fuzzystrmatch
40+
- name: address_standardizer
41+
- name: address_standardizer_data_us
42+
- name: postgis_tiger_geocoder
43+
- name: postgis_topology

0 commit comments

Comments
 (0)