Skip to content
ABRQ DATADocs Abrq DIP · latest
Product page Request a trial
On this page

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: 1 with a Recreate strategy — there is no beat.replicaCount value, 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 bitnamilegacy Docker 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: set postgresql.enabled=false and redis.enabled=false and put ABRQ_DATABASE_URL, ABRQ_DATASTORE_URL, and ABRQ_REDIS_URL in the runtime Secret.

Note. nameOverride and fullnameOverride are honoured by the chart's name helpers but are not listed in values.yaml. Do not rely on the chart's README.md, which is out of date; the templates and values.yaml are 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.json available at the ABRQ_LICENSE_FILE path 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 point ABRQ_LICENSE_FILE (via commonEnv) 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 (bad ABRQ_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.