Kubernetes operator

Kubernetes operator

Declare your PostgreSQL roles, memberships, and privileges as a Kubernetes resource, and let the operator keep the database matching it.


Know what it will do before you apply it

By default a PostgresPolicy runs in mode: apply with reconciliation_mode: authoritative, which means anything in the database but not in the policy is revoked or dropped. On an existing database that can be thousands of grants. Start with mode: observe, which executes no mutating SQL, and read staged adoption before pointing pgroles at something you care about.

A first policy

For a copyable install-to-approval walkthrough, start with the operator quick start. The shorter example below is deliberately non-mutating: it inspects the database, computes what it would change, and publishes that for review without executing any DDL.

apiVersion: pgroles.io/v1alpha1
kind: PostgresPolicy
metadata:
  name: myapp-roles
  namespace: default
spec:
  connection:
    secretRef:
      name: myapp-db-credentials
    secretKey: DATABASE_URL

  mode: observe       # inspect and publish a plan; change nothing
  approval: manual
  interval: 5m

  roles:
    - name: myapp-readonly
      login: false
    - name: myapp-service
      login: true

  memberships:
    - role: myapp-readonly
      members:
        - name: myapp-service

  grants:
    - role: myapp-readonly
      object: { type: database }
      privileges: [CONNECT]

The Secret it references holds the connection string. Read it from a file rather than passing it as an argument, so the password stays out of shell history and out of the process list:

umask 077
read -rsp 'Database URL: ' DATABASE_URL && printf '\n'
printf '%s' "$DATABASE_URL" > db-url
unset DATABASE_URL
kubectl create secret generic myapp-db-credentials -n default \
  --from-file=DATABASE_URL=db-url
rm db-url

In production, prefer a secret manager — External Secrets Operator, Vault, or your cloud provider's CSI driver — over creating the Secret by hand.

Apply it and read the result:

kubectl apply -f policy.yaml
kubectl get pgr myapp-roles -n default
NAME           READY   MODE      DRIFT   CHANGES   LAST RECONCILE   AGE
myapp-roles    True    observe   True    3         2s               2s

DRIFT is True because there are pending changes and mode: observe never applies them. To see the SQL it would run, follow the policy to its plan — see plan and approval.

When you are ready to let it execute, switch to mode: apply. Choose approval: auto to apply immediately, or approval: manual to require a human to approve each plan first.

How it works

The operator watches PostgresPolicy resources and reconciles on a configurable interval, using the same expansion, diff, and SQL engine as the CLI. The policy sections — roles, profiles, schemas, grants, memberships, retirements — are the same ones the CLI manifest uses, so the manifest guide and manifest reference describe them field by field, nested under spec: rather than at the top level.

  • Database credentials come from Kubernetes Secrets
  • Every reconcile applies in a single transaction, or not at all
  • Reconciliation is serialized per database, in-process and across replicas
  • Status conditions and change summaries report what happened
  • A finalizer cleans up on deletion — deletion means "stop managing", not "undo"

The custom resources have short names in kubectl: pgr for PostgresPolicy, pgplan for PostgresPolicyPlan, pgeap for EphemeralAccessPolicy, and pgear for EphemeralAccessRequest.

Where to go next

PageWhat it covers
Operator quick startInstall, preview, approve, and verify a safe first policy
Install the operatorHelm install, chart values, CRD upgrades
The PostgresPolicy resourceSpec fields, execution and approval modes, role passwords
Database connectionsConnection URLs, structured parameters, cloud IAM auth
Plan and approvalPreviewing changes and gating execution behind review
Running the operatorIntervals, force reconcile, suspend, reconciliation mode, deletion
Status and telemetryConditions, Events, metrics, what to alert on
Troubleshooting indexFollow status reasons to the failing boundary
RBAC and securityPermissions the operator needs
Production statusMaturity and known gaps — read before deploying

Before pointing the operator at a real database, check executor privileges for what its database role needs, and staged adoption for how to roll it out without revoking access you meant to keep.

For bounded, request-driven membership on top of a durable policy, see ephemeral access. For the internal controller design, see operator architecture.

Bundle composition reaches the operator via render-bundle

The operator reconciles a single PostgresPolicy per resource — it does not load bundle fragments directly. To get cross-team or cross-environment fragment composition under the operator, compose the bundle in CI with pgroles render-bundle --bundle pgroles.bundle.yaml --output pgroles.yaml, then wrap the rendered manifest into a PostgresPolicy resource (the manifest fields go under spec: alongside connection:). Gate the bundle ↔ rendered-manifest relationship with pgroles render-bundle --bundle … --check pgroles.yaml in CI. See the bundle composition guide for the full workflow.