Skip to content

Deploy on Kubernetes (Helm)

Busbar ships an official Helm chart. It renders the Deployment, the two Services Busbar needs (the public data plane and the separate admin plane), the config ConfigMap, and the Secret wiring for provider keys, with sensible, secure defaults.

  • Kubernetes 1.24+ and Helm 3.8+.
  • cert-manager if you expose the admin plane in-cluster with mTLS (the default posture; see the two-listener model below). Not needed if you keep the admin plane loopback-only.
Terminal window
helm repo add busbar https://getbusbar.github.io/helm-charts
helm repo update
helm install busbar busbar/busbar -n busbar --create-namespace \
-f my-values.yaml

The chart’s default image is getbusbar/busbar, pinned to the chart’s appVersion (currently 1.5.3, so a plain helm install with no image.tag override already gets you the current release). Pin your own release with --set image.tag=1.5.3.

Busbar is configured by a config.yaml (the deploy config) and providers.yaml (the provider catalog, which ships inside the image; you only override it if you add providers). The chart renders config into config.yaml, mounts it at /etc/busbar/, and injects secrets as env vars that Busbar interpolates with ${VAR}:

my-values.yaml
image:
tag: "1.5.3"
# Provider keys + admin token + signing key → a Secret. Provider keys are read via secret
# references (api_key: { env: VAR }); the admin token is read from BUSBAR_ADMIN_TOKEN.
secrets:
create: true
data:
ANTHROPIC_KEY: sk-ant-...
BUSBAR_ADMIN_TOKEN: a-long-random-string
BUSBAR_SIGNING_KEY: a-64-hex-char-key # see "Generating a signing key" below; required by `keys`
# Rendered to /etc/busbar/config.yaml. `listen` and `admin_listen` are managed by the chart.
config:
identity-providers:
# DEFINE the provider once; the chains below reference it BY BARE NAME (1.5.3).
admin-tokens: { module: admin-tokens, token: { env: BUSBAR_ADMIN_TOKEN } }
auth:
chain:
- keys # verify busbar-minted signed virtual keys
signing_key: { env: BUSBAR_SIGNING_KEY }
admin_auth: [admin-tokens]
providers:
anthropic:
api_key: { env: ANTHROPIC_KEY } # a secret reference, resolved at boot
models:
claude-sonnet:
provider: anthropic
upstream_model: claude-sonnet-4-5

auth.signing_key is required whenever auth.chain includes keys; Busbar 1.5.3 does not auto-generate one, and boot fails without it. Generate a key with the same image before you helm install (it must stay the same across every pod and every upgrade; rotating it revokes every key minted under the old one):

Terminal window
docker run --rm getbusbar/busbar:1.5.3 --generate-signing-key

Put the printed 64-hex-char value in secrets.data.BUSBAR_SIGNING_KEY above.

Send LLM traffic to the data Service on port 8080:

Terminal window
kubectl -n busbar port-forward svc/busbar 8080:8080
curl -s localhost:8080/healthz

Busbar always runs its admin API (/api/v1/admin/…) on a separate listener from public LLM traffic, so the two never share a port, bind, or firewall posture.

  • Data plane: 0.0.0.0:8080, the busbar Service. Front it with an Ingress or Gateway for LLM traffic.
  • Admin plane: admin_listen, defaults to loopback. A network-exposed admin listener refuses to boot unless it requires mTLS or is explicitly marked insecure. So the chart keeps the admin plane loopback-only unless you opt in:
service:
admin:
enabled: true # expose the admin plane on an in-cluster Service (:8081)
adminTLS:
enabled: true # required for a non-loopback admin bind
certManager:
enabled: true
issuerRef:
name: busbar-admin-ca
kind: ClusterIssuer
networkPolicy:
enabled: true
admin:
allowedNamespaces: ["platform"] # who may reach the admin Service

cert-manager issues the admin server cert (and, optionally, client certs for your tooling); the NetworkPolicy restricts who can reach it. For a lab you can instead set adminInsecure: true, but never do that on a cluster reachable by untrusted workloads.

Known chart/binary mismatch: as published, adminTLS.enabled renders an admin_tls: { cert_file, key_file } block, but Busbar 1.5.3 expects admin_tls: { cert, key, client_ca } and rejects the chart’s field names (unknown field 'cert_file'). Render and check before you rely on it (helm template ... | grep -A3 admin_tls), and fall back to existingConfigMap (see Governance below) if it doesn’t match.

Both ports serve GET /healthz unauthenticated; the chart uses it for liveness and readiness on the data port (readiness returns 200 once at least one lane is usable).

By default Busbar keeps governance ledgers (keys, usage) in ephemeral per-replica RAM: the chart runs a Deployment you can scale horizontally (breaker and health state are correctly per-replica), but keys and usage do not persist or share across replicas. Busbar 1.5.3 itself persists this state through a store: { module: ..., settings: { db_path: ... } } config block (sqlite, or a signed postgres/valkey store plugin), but as of chart 0.2.7, the published GetBusbar/helm-charts chart does not yet render a working values-file path to that schema. Two things worth knowing before you rely on it:

  • There is no store: values key in the chart at all. Its only persistence knob today is the legacy governance.enabled / governance.dbPath / governance.adminTokenEnv values, which render an old-style governance: { enabled, db_path, admin_token } block, a config shape 1.5.3 refuses to boot with (it fails --validate with a “looks like a busbar 1.x config” error).
  • The admin API requires an admin token regardless of persistence: the admin auth chain defaults to admin-tokens, whose credential Busbar reads from BUSBAR_ADMIN_TOKEN. Put it in the Secret under that name (the chart wires it into Busbar’s auth.admin_auth chain for you; helm install fails fast if it’s missing). See the example values file above.

Until the chart catches up, use existingConfigMap to supply your own 1.5.3-schema config.yaml (a real, documented escape hatch in values.yaml) rather than the governance.* convenience values, and provision the PVC yourself if you need sqlite durability:

existingConfigMap: busbar-config # a ConfigMap you author/manage, containing config.yaml

Author config.yaml directly with the 1.5.3 store: schema (see Configuration for the full field reference), mount a PVC for it if you’re using sqlite, and track GetBusbar/helm-charts for a release that renders store: natively. Until then, this is the only values-file path that produces a durable store Busbar 1.5.3 will actually boot with.

The single-writer sqlite store cannot be scaled horizontally; scale it vertically, or point at a cluster-shared postgres / valkey store plugin to run multiple replicas against one store. Note that limit windows (requests/tokens) are per-process, so those caps enforce per node even over a shared store; budgets accrue additively to the shared store. See Running multiple instances (HA).

autoscaling:
enabled: true # HPA on the stateless data Deployment (not the governance StatefulSet)
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
className: nginx
hosts:
- host: llm.example.com
paths: [{ path: /, pathType: Prefix }]

Changing config and running helm upgrade updates the ConfigMap; a checksum annotation rolls the pods so the new config takes effect (set reloadOnConfigChange: false to opt out). Because the config is identical across replicas, rollouts are safe.

Terminal window
kubectl -n busbar get pods
kubectl -n busbar exec deploy/busbar -- /busbar --validate # config parses + validates
kubectl -n busbar port-forward svc/busbar 8080:8080 &
curl -s localhost:8080/healthz && echo ok

The full values surface is documented in the chart’s README. For the admin API the Services expose, see the Admin API reference and the live API reference.