Skip to content

Implementing Kubernetes Gateway API using AWS Load Balancer Controller - part II: L7 (ALBGatewayAPI)

6 minute read
Content level: Expert
0

The open-source AWS Load Balancer Controller supports Gateway API in GA (General Available) since v3.0.0.

This two-part article series provides a comprehensive example of installing AWS Load Balancer Controller (LBC) with Gateway API support, using Pod Identity and two sample implementations using L4 (NLBGatewayAPI) and L7 ((ALBGatewayAPI) routing and sample AWS CLI and kubectl outputs for reference.

Background

The open-source AWS Load Balancer Controller supports Gateway API in GA (General Available) since v3.0.0.

This fact and customer demand for implementing a Gateway API using AWS Load Balancer Controller (LBC), led to the creation of this article.

Prerequisites

Follow installation steps from rePost article Implementing Kubernetes Gateway API using AWS Load Balancer Controller - part I: L4 (NLBGatewayAPI) including sections Prerequisites, AWS Load Balancer Controller installation and Common configurations.

L7 (ALBGatewayAPI) routing.

LBC comes with two controllers. L7 routing uses the controller gateway.k8s.aws/alb and provisions ALB.

First create the GatewayClass YAML: gwc-alb.yaml

apiVersion: gateway.networking.k8s.io/v1beta1
kind: GatewayClass
metadata:
  name: aws-alb-gateway-class
spec:
  controllerName: gateway.k8s.aws/alb

and apply it to the cluster:

$ kubectl apply -f gwc-alb.yaml
gatewayclass.gateway.networking.k8s.io/aws-alb-gateway-class created

$ kubectl get gc aws-alb-gateway-class   
NAME                    CONTROLLER            ACCEPTED   AGE
aws-alb-gateway-class   gateway.k8s.aws/alb   True       96s

Now create the Gateway resource which references the LoadBalancerConfiguration internet-facing-config we created earlier: gw-public-alb.yaml

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
  name: aws-alb-gateway
spec:
  gatewayClassName: aws-alb-gateway-class
  infrastructure:
    parametersRef:
      kind: LoadBalancerConfiguration
      name: internet-facing-config
      group: gateway.k8s.aws
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: Same

apply it to the cluster and check the ADDRESS. An ALB will be provisioned, which takes some time until PROGRAMMED shows True:

$ kubectl k apply -f gw-public-alb.yaml
gateway.gateway.networking.k8s.io/aws-alb-gateway created

$ kubectl get gateway aws-alb-gateway 
NAME              CLASS                   ADDRESS                                                                  PROGRAMMED   AGE
aws-alb-gateway   aws-alb-gateway-class   k8s-default-awsalbga-<redacted1>-<redacted2>.eu-west-1.elb.amazonaws.com   Unknown      14s

One can check the ALB using the following AWS elbv2 command for reference:

# ELB is an internet-facing ALB (Type: application)
$ aws elbv2 describe-load-balancers --names k8s-default-awsalbga-<redacted1>
{
    "LoadBalancers": [
        {
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>",
            "DNSName": "k8s-default-awsalbga-<redacted1>-<redacted2>.eu-west-1.elb.amazonaws.com",
            "CanonicalHostedZoneId": "Z32O12XQLNTSW2",
            "CreatedTime": "2026-03-16T08:16:08.190000+00:00",
            "LoadBalancerName": "k8s-default-awsalbga-<redacted1>",
            "Scheme": "internet-facing",
            "VpcId": "vpc-<redacted>",
            "State": {
                "Code": "active"
            },
            "Type": "application",
            "AvailabilityZones": [
                {
                    "ZoneName": "eu-west-1b",
...

This ALB does not yet have a listener provisioned, even though it is referenced already as spec.listeners.name: http, because this still requires a HTTPRoutes resource:

$ aws elbv2 describe-listeners --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>
{
    "Listeners": []
}

Note the API version of HTTPRoutes (and others), which is already in GA (generally available) status !!!

$  kubectl api-resources | grep -w httproutes
httproutes                                                 gateway.networking.k8s.io/v1           true         HTTPRoute

$  kubectl api-resources | grep -w gateway.networking.k8s.io/v1 
backendtlspolicies                  btlspolicy             gateway.networking.k8s.io/v1           true         BackendTLSPolicy
gatewayclasses                      gc                     gateway.networking.k8s.io/v1           false        GatewayClass
gateways                            gtw                    gateway.networking.k8s.io/v1           true         Gateway
grpcroutes                                                 gateway.networking.k8s.io/v1           true         GRPCRoute
httproutes                                                 gateway.networking.k8s.io/v1           true         HTTPRoute

The HTTPRoutes references the backend service created earlier and the resource YAML looks like: httproute.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-backend-aws
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: aws-alb-gateway
    sectionName: http
  hostnames:
    - "aws-k8s-gw-alb.<redacted domain name>"
  rules:
    - backendRefs:
        - group: ""
          kind: Service
          name: backend
          port: 3000
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /

and apply it to the cluster:

$ kubectl apply -f  httproute.yaml 
httproute.gateway.networking.k8s.io/http-backend-aws created

Now check the ALB for related AWS resources listener, rules, target groups and target health using the following AWS elbv2 commands for reference:

# describe ALB listener, which listens on port 80 and has a default HTTP response 404 Not Found
$ aws elbv2 describe-listeners --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>
{
    "Listeners": [
        {
            "ListenerArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:listener/app/k8s-default-awsalbga-<redacted1>/<redacted2>/<redacted3>",
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>",
            "Port": 80,
            "Protocol": "HTTP",
            "DefaultActions": [
                {
                    "Type": "fixed-response",
                    "Order": 1,
                    "FixedResponseConfig": {
                        "StatusCode": "404",
                        "ContentType": "text/plain"
                    }
                }
            ]
        }
    ]
}

# describe ALB listener forward rule to target group k8s-default-httpback-<redacted>
$ aws elbv2 describe-rules --listener-arn arn:aws:elasticloadbalancing:eu-west-1:<redacted>:listener/app/k8s-default-awsalbga-<redacted1>/<redacted2>/<redacted3>
{
    "Rules": [
        {
            "RuleArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:listener-rule/app/k8s-default-awsalbga-<redacted1>/<redacted2>/<redacted3>",
            "Priority": "1",
            "Conditions": [
                {
                    "Field": "host-header",
                    "Values": [
                        "aws-k8s-gw-alb.<redacted domain name>"
                    ],
                    "HostHeaderConfig": {
                        "Values": [
                            "aws-k8s-gw-alb.<redacted domain name>"
                        ]
                    }
                },
                {
                    "Field": "path-pattern",
                    "Values": [
                        "/*"
                    ],
                    "PathPatternConfig": {
                        "Values": [
                            "/*"
                        ]
                    }
                }
            ],
            "Actions": [
                {
                    "Type": "forward",
                    "TargetGroupArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:targetgroup/k8s-default-httpback-<redacted>/<redacted>",
                    "Order": 1,
                    "ForwardConfig": {
                        "TargetGroups": [
                            {
                                "TargetGroupArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:targetgroup/k8s-default-httpback-<redacted>/<redacted>",
                                "Weight": 1
                            }
                        ],
                        "TargetGroupStickinessConfig": {
                            "Enabled": false
                        }
                    }
                }
            ],
            "IsDefault": false,
            "Transforms": []
        },
        {
            "RuleArn": "arn:aws:elasticloadbalancing:eu-west-1:618668373247:listener-rule/app/k8s-default-awsalbga-<redacted1>/<redacted2>/<redacted3>/<redacted4>",
            "Priority": "default",
            "Conditions": [],
            "Actions": [
                {
                    "Type": "fixed-response",
                    "Order": 1,
                    "FixedResponseConfig": {
                        "StatusCode": "404",
                        "ContentType": "text/plain"
                    }
                }
            ],
            "IsDefault": true,
            "Transforms": []
        }
    ]
}

# describe ALB target group, which is of targetType: ip and is using targets from backend service on port 3000 using HTTP
$ aws elbv2 describe-target-groups --load-balancer-arn arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>
{
    "TargetGroups": [
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:targetgroup/k8s-default-httpback-<redacted>/<redacted>",
            "TargetGroupName": "k8s-default-httpback-<redacted>",
            "Protocol": "HTTP",
            "Port": 3000,
            "VpcId": "vpc-<redacted>",
            "HealthCheckProtocol": "HTTP",
            "HealthCheckPort": "traffic-port",
            "HealthCheckEnabled": true,
            "HealthCheckIntervalSeconds": 15,
            "HealthCheckTimeoutSeconds": 5,
            "HealthyThresholdCount": 3,
            "UnhealthyThresholdCount": 3,
            "HealthCheckPath": "/",
            "Matcher": {
                "HttpCode": "200-399"
            },
            "LoadBalancerArns": [
                "arn:aws:elasticloadbalancing:eu-west-1:<redacted>:loadbalancer/app/k8s-default-awsalbga-<redacted1>/<redacted2>"
            ],
            "TargetType": "ip",
            "ProtocolVersion": "HTTP1",
            "IpAddressType": "ipv4"
        }
    ]
}

# targets are the backend deployment pod(s)
$ aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:eu-west-1:<redacted>:targetgroup/k8s-default-httpback-<redacted>/<redacted>
{
    "TargetHealthDescriptions": [
        {
            "Target": {
                "Id": "192.168.165.118",
                "Port": 3000,
                "AvailabilityZone": "eu-west-1c"
            },
            "HealthCheckPort": "3000",
            "TargetHealth": {
                "State": "healthy"
            },
            "AdministrativeOverride": {
                "State": "no_override",
                "Reason": "AdministrativeOverride.NoOverride",
                "Description": "No override is currently active on target"
            }
        }
    ]
}

# backend K8s pod
$ kubectl get pod backend-86c6c76f-vwbxt -o wide
NAME                     READY   STATUS    RESTARTS   AGE    IP                NODE                                           NOMINATED NODE   READINESS GATES
backend-86c6c76f-vwbxt   1/1     Running   0          4d8h   192.168.165.118   ip-<redacted>.eu-west-1.compute.internal   <none>           <none>

Finally create a Route53 alias record pointing to the NLB, check it and use curl to generate some traffic:

$ aws route53 list-resource-record-sets --hosted-zone-id <public hosted zoneID> --query 'ResourceRecordSets[?Name==`aws-k8s-gw-alb.<redacted domain name>`]'
[
    {
        "Name": "aws-k8s-gw-alb.<redacted domain name>",
        "Type": "A",
        "AliasTarget": {
            "HostedZoneId": "Z32O12XQLNTSW2",
            "DNSName": "dualstack.k8s-default-awsalbga-<redacted1>-<redacted2>.eu-west-1.elb.amazonaws.com.",
            "EvaluateTargetHealth": true
        }
    }
]

$ dig +short aws-k8s-gw-alb.<redacted domain name>
<public ALB IP1>
<public ALB IP2>
<public ALB IP3>

$ curl -s -o /dev/null -w "%{http_code}\n" aws-k8s-gw-alb.<redacted domain name>
200

$ curl aws-k8s-gw-alb.waltju.people.<redacted domain name>
{
 "path": "/",
 "host": "aws-k8s-gw-alb.<redacted domain name>",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Amzn-Trace-Id": [
   "Root=1-69b7d989-5d038fe15420da4844429c41"
  ],
  "X-Forwarded-For": [
   "95.90.210.65"
  ],
  "X-Forwarded-Port": [
   "80"
  ],
  "X-Forwarded-Proto": [
   "http"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-86c6c76f-vwbxt"
}

$ curl -v aws-k8s-gw-alb.<redacted domain name>
* Host aws-k8s-gw-alb.<redacted domain name>.:80 was resolved.
* IPv6: (none)
* IPv4: <public ALB IP1>, <public ALB IP2>, <public ALB IP3>
*   Trying <public ALB IP1>:80...
* Connected to aws-k8s-gw-alb.<redacted domain name> (<public ALB IP1>) port 80
> GET / HTTP/1.1
> Host: aws-k8s-gw-alb.<redacted domain name>
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Mon, 16 Mar 2026 10:36:18 GMT
< Content-Type: application/json
< Content-Length: 477
< Connection: keep-alive
< X-Content-Type-Options: nosniff
< 
{
 "path": "/",
 "host": "aws-k8s-gw-alb.<redacted domain name>",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Amzn-Trace-Id": [
   "Root=1-69b7dd22-51c4f8fc6968fd470d1eef71"
  ],
  "X-Forwarded-For": [
   "95.90.210.65"
  ],
  "X-Forwarded-Port": [
   "80"
  ],
  "X-Forwarded-Proto": [
   "http"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-86c6c76f-vwbxt"
* Connection #0 to host aws-k8s-gw-alb.<redacted domain name> left intact
}

This concludes the two-part series.

AWS
EXPERT
published 2 months ago425 views