Back to blog

Tutorial: Getting Started with the Cilium Gateway API

Nico Vibert
Nico Vibert
Published: Updated: Cilium
Tutorial: Getting Started with the Cilium Gateway API

Last updated: April 2023

Gateway API support has landed on Cilium! Gateway API is the long term replacement for Kubernetes Ingress and provides operators a role-based, portable and extensible model to route traffic into your clusters.

In this blog post, we will walk you through how to install, configure and manage the Cilium Gateway API for a number of use cases. If you would like to understand the “what” and the “why” of the Cilium Gateway API, head over to that post.

This post will focus on the “how”.

If you would rather do than read, head to the Cilium Gateway API lab and Cilium Advanced Gateway API Use Cases lab instead.

Installing Cilium with Gateway API

In our environment, we start with a kind-based Kubernetes cluster. You can use a configuration such as simple as this one below (note the disableDefaultCNI: true as we will be installing Cilium instead of the default CNI):

---
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
networking:
  disableDefaultCNI: true

Save this configuration as kind-config.yaml and deploy it (for example, with kind create cluster --image kindest/node:v1.24.0 --config kind-config.yaml).

Before we install Cilium with Gateway API, we need to make sure we install the Gateway API CRDs prior to the Cilium install (Gateway API is a Custom Resource Definition (CRD) based API so you’ll need to install the CRDs onto a cluster to use the API):

kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.5.1/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.5.1/config/crd/standard/gateway.networking.k8s.io_gateways.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.5.1/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.5.1/config/crd/experimental/gateway.networking.k8s.io_referencegrants.yaml

You can check the CRDs have been installed with the following command:

root@server:~# kubectl get crd gatewayclasses.gateway.networking.k8s.io
kubectl get crd gateways.gateway.networking.k8s.io
kubectl get crd httproutes.gateway.networking.k8s.io
kubectl get crd referencegrants.gateway.networking.k8s.io
NAME                                       CREATED AT
gatewayclasses.gateway.networking.k8s.io   2023-03-09T13:39:22Z
NAME                                 CREATED AT
gateways.gateway.networking.k8s.io   2023-03-09T13:39:22Z
NAME                                   CREATED AT
httproutes.gateway.networking.k8s.io   2023-03-09T13:39:23Z
NAME                                        CREATED AT
referencegrants.gateway.networking.k8s.io   2023-03-09T13:39:23Z

We can now go ahead and deploy Cilium. We’ve talked about the many ways to deploy Cilium in a previous post. This time, let’s use Helm. Notice the main requirement for Gateway API to work: Cilium must be configured with kubeProxyReplacement set to partial or strict.

helm repo add cilium https://helm.cilium.io

helm upgrade --install cilium cilium/cilium --version 1.13.0 --namespace kube-system --set kubeProxyReplacement=strict --set gatewayAPI.enabled=true

Let’s double check it was successfully set up:

root@server:~# cilium config view | grep "enable-gateway-api"
enable-gateway-api                             true
enable-gateway-api-secrets-sync                true

To access the service that will be exposed via the Gateway API, we need to allocate an external IP address. When a Gateway is created, an associated Kubernetes Services of the type LoadBalancer is created. When using a managed Kubernetes Service like EKS, AKS or GKE, the LoadBalancer is assigned an IP (or DNS name) automatically. For private cloud or for home labs, we need another tool – such as MetalLB below – to allocate an IP Address and to provide L2 connectivity (as in – advertising to said IP to the network with gratuitous ARPs). Note that Cilium itself provides Load-Balancer IP Address Management support but not Layer 2 connectivity (that is a work-in-progress).

KIND_NET_CIDR=$(docker network inspect kind -f '{{(index .IPAM.Config 0).Subnet}}')
METALLB_IP_START=$(echo ${KIND_NET_CIDR} | sed "s@0.0/16@255.200@")
METALLB_IP_END=$(echo ${KIND_NET_CIDR} | sed "s@0.0/16@255.250@")
METALLB_IP_RANGE="${METALLB_IP_START}-${METALLB_IP_END}"

cat << EOF > metallb_values.yaml
configInline:
  address-pools:
  - name: default
    protocol: layer2
    addresses:
    - ${METALLB_IP_RANGE}
EOF

helm install --namespace metallb-system --create-namespace \
  --repo https://metallb.github.io/metallb metallb metallb \
  --version 0.12.1 --values metallb_values.yaml

What is a GatewayClass and a Gateway?

Before we actually start routing traffic into the cluster, we should explain what these CRDs were and why they were required.

If the CRDs have been deployed beforehand, a GatewayClass will be deployed by Cilium during its installation (assuming the Gateway API option has been selected).

Let’s verify that a GatewayClass has been deployed and accepted:

root@server:~# kubectl get gatewayclasses.gateway.networking.k8s.io 
NAME     CONTROLLER                     ACCEPTED   AGE
cilium   io.cilium/gateway-controller   True       13m

The GatewayClass is a type of Gateway that can be deployed: in other words, it is a template. This is done in a way to let infrastructure providers offer different types of Gateways. Users can then choose the Gateway they like.

For instance, an infrastructure provider may create two GatewayClasses named internet and private for two different purposes and possibly with different features: one to proxy Internet-facing services and one for private internal applications.

In our case, we will instantiate Cilium Gateway API (io.cilium/gateway-controller).

HTTP Routing

Let’s now deploy an application and set up GatewayAPI HTTPRoutes to route HTTP traffic into the cluster. We will use bookinfo as a sample application.

This demo set of microservices provided by the Istio project consists of several deployments and services:

  • 🔍 details
  • ⭐ ratings
  • ✍ reviews
  • 📕 productpage

We will use several of these services as bases for our Gateway APIs.

Deploy an application

Let’s deploy the sample application in the cluster.

root@server:~#  kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.12/samples/bookinfo/platform/kube/bookinfo.yaml
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created

Check that the application is properly deployed:

root@server:~# kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
details-v1-586577784f-nr5t2       1/1     Running   0          61s
productpage-v1-589b848cc9-prrnh   1/1     Running   0          61s
ratings-v1-679fc7b4f-g48xz        1/1     Running   0          61s
reviews-v1-7b76665ff9-6g7fq       1/1     Running   0          61s
reviews-v2-6b86c676d9-v65k9       1/1     Running   0          61s
reviews-v3-b77c579-pqmgl          1/1     Running   0          61s

You should see multiple pods being deployed in the default namespace.

Notice that with Cilium Service Mesh, there is no Envoy sidecar created alongside each of the demo app microservices. With a sidecar implementation, the output would show 2/2 READY: one for the microservice and one for the Envoy sidecar.

Have a quick look at the Services deployed:

root@server:~# kubectl get svc
NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
details       ClusterIP   10.96.41.65    <none>        9080/TCP   2m3s
kubernetes    ClusterIP   10.96.0.1      <none>        443/TCP    11m
productpage   ClusterIP   10.96.147.97   <none>        9080/TCP   2m3s
ratings       ClusterIP   10.96.105.89   <none>        9080/TCP   2m3s
reviews       ClusterIP   10.96.149.14   <none>        9080/TCP   2m3s

Note these Services are only internal-facing (ClusterIP) and therefore there is no access from outside the cluster to these Services.

Deploy the Gateway and the HTTPRoutes

Before deploying the Gateway and HTTPRoutes, let’s review the configuration we’re going to use. Let’s review it section by section:

spec:
  gatewayClassName: cilium
  listeners:
  - protocol: HTTP
    port: 80
    name: web-gw
    allowedRoutes:
      namespaces:
        from: Same

First, note in the Gateway section that the gatewayClassName field uses the value cilium. This refers to the Cilium GatewayClass previously configured.

The Gateway will listen on port 80 for HTTP traffic coming southbound into the cluster.
The allowedRoutes is here to specify the namespaces from which Routes may be attached to this Gateway. Same means only Routes in the same namespace may be used by this Gateway.

Note that, if we were to use All instead of Same, we would enable this gateway to be associated with routes in any namespace and it would enable us to use a single gateway across multiple namespaces that may be managed by different teams.

We could specify different namespaces in the HTTPRoutes – therefore, for example, you could send the traffic to https://acme.com/payments in a namespace where a payment app is deployed and https://acme.com/ads in a namespace used by the ads team for their application.

Let’s now review the HTTPRoute manifest. HTTPRoute is a GatewayAPI type for specifiying routing behaviour of HTTP requests from a Gateway listener to a Kubernetes Service.

It is made of Rules to direct the traffic based on your requirements.

This first Rule is essentially a simple L7 proxy route: for HTTP traffic with a path starting with /details, forward the traffic over to the details Service over port 9080.

  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /details
    backendRefs:
    - name: details
      port: 9080

The second rule is similar but it’s leveraging different matching criteria.
If the HTTP request has:

  • a HTTP header with a name set to magic with a value of foo, AND
  • the HTTP method is “GET”, AND
  • the HTTP query param is named great with a value of example,
    Then the traffic will be sent to the productpage service over port 9080.
rules:
  - matches:
   - headers:
      - type: Exact
        name: magic
        value: foo
      queryParams:
      - type: Exact
        name: great
        value: example
      path:
        type: PathPrefix
        value: /
      method: GET
    backendRefs:
    - name: productpage
      port: 9080

As you can see, you can deploy sophisticated L7 traffic rules that are consistent. With Ingress API, annotations were often required to achieve such routing goals and that created inconsistencies from one Ingress controller to another.

One of the benefits of these new APIs is that the Gateway API is essentially split into separate functions – one to describe the Gateway and one for the Routes to the back-end services. By splitting these two functions, it gives operators the ability to change and swap gateways but keep the same routing configuration.

In other words: if you decide you want to use a different Gateway API controller instead, you will be able to re-use the same manifest.

Let’s now deploy the Gateway and the HTTPRoute:

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.13.0/examples/kubernetes/gateway/basic-http.yaml

Let’s now look at the Service created by the Gateway:

root@server:~# kubectl get svc cilium-gateway-my-gateway 
NAME                        TYPE           CLUSTER-IP   EXTERNAL-IP      PORT(S)        AGE
cilium-gateway-my-gateway   LoadBalancer   10.96.1.63   172.18.255.201   80:31033/TCP   12s

You will see a LoadBalancer service named cilium-gateway-my-gateway which was created for the Gateway API. MetalLB will automatically provision an IP address for it.

The same external IP address is also associated to the Gateway:

root@server:~# kubectl get gateway
NAME         CLASS    ADDRESS          READY   AGE
my-gateway   cilium   172.18.255.201   True    61s

Let’s retrieve this IP address:

root@server:~# GATEWAY=$(kubectl get gateway my-gateway -o jsonpath='{.status.addresses[0].value}')
echo $GATEWAY
172.18.255.201

HTTP Path matching

Let’s now check that traffic based on the URL path is proxied by the Gateway API.

Because of the path starts with /details, this traffic will match the first rule and will be proxied to the details Service over port 9080. The curl request is successful:

root@server:~# curl --fail -s http://$GATEWAY/details/1 | jq
{
  "id": 1,
  "author": "William Shakespeare",
  "year": 1595,
  "type": "paperback",
  "pages": 200,
  "publisher": "PublisherA",
  "language": "English",
  "ISBN-10": "1234567890",
  "ISBN-13": "123-1234567890"
}

If you enable Hubble (either during the Cilium installation or later on with cilium hubble enable), you can track the flows of this particular HTTP transaction. Note how you can filter flows based on the HTTP Path.

root@server:~# hubble observe --http-path "/details"
Mar 15 10:37:23.000: 172.18.0.1:44128 (world) -> default/cilium-gateway-my-gateway:80 (world) http-request FORWARDED (HTTP/1.1 GET http://172.18.255.201/details/1)
Mar 15 10:37:23.002: 172.18.0.1:44128 (world) <- default/cilium-gateway-my-gateway:80 (world) http-response FORWARDED (HTTP/1.1 200 2ms (GET http://172.18.255.201/details/1))

HTTP Header Matching

This time, we will route traffic based on HTTP parameters like header values, method and query parameters.

rules:
  - matches:
   - headers:
      - type: Exact
        name: magic
        value: foo
      queryParams:
      - type: Exact
        name: great
        value: example
      path:
        type: PathPrefix
        value: /
      method: GET
    backendRefs:
    - name: productpage
      port: 9080

With curl, we can specify the headers values and query parameters to match this particular rule above:

root@server:~# curl -v -H 'magic: foo' http://"$GATEWAY"\?great\=example
*   Trying 172.18.255.201:80...
* Connected to 172.18.255.201 (172.18.255.201) port 80 (#0)
> GET /?great=example HTTP/1.1
> Host: 172.18.255.201
> User-Agent: curl/7.81.0
> Accept: */*
> magic: foo
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
[Output truncated for brevity]

The curl query is successful and returns a successful 200 code and a verbose HTML reply.

Likewise, Hubble lets you visualize and filter traffic based on some HTTP values, such as the method (GET, POST, etc…) or the status code (200, 404, etc…):

root@server:~# hubble observe --http-method GET
Mar 15 10:51:46.364: 172.18.0.1:44134 (world) -> default/cilium-gateway-my-gateway:80 (world) http-request FORWARDED (HTTP/1.1 GET http://172.18.255.201/?great=example)
Mar 15 10:51:46.374: 172.18.0.1:44134 (world) <- default/cilium-gateway-my-gateway:80 (world) http-response FORWARDED (HTTP/1.1 200 10ms (GET http://172.18.255.201/?great=example))

root@server:~# hubble observe --http-status 200
Mar 15 10:51:46.374: 172.18.0.1:44134 (world) <- default/cilium-gateway-my-gateway:80 (world) http-response FORWARDED (HTTP/1.1 200 10ms (GET http://172.18.255.201/?great=example))

TLS Termination

While routing HTTP traffic was easy to understand, secure workloads will require the use of HTTPS and TLS certificates. Let’s start this walkthrough with first deploying the certificate.

For demonstration purposes we will use a TLS certificate signed by a made-up, self-signed certificate authority (CA). One easy way to do this is with mkcert.

First, let’s create a certificate that will validate bookinfo.cilium.rocks and hipstershop.cilium.rocks, as these are the host names used in this Gateway example:

root@server:~# mkcert '*.cilium.rocks'
Created a new local CA 💥
Note: the local CA is not installed in the system trust store.
Run "mkcert -install" for certificates to be trusted automatically ⚠️

Created a new certificate valid for the following names 📜
 - "*.cilium.rocks"

Reminder: X.509 wildcards only go one level deep, so this won't match a.b.cilium.rocks ℹ️

The certificate is at "./_wildcard.cilium.rocks.pem" and the key at "./_wildcard.cilium.rocks-key.pem"
It will expire on 9 June 2025 🗓

Mkcert created a key (_wildcard.cilium.rocks-key.pem) and a certificate (_wildcard.cilium.rocks.pem) that we will use for the Gateway service.

Let’s now create a Kubernetes TLS secret with this key and certificate:

root@server:~# kubectl create secret tls demo-cert --key=_wildcard.cilium.rocks-key.pem --cert=_wildcard.cilium.rocks.pem
secret/demo-cert created

We can now deploy another Gateway for HTTPS Traffic:

root@server:~# kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.13.0/examples/kubernetes/gateway/basic-https.yaml
gateway.gateway.networking.k8s.io/tls-gateway created
httproute.gateway.networking.k8s.io/https-app-route-1 created
httproute.gateway.networking.k8s.io/https-app-route-2 created

Let’s review the configuration. It is almost identical to the one we reviewed previously. Just notice the following in the Gateway manifest:

spec:
  gatewayClassName: cilium
  listeners:
  - name: https-1
    protocol: HTTPS
    port: 443
    hostname: "bookinfo.cilium.rocks"
    tls:
      certificateRefs:
      - kind: Secret
        name: demo-cert

And the following in the HTTPRoute manifest:

spec:
  parentRefs:
  - name: tls-gateway
  hostnames:
  - "bookinfo.cilium.rocks"

The HTTPS Gateway API examples build up on what was done in the HTTP example and adds TLS termination for two HTTP routes:

  • the /details prefix will be routed to the details HTTP service deployed earlier
  • the / prefix will be routed to the productpage HTTP service deployed earlier

These services will be secured via TLS and accessible on two domain names:

  • bookinfo.cilium.rocks
  • hipstershop.cilium.rocks

In our example, the Gateway serves the TLS certificate defined in the demo-cert Secret resource for all requests to bookinfo.cilium.rocks and to hipstershop.cilium.rocks.

After the deployment, the Gateway will pick up and IP address from MetalLB:

root@server:~# kubectl get gateway tls-gateway
NAME          CLASS    ADDRESS          READY   AGE
tls-gateway   cilium   172.18.255.202   True    33s
root@server:~# 
root@server:~# GATEWAY_IP=$(kubectl get gateway tls-gateway -o jsonpath='{.status.addresses[0].value}')
echo $GATEWAY_IP
172.18.255.202

In this Gateway configuration, the host names hipstershop.cilium.rocks and bookinfo.cilium.rocks are specified in the path routing rules.

Since we do not have DNS entries for these names, we will modify the /etc/hosts file on the host to manually associate these names to the known Gateway IP we retrieved.

root@server:~# cat << EOF >> /etc/hosts
${GATEWAY_IP} bookinfo.cilium.rocks
${GATEWAY_IP} hipstershop.cilium.rocks
EOF
root@server:~# tail -n 2 /etc/hosts 
172.18.255.202 bookinfo.cilium.rocks
172.18.255.202 hipstershop.cilium.rocks

Requests to these names will now be directed to the Gateway.

Let’s install the Mkcert CA into your system so cURL can trust it:

root@server:~# mkcert -install
The local CA is now installed in the system trust store! ⚡️

Let’s now make a HTTPS request to the Gateway:

root@server:~# curl -s https://bookinfo.cilium.rocks/details/1 | jq
{
  "id": 1,
  "author": "William Shakespeare",
  "year": 1595,
  "type": "paperback",
  "pages": 200,
  "publisher": "PublisherA",
  "language": "English",
  "ISBN-10": "1234567890",
  "ISBN-13": "123-1234567890"
}

The data was be properly retrieved, using HTTPS (and thus, the TLS handshake was properly achieved).

Traffic Splitting

For this particular use case, we’re going to use Gateway API to load-balance incoming traffic to different backends, with different weights associated.

We will use a deployment made of echo servers – they will reply to our curl queries with the pod name and the node name.

root@server:~# kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/main/echo-servers.yml
service/echo-1 created
deployment.apps/echo-1 created
service/echo-2 created
deployment.apps/echo-2 created

Let’s now deploy the Gateway and the HTTPRoute:

root@server:~# kubectl apply -f gateway.yaml
kubectl apply -f http-route.yaml
gateway.gateway.networking.k8s.io/cilium-gw created
httproute.gateway.networking.k8s.io/example-route-1 created
root@server:~# kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/main/gateway.yaml
gateway.gateway.networking.k8s.io/my-example-gateway created
root@server:~# 
root@server:~# 
root@server:~# kubectl apply -f https://raw.githubusercontent.com/nvibert/gateway-api-traffic-splitting/main/httpRoute.ymlhttproute.gateway.networking.k8s.io/example-route-1 configured

The HTTPRoute Rules includes two different backendRefs and weights associated with them

    backendRefs:
    - kind: Service
      name: echo-1
      port: 8080
      weight: 50
    - kind: Service
      name: echo-2
      port: 8090
      weight: 50

Access is successful and as described above, we get, in the reply, the pod name and the node name:

root@server:~# GATEWAY=$(kubectl get gateway cilium-gw -o jsonpath='{.status.addresses[0].value}')
echo $GATEWAY
172.18.255.200
root@server:~# 
root@server:~# 
root@server:~# curl --fail -s http://$GATEWAY/echo


Hostname: echo-1-7d88f779b-trmzt

Pod Information:
        node name:      kind-worker
        pod name:       echo-1-7d88f779b-trmzt
        pod namespace:  default
        pod IP: 10.0.2.228

Server values:
        server_version=nginx: 1.12.2 - lua: 10010

Request Information:
        client_address=10.0.1.231
        method=GET
        real path=/echo
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://172.18.255.200:8080/echo

Request Headers:
        accept=*/*  
        host=172.18.255.200  
        user-agent=curl/7.81.0  
        x-forwarded-proto=http  
        x-request-id=d035c99d-cd5d-45ce-9eff-3fcf614ebdcc  

Request Body:
        -no body in request-

When repeating the curl, the traffic is split roughly between two services. To verify, we can run a loop and count how the replies are spread:

root@server:~# while true; do curl -s -k "http://$GATEWAY/echo" >> curlresponses.txt ;done
^C
root@server:~# cat curlresponses.txt| grep -c "Hostname: echo-1"
2536
root@server:~# cat curlresponses.txt| grep -c "Hostname: echo-2"
2486

Update the HTTPRoute weights (either by updating the value in the original manifest before reapplying it or by using kubectl edit httproute) to, for example, 99 for echo-1 and 1 for echo-2:

Running the same loop validates that traffic is split based on the new weights:

root@server:~# kubectl describe httproutes.gateway.networking.k8s.io | grep -A 10 Rules
  Rules:
    Backend Refs:
      Group:   
      Kind:    Service
      Name:    echo-1
      Port:    8080
      Weight:  99
      Group:   
      Kind:    Service
      Name:    echo-2
      Port:    8090
root@server:~# kubectl describe httproutes.gateway.networking.k8s.io | grep -A 11 Rules
  Rules:
    Backend Refs:
      Group:   
      Kind:    Service
      Name:    echo-1
      Port:    8080
      Weight:  99
      Group:   
      Kind:    Service
      Name:    echo-2
      Port:    8090
      Weight:  1
root@server:~# cat curlresponses991.txt| grep -c "Hostname: echo-1"
1711
root@server:~# cat curlresponses991.txt| grep -c "Hostname: echo-2"
14

HTTP Request Header Modification

With this functionality, the Cilium Gateway API lets us add, remove or edit HTTP Headers of incoming traffic.

This is best validated by trying without and with the functionality. We’ll use the same echo servers.

Let’s add another HTTPRoute (we can use the Gateway created in any of the previous HTTP-related use cases):

---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: header-http-echo
spec:
  parentRefs:
  - name: cilium-gw
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /cilium-add-a-request-header
    filters:
    - type: RequestHeaderModifier
      requestHeaderModifier:
        add:
        - name: my-cilium-header-name
          value: my-cilium-header-value
    backendRefs:
      - name: echo-1
        port: 8080

A curl is successful and the reply sent back from the echo server is the original HTTP Header:

root@server:~# GATEWAY=$(kubectl get gateway cilium-gw -o jsonpath='{.status.addresses[0].value}')
echo $GATEWAY
172.18.255.200
root@server:~# 
root@server:~# curl --fail -s http://$GATEWAY/cilium-add-a-request-header | grep -A 6 "Request Headers"
Request Headers:
        accept=*/*  
        host=172.18.255.200  
        user-agent=curl/7.81.0  
        x-forwarded-proto=http  
        x-request-id=7ebb351c-f630-4f03-b4db-74945525c0a8  

root@server:~# 

To add the header, we will be using the filters field. Add this to the HTTPRoute rules spec (on the same indentation as matches and backendRefs) and re-apply it.

    filters:
    - type: RequestHeaderModifier
      requestHeaderModifier:
        add:
        - name: my-cilium-header-name
          value: my-cilium-header-value

When running the curl test again, notice how the header has been added:

root@server:~# curl -s http://$GATEWAY/cilium-add-a-request-header | grep -A 5 "Request Headers"
Request Headers:
        accept=*/*  
        host=172.18.255.200  
        my-cilium-header-name=my-cilium-header-value  
        user-agent=curl/7.81.0  
        x-forwarded-proto=http  

To remove a header, you can add the following fields:

    - type: RequestHeaderModifier
      requestHeaderModifier:
        remove: ["x-request-id"]

Notice how the x-request-id header has been removed:

root@server:~# curl --fail -s http://$GATEWAY/cilium-add-a-request-header | grep -A 6 "Request Headers"
Request Headers:
        accept=*/*  
        host=172.18.255.200  
        user-agent=curl/7.81.0  
        x-forwarded-proto=http  

Request Body:

HTTP Response Header Modification

Just like editing request headers can be useful, the same goes for response headers. For example, it allows teams to add/remove cookies for only a certain backend, which can help in identifying certain users that were redirected to that backend previously.

Another potential use case could be when you have a frontend that needs to know whether it’s talking to a stable or a beta version of the backend server, in order to render different UI or adapt its response parsing accordingly.

At time of writing, this feature is currently only included in the “Experimental” channel of Gateway API. Therefore, before using it, we need to deploy the experimental Gateway CRDs.

root@server:~# kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.6.0/config/crd/experimental/gateway.networking.k8s.io_gatewayclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.6.0/config/crd/experimental/gateway.networking.k8s.io_gateways.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.6.0/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v0.6.0/config/crd/experimental/gateway.networking.k8s.io_referencegrants.yaml
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io configured
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io configured
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io configured
customresourcedefinition.apiextensions.k8s.io/referencegrants.gateway.networking.k8s.io configured

Find more information about the experimental features on the Gateway API website.

Let’s review the HTTPRoute we will be using to modify the response headers:

---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
  name: response-header-modifier
spec:
  parentRefs:
  - name: cilium-gw
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /multiple
    filters:
    - type: ResponseHeaderModifier
      responseHeaderModifier:
        add:
        - name: X-Header-Add-1
          value: header-add-1
        - name: X-Header-Add-2
          value: header-add-2
        - name: X-Header-Add-3
          value: header-add-3
    backendRefs:
    - name: echo-1
      port: 8080

Notice how this time, the header’s response is modified, using the type: ResponseHeaderModifier filter. We are going to add 3 headers in one go.

Apply the HTTPRoute manifest above and start making HTTP requests to the Gateway address (we’re using the same one as before). Remember that the body of the response packet includes details about the original request. Note that no specific header was added to the original request.

root@server:~# curl --fail -s http://$GATEWAY/multiple | grep "Request Headers" -A 10
Request Headers:
        accept=*/*  
        host=172.18.255.200  
        user-agent=curl/7.81.0  
        x-forwarded-proto=http  
        x-request-id=d497e7d8-525b-4a95-9a38-b4e67351e88a  

Request Body:
        -no body in request-

To show the headers of the response, we can run curl in verbose mode:

root@server:~# curl -v --fail -s http://$GATEWAY/multiple
*   Trying 172.18.255.200:80...
* Connected to 172.18.255.200 (172.18.255.200) port 80 (#0)
> GET /multiple HTTP/1.1
> Host: 172.18.255.200
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< date: Mon, 24 Apr 2023 10:20:56 GMT
< content-type: text/plain
< server: envoy
< x-envoy-upstream-service-time: 0
< x-header-add-1: header-add-1
< x-header-add-2: header-add-2
< x-header-add-3: header-add-3
< transfer-encoding: chunked
< 


Hostname: echo-1-7d88f779b-pmssf

Pod Information:
        node name:      kind-worker
        pod name:       echo-1-7d88f779b-pmssf
        pod namespace:  default
        pod IP: 10.0.2.54

[OUTPUT TRUNCATED FOR BREVITY]

You should see, in the fields starting with <, the response header. You should see the headers added by the Gateway API to the response:

< x-header-add-1: header-add-1
< x-header-add-2: header-add-2
< x-header-add-3: header-add-3

Again, you can see how simple it is to use Cilium Gateway API to modify HTTP traffic – incoming requests or outgoing responses.

Conclusion

We hope you found this deep dive into Gateway API use cases useful. As more features are added to the Gateway API, we will update this post periodically with more features.

To learn more:

Thanks for reading.

Nico Vibert
AuthorNico VibertSenior Staff Technical Marketing Engineer

Industry insights you won’t delete. Delivered to your inbox weekly.