Kubernetes reference deployment
deploy/kubernetes/lake.yaml is a
production-oriented template, not a turnkey cluster installer. It keeps the
stateless Query tier separate from the bounded metadata authority and maps the
runtime’s existing security, health, metrics, resource, and shutdown contracts
into Kubernetes. It deliberately contains the invalid
REPLACE_WITH_RELEASE_MANIFEST_DIGEST value, so applying it before all image
references are replaced with one real manifest digest fails instead of silently
deploying a mutable image tag.
Build and pin the image
The root Dockerfile is multi-stage and runs the final process as numeric user
65532. It also copies the versioned grpc_health_probe binary used for TLS
and bearer-authenticated exec probes. Publishing a GitHub release creates the
official ghcr.io/rararulab/lake:vX.Y.Z and ghcr.io/rararulab/lake:X.Y.Z
multi-platform image tags. Resolve the published manifest-list digest, then
replace every image reference in this deployment with that immutable digest:
docker buildx imagetools inspect ghcr.io/rararulab/lake:v1.0.0
# Copy the manifest-list digest, then set every image to
# ghcr.io/rararulab/lake@sha256:<digest>
Do not deploy a mutable tag in production. Replace every occurrence of
REPLACE_WITH_RELEASE_MANIFEST_DIGEST before applying the template. The
health-probe stage is version pinned independently so its updates remain
visible in review.
Supply cloud configuration
Edit the lake-runtime ConfigMap before applying it. LAKE_S3_BUCKET must be
the production bucket; AWS_REGION, prefixes, and the DynamoDB table must
match pre-provisioned infrastructure. The manifest deliberately does not
contain static AWS credentials. Annotate the separate lake-query and
lake-metasrv ServiceAccounts for the cluster’s workload-identity mechanism:
- Query receives read access to
$LAKE_MANIFEST_DYNAMODB_TABLEand its_prefix_v2companion plus table/object prefixes. Query must have no access to$LAKE_DYNAMODB_TABLEor its companion; catalog reads use authenticated Metasrv Flight RPCs. - Query receives conditional read/write access to the separate async-query
tables and read/write/delete access to
LAKE_ASYNC_RESULT_PREFIX. - Metasrv receives registry conditional-write, manifest conditional-write, and table/object read/write access.
The exact IAM resources are deployment-specific and are intentionally not
created by this repository. Query and Metasrv both use DynamoDB/S3; neither
StatefulSet identity nor an emptyDir is authoritative data.
Provision both $LAKE_DYNAMODB_TABLE (HASH key pk) and its companion
$LAKE_DYNAMODB_TABLE_prefix_v2 (HASH bucket, RANGE pk) with on-demand
billing, or grant the one-shot migration identity CreateTable. Roll all
Query and Metasrv pods to a dual-capable image before running
lake dynamo-migrate. Do not set --acknowledge-dual-rollout while an old
v1-only writer exists. Pause metadata write admission before acknowledging
--acknowledge-write-quiescence; after finalization, roll Query and Metasrv so
they observe v2 authority before resuming writes. Runtime identities need
DescribeTable plus their normal data-plane permissions, not CreateTable.
Keep both table ARNs in the runtime IAM policy and retain v1 for at least one
append-operation retention horizon.
Separately provision $LAKE_MANIFEST_DYNAMODB_TABLE (HASH key pk) and its
_prefix_v2 companion. The name must differ from $LAKE_DYNAMODB_TABLE.
Metasrv mutates these pre-provisioned physical Lance pointers; Query startup never
creates tables and its workload identity should receive only
dynamodb:DescribeTable and dynamodb:GetItem for the manifest pair. The
read-only adapter rejects missing latest pointers before any prefix enumeration
or mutation.
For an existing shared-table deployment, cut over the manifest authority independently from the registry migration:
-
While the old authority is live, ensure every dataset has a fixed
lance-manifest-latest/pointer. Query will not perform this migration. -
Pause metadata writes. Copy
lance-manifest/,lance-manifest-latest/, andlance-manifest-cleanup/from the old registry table into the new manifest v1 table, then verify exact key/value equality for those three families. -
With a one-shot migration identity, override
LAKE_DYNAMODB_TABLEonly in the migrator shell (never in the runtime ConfigMap), and run bounded pages untilpage.completeistrue:export LAKE_DYNAMODB_TABLE="$LAKE_MANIFEST_DYNAMODB_TABLE" lake dynamo-migrate --page-size 500 --json -
Keep writes paused and finalize the manifest pair independently:
lake dynamo-migrate --page-size 500 --finalize \ --acknowledge-dual-rollout --acknowledge-write-quiescence --jsonRequire
verification.finalized=trueand equallegacy_items/v2_items. The migration identity needs create/scan/query/read/write permissions on the manifest pair; runtime Query does not. -
Restore the ordinary registry environment, deploy the distinct manifest table name to every Metasrv and Query process, wait for readiness, then resume writes. Retain the old manifest keys for at least one append-operation retention horizon. Rollback requires another write pause and exact reverse synchronization; never point Query at the registry table as a live fallback.
The Query adapter rejects all manifest mutations, including legacy latest-pointer installation. Lake intentionally does not fall back to registry storage when the manifest table is missing.
Optional Iceberg federation
The base manifest deliberately has no LAKE_ICEBERG_* values. Query reads only
the listed optional keys from lake-iceberg-runtime; when neither resource
exists, Iceberg is disabled. The manifest accepts endpoint, warehouse,
namespace, timeout, and OAuth metadata keys from the ConfigMap, but accepts the
two authentication keys only from the Secret. Extra ConfigMap or Secret keys
are not injected into Query. Do not create either resource with empty or partial
values: Query requires the endpoint, warehouse, and namespace allowlist as one
complete configuration before it binds.
Create the non-secret configuration in the same namespace. The external endpoint must be credential-free HTTPS, and the timeout is optional (the normal default is 10 seconds):
kubectl -n lake-system create configmap lake-iceberg-runtime \
--from-literal=LAKE_ICEBERG_REST_ENDPOINT=https://catalog.example.com \
--from-literal=LAKE_ICEBERG_WAREHOUSE=s3://embodied-warehouse \
--from-literal=LAKE_ICEBERG_NAMESPACES=analytics,models \
--from-literal=LAKE_ICEBERG_REST_TIMEOUT_MS=10000
The optional Secret is imported by the lake-query pod only; it is not mounted
by or exposed to Metasrv, and the reference never commits a Secret object or
credential value. Use no Secret for an unauthenticated catalog. For an
authenticated catalog, create exactly one of the following secret forms — never
put both keys in one Secret:
# Static bearer-token catalog
kubectl -n lake-system create secret generic lake-iceberg-runtime \
--from-file=LAKE_ICEBERG_REST_TOKEN=./catalog-token
# OAuth client-credentials catalog
kubectl -n lake-system create secret generic lake-iceberg-runtime \
--from-file=LAKE_ICEBERG_REST_CREDENTIAL=./catalog-client-credential
For OAuth metadata, add the credential-free optional
LAKE_ICEBERG_REST_OAUTH2_SERVER_URI, LAKE_ICEBERG_REST_OAUTH_SCOPE,
LAKE_ICEBERG_REST_OAUTH_AUDIENCE, or LAKE_ICEBERG_REST_OAUTH_RESOURCE
values to the ConfigMap before the rollout. The OAuth endpoint follows the same
credential-free HTTPS rule. The full runtime semantics, including direct
Iceberg object reads and snapshot pinning, are in the
Iceberg federation guide.
After creating the ConfigMap and, when needed, the Secret, restart Query so the new environment is present before its startup validation and namespace checks:
kubectl -n lake-system rollout restart deployment/lake-query
kubectl -n lake-system rollout status deployment/lake-query
The reference uses Recreate for Query, so this activation has the same
controlled Query availability window as every Query rollout. Pause and drain
admission according to the async rollout procedure below; do not present this
configuration change as a zero-downtime rollout.
Limit Secret read/update and Pod exec permissions to the deployment operators. The Query workload identity needs direct read access (and, where applicable, KMS decrypt permission) to the selected Iceberg warehouse’s metadata, manifests, and Parquet objects. It also needs egress to the catalog and OAuth endpoints. Metasrv receives none of those REST credentials; this does not grant the client SDK access to the Iceberg warehouse.
To rotate a token or credential, apply a replacement Secret with the same name, then restart and wait for the Query Deployment so every process starts one bounded in-memory REST session with the new value. To disable federation, remove the Secret first when present, then remove the complete configuration and roll Query:
kubectl -n lake-system delete secret lake-iceberg-runtime --ignore-not-found
kubectl -n lake-system delete configmap lake-iceberg-runtime --ignore-not-found
kubectl -n lake-system rollout restart deployment/lake-query
kubectl -n lake-system rollout status deployment/lake-query
When LAKE_ASYNC_QUERIES=true, also provision
$LAKE_ASYNC_DYNAMODB_TABLE and
$LAKE_ASYNC_DYNAMODB_TABLE_prefix_v2 with the same key schemas. These tables
hold only bounded async coordination records and are deliberately separate
from the registry tables. Result jobs, Arrow IPC parts, and manifests live
under LAKE_ASYNC_RESULT_PREFIX; lifecycle cleanup requires List, Get, Put,
and DeleteObject on that exact prefix.
The reference ConfigMap also sets four total async workers, one worker per
tenant, four shared cluster executions, one shared execution per tenant, and a
30-minute execution deadline. Tune
LAKE_ASYNC_WORKER_CONCURRENCY,
LAKE_ASYNC_WORKER_CONCURRENCY_PER_TENANT, and
LAKE_ASYNC_EXECUTION_TIMEOUT_MS together with pod CPU, memory, spill, and
result-prefix capacity. The local settings remain per replica. The complete
LAKE_ASYNC_GLOBAL_WORKER_CONCURRENCY +
LAKE_ASYNC_GLOBAL_WORKER_CONCURRENCY_PER_TENANT pair is a shared execution
lease ceiling in the dedicated async state store, so autoscaling does not
multiply running scan capacity. It is not a global queue or strict scheduling
fairness policy.
The ConfigMap also sets LAKE_ASYNC_MAX_OUTSTANDING_PER_TENANT=8 and
LAKE_ASYNC_MAX_RESULT_BYTES=17179869184. Unlike worker concurrency, these
limits are enforced by shared durable state: every new job reserves one tenant
entry before object upload, and its result ceiling is immutable in the job
record. Allowed ranges are 1..=128 jobs and 64 MiB..=256 GiB. A failed replica
may temporarily over-count until the five-minute reservation grace is
reconciled by bounded point reads; it never admits capacity by under-counting a
live record. Size storage and lifecycle policies for the result prefix from
these retained-object bounds; they do not reserve pod memory or CPU.
Async schema-v2 rollout
Async schema-v2 records are intentionally not readable by a schema-v1 Query
binary. Cluster execution leases are also unknown to older workers. Do not use
a rolling rollout, do not mix images, and do not mix durable quota or shared
execution-limit values against one async authority. The reference lake-query
Deployment therefore uses strategy: Recreate.
For the first v2 enablement, pause PollFlightInfo async submission at the
edge, leave one schema-v1 worker fleet running until every existing v1 async
record has expired and fenced cleanup has removed its async-query/ state and
scoped result objects, then drain and recreate all Query replicas before
resuming admission. The same drain is required before enabling, disabling, or
changing either shared execution-limit value or either durable quota value.
Once any v2 record exists, an old image must not be rolled back into the async
fleet: forward-fix the v2 image, or pause admission and drain all v2
records/objects before a full old-image replacement. This procedure is
necessary because v1 jobs have no durable tenant reservation or shared
execution lease and must never be silently counted as v2 jobs.
All Query pods sharing this authority need synchronized wall clocks. Durable job and execution leases use bounded UTC expiry, so keep the normal Kubernetes node time-synchronization service healthy before relying on a shared execution ceiling.
A failed finalize leaves its durable barrier held. Keep admission paused, finish backfill, and rerun finalize. Do not delete the barrier as a routine rollback mechanism; doing so can re-admit stale dual writers during parity verification.
After the Dynamo v2 migration and writer rollout are complete, keep metadata write admission paused and activate catalog directory generations once:
lake catalog-finalize \
--acknowledge-writer-rollout \
--acknowledge-write-quiescence \
--json
Repeat execution is safe and reports finalized: false once authority already
exists. Resume writes only after the command succeeds. Do not roll any registry
writer back to an older image afterward; old Query replicas are compatible,
but old writers do not publish the required atomic generation signal.
Create required secrets
Create two Secrets rather than editing credentials into the manifest. Secret
files are first mounted read-only, then a non-root init container copies them
to a memory-backed volume with mode 0600; this is required by Lake’s
principal-map permission check.
lake-query-runtime must contain:
tls.crt,tls.key, andca.crt;principals.jsonfor inbound SDK users and the health principal;ticket-keys.json, shared byte-for-byte by every Query replica;health-token, matching a principal-map entry;metadata-token, matching aquery_serviceprincipal accepted by Metasrv.
lake-metasrv-runtime must contain:
tls.crt,tls.key, andca.crt;principals.jsonfor Query, metadata peers, administrators, and health;health-token, matching a principal-map entry;peer-token, matching ametadata_peerprincipal.
Create them from protected local files:
kubectl -n lake-system create secret generic lake-query-runtime \
--from-file=tls.crt=query/tls.crt \
--from-file=tls.key=query/tls.key \
--from-file=ca.crt=ca.crt \
--from-file=principals.json=query/principals.json \
--from-file=ticket-keys.json=query/ticket-keys.json \
--from-file=health-token=query/health-token \
--from-file=metadata-token=query/metadata-token
kubectl -n lake-system create secret generic lake-metasrv-runtime \
--from-file=tls.crt=metasrv/tls.crt \
--from-file=tls.key=metasrv/tls.key \
--from-file=ca.crt=ca.crt \
--from-file=principals.json=metasrv/principals.json \
--from-file=health-token=metasrv/health-token \
--from-file=peer-token=metasrv/peer-token
The ticket key file is protected JSON. Secrets are high-entropy strings of at least 32 bytes; they are never key identifiers and must not be reused as bearer credentials. The active secret seals new statement tickets and up to three verification secrets decrypt tickets created during a rollout:
{
"active": "replace-with-at-least-32-random-bytes",
"verification": []
}
Rotate without breaking requests crossing Query replicas:
- preload — add the new secret to
verificationwhile the old secret remainsactive, update the Secret, and finish the full Query rollout. - activate — make the new secret
active, retain the old secret inverification, update the Secret, and finish a second full rollout. - retire — wait longer than
LAKE_QUERY_TICKET_TTL_SECSafter the second rollout, remove the old secret, and roll Query once more.
Skipping preload lets a new replica issue tickets that old replicas cannot decrypt. Keep the file identical across all replicas behind one Flight endpoint; a remotely exposed Query refuses startup when it is absent.
The first release that introduces encrypted statement tickets is not wire-compatible with an older Query binary that emits raw SQL handles. Adopt it with a blue/green Query Service cutover, or stop admission, drain all old Query connections, replace every replica, and then resume. Do not perform that one-time binary transition as an ordinary mixed-version rolling update. After every replica understands this envelope, later key changes use the staged rolling procedure above without downtime.
The Query certificate must cover
lake-query.lake-system.svc.cluster.local; the Metasrv certificate must cover
lake-metasrv.lake-system.svc.cluster.local. Replace these names consistently
if the namespace or Services change.
Health, metrics, and lifecycle
Kubernetes’ native gRPC probe cannot attach the bearer metadata and TLS
server-name override required by Lake. The reference therefore uses
grpc_health_probe through an exec probe. Liveness checks the empty standard
Health service; readiness checks arrow.flight.protocol.FlightService. Do not
replace these with anonymous TCP or native gRPC probes.
An authenticated startup probe grants up to 150 seconds for cold cloud/client
initialization before liveness enforcement begins.
Prometheus listens at 127.0.0.1:9090 and no Service exposes that port. Run a
collector as a sidecar or node agent that can scrape pod loopback; do not
change Lake’s listener to a wildcard address.
OTLP tracing remains opt-in. The reference assigns distinct
OTEL_SERVICE_NAME values and a five-second owned shutdown bound, but does not
guess a collector. To enable export, add an in-cluster collector origin such as
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector:4317 to the
lake-runtime ConfigMap. Use a NetworkPolicy to restrict that egress. The
collector is not part of Lake’s availability path: an unavailable collector
cannot stop either service, and shutdown remains bounded.
Lake drains for at most 30 seconds after SIGTERM. Kubernetes grants 45 seconds,
leaving time for probe withdrawal and process/container cleanup. The Query
spill emptyDir is capped at 16 GiB and is disposable. Tune its memory/spill
budgets and pod limits together: the configured 6 GiB query pool sits below
the 8 GiB container limit.
The reference gives each Query replica 64 aggregate slots and 8 slots per authenticated tenant, with 4096 bounded tenant trackers. Because these limits are replica-local, autoscaling changes cluster aggregate capacity. Keep the per-tenant value proportional to pod size and use a tenant-aware load balancer or a future distributed quota service when a strict cluster-wide entitlement is required.
Metasrv append-operation cleanup defaults to 16 metadata pages of 128 records
per one-minute maintenance tick. Monitor the finite-label
append_operations/budget_exhausted and append_operations/time_exhausted
maintenance counters and the deleted-item rate. If either ceiling stays
exhausted and deletion trails sustained append throughput, raise
LAKE_MAINTENANCE_OPERATION_GC_MAX_PAGES,
LAKE_MAINTENANCE_OPERATION_GC_MAX_MS, or
LAKE_APPEND_OPERATION_GC_PAGE_SIZE while keeping the product and duration
within the node’s Dynamo request, table-maintenance, and shutdown budgets.
Availability model
- Query is a three-replica Deployment with topology spreading and a disruption
budget. It remains stateless and can be autoscaled externally. Its deliberate
Recreatestrategy takes every Query replica down during a rollout, including the async schema-v2 transition; pause and drain admission, then plan that bounded unavailability window instead of treating the disruption budget as a zero-downtime upgrade mechanism. - Metasrv is exactly three replicas with stable StatefulSet pod identity,
topology spreading, and
minAvailable: 2. It still stores authority in DynamoDB. Each pod advertises its downward-API pod IP for leader forwarding; advertising0.0.0.0would make the elected leader unreachable. - Flight Services expose only ports 50051/50052. Metrics stay pod-private.
Apply only after replacing the image, cloud values, identities, certificates, and Secrets:
mise run k8s-validate
kubectl apply --server-side --dry-run=server -f deploy/kubernetes/lake.yaml
kubectl apply --server-side -f deploy/kubernetes/lake.yaml
kubectl -n lake-system rollout status deployment/lake-query
kubectl -n lake-system rollout status statefulset/lake-metasrv
mise install provides the pinned kubeconform release used by the first
command. It validates strictly against the pinned Kubernetes 1.32 schema;
server-side dry-run then covers cluster admission and policy.