Skip to content

Deploying on Google Kubernetes Engine (GKE)

K-Lake installs on GKE with the same Helm chart as every other cluster. This page covers what is specific to Google Cloud. For the install itself see Production deployment; for how many extractors to run see Sizing for ingest time.

Validated on GKE Standard 1.36 in europe-west2, with the Google Cloud Storage source and the private console address from 0.1.53.

Before you start

  • A GKE Standard cluster on x86 nodes. Autopilot has not been validated. SMB and NFS sources mount shares from inside the pod, which needs CAP_SYS_ADMIN; GKE Standard admits it. If you index only object storage, set sources.smbEnabled=false and the pods run without it.
  • vCPU quota. A new project's default is 24 vCPU across all regions. Three n2-standard-8 nodes use all of it. Request more before you size above that.
  • Zone capacity. A zone can run out of a machine type. If node creation sits on ZONE_RESOURCE_POOL_EXHAUSTED, add a node pool in another zone of the same region:

    gcloud container node-pools create pool-b --cluster <cluster> \
      --zone <cluster-zone> --node-locations <other-zone> \
      --machine-type n2-standard-8 --num-nodes 3
    
  • Outbound HTTPS to ghcr.io for the images.

gcloud container clusters create and get-credentials switch your current kubectl context to the new cluster.

Starting point
Node pool 3 × n2-standard-8 (8 vCPU, 32 GB)
Workload Identity Enabled (--workload-pool=<project>.svc.id.goog), needed for keyless Google Cloud Storage
Storage The default standard-rwo StorageClass. Leave global.storageClass empty.

That holds the small tier with one kdoc2 replica, the embedding service and up to 11 klex extractors. Measured: about 18,000 mixed documents an hour.

Install

helm install kdbl oci://ghcr.io/kdbl-consulting/kdbl --version <X.Y.Z> \
  --namespace kdbl --create-namespace \
  --set tier=small \
  --set licence.blob="$KDBL_LICENCE"

All pods were ready about 2.5 minutes after install. Some restart once or twice while the database comes up; that is expected.

Console access

Leave console.access at its default, internalLB. From 0.1.53 the chart asks GKE for an internal load balancer (networking.gke.io/load-balancer-type: Internal): a private IP reachable from the VPC, peered networks and VPN.

Chart 0.1.52 and earlier

The internal load-balancer annotation was Azure's only, and GKE ignores it, so internalLB got a public IP open to every address. On those versions use --set console.access=portForward or an ingress, and check any existing install with kubectl get svc kdbl-ui -n kdbl. See Upgrading.

GPU OCR (optional)

A GPU makes kdoc2, which reads scanned pages and the pictures inside Office files, several times faster. Add one GPU node pool and give the kdoc2 fleet the GPU:

gcloud container node-pools create gpu-l4 --cluster <cluster> --zone <zone> \
  --machine-type g2-standard-8 \
  --accelerator type=nvidia-l4,count=1,gpu-driver-version=latest \
  --num-nodes 1 --workload-metadata=GKE_METADATA
extract:
  additionalFleets:
    - plugin: kdoc2
      replicas: 1
      gpu: {enabled: true}   # 4 OCR pipelines per pod by default (gpu.ocrPipelines)
  • GKE installs the driver (gpu-driver-version=latest), taints the node, and lets only pods that ask for a GPU onto it. The kdoc2 pod asks for one.
  • One pod runs several OCR pipelines on the one GPU. You do not need GPU time-sharing. See Sizing for ingest time for the figures.
  • A new project's GPU quota is 0. Ask for GPUs (all regions) under IAM & Admin → Quotas before you create the pool.
  • Capacity varies by zone and hour. In October 2026 on-demand and Spot L4s were both unavailable in europe-west2-a and -b for hours (ZONE_RESOURCE_POOL_EXHAUSTED), while europe-west4-a had Spot capacity. A node pool that cannot get a GPU keeps retrying quietly. To find capacity quickly, create a throwaway VM with the same machine type in each zone you could use; that fails within seconds.
  • A Spot GPU costs about 40% less, but Google can reclaim it at any time. kdoc2 work in flight is retried when the pod returns. Use Spot when a slower second pass is acceptable.
  • Chart 0.1.52 and earlier: the GPU build of kdoc2 cannot find GKE's driver, and the pod stops with libcuda.so.1: cannot open shared object file. Upgrade to 0.1.53 or later, which finds it.

Indexing Google Cloud Storage

From 0.1.53, gs:// is a native source type (see Sources). The keyless route uses GKE Workload Identity:

  1. Create a Google service account and let it read the bucket:

    gcloud iam service-accounts create kdbl-gcs-reader --project <project>
    gcloud storage buckets add-iam-policy-binding gs://<bucket> \
      --member serviceAccount:kdbl-gcs-reader@<project>.iam.gserviceaccount.com \
      --role roles/storage.objectViewer
    
  2. Let the chart's kdbl-source-reader Kubernetes service account act as it:

    gcloud iam service-accounts add-iam-policy-binding \
      kdbl-gcs-reader@<project>.iam.gserviceaccount.com \
      --role roles/iam.workloadIdentityUser \
      --member "serviceAccount:<project>.svc.id.goog[kdbl/kdbl-source-reader]"
    
  3. Install or upgrade with --set sources.workloadIdentity.gcpServiceAccount=kdbl-gcs-reader@<project>.iam.gserviceaccount.com.

  4. Allow the tenant to use the deployment's identity (cluster admin):

    kdbl-control tenant source-policy --slug <tenant> --allow-ambient-credentials true
    
  5. Add the source with no key, then crawl:

    kdbl-control source add-gcs --source-id gs://<bucket> --bucket <bucket>
    kdbl-control crawl --source-id gs://<bucket>
    

Adding a source does not start a crawl; run crawl (or start one from the console).

Many organisations block service-account key creation by policy (iam.managed.disableServiceAccountKeyCreation), which is another reason to prefer Workload Identity. A JSON key also works: add-gcs --key-file.

Before 0.1.53, index a bucket through the S3 source and Cloud Storage's S3-compatible endpoint, with an HMAC key for a service account that has roles/storage.objectViewer:

gcloud storage hmac create <sa-email>
echo "<hmac-secret>" | kdbl-control source add-s3 \
  --source-id s3://<bucket> --bucket <bucket> \
  --endpoint-url https://storage.googleapis.com --region auto \
  --access-key-id <hmac-access-id> --secret-access-key-stdin

Cleaning up

helm uninstall leaves the database and cache PersistentVolumeClaims, which keep their disks. Delete them, then the cluster:

helm uninstall kdbl -n kdbl
kubectl delete pvc --all -n kdbl     # waits until the disks are released
gcloud container clusters delete <cluster> --zone <zone>

Delete the cluster only after the claims are gone. A cluster deleted while its volumes are still attached leaves the disks behind, still billed. Check for them afterwards and delete any that remain:

gcloud compute disks list --filter="labels.goog-k8s-cluster-name=<cluster>"