Deploy on Amazon EKS
Ce contenu n’est pas encore disponible dans votre langue.
Run GeoLens on Amazon EKS using the community Helm chart. This page covers only what is specific to Kubernetes on AWS; the managed-service prerequisites — creating the PostGIS extensions, the database initialization SQL, and the S3 CORS policy for presigned uploads — are shared with the Compose path and live in Self-host on AWS, GCP, or DigitalOcean.
What you need
Section titled “What you need”-
An EKS cluster. Kubernetes 1.31–1.36 are offered by EKS today; this guide was validated on 1.33. Note that 1.33 left standard support on 2026-07-29, so give a new cluster a standard-support version (1.34–1.36): extended-support versions bill the control plane at several times the standard rate.
-
An RDS PostgreSQL instance (see Database below).
-
An S3 bucket in the same region.
-
An ingress controller and a way to issue certificates. The chart renders a standard
Ingress(ingress.classNameplus annotations), so any controller that reconcilesIngressworks — a Gateway API implementation alone is not enough, because the chart renders noHTTPRoute. This guide was validated with ingress-nginx and cert-manager, but ingress-nginx was retired upstream in March 2026 and no longer receives releases or security fixes, so pick a maintained controller for a new internet-facing install: on EKS the natural choice is the AWS Load Balancer Controller; Traefik and HAProxy qualify too. The routing and client-IP mechanics below translate to other controllers; TLS does not. With ALB, skip the cert-manager steps — the listener terminates with an ACM certificate — and set its three deltas together:ingress:className: albannotations:# each of these overrides an ALB default that breaks this chart:alb.ingress.kubernetes.io/scheme: internet-facing # default is internalalb.ingress.kubernetes.io/target-type: ip # instance targets need NodePort; the chart's Service is ClusterIPalb.ingress.kubernetes.io/certificate-arn: <acm-arn> # ALB ignores tls.secretName
The chart’s images are published for both linux/amd64 and linux/arm64, so Graviton node groups are a reasonable choice on cost.
Database (RDS PostgreSQL)
Section titled “Database (RDS PostgreSQL)”RDS PostgreSQL 18 carries everything GeoLens needs, verified on 18.4:
| Extension | Version on RDS PG 18.4 |
| --- | --- |
| postgis | 3.6.3 |
| postgis_raster | 3.6.3 |
| vector (pgvector) | 0.8.2 |
| pg_trgm | 1.6 |
| unaccent | 1.1 |
| pg_stat_statements | 1.12 |
pg_stat_statements is in shared_preload_libraries on the default postgres18 parameter group already, so it needs no parameter-group change — just CREATE EXTENSION.
Create the extensions, schemas and reader role once with a privileged role before installing, exactly as in the shared prerequisites. The migrate hook fails with schema "data" does not exist otherwise.
TLS to the database
Section titled “TLS to the database”Set the TLS mode through the chart, not through the DSN:
database: sslMode: requireverify-full additionally authenticates the server and needs the RDS CA bundle mounted as a file into the api, worker and migrate pods — the migrate hook runs before the api rolls, so a bundle it cannot read fails the upgrade there. The chart README’s external-database section carries that recipe.
Object storage (S3)
Section titled “Object storage (S3)”Use storage.backend: s3. Give the application a principal scoped to the one bucket:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket", "s3:GetBucketLocation", "s3:ListBucketMultipartUploads"], "Resource": "arn:aws:s3:::YOUR-BUCKET" }, { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts" ], "Resource": "arn:aws:s3:::YOUR-BUCKET/*" } ]}TiTiler only ever reads managed rasters, so it can take a narrower principal — s3:GetObject under rasters/* and tenants/*/rasters/* only — selected with titiler.s3Credentials. That split was verified against real IAM: TiTiler rendered tiles while being denied both writes and reads outside the raster prefixes.
Leave the staging volume disabled
Section titled “Leave the staging volume disabled”staging.persistence is for a shared /app/staging filesystem. On EKS with storage.backend: s3, leave it off. Uploads go to S3 under a relative key and the worker fetches them from there, so the api/worker handoff never needs a shared volume — verified by ingesting vector and raster data across pods with no shared volume at all.
Enabling it with chart defaults cannot work on a stock EKS cluster, for two independent reasons:
accessModesdefaults toReadWriteMany, which the EBS CSI driver rejects outright (Volume capabilities not supported). RWX on AWS means EFS.- An empty
storageClassrenders nostorageClassName, which requires a cluster default StorageClass. An eksctl-built cluster hasgp2but does not mark it default, so the claim sitsPendingforever.
If you do want a shared filesystem, use the EFS CSI driver and set staging.persistence.storageClass explicitly.
Install
Section titled “Install”Start from examples/values-aws.yaml in the chart repo, then:
helm repo add geolens https://geolens-io.github.io/geolens-deploymentshelm upgrade --install geolens geolens/geolens -f values-aws.yamlKeep the database DSN and admin credentials in a Secret you manage and point secrets.existingSecret at it, so nothing sensitive reaches a shell history or a values file in Git.
Ingress and TLS
Section titled “Ingress and TLS”The chart routes all traffic to the frontend, whose bundled nginx is the application edge: it proxies /api, caches raster tiles, blocks the unauthenticated /api/metrics, and rate-limits anonymous raster traffic. Do not add ingress paths that bypass it.
ingress: enabled: true className: nginx host: geolens.example.com annotations: cert-manager.io/cluster-issuer: letsencrypt tls: - hosts: [geolens.example.com] secretName: geolens-tlsWith ingress-nginx and cert-manager in front, the edge behaves correctly end to end: security headers survive both nginx layers, /api/metrics returns 404, public raster tiles cache and hit, and dataset exports keep a strong ETag with working range requests.
Preserving the client IP
Section titled “Preserving the client IP”frontend.trustedProxyCidrs tells the app edge which proxies to trust when resolving X-Forwarded-For. Without it, every request is attributed to the ingress controller, so the anonymous raster rate limit shares one bucket across all users and access logs lose the real client.
On EKS that setting is necessary but not sufficient. With the default externalTrafficPolicy: Cluster, kube-proxy translates the source address before the ingress controller ever sees it — measured on a real cluster, the controller’s own log showed a node IP for a request from a public one, so there was nothing left for the app to recover.
Upgrades
Section titled “Upgrades”helm upgrade runs Alembic as a pre-upgrade hook Job before any new pod rolls, then rolls the Deployments. An upgrade from chart 0.4.23 / app 1.14.0 to 1.14.1 was validated with live state present: dataset IDs, feature counts, visibility and duplicate-named maps all came through unchanged, and a bound PVC kept the same underlying volume.
An RWO staging PVC does not work here. The api and worker Deployments both mount it, and an EBS volume attaches to one node at a time, so the two consumers collide whenever the scheduler separates them — a Multi-Attach error, in steady state as much as during an upgrade. Rolling upgrades make the collision certain (the replacement pod comes up before the outgoing one dies, and dropping to one replica does not help: a one-replica RollingUpdate still surges), but a non-surging strategy such as Recreate or maxSurge: 0 only removes the overlap within a single Deployment — nothing pins api and worker to the same node.
That leaves EFS, which is ReadWriteMany and sidesteps the whole question. Simplest of all: leave staging.persistence off and use S3, per the section above.
Backup and disaster recovery
Section titled “Backup and disaster recovery”The chart ships no backup workload. On EKS that means:
-
The database is your provider’s responsibility. RDS automated backups plus point-in-time restore work, and were drilled end to end: a deleted dataset was recovered by restoring to a timestamp before the incident. Two timings matter — the restorable window trails real time by several minutes, and provisioning the restored instance took about ten more. A restore also produces a new endpoint, so recovery ends by re-pointing the DSN —
secrets.databaseUrlOverride, or your external Secret undersecrets.existingSecret— and then restarting the pods that consume it (kubectl rollout restart deploy/<release>-api deploy/<release>-worker): running pods keep the old environment until they restart. The RUNBOOK linked below has the full ordered cutover, including quiescing the writers first. -
Object storage is covered by nothing GeoLens runs. Enable bucket versioning before you need it:
Terminal window aws s3api put-bucket-versioning --bucket YOUR-BUCKET \--versioning-configuration Status=Enabled
A database-only restore recovers your catalog asymmetrically, which is the part worth internalising:
- Vector datasets come back whole — their features live in PostGIS. A lost object costs the original upload file and the quicklook thumbnail, not the data.
- Uploaded rasters do not. A COG you ingested exists only in your bucket, so the row returns reading
publishedwhile every tile request fails, and nothing in the catalog marks it broken. - Rasters imported by reference are fine, as far as GeoLens is concerned: STAC and public-COG imports keep the upstream asset URL and are served from it, so a restore recovers a working pointer — for as long as that upstream asset exists, which is someone else’s retention policy.
The full procedure, including the Kubernetes-specific steps, is in RUNBOOK.md.