Skip to content

Install

The provider is installed with the Helm chart in charts/bnerd-database-provider, from the repository checked out at a release tag. Landscapes consume tagged versions only.

Prerequisites

  • Kubernetes 1.30 or later (the chart uses admissionregistration.k8s.io/v1 ValidatingAdmissionPolicies and the kubelet sleep preStop action; on older clusters set deletionProtection.enabled=false and preStopSleepSeconds=0).
  • A Kubernetes cluster that can reach the shards' primary Services (the provider runs DDL there, never through pgBouncer).
  • A shard admin Secret per shard (see Add a shard).
  • A TLS certificate for the API (below). It is required: the chart refuses to render without tls.secretName, and the provider exits at start without a certificate. The API never serves plain HTTP.
  • The Prometheus Operator CRDs only if you enable metrics.serviceMonitor or prometheusRule.

1. Create the serving certificate

The certificate is self-issued: one CA per DB cluster, which consumers pin. The server certificate's SAN must cover the private load-balancer IP consumers use and the in-cluster DNS name of the provider Service. The Service is named <release>-bnerd-database-provider (or just <release> if the release name already contains bnerd-database-provider); check with kubectl -n <namespace> get svc after the first install, or render it first with helm template.

# CA (keep ca.key offline; consumers only need ca.crt)
openssl req -x509 -newkey rsa:4096 -nodes -keyout ca.key -out ca.crt \
  -days 3650 -subj "/CN=database-provider CA"

# server key + CSR
openssl req -newkey rsa:2048 -nodes -keyout tls.key -out server.csr \
  -subj "/CN=database-provider"

# sign: SAN = LB IP + in-cluster DNS name
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out tls.crt -days 825 -extfile <(printf '%s\n' \
    'subjectAltName=IP:<LB-IP>,DNS:<service>.<namespace>.svc' \
    'extendedKeyUsage=serverAuth' 'basicConstraints=CA:FALSE')

# check
openssl x509 -in tls.crt -noout -ext subjectAltName
openssl verify -CAfile ca.crt tls.crt

Create the Secret in the release namespace (in a GitOps landscape: as a SOPS-encrypted manifest of the same shape). It must be a kubernetes.io/tls style Secret with the keys tls.crt and tls.key:

kubectl -n <namespace> create secret tls <tls-secret-name> --cert=tls.crt --key=tls.key

Hand ca.crt to each consumer (it is public). Never put ca.key or tls.key into chat, logs or a shared channel.

The provider re-reads the certificate files when they change (checked on TLS handshakes, at most every 10 seconds, after the kubelet has synced the updated Secret into the pod); a renewal needs no restart. A reload that fails keeps serving the last good certificate and is logged.

2. Apply the CRDs

The CRDs are in the chart's crds/ directory. Helm installs them on helm install only and never upgrades them. Apply them explicitly, before the first install and on every upgrade, before the new image rolls out:

kubectl apply -f charts/bnerd-database-provider/crds/

(With Flux: ship the crds/ directory as its own Kustomization that the HelmRelease depends on.) Skipping this on an upgrade leaves the API server pruning new fields the new image writes.

3. Install the chart

helm upgrade --install db-provider charts/bnerd-database-provider \
  --namespace <namespace> --create-namespace \
  --set tls.secretName=<tls-secret-name> \
  --set 'shardSecrets[0].namespace=<namespace-of-shard-admin-secrets>' \
  --set 'shardSecrets[0].names={<admin-secret-of-shard01>,<admin-secret-of-shard02>}'

Values you will usually set (defaults in values.yaml):

Value Default Meaning
tls.secretName (none, required) Secret with tls.crt / tls.key
replicas 2 API is served by every replica; reconciles run on the leader only
leaderElection.enabled true must stay true when replicas > 1; the chart fails to render otherwise
deletion.gracePeriod 168h soft-delete grace period before DROP; below 24h the provider refuses to start unless deletion.allowShortGracePeriod: true
connectionLimit.default 20 role connection limit when the request has no sizeClass
sizeClasses small: 20, medium: 50 sizeClass to connection limit per role
shardSecrets [{namespace: databases, names: [pg-shard01-pguser-provisioner]}] shard admin Secrets outside the release namespace; the provider gets get on exactly these names (no list/watch)
deletionProtection.enabled, .breakGlassGroups true, [bnerd:db-provider-break-glass] admission policy refusing DELETE on Databases (and on Shards that still hold databases); see hold versus soft delete
networkPolicy.enabled true NetworkPolicy, see below
podDisruptionBudget.enabled true minAvailable: 1 (only with replicas > 1); replicas spread across nodes by a preferred anti-affinity unless affinity is set
terminationGracePeriodSeconds, preStopSleepSeconds 30, 5 graceful shutdown
logLevel info info or debug
listen.port 8443 TLS API port
metrics.port / health.port 8080 / 8081 plain-HTTP metrics and probes
metrics.serviceMonitor.enabled false render a ServiceMonitor
prometheusRule.enabled, .cluster, .jobSelector off see Monitoring

The chart sets the environment variable PROVIDER_NAMESPACE from the pod's namespace. It is the release namespace that scopes everything the provider manages: Database objects, their credentials Secrets, consumer token Secrets, and the Secrets cache. The provider exits at start if it is unset. PROVIDER_CONFIG (mounted ConfigMap) and PROVIDER_TLS_DIR are set by the chart.

RBAC is namespaced: Secrets are accessible in the release namespace (credentials and consumer token Secrets, cached there) and, outside it, only get on the admin Secrets named in shardSecrets. Those are read uncached by name; the provider never lists or watches another namespace's Secrets (the Percona namespace also holds replication certificates, backup keys and superuser credentials). There is no cluster-wide Secret access.

Every Consumer needs a maxDatabases quota (why); there is no default.

Network policy

With networkPolicy.enabled (default) the provider pods accept only:

  • the API port from networkPolicy.apiFrom (default 192.168.0.0/16, the private network the consumers and the private DB load balancer use). Check the source address the pods actually see: if the load balancer or kube-proxy SNATs to node addresses (externalTrafficPolicy: Cluster), add the node CIDR, or /readyz and the API time out from outside;
  • the metrics port from networkPolicy.metricsFrom (default: any pod in namespace monitoring);

and may only reach DNS (port 53), the kube-apiserver (networkPolicy.apiServerCIDRs, ports 443/6443; tighten to the API server address where known) and the shards' admin Services in networkPolicy.shardNamespaces on networkPolicy.shardPorts (default databases, 5432). Kubelet health probes come from the node and are allowed by common CNIs (Calico, Cilium) without a rule; otherwise list the node CIDR in networkPolicy.healthFrom.

4. Verify

kubectl -n <namespace> rollout status deploy/<release>-bnerd-database-provider
kubectl -n <namespace> get pods
# from a host that can reach the LB / Service, with the CA you generated
curl --cacert ca.crt https://<LB-IP>:8443/readyz

/healthz and /readyz need no token and answer {"status":"ok"}; every response carries X-Provider-Version. Then create Shard and Consumer objects (next page).

Upgrading

  1. kubectl apply -f charts/bnerd-database-provider/crds/ (see above; not done by Helm).
  2. helm upgrade with the new chart. Read CHANGES.md for the target version first.
  3. Check /readyz and that all Shard objects are Ready.