Use Gateway API rather than Ingress
Status: Accepted Date: 2026-08-21 Deciders: Smana (Platform Owner) Related Design: N/A — records a choice predating the design workflow Related: ADR-0009 — Cilium is the GatewayClass implementation this decision routes through
Context
Every request into this platform, public or private, goes through Gateway
API — GatewayClass, Gateway, HTTPRoute — never a Kubernetes
Ingress. There is no kind: Ingress manifest anywhere in this
repository, and Cilium’s own Ingress controller
(ingressController.enabled) is not turned on in
opentofu/aws/eks/init/helm_values/cilium.yaml; only gatewayAPI.enabled: true is set. Update (2026-08-30): the one literal ingress-nginx
reference this record used to cite — a subchart toggle set to enabled: false
in the OnCall HelmRelease — is gone along
with the rest of the Grafana OnCall estate, removed 2026-08-29
(ADR-0029). Zero literal ingress-nginx
references remain anywhere in this repository’s manifests.
ADR-0009 already decided that Cilium
replaces the VPC CNI, kube-proxy, and the NetworkPolicy engine, and named
“implements the Gateway API GatewayClass this platform’s Tailscale and
public ingress already depend on” as one of its reasons. That ADR’s
Option 2 (ingress-nginx) was rejected mainly on CNI-consolidation
grounds — running a fifth component just for NetworkPolicy. This record
exists to state the routing-model choice on its own terms and name the
cost that choice carries by itself, independent of the CNI decision:
Gateway API’s version is not free to move independently of Cilium’s.
Three resource roles carry the model
(Gateway API):
GatewayClass selects the controller, Gateway owns listeners,
hostnames, and TLS, and HTTPRoute owns routing rules and attaches to a
Gateway via parentRefs. The platform authors every Gateway under
infrastructure/base/gapi/ (the AWS-only public Gateway lives in
infrastructure/aws-0/gapi/ instead — see Implementation Notes); applications
own their HTTPRoute through
the App Crossplane claim. An Ingress has no equivalent split — one
object carries both concerns, disambiguated only by vendor-specific
annotations.
Decision Drivers
- Role separation. The platform should own listener/TLS/hostname
configuration; applications should own routing rules. Mixing both into
one
Ingressobject, disambiguated by annotations, does not offer that split. - One mechanism for both public and private ingress. The two
Tailscale-private Gateways (
platform-tailscale-general,platform-tailscale-admin) needloadBalancerClass: tailscaleon the Service Cilium creates for them — aCiliumGatewayClassConfigparametersRefon theGatewayClass. Reaching the same outcome throughIngresswould need a second mechanism, since Cilium’s own Ingress controller is not enabled here at all. - No new controller. ADR-0009 already
requires Cilium as the CNI and
CiliumNetworkPolicyengine; adopting Gateway API turns Cilium’s existinggatewayAPI.enabled: truesetting into the ingress mechanism too, rather than installing a separate controller alongside it. - Native ExternalDNS integration. ExternalDNS’s
gateway-httproutesource watchesHTTPRouteobjects directly (infrastructure/base/external-dns/helmrelease.yaml), so a new hostname needs no separate annotation convention.
Considered Options
Option 1: Gateway API with Cilium as the GatewayClass implementation
Two GatewayClasses back three Gateways. cilium is created
automatically once gatewayAPI.enabled: true is set in the Cilium Helm
values (no manifest for it in this repository); cilium-tailscale is
hand-authored in infrastructure/base/gapi/tailscale-gatewayclass.yaml
with a parametersRef to a CiliumGatewayClassConfig. platform-public
uses cilium; platform-tailscale-general and platform-tailscale-admin
use cilium-tailscale.
Pros:
- Platform-owned
Gateway, application-ownedHTTPRoute— no per-app vendor annotations to keep aligned with a shared object. - One CRD family covers both the internet-facing gateway and the two
Tailscale-private gateways; only the
GatewayClassdiffers. - Cilium already runs as the CNI and
NetworkPolicyengine (ADR-0009); turning ongatewayAPIadds no separate controller. - ExternalDNS’s
gateway-httproutesource watchesHTTPRoutenatively.
Cons:
- The Gateway API CRD version cannot move independently of the Cilium minor installed — see Consequences.
cilium-operatoronly detects new Gateway API CRDs at its own startup — see Consequences.- Smaller ecosystem than
Ingress: fewer worked examples, and some upstream charts still ship only aningress:values stanza with no Gateway API templates (the since-removed OnCall HelmRelease — ADR-0029 — disabled bothingressand a bundledingress-nginxsubchart it shipped with no alternative).
Option 2: ingress-nginx
The mature, widely deployed Ingress controller
ADR-0009 already considered and rejected
as its Option 2.
Pros (per ADR-0009’s Option 2):
- Fully community-supported, large install base.
Cons:
- Speaks
Ingress, notGatewayClass— does not satisfy this platform’s Gateway-API-only routing model without a shim (ADR-0009 Option 2). - No mechanism comparable to
CiliumGatewayClassConfig’sloadBalancerClass: tailscalefor the two private gateways; reaching them throughIngresswould need a second, ingress-nginx-specific path, not the one CRD family Option 1 uses for all three Gateways.
Option 3: AWS Load Balancer Controller Ingress
The AWS Load Balancer Controller does run in this repository
(infrastructure/base/aws-load-balancer-controller/helmrelease.yaml,
chart aws-load-balancer-controller), but not as an Ingress
controller: no manifest under infrastructure/, tooling/,
observability/, security/, or apps/ sets ingressClassName or an
alb.ingress.kubernetes.io/* annotation, and no kind: Ingress object
exists anywhere in the repository. Its actual job is provisioning the AWS
NLB behind the one internet-facing Gateway, platform-public, whose
service.beta.kubernetes.io/aws-load-balancer-* annotations sit under
Gateway.spec.infrastructure.annotations
(infrastructure/aws-0/gapi/platform-public-gateway.yaml) rather than on
an Ingress object.
Pros:
- Deep AWS integration for the load balancer itself (NLB target type,
scheme, naming) — already used today, just attached to a
Gatewayinstead of anIngress.
Cons:
- Running it as an
Ingresscontroller reintroduces the same role-separation loss as Option 2: listener and routing configuration merge into one object, disambiguated byalb.ingress.kubernetes.io/*annotations instead of Cilium/Tailscale ones. - Still no equivalent to
CiliumGatewayClassConfig’sloadBalancerClass: tailscalefor the two private gateways — AWS LBC provisions AWS load balancers, not Tailscale-backed Services, so the Tailscale gateways would still need a wholly separate mechanism.
Decision Outcome
Chosen option: “Option 1 — Gateway API with Cilium as the GatewayClass implementation”
Rationale: Cilium is already required for the CNI, kube-proxy
replacement, and NetworkPolicy engine
(ADR-0009), and it already ships a
GatewayClass implementation gated by one Helm value. Adopting Gateway
API is therefore not a new component, it is switching on a capability
Cilium already has, while Option 2 and Option 3 would each add one.
Gateway API’s role split also lets CiliumGatewayClassConfig carry the
loadBalancerClass: tailscale setting the two private Gateways depend
on, through the same GatewayClass/Gateway/HTTPRoute triple used for
the public Gateway — neither ingress-nginx nor AWS Load Balancer
Controller’s Ingress mode has a comparable path to that outcome without
a second, ingress-specific mechanism bolted on for the private case. The
cost is real and is not shared with the CNI decision: this platform’s
Gateway API CRD version is now pinned to what the installed Cilium minor
can run, not chosen freely.
Consequences
Positive
- The platform owns every
Gateway(infrastructure/base/gapi/, plus the AWS-onlyplatform-publicGateway ininfrastructure/aws-0/gapi/); applications own theirHTTPRoutethrough theAppclaim — no vendor annotations on a shared object to keep in sync. - One CRD family serves all three Gateways:
platform-public(internet facing,cilium),platform-tailscale-generalandplatform-tailscale-admin(Tailscale-private,cilium-tailscale). - No separate ingress controller to operate — Cilium implements
GatewayClassas part of the component ADR-0009 already requires. - ExternalDNS’s
gateway-httproutesource (infrastructure/base/external-dns/helmrelease.yaml) watchesHTTPRoutedirectly and, withpolicy: sync, removes Route53 records when a route is deleted — no manual DNS step.
Negative
- Version lockstep with Cilium. Cilium is the
GatewayClassimplementation, so the Gateway API CRD version cannot move independently of it: Cilium ≤1.19.4 crashes on Gateway API ≥v1.5.0 (TLSRoute-v1, cilium#45139, fixed in 1.19.5).gateway_api_versionlives besidecilium_versioninopentofu/config.tm.hcl’sglobalsand is passed to both clouds'configurestacks by-var, so the two clusters cannot diverge. It must equal the tagflux/sources/gitrepo-gateway-api.yamlpins Flux’sGitRepositoryto — both currentlyv1.6.2— while the installedcilium_version(alsoopentofu/config.tm.hcl, currently1.20.2) must stay at or above the 1.19.5 floor.- Mitigation: those two pins are checked against each other by the
gateway-api-versionclaim in.doc-claims.yaml(./scripts/ci/validate-doc-claims.sh), so a bump that misses one fails CI. The Cilium floor is still a manual check on every upgrade of either component.
- Mitigation: those two pins are checked against each other by the
- CRDs must exist before
cilium-operatorstarts. It probes for the Gateway API CRDs exactly once, at startup, and permanently disables its Gateway API controller for the process lifetime if any is missing — no crash, no alert. EveryGatewayClassthen sits atAccepted=Unknown, everyGatewaystays unprogrammed, everyHTTPRoutegets nostatus.parents, and anyAppclaim that owns a route is stuckREADY=False(infrastructure/AGENTS.md, “Gateways stuckWaiting for controller”). This already happened once:backendtlspolicieswas absent from the hand-written CRD list and broke Gateway API on the 2026-08-19 rebuild. The list, and the comment that recorded the incident, are both gone — seeopentofu/shared/modules/gateway-api-crds/main.tf.- Mitigation:
kubectl rollout restart -n kube-system deployment/cilium-operatorreruns the probe immediately. Durably, that enumeration was retired — both clouds install the whole experimental-channel bundle viaopentofu/shared/modules/gateway-api-crds, so the installed set cannot be a subset of what Cilium probes for.
- Mitigation:
- Smaller ecosystem than
Ingress. Fewer worked examples exist for Gateway API than forIngress, and some upstream charts still ship only aningress:values stanza with no Gateway API template — the OnCall HelmRelease (removed 2026-08-29 with the estate, ADR-0029) turned off both aningress:stanza and a bundledingress-nginxsubchart the chart offered as its only routing option.- Mitigation: none beyond authoring a standalone
HTTPRoutenext to such a chart, astooling/base/homepage/httproute.yamlalready does.
- Mitigation: none beyond authoring a standalone
Neutral
- Moving from
Ingressto Gateway API did not remove the AWS Load Balancer Controller from the platform.platform-publicstill gets its internet-facing NLB fromaws-load-balancer-controller(infrastructure/base/aws-load-balancer-controller/helmrelease.yaml); only the object carrying the AWS annotations changed, from anIngresstoGateway.spec.infrastructure.annotations.
Implementation Notes
infrastructure/base/gapi/ holds the GatewayClass/CiliumGatewayClassConfig
plumbing and the two Tailscale Gateway manifests:
tailscale-gatewayclass.yaml (the cilium-tailscale GatewayClass),
tailscale-gatewayclass-config.yaml (the CiliumGatewayClassConfig
setting service.type: LoadBalancer, loadBalancerClass: tailscale, and
JSON Envoy access logs via spec.telemetry.accessLogs), and the two
Tailscale Gateway manifests, which restrict allowedRoutes to a namespace
allowlist that has to be kept in sync by hand.
Update (2026-08-30): the third Gateway, platform-public-gateway.yaml,
moved to infrastructure/aws-0/gapi/ on 2026-08-27 — it carries
service.beta.kubernetes.io/aws-load-balancer-* annotations and a
${domain_name}-built hostname that a GCP cluster’s vars ConfigMap does not
define, so leaving it in base/ risked both clusters referencing it and GCP
silently rendering an empty hostname. It still restricts allowedRoutes to
the runlore namespace and carries the AWS NLB annotations, unchanged from
what this record originally described.
Cilium’s Gateway API support is turned on entirely through Helm values in
opentofu/aws/eks/init/helm_values/cilium.yaml: gatewayAPI.enabled: true
and envoy.enabled: true (the L7 proxy). ingressController is not set,
so Cilium’s own Ingress support stays off.
The Gateway API CRDs themselves are not chart-managed: flux/sources/gitrepo-gateway-api.yaml
pins a GitRepository to kubernetes-sigs/gateway-api tag v1.6.2, and
opentofu/shared/modules/gateway-api-crds installs the same-versioned
experimental-channel bundle — every CRD in the release, backendtlspolicies and
listenersets included — before Cilium’s Stage 2 install, so
cilium-operator’s startup probe finds them. Both clouds use that one module,
so the two clusters cannot present Cilium with different Gateway API surfaces.
References
- Gateway API — the resource model, the three Gateways, TLS, and ExternalDNS integration this ADR summarizes
- ADR-0009 — Cilium as the CNI and
GatewayClassimplementation this decision depends on infrastructure/AGENTS.md— “Gateways stuckWaiting for controller” troubleshooting entryopentofu/shared/modules/gateway-api-crds— the bundle install both clouds use, and thebackendtlspoliciesincident that motivated itopentofu/aws/eks/configure/variables.tf—gateway_api_versionflux/sources/gitrepo-gateway-api.yaml— the FluxGitRepositorypin that must matchgateway_api_versionopentofu/aws/eks/init/helm_values/cilium.yaml—gatewayAPI.enabled,envoy.enabledinfrastructure/base/gapi/— everyGatewayClass,CiliumGatewayClassConfig, and the two TailscaleGatewaymanifestsinfrastructure/aws-0/gapi/—platform-public-gateway.yaml, moved out ofbase/2026-08-27 for carrying AWS-only annotationsinfrastructure/base/external-dns/helmrelease.yaml— thegateway-httproutesource andpolicy: sync- cilium#45139 — the Cilium ≤1.19.4 crash on Gateway API ≥v1.5.0 behind the version lockstep