Skip to content

Repository files navigation

pg-mini-operator

A minimal Kubernetes operator that manages PostgreSQL declaratively through three custom resources:

  • PostgresInstance — provisions a running server: a Secret with generated credentials, a StatefulSet backed by a PersistentVolumeClaim, the Services needed to reach it, and an optional scheduled backup CronJob.
  • PostgresUser — creates a login role inside an instance and publishes a ready-to-mount credentials Secret.
  • PostgresDatabase — creates a database owned by a role, with a deletionPolicy that decides whether the real database is dropped on deletion.

Built with kubebuilder and controller-runtime. This is a learning project, so the code favours being readable over being feature-complete — see Roadmap for what is intentionally left out.

How it works

Applying a single PostgresInstance makes the operator reconcile five child objects, each owned by the instance (so they are garbage-collected when it is deleted):

PostgresInstance "demo"
├── Secret demo-superuser generated postgres password (created once)
├── Service demo ClusterIP — stable address for clients
├── Service demo-hl headless — stable DNS identity for the pod
├── StatefulSet demo postgres:<version>, 1 replica
│ └── PVC data-demo-0 survives pod restarts
└── status phase / endpoint / Ready condition

The reconcile loop is idempotent and level-based: it looks at the desired state (spec) and the real cluster state, makes them match, and records what it observed back into status.

PostgresUser and PostgresDatabase connect to the instance over the PostgreSQL protocol (via pgx) to run CREATE ROLE / CREATE DATABASE. Because a role or database lives inside Postgres — where the Kubernetes garbage collector cannot see it — each uses a finalizer to clean up on deletion. They also watch their referenced PostgresInstance, so a dependent that was waiting reconciles the moment the instance becomes Ready instead of polling.

Backups

When spec.backup is set, the operator maintains a CronJob that runs a small image (backup/, built from postgres:<version> plus the MinIO client). On each run it streams pg_dumpall | gzip straight into an S3-compatible bucket:

pg_dumpall | gzip | mc pipe s3/<bucket>/<instance>/<timestamp>.sql.gz

pg_dumpall captures every database and the global roles in one file, so a dump is a complete, restorable snapshot of the server. The Postgres password comes from the instance's superuser Secret; the S3 credentials come from the user-supplied Secret named by spec.backup.s3.credentialsSecretRef — the operator references both by name and never reads them itself. Clearing spec.backup deletes the CronJob.

Example

apiVersion: dbaas.nitroline.dev/v1alpha1kind: PostgresInstancemetadata:
name: demospec:
version: "17"storage:
size: 1Gi
---
apiVersion: dbaas.nitroline.dev/v1alpha1kind: PostgresUsermetadata:
name: app-userspec:
instanceRef: demoname: app_user
---
apiVersion: dbaas.nitroline.dev/v1alpha1kind: PostgresDatabasemetadata:
name: app-dbspec:
instanceRef: demoname: appowner: app_userdeletionPolicy: Retain # keep the data if this resource is deleted
$ kubectl get postgresinstancesNAME VERSION PHASE ENDPOINTdemo 17 Running demo.default.svc.cluster.local:5432
$ kubectl get secret app-user-credentials -o jsonpath='{.data.password}'| base64 -d

The instance reports readiness through a standard condition, so it composes with kubectl wait:

$ kubectl wait postgresinstance/demo --for=condition=Ready --timeout=120s

API

PostgresInstance (dbaas.nitroline.dev/v1alpha1):

FieldRequiredDescription
spec.versionyesPostgreSQL major version, used as the image tag
spec.storage.sizeyesPVC size, e.g. 1Gi
spec.storage.storageClassNamenoStorage class; cluster default when omitted
spec.resourcesnoContainer resource requests/limits
spec.backup.schedulenoCron schedule for pg_dumpall backups
spec.backup.s3noS3 endpoint, bucket, credentialsSecretRef

Status exposes phase (Pending / Running / Failed), endpoint, and a Ready condition.

PostgresUser (dbaas.nitroline.dev/v1alpha1):

FieldRequiredDescription
spec.instanceRefyesName of a PostgresInstance in the same namespace
spec.nameyesRole name in PostgreSQL

Publishes a <name>-credentials Secret (username, password, endpoint, port) owned by the resource.

PostgresDatabase (dbaas.nitroline.dev/v1alpha1):

FieldRequiredDescription
spec.instanceRefyesName of a PostgresInstance in the same namespace
spec.nameyesDatabase name
spec.ownernoOwning role; defaults to postgres
spec.deletionPolicynoRetain (default) keeps the data; Drop drops the DB

Getting started

Requires Go 1.24+, Docker, and a running cluster (minikube is fine).

# install the CRD
make install
# build the operator image and run it in the cluster
make docker-build deploy IMG=pg-mini-operator:dev
# create an instance, a user and a database
kubectl apply -f config/samples/
kubectl wait postgresinstance/postgresinstance-sample --for=condition=Ready --timeout=120s

Deleting an instance cascades to its owned objects (Secret, Services, StatefulSet, PVC, backup CronJob) via owner references. Deleting a PostgresUser drops its role; deleting a PostgresDatabase drops the database only when deletionPolicy: Drop — both via finalizers.

make undeploy
make uninstall

Backup demo

config/samples/demo/minio.yaml deploys an in-cluster MinIO (S3-compatible storage) plus a credentials Secret, so the whole backup path runs with no external dependencies:

kubectl apply -f config/samples/demo/minio.yaml
docker build -t pg-mini-operator-backup:dev ./backup # build the backup image# apply an instance whose spec.backup points at http://minio.default.svc:9000
kubectl create job manual-backup --from=cronjob/<instance>-backup # trigger now

Roadmap

  • Streaming replication and failover.

Run make help for all available targets.

License

Apache 2.0.

About

Kubernetes operator for simple manage PostgreSQL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages