Skip to content

Implementing Kubernetes Gateway API in EKS Auto Mode using Envoy Gateway

7 minute read
Content level: Expert
0

Currently EKS Auto Mode build-in load balancer controller does not support Kubernetes (K8s) Gateway API. This article shows a way to implement it using Envoy Gateway, a popular open-source software.

Background

The open-source AWS Load Balancer Controller supports Gateway API in GA (General Available) since v3.0.0, but EKS Auto Mode built-in load balancer controller does not yet support Gateway API.

This fact and customer demand for implementing a Gateway API implementation of their choice, for example Envoy Gateway, led to the creation of this article.

Prerequisites

We use the EKS Auto Mode build-in system nodepool for system-critical components like Envoy Gateway. To use it , Envoy Gateway needs to tolerate the key CriticalAddonsOnly.

Envoy Gateway installation

The following Helm values.yaml file is used for customisation of the installation:

deployment:
  priorityClassName: system-cluster-critical
  replicas: 2
  pod:
    tolerations:
    - key: CriticalAddonsOnly
      operator: Exists
    nodeSelector:
      karpenter.sh/nodepool: system
    topologySpreadConstraints:
    - labelSelector:
        matchLabels:
          app.kubernetes.io/instance: eg
          app.kubernetes.io/name: gateway-helm
          control-plane: envoy-gateway
      maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule

Installation is done using the Helm chart as follows:

$ helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.7.0 -n envoy-gateway-system --create-namespace -f values-envoy-gw.yaml
Pulled: docker.io/envoyproxy/gateway-helm:v1.7.0
Digest: sha256:019b2ed08c3514f41954251e7a80d3ef62ae0549209f637103f5fc1351c56518
NAME: eg
LAST DEPLOYED: Thu Mar 12 13:37:54 2026
NAMESPACE: envoy-gateway-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
**************************************************************************
*** PLEASE BE PATIENT: Envoy Gateway may take a few minutes to install ***
**************************************************************************

Envoy Gateway is an open source project for managing Envoy Proxy as a standalone or Kubernetes-based application gateway.

Thank you for installing Envoy Gateway! 🎉

Your release is named: eg. 🎉

Your release is in namespace: envoy-gateway-system. 🎉

To learn more about the release, try:

  $ helm status eg -n envoy-gateway-system
  $ helm get all eg -n envoy-gateway-system

To have a quickstart of Envoy Gateway, please refer to https://gateway.envoyproxy.io/latest/tasks/quickstart.

To get more details, please visit https://gateway.envoyproxy.io and https://github.com/envoyproxy/gateway.

which creates a namespace envoy-gateway-system with some resources:

$ k get ns envoy-gateway-system 
NAME                   STATUS   AGE
envoy-gateway-system   Active   93s

$ k get all -n envoy-gateway-system 
NAME                                 READY   STATUS    RESTARTS   AGE
pod/envoy-gateway-6f78c85894-298wz   1/1     Running   0          71s
pod/envoy-gateway-6f78c85894-r7zbr   1/1     Running   0          71s

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                                            AGE
service/envoy-gateway   ClusterIP   10.100.123.73   <none>        18000/TCP,18001/TCP,18002/TCP,19001/TCP,9443/TCP   72s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/envoy-gateway   2/2     2            2           73s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/envoy-gateway-6f78c85894   2         2         2       73s

Envoy Gateway Quickstart

By default Envoy Gateway provisions an AWS Classic Load Balancer (CLB) using the deprecated K8s in-tree code. I use a slightly modified version of Envoy Gateway doc Quickstart, which will provision a Network Load Balancer (NLB) instead of a CLB. To implement this, the envoyService will utilise common NLB service Annotations.

This follows a common pattern of using an NLB in front of a L7 routing entity and was already used a lot for NGINX Ingress Controller by utilising respective NLB annotations on the service.

quickstart.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: custom-proxy-config
  namespace: default
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyDeployment:
        replicas: 2
      envoyService:
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
          service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
          service.beta.kubernetes.io/aws-load-balancer-name: envoy-gw-nlb
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: custom-proxy-config
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: backend
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
    service: backend
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      version: v1
  template:
    metadata:
      labels:
        app: backend
        version: v1
    spec:
      serviceAccountName: backend
      containers:
        - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          imagePullPolicy: IfNotPresent
          name: backend
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: backend
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "envoy-auto-mode.<redacted>"
  rules:
    - backendRefs:
        - group: ""
          kind: Service
          name: backend
          port: 3000
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /

The important part here is the custom EnvoyProxy resource. It contains the service annotations to tell the Auto Mode build-in load balancer to create an NLB pointing to the Gateway service. This custom EnvoyProxy is referenced in the Gateway resource in spec.infrastructure.parametersRef section.

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: custom-proxy-config
  namespace: default
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyDeployment:
        replicas: 2
      envoyService:
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
          service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
          service.beta.kubernetes.io/aws-load-balancer-name: envoy-gw-nlb
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: custom-proxy-config
...

and apply it to the cluster:

$ kubectl apply -f quickstart.yaml
gatewayclass.gateway.networking.k8s.io/eg created
envoyproxy.gateway.envoyproxy.io/custom-proxy-config created
gateway.gateway.networking.k8s.io/eg created
serviceaccount/backend created
service/backend created
deployment.apps/backend created
httproute.gateway.networking.k8s.io/backend created

The Gateway resource will now provision an internet-facing NLB with a listener on port 80 in IP target mode pointing to the two replicas of the service:

$ kubectl get gtw
NAME   CLASS   ADDRESS                                                     PROGRAMMED   AGE
eg     eg      envoy-gw-nlb-<redacted>.elb.eu-west-1.amazonaws.com   False        5s

$ kubectl get svc -n envoy-gateway-system 
NAME                        TYPE           CLUSTER-IP      EXTERNAL-IP                                                 PORT(S)                                            AGE
envoy-default-eg-e41e7b31   LoadBalancer   <redacted>   envoy-gw-nlb-<redacted>.elb.eu-west-1.amazonaws.com   80:31962/TCP                                       21s
envoy-gateway               ClusterIP      10.100.123.73   <none>                                                      18000/TCP,18001/TCP,18002/TCP,19001/TCP,9443/TCP   5m40s

$ aws elbv2 describe-load-balancers --name envoy-gw-nlb
{
    "LoadBalancers": [
        {
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/net/envoy-gw-nlb/<redacted>",
            "DNSName": "envoy-gw-nlb-<redacted>.elb.eu-west-1.amazonaws.com",
            "CanonicalHostedZoneId": "<redacted>",
            "CreatedTime": "2026-03-12T12:43:49.822000+00:00",
            "LoadBalancerName": "envoy-gw-nlb",
            "Scheme": "internet-facing",
            "VpcId": "vpc-<redacted>",
            "State": {
                "Code": "active"
            },
            "Type": "network",



$ kubectl get po -n envoy-gateway-system 
NAME                                         READY   STATUS    RESTARTS   AGE
envoy-default-eg-e41e7b31-5f5b94b5df-kktxj   2/2     Running   0          72s
envoy-default-eg-e41e7b31-5f5b94b5df-rvlp2   2/2     Running   0          72s

To match the spec.hostnames in HTTPRoute which I defined as "envoy-auto-mode.<redacted>", I just need to create an Route53 alias record as "envoy-auto-mode.<redacted>" (replace <redacted> with your public DNS zone) pointing to "envoy-gw-nlb-<redacted>.elb.eu-west-1.amazonaws.com" and test , that everything is working:

$ dig +short envoy-auto-mode.<redacted>
<first internet-facing NLB IP>
<second internet-facing NLB IP>

$ curl -s -o /dev/null -w "%{http_code}\n"  envoy-auto-mode.<redacted>
200

$ curl -v envoy-auto-mode.<redacted>
* Host envoy-auto-mode.<redacted>:80 was resolved.
* IPv6: (none)
* IPv4: <first internet-facing NLB IP>, <second internet-facing NLB IP>
*   Trying <first internet-facing NLB IP>...
* Connected to envoy-auto-mode.<redacted> (<first internet-facing NLB IP>) port 80
> GET / HTTP/1.1
> Host: envoy-auto-mode.<redacted>
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Thu, 12 Mar 2026 13:55:15 GMT
< content-length: 491
< 
{
 "path": "/",
 "host": "envoy-auto-mode.<redacted>",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Envoy-External-Address": [
   "192.168.13.66"
  ],
  "X-Forwarded-For": [
   "192.168.13.66"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "a10a3db4-5f97-430a-8ecc-9fcf0ba3b6e3"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-86c6c76f-nvnth"
* Connection #0 to host envoy-auto-mode.<redacted> left intact

$ kubectl get po backend-86c6c76f-nvnth -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP               NODE                  NOMINATED NODE   READINESS GATES
backend-86c6c76f-nvnth   1/1     Running   0          74m   192.168.189.50   i-0ad29068d6848bc07   <none>           <none>

The curl output shows, that the backend pod is used to serve the request.

Considerations for standard EKS cluster

This setup can be applied to a standard EKS cluster as well for customers which want to use a custom K8s Gateway API implementation.

The AWS Load Balancer Controller needs to be installed, but is it not used for K8s Gateway API implementation, but rather to provision the NLB which points to the Envoy Gateway service. The custom EnvoyProxy needs an additional annotation service.beta.kubernetes.io/aws-load-balancer-type: external: apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: custom-proxy-config namespace: default spec: provider: type: Kubernetes kubernetes: envoyService: annotations: service.beta.kubernetes.io/aws-load-balancer-type: external service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip service.beta.kubernetes.io/aws-load-balancer-name: envoy-gw-nlb

All other steps follow the implementation steps from above.
AWS
EXPERT
published a month ago541 views