Skip to content

From ingress-nginx to Gateway API (with Cilium)

Published: at 20:05 (9 min read)

The ingress-nginx ingress controller served many kubernetes installations for a good reason and the sheer amount of annotations and configmap settings made ingress-nginx a very complete implementation, but it should not come as a surprise to anyone that the project decided to deprecate the implementation. In fact, the project announced about the issues a couple of times throughout the years. Back in June 2022(!), one of the maintainers announced about code freeze and stabilisation:

And with new features, there are new bugs. One of the feelings we’ve got since we started maintaining Ingress is that usually, one or two users need a new feature, and they implement it (thanks for the PR!!), but when a bug happens, no one steps forward to fix it.

This lack of support becomes a burden to Ingress NGINX maintainers: we now have to split our time between issues, bug fixing, new feature reviews, and the bugs that may arise from this feature. We do this in our spare time, and it’s becoming hard for us to keep this pace.

There is this feeling that we probably support too many features. Some (a lot) of them are external to NGINX, and this turns out to need a complex build process, with modules that are sometimes not supported anymore and our slowing down the core evolution.

Earlier this year the project announced that it was the end of the road for ingress-nginx:

Once a stable release of InGate is available we will officially put the project in maintenance mode.

InGate never happened, as the community did not show up. As one of the many users of ingress-nginx, I’m grateful that Marco, James and Ricardo kept going on for so long.

Choosing a Gateway API implementation

There are many great and interesting implementations of Gateway API, many of them (but not all) based on envoy. Ingress objects were perhaps released a bit ahead of their time?

[https://kubernetes.io/blog/2020/08/26/kubernetes-release-1.19-accentuate-the-paw-sitive/#ingress-graduates-to-general-availability]:

Ingress graduates to General Availability

In terms of moving the Ingress API towards GA, the API itself has been available in beta for so long that it has attained de facto GA status through usage and adoption (both by users and by load balancer / ingress controller providers). Abandoning it without a full replacement is not a viable approach. It is clearly a useful API and captures a non-trivial set of use cases. At this point, it seems more prudent to declare the current API as something the community will support as a V1, codifying its status, while working on either a V2 Ingress API or an entirely different API with a superset of features.

Back in 2015, with Kubernetes v1.1.1 I see the Ingress being mentioned in the user guide for the first time, and the first commits into the ingress-nginx project looks like they happened in the beginning of 2016, be well before common usage patterns defining a service mesh (Istio project started in 2017). As such, there were not that many rules to conform with and the various implementations could rather freely extend in a way that suited the implementation.

Fast forward a couple of years. Sig-network, responsible for Gateway API, have worked hard on conformance and defined a set of features that each implementation should support, in order to be conformant against the specification. Portability is one of the design goals. Among the things that separates Gateway API from Ingress Controllers, is the role-oriented design.

There are many implementations of Gateway API, but as I’m already using the Cilium and it’s integrated implementation of Gateway API since more than two years (https://medium.com/@norlin.t/installing-cilium-service-mesh-with-external-kubernetes-control-plane-illumos-e5517253e011?source=friends_link&sk=c2f0e8dc4ec5cc2d17aa66dfc36b62a5 ), it feels like a natural and undramatic choice for me.

Endless possibilities with Cilium

In my design, I use an external control plane, running illumos (the continuation of OpenSolaris, the open-source release of Solaris operating system, which got killed by Oracle). The data plane on the other hand is running in another network segment than the control plane (which in turn is segmented into a network for etcd, and another for scheduler and controller-manager, with kube-apiserver as the frontend). I use BGP in my network to a fairly large extent and in my cluster I do route both PodCIDR and ClusterIP, however it is not announced/routable other than to the control plane. This is one of the many things possible, thanks to the extensive functionality of Cilium.

As a side note, IMHO I would like to see a kube-admission-controller in Kubernetes. A controller living as an extension in the control plane, instead of the current architecture with the dynamic admission controller running in the data plane and creates a bit of an anti pattern. Currently, with a admission controller like Kyverno, the kube-apiserver needs ask the worker node(!) in order to validate if a call should be accepted or not.

In my home network, I have control over several subnets and I usually let each exposed resource connect to its own gateway with a unique IP (and then I announce it with BGP and let external-dns talk to my internal DNS, with a DNS-01 challenge over RFC-2136 method, to let the clients resolve the domain name). This is something that, at least with IPv4, will not be feasible with an Internet exposed cluster, as public IP has become a scarce resource.

Setting up Gateway API

Exposing a NodePort

With ingress-nginx, a common pattern is to expose the cluster with an external load balancer (such as HAProxy, NGINX, Traefik) serving a frontend on port TCP/443 and TCP/80 and then talking to the ingress-nginx on a predefined NodePort.

The same pattern is applicable with the Cilium Gateway API (well, sort of) — a shared gateway controller with cross-namespace routing.

Become a member During installation/configuration of Cilium, there’s an option to allow the envoy agent to listen on hostNetwork. Helm values as follows:

gatewayAPI:
  enabled: true
  hostNetwork:
    enabled: true

As the official documentation states:

Once enabled, the host network port for a Gateway can be specified via spec.listeners.port. The port must be unique per Gateway resource and you should choose a port number higher than 1023 (see Bind to privileged port).

Within one gateway, the listeners must be declared unique with hostname, port and protocol.

Pre v1.15 Gateway API specification

In the gateway, you can specify one port that the listener will listen for, and in the spec allows for listen on a NodePort (which defaults to a range between 30000–32767) and with that you can refer the your external load balancer.

In this example, we are using Cert Manager to issue a certificate to the Gateway resource (with Ingress Controller, the certificate was instead issued to the Ingress object). With the allowedRoutes we control which namespaces that are allowed to use the listener. In this case we do not restrict any namespace.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  name: gateway
  namespace: gateway
spec:
  gatewayClassName: cilium
  listeners:
  - allowedRoutes:
      namespaces:
        from: All
    hostname: gateway.kubernaut.eu
    name: https
    port: 30011
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: gateway-tls-secret
      mode: Terminate
  - allowedRoutes:
      namespaces:
        from: All
    hostname: gateway.kubernaut.eu
    name: http
    port: 30012
    protocol: HTTP

Then in the namespace where traffic should reach, we create a HTTPRoute to point to the Service. In this example we reach the grafana service by browsing to gateway.kubernaut.eu/grafana. A requestDirect makes sure that access to the path /grafana on port 80 will receive a HTTP error 302 and redirect to the same path at port 443.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: grafana
  namespace: grafana
spec:
  hostnames:
  - gateway.kubernaut.eu
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: gateway
    namespace: gateway
  rules:
  - filters:
    - requestRedirect:
        port: 443
        scheme: https
        statusCode: 302
      type: RequestRedirect
    matches:
    - path:
        type: PathPrefix
        value: /grafana
  - backendRefs:
    - group: ""
      kind: Service
      name: grafana
      port: 80
      weight: 1
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          replacePrefixMatch: /
          type: ReplacePrefixMatch
    matches:
    - path:
        type: PathPrefix
        value: /grafana

Cert Manager (pre v1.15 Gateway API)

Cert manager needs to be deployed with the following helm values in order to observe HTTPRoutes

config:
  apiVersion: controller.config.cert-manager.io/v1alpha1
  enableGatewayAPI: true
  kind: ControllerConfiguration

Then, the ClusterIssuer (or Issuer) that handles ACME validation, needs to be updated to reflect the solver. In this example I show two solvers, but in most cases there would only be a single solver. As you can see, the DNS-01 remains unaffected:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: some-email-here@please
    privateKeySecretRef:
      name: letsencrypt-prod
    server: https://acme-v02.api.letsencrypt.org/directory
    solvers:
    - http01:
        gatewayHTTPRoute:
          parentRefs:
          - group: gateway.networking.k8s.io
            kind: Gateway
            name: gateway
            namespace: gateway
      selector:
        dnsNames:
        - gateway.kubernaut.eu
    - dns01:
        cloudflare:
          apiTokenSecretRef:
            key: api-token
            name: cloudflare-api-token-secret
      selector:
        dnsNames:
        - www.example.com

ListenerSets (Gateway API v1.15?)

In the case of shared gateways, current model has some limitation in that the gateway admin needs to be in charge of updating the listeners (and certificates), but this is about to change.

GEP-1713 describes a feature called ListenerSets, supposedly to become stable in v1.15 during early 2026. With ListenerSet, listeners instead can be defined in the affected namespace (much like the Ingress objects used to be).

It also enables Cert Manager to issue the certificates on a per namespace basis instead and the feature is planned to be implemented in the upcoming v1.20 release of Cert Manager. This issue is tracking the implementation.

With current specification, the maximum amount of listeners per gateway is 64, but with ListenerSets instead this limit is supposed to be 64 listeners per ListenerSet (so with some ingenuity and creativity the limit of 64 no longer applies).

Sample definition from the GEP-1713:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: parent-gateway
spec:
  gatewayClassName: example
  allowedListeners:
    namespaces:
      from: Same
  listeners:
  - name: foo2
    hostname: foo.com
    protocol: HTTP
    port: 80
  - name: foo3
    hostname: foo1.com
    protocol: HTTP
    port: 80
---
apiVersion: gateway.networking.x-k8s.io/v1alpha1
kind: XListenerSet
metadata:
  name: first-workload-listeners
spec:
  parentRef:
    name: parent-gateway
    kind: Gateway
    group: gateway.networking.k8s.io
  listeners:
  - name: foo
    hostname: first.foo.com
    protocol: HTTP
    port: 80

As you can se, the ListenerSet refers to a specified listener in a gateway. There’s lots of activity in the projects git repository, both with commits and design proposals, and this is a good opportunity (if you have the time and knowledge) to help the community. Just look at their GitHub project page.

Exciting times ahead!