Gateway API

Setup instructions

Stated here just for reference, so don’t execute any of these by yourselves. Besides, so far you haven’t been granted the permissions to do so anyways … :-)

Source: [Kubernetes Gateway API][k8s-gateway-api] and [Kubernetes Traefik][k8s-traefik]

Traefik supports the Kubernetes Gateway API, meaning it can install the Gateway API’s Custom Resource Definitions. As we are running Traefik for some of the exercises, we have already installed the Gateway API CRDs.

To install the Gateway API using Traefik, run this:

helm upgrade --install traefik oci://ghcr.io/traefik/helm/traefik \
  --namespace traefik --create-namespace --version 38.0.2 \
  --set ingressClass.enabled=false \
  --set experimental.kubernetesGateway.enabled=true \
  --set gateway.enabled=false \
  --set providers.kubernetesCRD.ingressClass=traefik \
  --set providers.kubernetesIngress.enabled=false \
  --set providers.kubernetesGateway.enabled=true \
  --set ports.web.redirections.entryPoint.to=websecure \
  --set ports.web.redirections.entryPoint.scheme=https \
  --set ports.web.redirections.entryPoint.permanent=true

or, to install the Gateway API CRDs yourself, run this:

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml

Note, that traefik can install both the Ingress and Gateway resources, as well as extra CRD’s such as the supporting Middleware CRD which can be used by both at the same time.

For our exercises we now start with Gateway API and Traefik , an Open Source Ingress Controller that is a safe and popular choice when you need a simple solution. It is maintained and well-integrated by the Kubernetes project itself. It has already been set up in our test cluster beforehand (via helm, by the way, see Setup instructions above) so we can jump right at making use of it.

As the name implies, this solution utilizes Traefik, an open-source Application Proxy for publishing services, with the means to easily configure it to our purposes. Under the hood things are not so simple, of course, and you can view the various components via

kubectl get all --all-namespaces -l app.kubernetes.io/name=traefik

(note the --all-namespaces), e.g.:

NAMESPACE   NAME                           READY   STATUS    RESTARTS   AGE
traefik     pod/traefik-754d89dc55-6pcx2   1/1     Running   0          7h16m

NAMESPACE   NAME                      TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
traefik     service/traefik           LoadBalancer   10.0.190.105   <ingress_ip_traefik>   80:30732/TCP,443:30445/TCP   7h16m
traefik     service/traefik-metrics   ClusterIP      10.0.153.12    <none>          9100/TCP                     7h16m

NAMESPACE   NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
traefik     deployment.apps/traefik   1/1     1            1           7h16m

NAMESPACE   NAME                                 DESIRED   CURRENT   READY   AGE
traefik     replicaset.apps/traefik-754d89dc55   1         1         1       7h16m

But for now we will just use what Traefik provides without diving into too much detail on how this will be achieved.

Note

As we will make use of the Traefik Ingress IP quite a lot, best export it to your environment: export INGRESS_IP_TRAEFIK=<your_ingress_ip>

export INGRESS_IP_TRAEFIK=$(kubectl get service --namespace traefik traefik -o jsonpath='{.status.loadBalancer.ingress[].ip}')

should do the trick, which you can then verify via

echo $INGRESS_IP_TRAEFIK

This variable will not persist over a logout nor will it spread to other separate sessions, so remember to set it again whenever you (re)connect to your user VM.

Namespace

With Traefik still being a pre-defined cluster-wide central solution, we again need to take care to not step on each others’ toes in the following exercises. For that we will prefix some parts of the following resources with a user-specific namespace.

Note

As we will use this personal namespace several times as a resource prefix, make it available now for easy consumption via a variable:

export NAMESPACE=$(kubectl config view --minify --output 'jsonpath={..namespace}'); echo $NAMESPACE

This variable will not persist over a logout nor will it spread to other separate sessions, so remember to set it again whenever you (re)connect to your user VM.

Exercise - Recreate our sample application

Remember how to use the command line to create a Deployment called sampleapp with the image novatec/technologyconsulting-hello-container:v0.1?

Solution

Just execute

kubectl create deployment sampleapp --image novatec/technologyconsulting-hello-container:v0.1

You still have that running, i.e. Kubernetes told you that Error from server (AlreadyExists): deployments.apps “sampleapp” already exists ? Doesn’t matter, let’s just keep using it then.

And now we will expose that Deployment using a LoadBalancer Service, in effect similarly to what we have covered in Services but with a different command:

kubectl expose deployment sampleapp --type LoadBalancer --port 8080

Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP

For good measure, let’s confirm that we can access it just fine. Do you know how?

Solution

Beware, it might take some time to allocate the external LoadBalancer IP, but generally:

kubectl get services sampleapp followed by curl --silent <your_lb_ip>:8080/hello; echo

or simply

SAMPLEAPP_IP=$(kubectl get services sampleapp -o jsonpath='{.status.loadBalancer.ingress[].ip}'); curl --silent $SAMPLEAPP_IP:8080/hello; echo

yielding e.g.

Hello World (from sampleapp-846cd66cfb-wxbql) to somebody

So far so good:

graph LR;
    I{Internet} -->|LoadBalancer:8080| S
    subgraph Kubernetes
        subgraph your Namespace
            S(sampleapp)
        end
    end

Exercise - Expose our sample application using Gateway API

Now create sampleapp-gateway.yaml by executing the following (yes, execute it all at once), utilizing your personal namespace as a host component, and our Traefik Ingress IP by way of nip.io wildcard DNS as a suffix:

cat <<.EOF > sampleapp-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sampleapp
spec:
  gatewayClassName: traefik
  listeners:
    - name: sampleapp
      hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: sampleapp
spec:
  parentRefs:
    - name: sampleapp
  hostnames:
    - hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: sampleapp
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /
.EOF

Create the Gateway API resources by running the following command:

kubectl apply -f sampleapp-gateway.yaml

Take note that this yaml includes two separate resources divided by a YAML document delimiter, the Gateway and a HTTPRoute that attaches to the Gateway via its parentRefs.

Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-GATEWAY

Verify the IP address is set:

kubectl get gateway,httproute sampleapp

NAME                                          CLASS     ADDRESS                PROGRAMMED   AGE
gateway.gateway.networking.k8s.io/sampleapp   traefik   <ingress_ip_traefik>   True         2m26s

NAME                                            HOSTNAMES                                                AGE
httproute.gateway.networking.k8s.io/sampleapp   ["hello.<your_namespace>.<ingress_ip_traefik>.nip.io"]   2m26s
Note

This can take a couple of minutes.

Conceptually this now really similar to what did in Services , we could visualize the access path like this:

graph LR;
    A{Internet} -->|LoadBalancer:80| G
    subgraph Kubernetes
        subgraph your Namespace
            G(Gateway)
            H(HTTPRoute)
            S(sampleapp)
            G -->|Listener:HTTP| H
            H -->|ClusterIP:8080| S
        end
    end

Then verify we can access our sampleapp through Gateway API:

curl --verbose http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo

* Host hello.<your_namespace>.<ingress_ip_traefik>.nip.io:80 was resolved.
* IPv6: (none)
* IPv4: <ingress_ip_traefik>
*   Trying <ingress_ip_traefik>:80...
* Connected to hello.<your_namespace>.<ingress_ip_traefik>.nip.io (<ingress_ip_traefik>) port 80
> GET /hello HTTP/1.1
> Host: hello.<your_namespace>.<ingress_ip_traefik>.nip.io
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 57
< Content-Type: text/plain;charset=UTF-8
< Date: Fri, 13 Feb 2026 14:48:28 GMT
<
* Connection #0 to host hello.<your_namespace>.<ingress_ip_traefik>.nip.io left intact
Hello World (from sampleapp-846cd66cfb-fgfq2) to somebody

OK, so this works. Now let’s not expose our sampleapp via a LoadBalancer Service anymore, but restrict access to ClusterIP, i.e. disallow direct access from outside of our cluster:

kubectl delete services sampleapp; kubectl expose deployment sampleapp --type ClusterIP --port 8080

Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SERVICE

And confirm we can still access it through Gateway API:

curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo

Hello World (from sampleapp-846cd66cfb-fgfq2) to somebody

Well, it still works: via Gateway API our request gets redirected cluster-internally to the ClusterIP of our Service, and then to the sampleapp pod. But how do we benefit from this? Not really so far with such a simple application, and with only a single application we didn’t even save on external IP addresses required. But let’s dive into more detail.

Exercise - Extend our sample application

Create sampleapp-subpath.yaml by executing the following (i.e. do everything we have done for our sampleapp again, but now a bit differently, and from YAML and not directly via command line):

cat <<.EOF > sampleapp-subpath.yaml
apiVersion: v1
kind: Service
metadata:
  name: sampleapp-subpath
spec:
  type: ClusterIP
  ports:
    - port: 8080
  selector:
    app: sampleapp-subpath
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sampleapp-subpath
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sampleapp-subpath
  template:
    metadata:
      name: sampleapp-subpath
      labels:
        app: sampleapp-subpath
    spec:
      containers:
        - name: sampleapp-subpath
          env:
            - name: PROPERTY
              value: everyone via Gateway API from a subpath
          image: novatec/technologyconsulting-hello-container:v0.1
      restartPolicy: Always
.EOF

Create these resources by running the following command:

kubectl apply -f sampleapp-subpath.yaml

Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SUBPATH

What would this application serve, and how could you access it right now?

Solution

We used this PROPERTY in the Docker Container exercises : so when accessing /hello it would reply with

Hello World (from sampleapp-subpath-<ReplicaSetId>-<PodId>) to everyone via Gateway API from a subpath

Unfortunately, right now we cannot access that application from outside of the cluster at all as it is only exposed via a ClusterIP. So please go ahead with further instructions!

Now extend our sampleapp-gateway.yaml as follows:

    - backendRefs:
        - name: sampleapp-subpath
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /subpath
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
Solution

It should then look like

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sampleapp
spec:
  gatewayClassName: traefik
  listeners:
    - name: sampleapp
      hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: sampleapp
spec:
  parentRefs:
    - name: sampleapp
  hostnames:
    - hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: sampleapp
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /
    - backendRefs:
        - name: sampleapp-subpath
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /subpath
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /

Apply the change:

kubectl apply -f sampleapp-gateway.yaml

Milestone: K8S/INGRESS/GATEWAYAPI-SAMPLEAPP-SUBPATH-GATEWAY

Verify that the HTTPRoute is configured as intended:

kubectl describe httproute sampleapp

Name:         sampleapp
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>
API Version:  gateway.networking.k8s.io/v1
Kind:         HTTPRoute
[...]
Spec:
  Hostnames:
    hello.<your_namespace>.<ingress_ip_traefik>.nip.io
  Parent Refs:
    Group:  gateway.networking.k8s.io
    Kind:   Gateway
    Name:   sampleapp
  Rules:
    Backend Refs:
      Group:
      Kind:    Service
      Name:    sampleapp
      Port:    8080
      Weight:  1
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /
    Backend Refs:
      Group:
      Kind:    Service
      Name:    sampleapp-subpath
      Port:    8080
      Weight:  1
    Filters:
      Type:  URLRewrite
      URL Rewrite:
        Path:
          Replace Prefix Match:  /
          Type:                  ReplacePrefixMatch
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /subpath
[...]

And then access our applications like this:

curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echo

which should yield

Hello World (from sampleapp-846cd66cfb-fgfq2) to somebody
Hello World (from sampleapp-subpath-577ddd78df-b66fs) to everyone via Gateway API from a subpath

In other words, the most specific match succeeds, and we have integrated two slightly different applications, each served by a separate microservice, into a single serving domain like this:

hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> /        sampleapp:8080
                                                                                 /subpath sampleapp-subpath:8080
graph LR;
    A{Internet} -->|LoadBalancer:80| G
    subgraph Kubernetes
        subgraph your Namespace
            G(Gateway)
            H(HTTPRoute)
            S(sampleapp)
            U(sampleapp-subpath)
            G -->|Listener:HTTP| H
            H -->|/<br />ClusterIP:8080| S
            H -->|/subpath<br />ClusterIP:8080| U
        end
    end

Of course, with our sampleapp this doesn’t yet make much sense. But think of different microservices each serving a specific version of an API, and having them all accessible at /api/v1/, /api/v2/, /api/v3/ … Or generally think of any multi-microservice-driven application that needs to comply with Same-origin policy , and you notice you cannot implement this via simple LoadBalancer Service definitions.

So, let’s continue with this and roll out a new version of our sampleapp.

Exercise - Gradual rollout a new version of our sampleapp

In a production scenario, we do not want to roll out a new version of an application straight away in case any issues that were not detected during development and testing arise. Therefore we want to gradually deploy our new version in a controlled manner.

  1. Manually target the new version via a request header (canary rollout).
  2. Weighted targeting of the new version via traffic splitting.
  3. Fully target the new version (weight 100%).

Let’s continue with these steps.

Exercise - Canary rollout

Create sampleapp-v2.yaml by executing the following:

cat <<.EOF > sampleapp-v2.yaml
apiVersion: v1
kind: Service
metadata:
  name: sampleapp-v2
spec:
  type: ClusterIP
  ports:
    - port: 8080
  selector:
    app: sampleapp-v2
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sampleapp-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sampleapp-v2
  template:
    metadata:
      name: sampleapp-v2
      labels:
        app: sampleapp-v2
    spec:
      containers:
        - name: sampleapp-v2
          env:
            - name: PROPERTY
              value: everyone via Gateway API from v2
          image: novatec/technologyconsulting-hello-container:v0.1
      restartPolicy: Always
.EOF

Create these resources by running the following command:

kubectl apply -f sampleapp-v2.yaml

To be able to access the new version extend our sampleapp-gateway.yaml as follows:

    - backendRefs:
        - name: sampleapp-v2
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /
        - headers:
          - name: version
            value: v2
Solution

It should then look like

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sampleapp
spec:
  gatewayClassName: traefik
  listeners:
    - name: sampleapp
      hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: sampleapp
spec:
  parentRefs:
    - name: sampleapp
  hostnames:
    - hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: sampleapp
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /
    - backendRefs:
        - name: sampleapp-v2
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /
        - headers:
          - name: version
            value: v2
    - backendRefs:
        - name: sampleapp-subpath
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /subpath
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /

Apply the change:

kubectl apply -f sampleapp-gateway.yaml

The sampleapp and sampleapp-subpath are still accessible as before. To access the sampleapp-v2, a header version: v2 is required. Access the three applications like this:

curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl -H "version: v2" http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo
curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echo

which should yield

Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpath

We have integrated a new sampleapp-v2 like this:

hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> /                          sampleapp:8080
                                                                                 /        header=version:v2 sampleapp-v2:8080
                                                                                 /subpath                   sampleapp-subpath:8080
graph LR;
    A{Internet} -->|LoadBalancer:80| G
    subgraph Kubernetes
        subgraph your Namespace
            G(Gateway)
            H(HTTPRoute)
            S(sampleapp)
            V(sampleapp-v2)
            U(sampleapp-subpath)
            G -->|Listener:HTTP| H
            H -->|/<br />ClusterIP:8080| S
            H -->|/<br />header=version:v2<br />ClusterIP:8080| V
            H -->|/subpath<br />ClusterIP:8080| U
        end
    end

Exercise - Blue-green traffic rollout

Adapt our sampleapp-gateway.yaml as follows:

    - backendRefs:
        - name: sampleapp
          port: 8080
          weight: 50
        - name: sampleapp-v2
          port: 8080
          weight: 50
      matches:
        - path:
            type: PathPrefix
            value: /
Solution

It should then look like this:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sampleapp
spec:
  gatewayClassName: traefik
  listeners:
    - name: sampleapp
      hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: sampleapp
spec:
  parentRefs:
    - name: sampleapp
  hostnames:
    - hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: sampleapp
          port: 8080
          weight: 50
        - name: sampleapp-v2
          port: 8080
          weight: 50
      matches:
        - path:
            type: PathPrefix
            value: /
    - backendRefs:
        - name: sampleapp-subpath
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /subpath
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /

Apply the change:

kubectl apply -f sampleapp-gateway.yaml

The sampleapp and sampleapp-subpath are still accessible, just as before. To access the sampleapp-v2 a header version: v2 is required. Access the three applications like this:

for i in {1..10}; do curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/hello; echo; done

curl http://hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/subpath/hello; echo

which should yield

Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-846cd66cfb-tvtlx) to somebody
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
...
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpath

Users no longer need to include a specific header (for example, when using curl) to access the new sampleapp version. Traffic is now distributed automatically based on the configured weight settings.

hello.<your_namespace>.<ingress_ip_traefik>.nip.io -> <ingress_ip_traefik>:80 -> /        weight=50 sampleapp:8080
                                                                                 /        weight=50 sampleapp-v2:8080
                                                                                 /subpath           sampleapp-subpath:8080
graph LR;
    A{Internet} -->|LoadBalancer:80| G
    subgraph Kubernetes
        subgraph your Namespace
            G(Gateway)
            H(HTTPRoute)
            S(sampleapp)
            V(sampleapp-v2)
            U(sampleapp-subpath)
            G -->|Listener:HTTP| H
            H -->|/<br />weight=50<br />ClusterIP:8080| S
            H -->|/<br />weight=50<br />ClusterIP:8080| V
            H -->|/subpath<br />ClusterIP:8080| U
        end
    end

Feel free to adjust the weights and see what happens.

To finish the gradual rollout we only have to fully route the traffic to the sampleapp-v2 by adjusting the weights. Adapt our sample-gateway.yaml just like this:

    - backendRefs:
        - name: sampleapp
          port: 8080
          weight: 0
        - name: sampleapp-v2
          port: 8080
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /
Solution

It should then look like

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sampleapp
spec:
  gatewayClassName: traefik
  listeners:
    - name: sampleapp
      hostname: hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: sampleapp
spec:
  parentRefs:
    - name: sampleapp
  hostnames:
    - hello.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: sampleapp
          port: 8080
          weight: 0
        - name: sampleapp-v2
          port: 8080
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /
    - backendRefs:
        - name: sampleapp-subpath
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /subpath
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /

Apply the change:

kubectl apply -f sampleapp-gateway.yaml

The traffic should only list responses from sampleapp-v2 as shown below:

Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
Hello World (from sampleapp-v2-6dc8794944-59s5w) to everyone via Gateway API from v2
...
Hello World (from sampleapp-subpath-577ddd78df-rz7q2) to everyone via Gateway API from a subpath

Now we have fully migrated to the new version v2 and we could delete the old resources, the sampleapp deployment, service and reference to the sampleapp from the httproute including the weight attributes.

Exercise - Extend our ToDo application

todo.<your_namespace>.<ingress_ip_traefik>.nip.io:80  -> todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443 -> <ingress_ip_traefik>:443 -> /            todoui:8090
                                                                                     /backend/v2  todobackend:8080 w/ basic auth (for debugging)
graph LR;
    A{Internet} -->|LoadBalancer:80<br />LoadBalancer:443| G
    subgraph Kubernetes
        subgraph your Namespace
            G(Gateway)
            H(HTTPRoute)
            T(HTTPRoute)
            U(todoui)
            B(todobackend)
            P(postgresdb)
        end
        G -->|Listener:HTTP| H
        H -->|redirect to HTTPS| G
        G -->|Listener:HTTPS| T
        T -->|/<br />ClusterIP:8090| U
        T -->|/backend/v2<br />basic auth<br />ClusterIP:8080| B
        U -->|ClusterIP:8080| B
        B -->|ClusterIP:5432| P
    end

So we will need a Gateway, one HTTPRoute to frontend and one to backend, a redirect to https, basic auth settings, and a certificate for TLS termination. (Of course, normally you’d have versioned all backend Deployments, but let’s just take the easy path here and work with what we already have.)

TLS certificate

Self-signed will suffice for now:

openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
    -keyout todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.key \
    -out todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt \
    -subj "/CN=todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io"

which will output some line(s) with symbols indicating its generating data.

Verify the certificate’s CN:

openssl x509 -in todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt -noout -subject

subject=CN=todo.<your_namespace>.<your_ingress_ip>.nip.io

Load this as a Kubernetes secret:

kubectl create secret tls todo-gateway-tls-secret --key todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.key --cert todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io.crt

secret/todo-gateway-tls-secret created

And verify it is present:

kubectl describe secrets todo-gateway-tls-secret

Name:         todo-gateway-tls-secret
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/tls

Data
====
tls.crt:  1180 bytes
tls.key:  1704 bytes

Remember from Exercise - create a Secret how to verify the contents?

Solution

kubectl get secrets todo-gateway-tls-secret -o jsonpath='{.data.tls\.crt}' | base64 --decode

will do the trick for the certificate, and the key can be checked likewise.

Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-CERT

Basic auth

Well, we do not have htpasswd installed locally. No problem, Docker to the rescue:

Info

This could take some time because your local docker daemon has to download the httpd:latest image!

docker run -it --rm -v $(pwd):/tempdir -w /tempdir httpd:latest htpasswd -c auth backenddebugger

New password: password
Re-type new password: password
Adding password for user backenddebugger

Yes, it is important that the file generated is named auth (actually - that the secret which we are about to create has a key data.auth), and yes, let’s use the literal password as password, just for the sake of simplicity.

Verify the contents which have conveniently been placed into our current work directory:

cat auth

backenddebugger:$apr1$K3X6TKeX$RQaGr8FqnXYxgDU4Z4lBa0

Load this as a Kubernetes secret, with a generic name as we are going to reuse this later with Traefik:

kubectl create secret generic todo-backend-basic-auth --from-file auth

secret/todo-backend-basic-auth created

And verify it is present and it contains the correct data:

kubectl describe secrets todo-backend-basic-auth

Name:         todo-backend-basic-auth
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
auth:  54 bytes

kubectl get secrets todo-backend-basic-auth -o jsonpath='{.data.auth}' | base64 --decode

backenddebugger:$apr1$K3X6TKeX$RQaGr8FqnXYxgDU4Z4lBa0

Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-AUTH

Gateway API

Thus, now it is time to plug it all together. Create a file todoapp-gateway.yaml by executing the following (yes, execute it all at once), again utilizing your personal namespace as a host prefix:

cat <<.EOF > todoapp-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: todo-gateway
spec:
  gatewayClassName: traefik
  listeners:
    - name: todo-http
      hostname: todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8000
      protocol: HTTP
    - name: todo-https
      hostname: todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
      port: 8443
      protocol: HTTPS
      tls:
        certificateRefs:
          - name: todo-gateway-tls-secret
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: todo-redirect
spec:
  parentRefs:
    - name: todo-gateway
      sectionName: todo-http
  hostnames:
    - todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: todo-backend-auth
spec:
  basicAuth:
    secret: todo-backend-basic-auth
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: todo
spec:
  parentRefs:
    - name: todo-gateway
      sectionName: todo-https
  hostnames:
    - todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io
  rules:
    - backendRefs:
        - name: todoui
          port: 8090
      matches:
        - path:
            type: PathPrefix
            value: /
    - backendRefs:
        - name: todobackend
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /backend/v2
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        - type: ExtensionRef
          extensionRef:
            group: traefik.io
            kind: Middleware
            name: todo-backend-auth
.EOF

Apply it with kubectl apply -f todoapp-gateway.yaml and verify what has been created:

kubectl describe gateway todo-gateway

Name:         todo-gateway
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>
API Version:  gateway.networking.k8s.io/v1
Kind:         Gateway
[...]
Spec:
  Gateway Class Name:  traefik
  Listeners:
    Allowed Routes:
      Namespaces:
        From:  Same
    Hostname:  todo.<your_namespace>.<ingress_ip_traefik>.nip.io
    Name:      todo-http
    Port:      8000
    Protocol:  HTTP
    Allowed Routes:
      Namespaces:
        From:  Same
    Hostname:  todo.<your_namespace>.<ingress_ip_traefik>.nip.io
    Name:      todo-https
    Port:      8443
    Protocol:  HTTPS
    Tls:
      Certificate Refs:
        Group:
        Kind:   Secret
        Name:   todo-gateway-tls-secret
      Mode:     Terminate
[...]

and

kubectl describe httproutes todo-redirect todo

Name:         todo-redirect
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>
API Version:  gateway.networking.k8s.io/v1
Kind:         HTTPRoute
[...]
Spec:
  Hostnames:
    todo.<your_namespace>.<ingress_ip_traefik>.nip.io
  Parent Refs:
    Group:         gateway.networking.k8s.io
    Kind:          Gateway
    Name:          todo-gateway
    Section Name:  todo-http
  Rules:
    Filters:
      Request Redirect:
        Scheme:       https
        Status Code:  301
      Type:           RequestRedirect
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /
[...]
Name:         todo
Namespace:    <your_namespace>
Labels:       <none>
Annotations:  <none>
API Version:  gateway.networking.k8s.io/v1
Kind:         HTTPRoute
[...]
Spec:
  Hostnames:
    todo.<your_namespace>.<ingress_ip_traefik>.nip.io
  Parent Refs:
    Group:         gateway.networking.k8s.io
    Kind:          Gateway
    Name:          todo-gateway
    Section Name:  todo-https
  Rules:
    Backend Refs:
      Group:
      Kind:    Service
      Name:    todoui
      Port:    8090
      Weight:  1
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /
    Backend Refs:
      Group:
      Kind:    Service
      Name:    todobackend
      Port:    8080
      Weight:  1
    Filters:
      Type:  URLRewrite
      URL Rewrite:
        Path:
          Replace Prefix Match:  /
          Type:                  ReplacePrefixMatch
      Extension Ref:
        Group:  traefik.io
        Kind:   Middleware
        Name:   todo-backend-auth
      Type:     ExtensionRef
    Matches:
      Path:
        Type:   PathPrefix
        Value:  /backend/v2
[...]

Milestone: K8S/INGRESS/GATEWAYAPI-TODOAPP-GATEWAY

Verification

Well, but does it actually work as intended? Let’s find out by first verifying TLS termination:

curl --verbose --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/ | head -n 20

[...]
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=todo.<your_namespace>.<your_ingress_ip>.nip.io
*  start date: Feb 16 08:46:33 2026 GMT
*  expire date: Feb 16 08:46:33 2027 GMT
*  issuer: CN=todo.<your_namespace>.<your_ingress_ip>.nip.io
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
[...]
<!DOCTYPE HTML>
<html>
<head>
    <title>Schönste aller Todo Listen</title>
[...]

Then let’s verify the redirect HTTP->HTTPS is present:

curl --verbose http://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/

[...]
< HTTP/1.1 301 Moved Permanently
< Location: https://todo.<your_namespace>.<your_ingress_ip>.nip.io/
[...]
Moved Permanently

And confirm the contents on redirect:

curl --silent --location --insecure http://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/ | head -n 4

<!DOCTYPE HTML>
<html>
<head>
    <title>Schönste aller Todo Listen</title>

So, TLS termination, including a redirect HTTP->HTTPS, seems to work just fine.

Tip

More details / checks wanted? Run

docker run --rm -it drwetter/testssl.sh:latest https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/

and enjoy.

Now let’s check basic auth on backend:

curl --verbose --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/; echo

[...]
< HTTP/2 401
< content-type: text/plain
< content-length: 17
< www-authenticate: Basic realm="traefik"
< date: Mon, 16 Feb 2026 14:29:33 GMT
<
401 Unauthorized
[...]

OK, we indeed need to authenticate, so let’s try this while inserting sample data (if none already present):

curl --request POST --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/testabc --user backenddebugger:password; echo

added testabc

And query data from backend:

curl --silent --insecure https://todo.$NAMESPACE.$INGRESS_IP_TRAEFIK.nip.io/backend/v2/todos/ --user backenddebugger:password; echo

["testabc"]

All in all, now looking back at what we attempted to achieve we can see that we have indeed reached out goal and are finished:

todo.<your_namespace>.<ingress_ip_traefik>.nip.io:80  -> todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443
todo.<your_namespace>.<ingress_ip_traefik>.nip.io:443 -> <ingress_ip_traefik>:443 -> /            todoui:8090
                                                                                     /backend/v2/ todobackend:8080 w/ basic auth (for debugging)

The redirect HTTP->HTTPS is in place, TLS termination works and we can access our ToDo frontend just fine, and the backend is secured by basic auth.