DEV Community

Cover image for Managed PostgreSQL for Kubernetes apps | Kupe Cloud
Billy Walker for Kupe Cloud

Posted on Originally published at kupe.cloud

Managed PostgreSQL for Kubernetes apps | Kupe Cloud

Your app needs Postgres. The familiar Kubernetes route is to install a database operator, choose its size, arrange TLS, put credentials somewhere the app can reach, and keep the whole arrangement upgraded. That is real work, and we would rather not make every team repeat it on every cluster. It also means the database competes with your app for the cluster's CPU and memory, and you secure it on your own.

Kupe now provides managed PostgreSQL databases. You create a database for your account, then bind a role into the namespace where an app runs. The namespace receives a Service and a credentials Secret; the app mounts the Secret and uses the supplied connection URI. We built the feature around that last step. Creating a database is the easy half. The harder half is getting credentials to the right workload without pasting them into a manifest.

A database that outlives a cluster

A managed database belongs to your account rather than to one cluster. A cluster can be deleted and recreated, and your data should not go with it.

You choose CPU, memory and storage for an instance, with PostgreSQL 18 as the default engine version and PostgreSQL 17 also available. High availability adds a second instance with a synchronous standby. Storage can grow, but it cannot shrink; a database keeps its PostgreSQL major version, while minor updates are applied automatically.

There are a few guardrails worth knowing before you create one:

  • Deletion protection starts on. Turn it off before deleting a database.
  • Deleted storage is retained for seven days for support-assisted recovery. There is no self-service restore in v1.
  • A database can alert you when storage is running low, when one instance is down while another is serving, or when no instance is up. Those alerts sit with cluster alerts in the console.

Bind the database where the app runs

A binding joins one database role to one namespace in one cluster. It is the contract that tells Kupe where to deliver access. The cluster does not need to exist when you create the binding: if it is deleted, the binding waits and delivers again when a cluster with the same name returns.

In each bound namespace, Kupe creates a Service named after the database and a credentials Secret. The default Secret name is {database}-{role}. It follows the servicebinding.io key convention and includes host, port, database, username, password, sslmode, ca.crt, uri, type and provider.

The useful bit is the uri. It is already configured for TLS certificate verification with sslmode=verify-full, using the database's own CA. Connections without TLS are refused. Mount the Secret as a volume at /bindings/<secret-name>, for example /bindings/orders-db-readwrite, and set SERVICE_BINDING_ROOT=/bindings. The uri only works at that path, because its CA setting points at the mounted ca.crt. The database guide shows the workload configuration. Your application can then consume the binding without pasting a password or CA bundle into a Deployment manifest.

The Secret tracks the database. When a password rotates or the CA is renewed, every bound Secret receives the new value. Kubernetes updates mounted Secret files in place; an application that reads its password once at startup needs a restart to pick up the change.

Network access follows the same binding. Pods in the bound namespace of the bound cluster can connect; there is no public endpoint. Remove the binding and Kupe removes the Service, Secret and network path. That does not rotate the role password. If access may already have escaped the namespace, rotate the role credentials as well.

Use roles to separate migrations from the service

Managed PostgreSQL v1 has two roles:

  • owner can change the schema. It belongs with migrations.
  • readwrite can read and write the owner's tables but cannot change their schema. It belongs with the running service.

Put owner in orders-migrate and readwrite in orders-api, and the API namespace has no schema-changing credential to mount. That is the reason bindings are per namespace rather than a single database Secret copied around a cluster.

From the CLI, the database and both bindings take three commands:

kupe database create orders-db --cpu 250m --memory 512Mi --storage 5Gi
kupe database binding add orders-db --cluster dev --namespace orders-migrate --role owner
kupe database binding add orders-db --cluster dev --namespace orders-api --role readwrite
Enter fullscreen mode Exit fullscreen mode

create and binding add wait until the requested object is ready. The CLI reference also covers resizing, high availability, deletion protection, credential rotation and waiting in scripts.

The same binding in the console, CLI or Terraform

The console, CLI and Terraform provider create the same resource. Choose the one that fits how you already manage infrastructure.

In the console, open a database and choose Add Binding, then select the cluster, namespace and role. A binding row shows Ready once its Service and Secret are in place.

For infrastructure managed in pull requests, the Terraform provider has kupe_database, kupe_database_binding and a kupe_database data source. Here is the binding portion of a database definition:

resource "kupe_database_binding" "migrations" {
  database   = kupe_database.orders.name
  cluster    = kupe_cluster.prod.name
  namespaces = ["orders-migrate"]
  role       = "owner"
}

resource "kupe_database_binding" "api" {
  database   = kupe_database.orders.name
  cluster    = kupe_cluster.prod.name
  namespaces = ["orders-api"]
  role       = "readwrite"
}
Enter fullscreen mode Exit fullscreen mode

The provider rejects v1 changes that cannot work, including shrinking storage or changing the PostgreSQL major version. It also respects deletion protection. If a namespace already contains a same-named Service or Secret that Kupe did not create, the binding remains failed rather than overwriting somebody else's object. The binding stays failed until you remove or rename your object, which beats an apply that silently replaces a Secret your workload depends on.

The public managed-database sample puts this shape to use: a migration Job uses the owner role, a Go API uses readwrite, and its frontend has no database binding at all.

The edges of v1

This first version has clear limits. It is PostgreSQL only, supports one PostgreSQL database per instance, has no custom roles or extensions, no built-in connection pooling, no backups or self-service restore, and no in-place major version upgrades.

We are working towards backups with point-in-time restore, more control over roles and in-place major upgrades. Those changes are additive: an application built on a v1 binding will not need a different connection model when they arrive.

Start with a small database and one namespace. Bind readwrite, mount the Secret, and connect using its uri. The get-started guide has the full path; if you run into something rough while trying it, email support@kupe.cloud.

Top comments (0)