Kubernetes Deployment

August 4, 2026 · View on GitHub

This directory contains development/reference manifests for running duckgres in Kubernetes with the multitenant control plane spawning DuckDB worker pods on demand. The remote worker backend now requires a config store. These manifests are intended for local development and testing; production deployments should adapt them to your cluster's requirements (resource limits, persistent storage, ingress, monitoring, etc.).

Architecture

┌─────────────────────────────────────────────────────┐
│  Kubernetes Cluster                                  │
│                                                      │
│  ┌────────────────────────────────┐                  │
│  │ Control Plane Pod              │                  │
│  │  duckgres --mode control-plane │                  │
│  │  --worker-backend remote       │                  │
│  │  --config-store ...            │                  │
│  │                                │                  │
│  │  Creates worker pods via K8s   │                  │
│  │  API, routes queries via gRPC  │                  │
│  └──────────┬─────────────────────┘                  │
│             │ Arrow Flight SQL (TCP :8816)            │
│             ├──────────────┬──────────────┐           │
│             ▼              ▼              ▼           │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐  │
│  │ Worker Pod 0 │ │ Worker Pod 1 │ │ Worker Pod N │  │
│  │  DuckDB svc  │ │  DuckDB svc  │ │  DuckDB svc  │  │
│  │  Bearer auth │ │  Bearer auth │ │  Bearer auth │  │
│  └──────────────┘ └──────────────┘ └──────────────┘  │
│                                                      │
│  Runtime coordination lives in config-store Postgres │
│  (`cp_instances`, `worker_records`, admission rows)  │
│                                                      │
│  Worker pods have:                                   │
│  - SecurityContext: non-root (UID 1000)              │
│  - Bearer token from K8s Secret                      │
│  - ConfigMap mount for shared config                 │
└─────────────────────────────────────────────────────┘

The control plane handles TLS, authentication, PostgreSQL wire protocol, and SQL transpilation. Workers are thin DuckDB execution engines exposed via Arrow Flight SQL. Workers are spawned per org on demand (sized from the connection's duckgres.worker_cpu/worker_memory request) and moved to hot_idle after their last session. Hot-idle workers keep their org assignment and DuckLake attachment for same-org reuse (by exact shape), then are retired by the janitor at their duckgres.worker_ttl. Spawn latency is hidden by the node-headroom controller, which keeps low-priority placeholder pods ready for real workers to preempt. Planned rolling replacements mark old replicas draining and fail readiness before termination; unplanned control-plane failure still drops existing pgwire connections.

Manifests

FileDescription
namespace.yamlduckgres namespace
rbac.yamlControl-plane and shared worker ServiceAccounts plus required Roles/RoleBindings
configmap.yamlShared duckgres config (users, extensions, data dir)
secret.yamlBearer token secret (auto-populated by CP if empty)
managed-warehouse-secrets.yamlLocal secret payloads referenced by the seeded managed-warehouse contract
worker-identity.yamlLocal worker ServiceAccount referenced by the seeded managed-warehouse contract
networkpolicy.yamlRestricts worker ingress to CP pods only
control-plane-multitenant-local.yamlOptional OrbStack-oriented shared-worker control-plane manifest
kind/config-store.overlay.yamlCompose overlay that attaches local dependency containers to the external Docker kind network
kind/config-store.seed.sqlKind-oriented managed-warehouse seed for the shared-worker flow
kind/control-plane.yamlKind-first shared-worker control-plane manifest used by local dev and CI
orbstack/dependency-ports.overlay.yamlOptional OrbStack overlay that publishes local DuckLake and MinIO dependency ports on the host

Configuration

Key flags for Kubernetes multitenant mode:

FlagEnv VarDescription
--worker-backend remote-Use K8s remote workers in config-store-backed multitenant mode
--config-storeDUCKGRES_CONFIG_STOREPostgreSQL config-store connection string required for remote mode
--session-init-timeoutDUCKGRES_SESSION_INIT_TIMEOUTSession startup metadata initialization and catalog probe timeout (10s default)
--handover-drain-timeoutDUCKGRES_HANDOVER_DRAIN_TIMEOUTMax time to drain planned shutdowns/upgrades before forced exit (15m default in remote mode)
--sni-routing-modeDUCKGRES_SNI_ROUTING_MODEManaged-hostname routing: off, passthrough, or enforce. Postgres uses requested dbname first; managed SNI must resolve to the same org, and SNI supplies the database only when dbname is empty
--managed-hostname-suffixesDUCKGRES_MANAGED_HOSTNAME_SUFFIXESComma-separated managed hostname suffixes such as .dw.test.local
--k8s-worker-imageDUCKGRES_K8S_WORKER_IMAGEDocker image for worker pods
--k8s-worker-image-pull-policyDUCKGRES_K8S_WORKER_IMAGE_PULL_POLICYImage pull policy (Never, IfNotPresent, Always)
--k8s-worker-service-accountDUCKGRES_K8S_WORKER_SERVICE_ACCOUNTShared ServiceAccount name for worker pods (duckgres-worker default)
--k8s-worker-secretDUCKGRES_K8S_WORKER_SECRETK8s Secret name for bearer token
--k8s-worker-configmapDUCKGRES_K8S_WORKER_CONFIGMAPConfigMap name for worker config
-DUCKGRES_K8S_WORKER_CPU_REQUESTDefault worker pod CPU request/limit. Local manifests set 500m; production should override for workload needs.
-DUCKGRES_K8S_WORKER_MEMORY_REQUESTDefault worker pod memory request/limit. Local manifests set 512Mi; production should override for workload needs.

The worker Secret setting is a base name for per-worker RPC Secrets. Each worker pod gets its own derived Secret containing its RPC bearer token and TLS material. If the derived Secret does not exist, the control plane creates it before spawning the pod.

Shared workers should use the shared duckgres-worker ServiceAccount with automountServiceAccountToken: false. Tenant authority must arrive only through activation-time scoped credentials.

For managed-hostname routing, passthrough logs legacy/non-managed callers while allowing them to route by requested dbname. enforce rejects Postgres clients whose TLS SNI does not match a configured managed suffix. In both managed modes, when SNI does match a suffix, the hostname prefix and requested Postgres database must resolve to the same org; if the client omits the startup database, the SNI prefix is used as the fallback database source. Unknown routing-mode values behave like off.

For seamless planned deployments, use a rolling strategy with overlap and enough termination grace period for drain completion. The provided control-plane manifests now set:

  • strategy.rollingUpdate.maxUnavailable: 0
  • strategy.rollingUpdate.maxSurge: 1
  • terminationGracePeriodSeconds: 900
  • --handover-drain-timeout 15m

That gives the old replica time to fail readiness, stop taking new pgwire sessions, keep existing pgwire sessions alive during the drain window, and then force shutdown at the timeout boundary if sessions remain.

Local Development with kind

The primary shared-worker workflow now uses kind. Prerequisites: Docker, kubectl, kind, and just.

just run-multitenant-kind
just multitenant-port-forward-pg
just multitenant-port-forward-api
PGPASSWORD=postgres psql "host=127.0.0.1 port=5432 user=postgres dbname=duckgres sslmode=require"

just multitenant-port-forward-pg forwards pgwire on 5432.

just run-multitenant-kind recreates a local kind cluster, starts the config store plus the local warehouse DB, DuckLake metadata DB, and MinIO backing the seeded managed-warehouse contract, attaches those dependency containers to the Docker kind network, loads the locally built image into kind, and deploys the shared-worker control plane.

Default login: postgres / postgres

The local manifests default worker pods to 500m CPU and 512Mi memory so a single-node developer cluster can schedule workers. Override DUCKGRES_K8S_WORKER_CPU_REQUEST and DUCKGRES_K8S_WORKER_MEMORY_REQUEST when testing larger worker shapes.

Optional OrbStack Workflow

OrbStack remains available as an optional local workflow on macOS. Prerequisites: OrbStack Kubernetes enabled, Docker, kubectl, and just. This flow uses host.docker.internal to reach the local dependency containers from the cluster.

orb start k8s
just run-multitenant-local
just multitenant-port-forward-pg
just multitenant-port-forward-api
PGPASSWORD=postgres psql "host=127.0.0.1 port=5432 user=postgres sslmode=require"

just multitenant-port-forward-pg forwards pgwire on 5432.

The admin dashboard requires the admin token printed in the control-plane logs. Fetch it with:

kubectl -n duckgres logs deployment/duckgres-control-plane | rg "Generated admin API token"

The local seed populates one managed-warehouse contract for the local team. That row includes separate warehouse_database and metadata_store sections plus secret references only, not secret values. Every non-empty managed-warehouse secret ref stores an explicit namespace, and that namespace must match worker_identity.namespace.

Seeded warehouse contract notes:

  • The config store keeps at most one managed-warehouse row per team.
  • GET /api/v1/teams/local/warehouse reads that row.
  • PUT /api/v1/teams/local/warehouse replaces that row for the team.
  • just multitenant-seed-local is idempotent and updates the same local warehouse row rather than creating duplicates.
  • Destructive production teardown is covered by the Managed Warehouse Deprovision runbook.

Tear down the local environments:

just kind-cluster-down
just multitenant-config-store-down-kind
# or, for the optional OrbStack path:
just multitenant-config-store-down

After code changes, rerun just run-multitenant-kind to rebuild, redeploy, and reseed the default local environment.

Running Integration Tests

# Against an existing deployment (skip build/deploy, just run tests)
DUCKGRES_K8S_TEST_SKIP_SETUP=true go test -v -count=1 -tags k8s_integration -timeout 600s ./tests/k8s/...

# Full default setup (kind)
just test-k8s-integration

# Full setup against the optional OrbStack/local mode
DUCKGRES_K8S_TEST_SETUP=local go test -v -count=1 -tags k8s_integration -timeout 600s ./tests/k8s/...

Test environment variables:

VariableDefaultDescription
DUCKGRES_K8S_TEST_SKIP_SETUP-Set to true to skip build/deploy/teardown
DUCKGRES_K8S_TEST_SETUPkindManaged setup mode: kind for the default local/CI path, local for the optional OrbStack path
DUCKGRES_K8S_TEST_NAMESPACEduckgresK8s namespace for test resources