Skip to content

Commit 6505fd4

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

File tree

6 files changed

+326
-0
lines changed

6 files changed

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