How to Use AWS ALB Controller With Kubernetes Gateway API Using Pod Identity?
quoted summary
I wanted to put my 2048 game on EKS using the new Gateway API instead of the old Ingress. Sounds easy, but there are many small pieces: CRDs, a Helm chart, an IAM role, a service account, and one association to tie them all. Miss one name and the controller just sits there quietly doing nothing. So here is how I did it, in the order that actually worked for me.
What We Are Building
Gateway API objects go in, the ALB controller notices them, and a real AWS load balancer comes out.
flowchart LR
me["You: kubectl apply"] --> gw["Gateway + HTTPRoute"]
gw --> ctrl["ALB Controller"]
ctrl --> alb["AWS ALB"]
alb --> app["2048 Game Pods"]
The controller needs AWS permission to create that ALB. I am giving it permission using Pod Identity. This is the chain I will build:
flowchart LR
pod["Controller Pod"] --> sa["ServiceAccount"]
sa --> assoc["Pod Identity Association"]
assoc --> role["IAM Role"]
role --> policy["IAM Policy"]
Names matter a lot here. Keep this table open, everything below uses it:
| Thing | Name | Namespace |
|---|---|---|
| EKS cluster | 2048-game | - |
| ALB controller (Helm release) | aws-load-balancer-controller | kube-system |
| ServiceAccount | aws-alb-svc-acc | kube-system |
| IAM role | aws-alb-role | - |
| IAM policy | aws-alb-policy | - |
| The game | - | game-2048 |
Get The Cluster Ready
cat > managed-ng-cluster.yaml << EOF
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: 2048-game
region: ap-south-1
managedNodeGroups:
- name: managed-ng-1
minSize: 1
maxSize: 2
desiredCapacity: 1
instanceType: t3.medium
privateNetworking: true
vpc:
cidr: "10.0.0.0/24"
autoModeConfig:
enabled: false
EOF
eksctl create cluster -f managed-ng-cluster.yamlGo drink coffee (or may be you are tea addict), it takes a while.
Gateway API And ALB CRDs
The controller will not understand Gateway or HTTPRoute unless these CRDs are already there. These are from the prerequisites of the docs:
# gateway api crds
kubectl apply --server-side=true -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.0/standard-install.yaml
# alb gateway api crds
kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/config/crd/gateway/gateway-crds.yamlImportant
Install CRDs before the controller. The controller checks for CRDs only when it starts. If they come late, it silently disables the Gateway feature (you will see this in the logs section below).
Setting Up Pod Identity
I have written how Pod Identity works in another post, so here it is only the setup part.
Install The Pod Identity Addon
aws eks create-addon --cluster-name 2048-game --addon-name eks-pod-identity-agentCheck it:
aws eks list-addons --cluster-name 2048-game{
"addons": [
"coredns",
"eks-pod-identity-agent",
"kube-proxy",
"metrics-server",
"vpc-cni"
]
}Policy For The Controller
AWS already wrote the big IAM policy for us, no need to type it by hand. The URL says aws-alb-ingress-controller but the same policy works for Gateway API too.
curl -fsSLo iam-policy-aws-alb.json https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/main/docs/install/iam_policy.json
aws iam create-policy --policy-name aws-alb-policy --policy-document file://iam-policy-aws-alb.jsonI will keep my account ID in a variable so I don’t copy-paste it everywhere:
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)Role For The Controller
Trust policy says “EKS Pod Identity service can use this role”:
cat > trust_relation.json << EOF
{
"Version":"2012-10-17",
"Statement": [
{
"Sid": "AllowEksAuthToAssumeRoleForPodIdentity",
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
}
EOF
aws iam create-role --role-name aws-alb-role --assume-role-policy-document file://trust_relation.json
aws iam attach-role-policy --role-name aws-alb-role --policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/aws-alb-policy"ServiceAccount
cat > aws-alb-svc-acc.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: aws-alb-svc-acc
namespace: kube-system
EOF
kubectl apply -f aws-alb-svc-acc.yamlVerify:
kubectl get serviceaccount -n kube-system | grep albaws-alb-svc-acc 0 5sTie Them Together
Role is ready, service account is ready, but they don’t know each other yet. The association introduces them:
aws eks create-pod-identity-association \
--cluster-name 2048-game \
--role-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:role/aws-alb-role" \
--namespace kube-system \
--service-account aws-alb-svc-acc{
"association": {
"clusterName": "2048-game",
"namespace": "kube-system",
"serviceAccount": "aws-alb-svc-acc",
"roleArn": "arn:aws:iam::123456789012:role/aws-alb-role",
"associationId": "a-mhzuyj2a4axiclt3f"
}
}Warning
Namespace and service account name in the association must match exactly with the real service account. One wrong letter and the pod will start with no AWS credentials, and you won’t get any loud error.
Install The ALB Controller With Helm
Now the controller. I already created the service account myself, so I tell the chart not to create one, and give it the same name:
helm repo add eks https://aws.github.io/eks-charts
helm repo update eks
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=2048-game \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-alb-svc-accNAME: aws-load-balancer-controller
LAST DEPLOYED: Tue Oct 20 23:35:37 2026
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!Check everything:
kubectl get crds | grep elbv2.k8s.aws
helm status aws-load-balancer-controller -n kube-system
kubectl get deploy/aws-load-balancer-controller -n kube-systemNote
Not using Pod Identity (or IRSA)? Then skip the last two --set lines and let the chart create its own service account:
helm install aws-load-balancer-controller eks/aws-load-balancer-controller --set clusterName=2048-game -n kube-systemBut then you have to give AWS permission to it some other way, otherwise it cannot create the ALB.
Tip
If the controller pods crash with a complaint about VPC ID or metadata, pass them yourself:
VPC_ID=$(aws eks describe-cluster --name 2048-game --query cluster.resourcesVpcConfig.vpcId --output text)
helm upgrade aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --reuse-values --set region=ap-south-1 --set vpcId=$VPC_IDApply The Game
Everything is ready, now throw the manifests of the game in:
kubectl apply -f 2048-game/Check The Logs
The controller logs tell you the truth:
kubectl logs -n kube-system deploy/aws-load-balancer-controller --all-pods=true --tail=200Bad one, CRDs were missing when the controller started:
{"level":"info","logger":"setup","msg":"Disabling ALBGatewayAPI: missing required CRDs","missing":["Gateway","GatewayClass","HTTPRoute","GRPCRoute","ReferenceGrant"]}If you see this, install the CRDs and restart the controller:
kubectl rollout restart deploy/aws-load-balancer-controller -n kube-systemGood one, controller found the gateway and you get an address:
kubectl get gateway -n game-2048NAME CLASS ADDRESS PROGRAMMED AGE
my-alb-gateway aws-alb-gateway-class internal-k8s-game2048-myalbgat-c91ec960fa-654476805.ap-south-1.elb.amazonaws.com Unknown 21mInternal Or Internet-Facing?
Did you notice that internal- in the address? By default the load balancer is internal, so you can’t open it from your laptop. For a public one, make a LoadBalancerConfiguration:
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: external-lb
namespace: game-2048
spec:
scheme: internet-facingand point your Gateway to it:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-alb-gateway
namespace: game-2048
spec:
gatewayClassName: aws-alb-gateway-class
infrastructure:
parametersRef:
kind: LoadBalancerConfiguration
name: external-lb # here
group: gateway.k8s.aws
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
flowchart LR
gw["Gateway"] -->|parametersRef| lbc["LoadBalancerConfiguration"]
lbc -->|scheme| alb["internet-facing ALB"]
Note
An already created ALB can’t switch between internal and internet-facing, so decide this before the first apply.
Clean Up
Delete the game first, so the controller removes the ALB. If you delete the cluster first, the ALB stays behind and keeps charging you.
kubectl delete -f 2048-game/
helm uninstall aws-load-balancer-controller -n kube-system
eksctl delete cluster -f managed-ng-cluster.yaml
aws iam detach-role-policy --role-name aws-alb-role --policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/aws-alb-policy"
aws iam delete-role --role-name aws-alb-role
aws iam delete-policy --policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/aws-alb-policy"