Deploy AWS Load Balancer Controller¶
Deploy the AWS Load Balancer Controller on an existing EKS cluster to manage AWS Application Load Balancers (ALB) and Network Load Balancers (NLB) for Kubernetes Ingress and LoadBalancer Services. The controller provisions and configures an ALB or NLB based on the Ingress definition and keeps it in sync with cluster resources.
The controller can be deployed in two ways:
- With Helm charts.
- With Kubernetes manifests.
This runbook uses the Helm-based deployment.
Requirements¶
Ensure the following before starting:
- An EKS cluster is running and
kubectlis configured. eksctlis installed.- AWS CLI is installed and authenticated.
- Helm is installed.
- Access exists to create IAM policies and roles in the AWS account.
Set the following variables:
export CLUSTER_NAME=<eks-cluster-name>
export REGION=<region>
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export VPC_ID=$(aws eks describe-cluster \
--name "$CLUSTER_NAME" \
--query "cluster.resourcesVpcConfig.vpcId" \
--output text)
Note
VPC_ID is required for the Helm install. The controller cannot reliably discover the VPC from EC2 instance metadata (IMDS) in all EKS node configurations. Passing it explicitly avoids a startup crash.
Step 1: Associate the IAM OIDC provider¶
Associate the EKS cluster with an IAM OIDC provider:
Note
Treat this step as optional when the cluster is created with OIDC already enabled (for example, by Terraform or a manifest file). If the cluster already has an IAM OIDC provider, this command prints a message and performs no changes.
Step 2: Create the IAM policy¶
Download the official IAM policy for the AWS Load Balancer Controller:
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.14.1/docs/install/iam_policy.json
Create a customer-managed IAM policy from this file:
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
This sequence performs the following:
- Adds a new customer-managed IAM policy named
AWSLoadBalancerControllerIAMPolicyin the account - Stores the policy document that grants permissions to manage ALBs, NLBs, target groups, listeners, security groups, and related resources
- Leaves the Entities list empty initially (no roles are attached yet); this is expected at this stage
Step 3: Create the IAM role and ServiceAccount¶
Create the IAM role and the Kubernetes ServiceAccount for the controller with IRSA:
eksctl create iamserviceaccount \
--cluster=$CLUSTER_NAME \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--role-name aws-load-balancer-controller \
--override-existing-serviceaccounts \
--region $REGION \
--approve
This command performs four things:
- Creates an IAM role named
aws-load-balancer-controller - Creates a trust policy on that role that allows the
aws-load-balancer-controllerServiceAccount inkube-systemto assume it via the cluster's OIDC provider - Attaches the
AWSLoadBalancerControllerIAMPolicypolicy to this IAM role - Creates (or updates) the
aws-load-balancer-controllerServiceAccount inkube-systemand adds theeks.amazonaws.com/role-arnannotation pointing to this IAM role
The --override-existing-serviceaccounts flag ensures that if a ServiceAccount with the same name already exists, its metadata (including the IRSA annotation) is updated instead of leaving it unchanged.
Check the ServiceAccount:
kubectl get sa -n kube-system aws-load-balancer-controller
kubectl get sa -n kube-system aws-load-balancer-controller -o yaml | grep role-arn
Expect to see an annotation similar to:
Step 4: Deploy the controller with Helm¶
Add the EKS charts repository and update it:
Deploy the AWS Load Balancer Controller chart and reuse the existing ServiceAccount:
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--namespace kube-system \
--set clusterName="$CLUSTER_NAME" \
--set region="$REGION" \
--set vpcId="$VPC_ID" \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set controllerConfig.featureGates.NLBGatewayAPI=true \
--set controllerConfig.featureGates.ALBGatewayAPI=true \
--version 1.14.0
This Helm release is configured as follows:
serviceAccount.create=falseprevents Helm from creating a new ServiceAccount; the release uses the ServiceAccount created byeksctl.serviceAccount.name=aws-load-balancer-controllerselects that ServiceAccount explicitly.clusterName="$CLUSTER_NAME"passes the EKS cluster name so the controller can tag and manage AWS resources correctly.region="$REGION"passes the AWS region explicitly so the controller does not rely on IMDS for region discovery.vpcId="$VPC_ID"passes the VPC ID explicitly. Without this, the controller attempts to fetch the VPC from EC2 instance metadata, which can fail with acontext deadline exceedederror in some EKS node configurations, causing aCrashLoopBackOff.controllerConfig.featureGates.NLBGatewayAPI=trueenables the Gateway API support for Network Load Balancers.controllerConfig.featureGates.ALBGatewayAPI=trueenables the Gateway API support for Application Load Balancers.
Step 5: Verify the deployment¶
Check the controller deployment:
A healthy deployment typically looks like:
Check the ServiceAccount and annotation:
kubectl get sa -n kube-system aws-load-balancer-controller
kubectl get sa -n kube-system aws-load-balancer-controller -o yaml | grep role-arn
Check the webhook service has endpoints:
Check controller logs for errors:
Troubleshooting¶
CrashLoopBackOff — failed to get VPC ID from instance metadata¶
Symptom:
{"level":"error","logger":"setup","msg":"unable to initialize AWS cloud",
"error":"failed to get VPC ID: failed to fetch VPC ID from instance metadata:
error in fetching vpc id through ec2 metadata: get mac metadata: operation error
ec2imds: GetMetadata, canceled, context deadline exceeded"}
Cause: The controller tries to discover the VPC ID by calling the EC2 Instance Metadata Service (IMDS). This can time out when:
- The node group has IMDS hop limit set to 1 (default for some launch templates), which blocks containerised processes from reaching the metadata endpoint.
- The node security group or network ACLs block the metadata IP (
169.254.169.254).
Fix: Pass the VPC ID and region explicitly in the Helm install so the controller does not use IMDS for discovery:
helm upgrade --install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName="$CLUSTER_NAME" \
--set region="$REGION" \
--set vpcId="$VPC_ID" \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set controllerConfig.featureGates.NLBGatewayAPI=true \
--set controllerConfig.featureGates.ALBGatewayAPI=true \
--version 1.14.0
Quick sequence¶
export CLUSTER_NAME=<eks-cluster-name>
export REGION=<eks-cluster-region>
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export VPC_ID=$(aws eks describe-cluster \
--name "$CLUSTER_NAME" \
--query "cluster.resourcesVpcConfig.vpcId" \
--output text)
eksctl utils associate-iam-oidc-provider \
--region $REGION \
--cluster "$CLUSTER_NAME" \
--approve
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.14.1/docs/install/iam_policy.json
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
eksctl create iamserviceaccount \
--cluster=$CLUSTER_NAME \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--role-name aws-load-balancer-controller \
--override-existing-serviceaccounts \
--region $REGION \
--approve
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 \
--namespace kube-system \
--set clusterName="$CLUSTER_NAME" \
--set region="$REGION" \
--set vpcId="$VPC_ID" \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set controllerConfig.featureGates.NLBGatewayAPI=true \
--set controllerConfig.featureGates.ALBGatewayAPI=true \
--version 1.14.0
sleep 30
kubectl get deploy -n kube-system aws-load-balancer-controller
kubectl get sa -n kube-system aws-load-balancer-controller
kubectl get sa -n kube-system aws-load-balancer-controller -o yaml | grep role-arn
kubectl get endpoints -n kube-system aws-load-balancer-webhook-service