Kubernetes Deployment (Helm)
The Helm chart at deploy/helm/abrq-dip/ is the primary production
deployment path. It deploys four application components — API (default 2
replicas), Celery worker (default 2 replicas), a singleton Celery beat
scheduler, and the nginx-served frontend — plus, by default, an in-cluster
PostgreSQL and Redis via vendored subcharts.
Prerequisites#
| Requirement | Minimum | Why |
|---|---|---|
| Kubernetes | 1.25 | The chart uses autoscaling/v2 (HPA) and policy/v1 (PDB) APIs. |
| Helm | 3.8 | OCI registry support for the pinned subchart dependencies. |
| A runtime Secret | see below | The chart requires an existing Secret and never creates one. |
| License file | vendor-issued license.json |
Production startup verifies it; see the note under failure modes. |
The chart's own version is 0.2.0 (Chart.yaml); the subchart
dependencies — postgresql 16.7.27 and redis 20.13.4 — are vendored as
committed tarballs in deploy/helm/abrq-dip/charts/, so installation
needs no chart-repository access.
The required runtime Secret#
The chart refuses to render without an existing Secret named by
existingSecret (default abrq-dip-runtime). Every backend pod loads it
via envFrom. With the in-cluster subcharts on (the default), the
database and Redis URLs are auto-wired, so the Secret only needs the
non-connection secrets:
kubectl create secret generic abrq-dip-runtime \
--from-literal=ABRQ_MASTER_KEY=<FERNET_KEY> \
--from-literal=ABRQ_JWT_SECRET=<JWT_SECRET> \
--from-literal=ABRQ_INITIAL_ADMIN_PASSWORD=<ADMIN_PASSWORD> \
--from-literal=ABRQ_CORS_ALLOWED_ORIGINS=https://abrq.example.com
With external database and Redis (postgresql.enabled=false,
redis.enabled=false), also add ABRQ_DATABASE_URL,
ABRQ_DATASTORE_URL, and ABRQ_REDIS_URL to this Secret. Generation
commands for the secret values are in
choosing a deployment path. The full environment reference is in
environment variables.
Install walkthrough#
# 1. Create the namespace and the runtime Secret (previous section).
kubectl create namespace abrq
# 2. Install the chart from the repository checkout.
helm install abrq-dip deploy/helm/abrq-dip --namespace abrq
# 3. Watch the rollout.
kubectl -n abrq get pods -w
On first install the bundled PostgreSQL StatefulSet initializes, an initdb
script creates the second (datastore) database, each app pod's migrate
init container waits for the database and applies migrations under an
advisory lock, and then the API pods pass their readiness probe.
values.yaml reference#
Every key the chart accepts, with its default and effect.
Images and frontend#
| Key | Default | Effect |
|---|---|---|
image.repository |
abrq-dip-backend |
One backend image serves api, worker, beat, and migrator. |
image.tag |
latest |
Backend image tag. Pin to a release version in production. |
image.pullPolicy |
IfNotPresent |
Backend image pull policy. |
frontend.image.repository |
abrq-dip-frontend |
SPA + nginx image. |
frontend.image.tag |
latest |
Frontend image tag. |
frontend.image.pullPolicy |
IfNotPresent |
Frontend image pull policy. |
frontend.replicaCount |
1 |
Frontend replicas. |
frontend.service.type |
ClusterIP |
Frontend Service type. |
frontend.service.port |
80 |
Frontend Service port (targets container port 80). |
frontend.resources |
limits 128Mi / 100m; requests 64Mi / 50m | Frontend resource envelope. |
API#
| Key | Default | Effect |
|---|---|---|
api.replicaCount |
2 |
API replicas. |
api.resources |
limits 1Gi / 1000m; requests 256Mi / 100m | API resource envelope. |
api.service.type |
ClusterIP |
API Service type. |
api.service.port |
8000 |
API Service port — also the ingress /api backend port. |
api.livenessProbe |
HTTP GET /health on port 8000, initial delay 30s, period 15s |
/health pings the framework DB and Redis. |
api.readinessProbe |
HTTP GET /health, initial delay 5s, period 5s |
Same endpoint — there is no separate readiness path. |
api.env.ABRQ_GUNICORN_WORKERS |
"2" |
Gunicorn worker processes per API pod. |
api.env.ABRQ_DB_POOL_SIZE |
"10" |
Async DB pool size per engine. |
api.env.ABRQ_DB_MAX_OVERFLOW |
"5" |
DB pool overflow. |
api.env.ABRQ_RUNS_MODE |
"celery" |
Runs are enqueued to Redis, not executed inline. |
Worker#
| Key | Default | Effect |
|---|---|---|
worker.replicaCount |
2 |
Worker replicas — scale this for throughput. |
worker.resources |
limits 1Gi / 1000m; requests 256Mi / 100m | Worker resource envelope. |
worker.env.ABRQ_CELERY_CONCURRENCY |
"4" |
Concurrency per worker replica. |
worker.env.ABRQ_DB_POOL_SIZE |
"10" |
As for the API. |
worker.env.ABRQ_DB_MAX_OVERFLOW |
"5" |
As for the API. |
worker.env.ABRQ_RUNS_MODE |
"celery" |
As for the API. |
worker.terminationGracePeriodSeconds |
60 |
Drain budget on scale-down or rolling update. Keep at least as long as your longest task plus ABRQ_RUN_SHUTDOWN_GRACE_SECONDS. |
worker.probes.enabled |
true |
Liveness and readiness via exec celery inspect ping (proves broker-registered, not just process-alive). |
worker.probes.initialDelaySeconds |
30 |
Probe initial delay. |
worker.probes.periodSeconds |
30 |
Probe period. |
worker.probes.timeoutSeconds |
10 |
Ping timeout; the probe's own timeout is this value plus 5. |
Beat (singleton)#
| Key | Default | Effect |
|---|---|---|
beat.resources |
limits 256Mi / 200m; requests 64Mi / 50m | Beat resource envelope. |
beat.env.ABRQ_RUNS_MODE |
"celery" |
As for the API. |
beat.persistence.enabled |
true |
RWO PVC at /var/abrq-dip for the beat schedule state. |
beat.persistence.size |
1Gi |
Schedule-state PVC size. |
beat.persistence.storageClassName |
"" |
Empty means the cluster default class. |
Warning. Beat is a hard singleton: the template hardcodes
replicas: 1with aRecreatestrategy — there is nobeat.replicaCountvalue, no probes, no HPA, and no PDB for it. Never run more than one beat.
Backups (ADR 0040)#
| Key | Default | Effect |
|---|---|---|
backups.enabled |
false |
Mount a shared PVC at /var/abrq-dip/backups on api, worker, and beat pods. When false, the chart sets ABRQ_BACKUP_DURABLE=false and successful auto-backup runs record last_status=ok_ephemeral (visible as a warning in Settings) instead of a false green. |
backups.existingClaim |
"" |
Use a pre-created PVC (for example your own NFS or EFS claim) instead of provisioning one. |
backups.size |
5Gi |
Provisioned PVC size. |
backups.storageClassName |
"" |
Storage class for the provisioned PVC. |
backups.accessMode |
ReadWriteMany |
RWX is required to share one PVC across multi-replica api and worker pods (NFS, AWS EFS, Azure Files, CephFS). A single-node cluster can use ReadWriteOnce. |
Auto-backup is on by default in the application; without this PVC it still runs but writes to each pod's ephemeral filesystem. Production installs should enable it against RWX storage. See backup and restore.
Migrations#
| Key | Default | Effect |
|---|---|---|
migrator.initContainer |
true |
The default migration path: every api, worker, and beat pod runs the migrator entrypoint as an init container — it waits for the DB, takes a Postgres advisory lock, and runs alembic upgrade head; the lock means exactly one pod migrates while the rest no-op. Works for both in-cluster and external databases with no ordering deadlock. |
migrator.enabled |
false |
A pre-install/pre-upgrade hook Job. Off by default on purpose: with the in-cluster subchart database it deadlocks on first install, because Helm runs the hook before the PostgreSQL StatefulSet exists. Enable only with an external, already-running database — then you may set migrator.initContainer: false. |
migrator.resources |
limits 512Mi / 500m | Applies to both the init container and the hook Job. |
Secrets and common environment#
| Key | Default | Effect |
|---|---|---|
existingSecret |
abrq-dip-runtime |
Required. Name of the Secret holding ABRQ_MASTER_KEY, ABRQ_JWT_SECRET, ABRQ_INITIAL_ADMIN_PASSWORD, ABRQ_CORS_ALLOWED_ORIGINS, and — when the subcharts are off — the three connection URLs. The chart never creates a Secret. |
commonEnv.ABRQ_ENV |
"prod" |
Applied to every backend component. |
commonEnv.ABRQ_LOG_LEVEL |
"INFO" |
Log level for every backend component. |
commonEnv.ABRQ_ENABLE_HSTS |
"true" |
HSTS response header. |
Any additional key added under commonEnv is applied to every backend
component. Auto-wired connection URLs take precedence over the same keys
in existingSecret.
Ingress#
| Key | Default | Effect |
|---|---|---|
ingress.enabled |
false |
Render an networking.k8s.io/v1 Ingress. |
ingress.className |
"" |
Ingress class. |
ingress.annotations |
{} |
Passed through verbatim. |
ingress.apiPath |
/api |
Routed straight to the api Service as the first rule — see the rationale below. Set "" to disable the split. |
ingress.hosts |
one host, abrq.example.com, path / (Prefix) |
The / path routes to the frontend Service. |
ingress.tls |
[] |
Emitted verbatim. |
Why the /api split exists: the frontend nginx has an internal /api
proxy, but its upstream host is literally backend — a hostname that only
exists on the Docker Compose network. In Kubernetes that proxy target does
not resolve, so the Ingress must route /api (and hence all API traffic)
directly to the api Service, before the catch-all / rule sends
everything else to the frontend.
Service account, security context, availability#
| Key | Default | Effect |
|---|---|---|
serviceAccount.create |
true |
Create a ServiceAccount; name defaults to the release fullname. |
serviceAccount.name |
"" |
Override the ServiceAccount name. |
podSecurityContext |
runAsNonRoot: true, runAsUser: 1000, fsGroup: 1000 |
Applied to backend pods. The frontend hardcodes runAsNonRoot: false (nginx). |
autoscaling.api.enabled |
false |
HPA for the API: min 2, max 10, targets CPU 70% and memory 80%, behavior: {} (HPA defaults — the API is stateless). |
autoscaling.worker.enabled |
false |
HPA for workers: min 2, max 20, CPU 70% and memory 80%; scale-down stabilization 300s and at most 1 pod per 60s, so a removed worker drains in-flight tasks. |
podDisruptionBudgets.api |
enabled, minAvailable: 1 |
Protects against eviction storms during node drains. |
podDisruptionBudgets.worker |
enabled, minAvailable: 1 |
As above. |
podDisruptionBudgets.frontend |
disabled (minAvailable: 1 if enabled) |
Optional. |
Beat deliberately has no PDB: it is a singleton, one eviction during a drain is acceptable, and the runner-side advisory lock keeps schedules from double-firing.
In-cluster PostgreSQL subchart#
| Key | Default | Effect |
|---|---|---|
postgresql.enabled |
true |
Run the bundled PostgreSQL. Set false for managed/external Postgres. |
postgresql.autoWireUrl |
true |
Chart auto-wires ABRQ_DATABASE_URL and ABRQ_DATASTORE_URL to the in-cluster Service, with the password injected at runtime from the subchart-created Secret — it is never rendered into a manifest. |
postgresql.image.registry |
docker.io |
See the bitnamilegacy caveat below. |
postgresql.image.repository |
bitnamilegacy/postgresql |
See the caveat below. |
postgresql.datastoreDatabase |
abrq_dip_datastore |
The second database (CDC mirror landings), created by the initdb script. |
postgresql.auth.username |
abrq_dip |
Application database user. |
postgresql.auth.database |
abrq_dip_framework |
Framework (metadata) database. |
postgresql.auth.existingSecret |
"" |
Empty means the subchart generates a random password into the <RELEASE>-postgresql Secret; point at your own Secret for full control. |
postgresql.primary.persistence |
enabled, 20Gi |
Data volume. |
postgresql.primary.initdb.scripts |
CREATE DATABASE abrq_dip_datastore OWNER abrq_dip; |
Creates the datastore DB at first init. |
postgresql.primary.resources |
limits 1Gi / 500m | Postgres resource envelope. |
In-cluster Redis subchart#
| Key | Default | Effect |
|---|---|---|
redis.enabled |
true |
Run the bundled Redis. Set false for managed/external Redis. |
redis.autoWireUrl |
true |
Chart auto-wires ABRQ_REDIS_URL, including the generated password from the <RELEASE>-redis Secret. |
redis.image.registry |
docker.io |
See the bitnamilegacy caveat below. |
redis.image.repository |
bitnamilegacy/redis |
See the caveat below. |
redis.auth.enabled |
true |
Auth on by default: an unauthenticated Redis, even ClusterIP-only, is remote-code-execution-prone and reachable by any in-cluster pod without a NetworkPolicy. Set false only on a fully trusted single-tenant cluster. |
redis.architecture |
standalone |
Single Redis node. |
redis.master.persistence |
enabled, 4Gi |
AOF/state volume. |
redis.master.resources |
limits 256Mi / 200m | Redis resource envelope. |
Warning. The subchart images are pinned to the frozen
bitnamilegacyDocker Hub mirror, because Bitnami removed its free images in 2025 (docker.io/bitnami/*no longer pulls). Legacy images receive no security patches. For production, prefer external managed PostgreSQL and Redis: setpostgresql.enabled=falseandredis.enabled=falseand putABRQ_DATABASE_URL,ABRQ_DATASTORE_URL, andABRQ_REDIS_URLin the runtime Secret.
Note.
nameOverrideandfullnameOverrideare honoured by the chart's name helpers but are not listed invalues.yaml. Do not rely on the chart'sREADME.md, which is out of date; the templates andvalues.yamlare authoritative.
Post-install verification#
helm status abrq-dip -n abrq
kubectl -n abrq get pods
All pods should reach Running with ready containers (the migrate init
containers show as Init:0/1 while migrations apply on first install).
Then check application health end to end:
kubectl -n abrq port-forward svc/abrq-dip-frontend 8080:80
curl -fsS http://localhost:8080/health
The API's own health endpoint reports both dependencies:
kubectl -n abrq port-forward svc/abrq-dip-api 8000:8000
curl -fsS http://localhost:8000/health
Expected shape: {"status": "ok", ... "deps": {"framework_db": "ok", "redis": "ok"}}. A 503 with a dep marked down names the broken
dependency.
Finally, open the UI (through your Ingress host or the frontend
port-forward), log in as the initial admin user with the password from
your runtime Secret (ABRQ_INITIAL_ADMIN_PASSWORD), and complete the
forced password change.
Failure modes#
Install fails immediately: existingSecret missing#
If the Secret named by existingSecret does not exist or the value is
empty, rendering fails with:
existingSecret is required — create a Secret with ABRQ_MASTER_KEY,
ABRQ_JWT_SECRET, etc. and set .Values.existingSecret to its name
(see deploy/ENVIRONMENT.md).
Create the Secret (see above) and retry. Note the chart validates the
name at render time; a Secret that exists but is missing keys (for
example no ABRQ_MASTER_KEY) surfaces later as backend pods failing
startup with a configuration error in their logs.
Backend pods crash-loop: license#
In prod (the chart's default ABRQ_ENV), startup verifies the license
file and exits on failure, so license problems present as api, worker, and
beat pods in CrashLoopBackOff. Check the logs:
kubectl -n abrq logs deploy/abrq-dip-api
A healthy startup logs license_verified with the customer name and
expiry. Failures log license_check_failed with the reason — file not
found at ABRQ_LICENSE_FILE (default /var/abrq-dip/license.json),
invalid signature, or expired.
Warning. The chart currently ships no values key for mounting the license file — no template mounts one into the pods. You must make
license.jsonavailable at theABRQ_LICENSE_FILEpath on the api, worker, and beat pods yourself (for example by adding a Secret volume mount to the rendered manifests through your deployment tooling), or pointABRQ_LICENSE_FILE(viacommonEnv) at a path you provide. This is a known packaging gap — raise it with your vendor contact. See installing a license.
Probe crash loops#
- API pods restart repeatedly: the liveness probe is
GET /health, which requires the framework DB and Redis to answer. If either is down, the API is killed and restarted in a loop. Fix the data tier first; the probe is telling the truth. - Worker pods restart repeatedly: the exec probe runs
celery inspect ping— it fails when the worker cannot reach the Redis broker (badABRQ_REDIS_URL, Redis auth mismatch, Redis down), even if the process itself is alive. - First install on a slow cluster: image pulls plus Postgres init plus migrations can exceed the API's 30s liveness initial delay on very slow nodes. The migrate init container absorbs the migration wait; if you still see restarts before first readiness, check whether the data tier is actually up rather than tuning probe timings first.
Air-gapped clusters#
For clusters without registry egress — saving, retagging, and pushing images into a private registry, plus the current pull-secret limitation — see air-gapped installation. Upgrades and rollback are covered in upgrade and rollback.