|
| 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 | + |
0 commit comments