
Part 1 stood up the EKS cluster itself, the control plane, a managed node group, and kubectl connected correctly. A running cluster can host workloads, but it has no way yet to receive traffic from the outside world. This post closes that gap: installing and configuring the AWS Load Balancer Controller, which lets a standard Kubernetes Ingress resource provision and manage a real, AWS-native Application Load Balancer.
By the end of this post, the cluster is fully prepared to receive external HTTP/HTTPS traffic; nothing user-facing gets deployed yet (that’s Part 3), but every piece of plumbing required to expose it securely will be in place.
What This Controller Actually Does
Kubernetes has a built-in resource type called Ingress, which describes how external traffic should reach services running inside the cluster, but Ingress on its own is just a specification. Something has to watch for Ingress resources and actually make them real. The AWS Load Balancer Controller is that something: it runs inside the cluster, watches for Ingress objects, and translates them into actual AWS resources ALBs, target groups, and listener rules created and kept in sync automatically through the AWS API.
Getting there requires four pieces, in order: an IAM policy defining what the controller can do, an OIDC identity provider linking the cluster to IAM, a Kubernetes service account tied to that IAM identity, and finally the controller itself, installed via Helm.
Step 1: Downloading the IAM Policy for the Controller
cd curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json ls iam_policy.json
This policy is maintained directly by the aws-load-balancer-controller project itself, and it grants exactly the permissions the controller needs to create, manage, and delete ALBs, target groups, listeners, and related load-balancing resources on the cluster’s behalf.
Worth being deliberate about: this command pulls the policy from the main branch, which reflects the project’s latest in-development state, not necessarily the exact permission set the specific controller version you install in Step 5 requires. For a lab environment, main is fine. If you want this to be fully reproducible later, pin to a specific release tag instead, for example, replacing refs/heads/main with refs/tags/v2.x.x matching the Helm chart version you plan to install so the policy and the controller version stay in lockstep rather than drifting independently over time.
Step 2: Creating the IAM Policy
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
Confirm in the console under IAM → Policies that AWSLoadBalancerControllerIAMPolicy now exists.
Step 3: Configuring the OIDC Identity Provider
This lets Kubernetes service accounts assume IAM roles directly, using the same OIDC federation pattern used elsewhere in this project for GitHub Actions, but applied here to link the EKS cluster itself to IAM rather than an external CI system.
# Confirm the cluster's OIDC issuer URL exists before associating it aws eks describe-cluster --name kloudgov-cluster --query "cluster.identity.oidc.issuer" --output text eksctl utils associate-iam-oidc-provider --region us-east-1 --cluster kloudgov-cluster --approve # Confirm the association succeeded aws eks describe-cluster --name kloudgov-cluster --query "cluster.identity.oidc.issuer" --output text
If the first command returns a URL starting with https://oidc.eks., the cluster already has an OIDC issuer; every EKS cluster gets one automatically on creation. associate-iam-oidc-provider registers that issuer as a trusted identity provider in IAM, letting Kubernetes service accounts assume IAM roles securely without static credentials living anywhere in the cluster.
Step 4: Creating a Service Account and IAM Role for the Controller
This single eksctl command does three things at once: creates a Kubernetes service account named aws-load-balancer-controller in the kube-system namespace, creates a dedicated IAM role (AmazonEKSLoadBalancerControllerRole) carrying the policy from Step 2, and links the two together via the OIDC provider from Step 3.
Replace xxxxxxxxxxxx with your actual AWS account ID before running this:
eksctl create iamserviceaccount \ --region us-east-1 \ --cluster kloudgov-cluster \ --namespace kube-system \ --name aws-load-balancer-controller \ --role-name AmazonEKSLoadBalancerControllerRole \ --attach-policy-arn arn:aws:iam::xxxxxxxxxxxx:policy/AWSLoadBalancerControllerIAMPolicy \ --approve
A syntax note worth flagging directly: every flag above uses –flag value (a space, no equals sign) consistently. Mixing –flag= value an equals sign and a space is invalid bash syntax and will cause the command to fail or misinterpret the following argument.
Validating the IAM Role
aws iam get-role --role-name AmazonEKSLoadBalancerControllerRole
A successful response, with the role’s ARN and trust relationship, confirms it was created correctly.
Validating the Service Account
kubectl get serviceaccount aws-load-balancer-controller -n kube-system # List everything in the kube-system namespace, for broader context kubectl get serviceaccount -n kube-system
Validating the Link Between Them
kubectl get serviceaccount aws-load-balancer-controller -n kube-system -o yaml
Look for an annotations section containing:
annotations: eks.amazonaws.com/role-arn: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/AmazonEKSLoadBalancerControllerRole
This annotation is the mechanism connecting the two; it tells EKS to inject temporary, OIDC-derived AWS credentials into any pod using this service account, scoped to the IAM role’s permissions, with no static keys involved.
Step 5: Installing the Controller with Helm
# Add the AWS EKS charts repository to Helm helm repo add eks https://aws.github.io/eks-charts # Update the local Helm repository cache helm repo update eks # Install the controller, using the service account created in Step 4 helm install aws-load-balancer-controller eks/aws-load-balancer-controller \ -n kube-system \ --set clusterName=kloudgov-cluster \ --set serviceAccount.create=false \ --set serviceAccount.name=aws-load-balancer-controller
serviceAccount.create=false matters here; without it, the Helm chart would create a new service account instead of using the one already wired to the correct IAM role from Step 4. Passing the existing service account by name tells the chart to reuse exactly what was just configured, rather than duplicating (and potentially misconfiguring) it.
This chart defaults to 2 replicas for the controller’s deployment, meaning two controller pods run simultaneously for high availability of the controller itself, independent of your application’s availability. You can adjust this with –set replicaCount=NUMBER if you want a different count, but two is a sensible default for anything beyond pure experimentation.
Step 6: Checking the Controller’s Status
kubectl get deployment -n kube-system aws-load-balancer-controller
Expected output:
NAME READY UP-TO-DATE AVAILABLE AGE aws-load-balancer-controller 2/2 2 2 84s
2/2 confirms both replicas are running and healthy. For a closer look at the full deployment spec, including the replica count setting itself:
kubectl get deployment aws-load-balancer-controller -n kube-system -o yaml
What’s Actually Ready Now
Nothing user-facing exists in the cluster yet: no application, no Ingress resource, nothing publicly reachable. What exists now is the capability: any Ingress resource created from this point forward will be picked up automatically by this controller and turned into a real ALB, with target groups and listener rules managed entirely through Kubernetes manifests rather than manual AWS console work. That’s the payoff: Part 4 cashes in on creating Ingress resources per state, each producing its own routing rules on a shared ALB, exactly mirroring the host-based routing pattern used for EC2 and ECS earlier in this project, just expressed through Kubernetes objects instead of Terraform resources.

Key Takeaways
The AWS Load Balancer Controller bridges Kubernetes’ abstract Ingress resource and AWS’s ALB infrastructure; without it, Ingress objects in an EKS cluster do nothing.
OIDC federation for EKS service accounts works on the same underlying principle as the GitHub Actions OIDC setup earlier in this project: short-lived, automatically issued credentials, with zero static keys stored anywhere.
serviceAccount.create=false in the Helm install is essential; it’s what makes the chart reuse the specific, correctly linked service account from Step 4 instead of creating an unlinked duplicate.
Pin the IAM policy JSON to a specific release tag rather than main; if reproducibility matters, a moving target for permissions is a subtle source of drift between the policy and the controller version actually installed.
Double-check flag syntax carefully when copying eksctl/kubectl commands from any source –flag= value (equals sign plus space) is a common, easy-to-miss typo that silently breaks a command.
With the controller running and correctly authorized, Part 3 shifts to the application itself: packaging KloudGov into a container image, publishing it to ECR, and deploying it onto this cluster as actual Kubernetes workloads.

