Skip to content

feat!(networking): configurable ingress host address and group networking related config options under common networking key#6699

Open
florianzwagemaker wants to merge 9 commits into
loculus-project:mainfrom
florianzwagemaker:url_construction
Open

feat!(networking): configurable ingress host address and group networking related config options under common networking key#6699
florianzwagemaker wants to merge 9 commits into
loculus-project:mainfrom
florianzwagemaker:url_construction

Conversation

@florianzwagemaker

@florianzwagemaker florianzwagemaker commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

This PR modifies how networking and public-facing URLs are constructed for Loculus deployments.

New ingressHosts key in the helm chart

This modification allows for changing of the hostnames for each service Ingress rule (for each public facing service).
Every Ingress host rule was computed from .Values.host and .Values.subdomainSeparator with a predefined (hardcoded) prefix.
i.e. backend.host.xyz, lapis.host.xyz, or authentication.host.xyz.
The default behavior should be okay for the majority of deployments, however in some environments there might be a requirement for different hostnames either due to security policies, predefined hostname standards or potentially multiple deployments.
This change makes this optionally configurable to allow for compatibility with these potential usecases by introducing a new ingressHosts key with properties for each ingress resource, allowing ingress hostnames to be set independently of the public urls. An example can be found below.

⚠️ breaking change; introduction of common networking key and moving of several properties to this group

With the introduction of the ingressHosts properties i figured it would be neater to group all keys together that are involved in URLs and hostnames. Including the keys that are used in the automatic construction of urls. The moved properties are public (here renamed to publicHosts), subdomainSeparator, enforceHTTPS, and traefikVersion.

Resolution chain

Both publicHosts and ingressHosts can be set independently for different purposes: publicHosts controls the client-facing URLs baked into runtime config (what browsers/clients use), while ingressHosts controls the Kubernetes Ingress host rules (what the cluster routes on). They can be set together or independently.

In the URL templates (_urls.tpl, _common-metadata.tpl), the resolution chain for each service is:

networking.publicHosts.<key>        ← explicit external URL (highest priority, client-facing)
        ↓ (if empty)
networking.ingressHosts.<key>       ← ingress hostname, prefixed with https:// when used as URL fallback
        ↓ (if empty)
subdomainSeparator + host           ← legacy computed default (also prefixed with https://)
        ↓ (if local dev)
localhost:port                      ← development fallback

The https:// prefix is only applied when ingressHosts values are used as a fallback in the URL templates for public facing URLs. In the ingress route templates (ingressroute.yaml, lapis-ingress.yaml, etc.), ingressHosts values are used as bare hostnames (without https://) since Kubernetes Ingress host rules expect just the hostname.

This means that in the URL templates, ingressHosts is used as a fallback when no publicHosts key is set for the same service.

The ingressHosts keys are always used independently in the ingress route templates (ingressroute.yaml, lapis-ingress.yaml, docs-preview.yaml) to control the actual Ingress host rules, regardless of whether publicHosts is set.
If ingressHosts is kept empty then both the URL templates and the ingress route templates fall back to the previously computed default values based on subdomainSeparator + host.

This makes the logic mostly backwards compatible, but i figured it would be better to consider this a breaking change as the moved keys for public urls would require existing deployments to change the helm chart.

Per-service configuration reference

The tables below show the resulting values for each service across all combinations of publicHosts and ingressHosts.

to keep the tables a bit more readable, the networking. prefix is omitted from the column headers, so publicHosts.backendUrl refers to networking.publicHosts.backendUrl and ingressHosts.backend refers to networking.ingressHosts.backend.

The "Runtime URL" column is what gets baked into client-facing runtime config (full URL with scheme); the "Ingress host" column is the bare hostname used in the Kubernetes Ingress resource (no scheme).

Backend
(relevant files: _urls.tpl, ingressroute.yaml)

publicHosts.backendUrl ingressHosts.backend Runtime backendUrl Ingress host
(empty) (empty) https://backend<sep><host> backend<sep><host>
(empty) api.cluster.local https://api.cluster.local api.cluster.local
https://api.public.com (empty) https://api.public.com backend<sep><host>
https://api.public.com api.cluster.local https://api.public.com api.cluster.local

Website
(relevant files: _urls.tpl, ingressroute.yaml, keycloak-config-map.yaml)

The loculus.websiteUrl template is also used to derive the Keycloak backend-client redirectUris (and by extension the CORS webOrigins, since Keycloak derives them from redirect URIs). This ensures that after login, Keycloak redirects the user back to the website URL that the client actually uses and not the ingress hostname.

publicHosts.websiteUrl ingressHosts.website Runtime websiteUrl Ingress host Keycloak redirectUris
(empty) (empty) https://<host> <host> (+ www.<host>) https://<host>/*
(empty) frontend.cluster.local https://frontend.cluster.local frontend.cluster.local https://frontend.cluster.local/*
https://loculus.public.com (empty) https://loculus.public.com <host> (+ www.<host>) https://loculus.public.com/*
https://loculus.public.com frontend.cluster.local https://loculus.public.com frontend.cluster.local https://loculus.public.com/*

The http://localhost:3000/* redirect URI is always included in the keycloak template regardless of the configuration above.

Keycloak
(relevant files: _urls.tpl, ingressroute.yaml, keycloak-deployment.yaml, keycloak-config-map.yaml)

The loculus.keycloakUrl template feeds into KC_HOSTNAME_URL, KC_HOSTNAME_ADMIN_URL, and the realm frontendUrl. These control the hostname that Keycloak uses for its own external-facing URLs (issuer, token endpoint, etc.).

publicHosts.keycloakUrl ingressHosts.keycloak KC_HOSTNAME_URL / frontendUrl Ingress host
(empty) (empty) https://authentication<sep><host> authentication<sep><host>
(empty) auth.cluster.local https://auth.cluster.local auth.cluster.local
https://auth.public.com (empty) https://auth.public.com authentication<sep><host>
https://auth.public.com auth.cluster.local https://auth.public.com auth.cluster.local

LAPIS
(relevant files: _common-metadata.tpl, lapis-ingress.yaml)

publicHosts.lapisUrlTemplate ingressHosts.lapis Runtime lapisUrls Ingress host
(empty) (empty) https://lapis<sep><host>/%organism% lapis<sep><host>
(empty) lapis.cluster.local https://lapis.cluster.local/%organism% lapis.cluster.local
https://lapis.public.com/%organism% (empty) https://lapis.public.com/%organism% lapis<sep><host>
https://lapis.public.com/%organism% lapis.cluster.local https://lapis.public.com/%organism% lapis.cluster.local

S3 / MinIO
(relevant files: _urls.tpl, ingressroute.yaml. (only applies when runDevelopmentS3: true))

ingressHosts.minio External s3Url Internal s3UrlInternal Ingress host
(empty) https://s3<sep><host> http://loculus-minio-service:8084 s3<sep><host>
s3.cluster.local https://s3.cluster.local http://loculus-minio-service:8084 s3.cluster.local

S3/MinIO has no publicHosts equivalent; it uses ingressHosts.minio or s3.bucket.endpoint directly.

Docs
(relevant files: docs-preview.yaml. (only applies when previewDocs: true))

ingressHosts.docs Ingress host
(empty) docs<sep><host>
docs.cluster.local docs.cluster.local

Docs has no publicHosts equivalent and no URL template; it is purely ingress-driven.

Full example of proposed structure:

networking:
  publicHosts: # <-- moved and renamed from `public`
    websiteUrl: "https://loculus.host.org"
    backendUrl: "https://backend.loculus.host.org"
    keycloakUrl: "https://auth.loculus.host.org"
    lapisUrlTemplate: "https://lapis.loculus.host.org/%organism%"
  ingressHosts: # <-- newly introduced
    website: "loculus.cluster.local"
    backend: "api.cluster.local"
    keycloak: "authentication.cluster.local"
    lapis: "lapis.cluster.local"
    minio: "s3.cluster.local"
    docs: "docs-preview.cluster.local"
  subdomainSeparator: "." # <-- moved from root property to nested
  enforceHTTPS: true # <-- moved from root property to nested
  traefikVersion: "2" # <-- moved from root property to nested

networking is just a suggestion of course, if please feel free to suggest another key name if necessary.

all URL and host references in templates (such as _urls.tpl, ingressroute.yaml, lapis-ingress.yaml, etc.) have been changed to use the new .Values.networking structure

I figured to not move the keys host and localHost as i believe these are used too broadly throughout the chart, also outside of networking, or are used only in a development setting.

Please let me know your opinions on this, happy to make adjustments or to explore a different approach if this is not in line with your roadmap :)

🚀 Preview: Add preview label to enable

florianzwagemaker and others added 4 commits June 14, 2026 14:31
Copilot AI review requested due to automatic review settings June 17, 2026 10:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR restructures Helm chart configuration related to networking by moving URL/host/ingress-related values under a new .Values.networking object and updating templates/helpers to consume the new structure.

Changes:

  • Introduces .Values.networking with publicHosts, ingressHosts, subdomainSeparator, enforceHTTPS, and traefikVersion.
  • Updates multiple Helm templates/helpers to compute hosts/URLs from .Values.networking.* and optional ingress host overrides.
  • Updates deployment tooling (deploy.py) to set networking.publicHosts instead of the previous top-level public.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
kubernetes/loculus/values.yaml Moves enforceHTTPS, traefikVersion, and subdomainSeparator under networking.
kubernetes/loculus/values.schema.json Adds schema for networking (publicHosts/ingressHosts + related fields) and removes old top-level keys.
kubernetes/loculus/templates/lapis-ingress.yaml Uses networking.ingressHosts + networking.subdomainSeparator + networking.traefikVersion + networking.enforceHTTPS.
kubernetes/loculus/templates/keycloak-config-map.yaml Switches Keycloak host/redirect URIs to use networking.ingressHosts overrides.
kubernetes/loculus/templates/ingressroute.yaml Uses networking.* and introduces overridable website/backend/keycloak/minio ingress hosts.
kubernetes/loculus/templates/ingest-config.yaml Uses the shared loculus.backendUrl helper for server backend URL.
kubernetes/loculus/templates/ena-submission-config.yaml Uses the shared loculus.backendUrl helper for server backend URL.
kubernetes/loculus/templates/docs-preview.yaml Uses networking.ingressHosts.docs override for docs hostname.
kubernetes/loculus/templates/autoapprove-config.yaml Uses the shared loculus.backendUrl helper for server backend URL.
kubernetes/loculus/templates/_urls.tpl Updates URL helpers to read from networking.publicHosts and networking.ingressHosts.
kubernetes/loculus/templates/_common-metadata.tpl Updates runtime config generation to use networking.publicHosts and networking.ingressHosts.
deploy.py Updates Helm --set-json path from public to networking.publicHosts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kubernetes/loculus/templates/ingressroute.yaml Outdated
Comment thread kubernetes/loculus/templates/ingressroute.yaml Outdated
Comment thread kubernetes/loculus/templates/ingressroute.yaml Outdated
Comment thread kubernetes/loculus/templates/_urls.tpl Outdated
Comment thread kubernetes/loculus/values.schema.json Outdated
@corneliusroemer

Copy link
Copy Markdown
Contributor

@florianzwagemaker I've started looking at this and have a question regarding your deployment needs. Do you just need to escape the hardcoded prefixes and allow setting urls independently or do you genuinely need public hosts to be entirely different from ingress host? If it's just the former we could reduce complexity compared to the latter. Maybe a a bit more context about your intended deployment environment could help us understand requirements better and keep things as simple as possible.

Comment on lines +6 to +7
{{- else if $ingressHosts.backend -}}
{{- printf "https://%s" $ingressHosts.backend -}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PR description you write:

This means that the ingressHosts keys will only be used if a publicHosts key is set for the same service.

IIUC ingressHost overwrites are also used if publicHosts are empty here? Maybe I'm misunderstanding the line from the PR desc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct! that sentence in my original PR description was effectively the inverse of what is happening and my wording was rather confusing.

What is actually happening is that ingressHosts is only used in the URL templates if publicHosts is not set
In other words, a key for ingressHosts is only used as a fallback for a specific service if no publicHosts key is set.

I've updated the PR description to properly reflect this and added a per-service table of combinations of set keys in publicHosts and ingressHosts and the resulting runtime URLs and ingress routes, hopefully these examples make it easier to see what the result will be of various combinations of set keys / overrides

…tually contains the websiteUrl that end-users will interact with
@florianzwagemaker

Copy link
Copy Markdown
Contributor Author

First of all my apologies for the delayed response.

Do you just need to escape the hardcoded prefixes and allow setting urls independently or do you genuinely need public hosts to be entirely different from ingress host?

The primary goal for us was to escape the hardcoded prefixes. However, that would only serve the deployment goal that we have today.

I agree that it adds some initial schema layout which is not ideal, but i think this reduces the deployment complexity for admins in various cloud environments and I'd argue that separating these values is practically unavoidable in a longer term.

A client-facing URL and what a cluster routes on serve two very different purposes and tightly coupling these two would require loads of workarounds like custom proxy rewrites or DNS hacks just to make the chart work in more locked-down environments. Making these independently configurable makes the chart more agnostic to the network in which it will live.

As for our own direct deployment(s), there are some aspects that would be made significantly easier by separating ingress from client-facing urls. Two that directly come to mind are multi-instance deployments (e.g. multiple deployments that share one keycloak instance for authentication), and our hybrid (cloud & on-premise) environment that have to deal with internal firewalls. Both of which would benefit greatly from separated ingress and public-facing url configurations.

Most of the added template complexity comes from the resolution chain ensuring the automatic fallback to the computed defaults if the various keys are left empty. Which should keep things mostly straightforward for standard deployments while also accommodating more advanced use-cases/environments.

Comment thread kubernetes/loculus/templates/_urls.tpl Outdated
{{- printf "https://%s" $ingressHosts.website -}}
{{- else if eq $.Values.environment "server" -}}
{{- (printf "https://%s" $.Values.host) -}}
{{- (printf "https://%s" (.Values.host | default "")) -}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need those changes replacing $.Values.host with .Values.host - seems unnecessary for this PR

@florianzwagemaker florianzwagemaker Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch, going to .Values.host from $.Values.host is indeed not needed and only cosmetic.

I'll change this back throughout the modified files (this pattern is also used in _common_metadata.tpl for example)

When looking closer now though, most of the | default "" fallbacks can be safely removed now as well. These were initially added as guardrails when i was working on the the resolution chain, but the if/else branching already provides sufficient safety (both were added in the same commit here)

The one exception is the line in ingressroute.yml where the default fallback serves as a guardrail for the hasPrefix check.
https://github.com/florianzwagemaker/loculus/blob/e988e2317a3a1d79783b6f8fb94f12cb2fa86609/kubernetes/loculus/templates/ingressroute.yaml#L52

I'll clean both up in the next commit


updated in e988e23

- revert `$.Values.host` -> `.Values.hos` changes
- Remove redundant `| default ""` fallbacks in URL construction (except for ingressroute.yaml `hasPrefix` guard)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants