Amazon EKS: Managed Node Group, IRSA, and ALB Ingress Controller
Scenario
Your platform team is migrating workloads to Kubernetes on EKS. You need a production-ready cluster with managed node groups (so AWS handles node upgrades), IRSA (IAM Roles for Service Accounts) so pods can access AWS services without static credentials, and the AWS Load Balancer Controller to provision ALBs automatically from Kubernetes Ingress resources.
- Create an EKS cluster with private endpoint access. Create an IAM role for the cluster control plane and attach the AmazonEKSClusterPolicy managed policy.
- Create a Managed Node Group with t3.medium instances (min 2, max 5, desired 2). Create the node IAM role with AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, and AmazonEC2ContainerRegistryReadOnly.
- Create an OIDC provider for the cluster using the cluster's OIDC issuer URL and the TLS thumbprint.
- Create an IAM role for the AWS Load Balancer Controller using IRSA: trust policy scoped to the aws-load-balancer-controller service account and the ALB controller policy.
- Output the cluster endpoint, kubeconfig command, OIDC provider ARN, and the LBC role ARN.
Additional Context
IRSA (IAM Roles for Service Accounts) eliminates the need to store AWS credentials in pods. An OIDC provider bridges Kubernetes service accounts and AWS IAM — EKS issues a signed JWT token to each pod, and AWS STS exchanges it for temporary credentials via AssumeRoleWithWebIdentity. The trust policy's condition scopes the role to a specific namespace and service account.
The TLS thumbprint for the OIDC provider can be retrieved programmatically with aws eks describe-cluster ... | openssl s_client | openssl x509 -fingerprint -sha1, or you can use the static thumbprint 9e99a48a9960b14926bb7f3b02e22da2b0ab7280 which valid for OIDC endpoints backed by ACM.
variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "cluster_name" { type = string; default = "prod-eks-cluster" }
variable "cluster_version" { type = string; default = "1.30" }
variable "vpc_id" { type = string }
variable "private_subnets" { type = list(string); description = "Private subnets for node groups." }
variable "public_subnets" { type = list(string); description = "Public subnets for the control plane ENIs." }
variable "environment" { type = string; default = "prod" }main.tf
provider "aws" { region = var.aws_region }
locals { tags = { Environment = var.environment, ManagedBy = "terraform" } }
data "aws_caller_identity" "current" {}
# EKS Cluster IAM role.
data "aws_iam_policy_document" "eks_assume" {
statement { effect = "Allow"; actions = ["sts:AssumeRole"]
principals { type = "Service"; identifiers = ["eks.amazonaws.com"] } }
}
resource "aws_iam_role" "cluster" {
name = "${var.cluster_name}-cluster-role"
assume_role_policy = data.aws_iam_policy_document.eks_assume.json
}
resource "aws_iam_role_policy_attachment" "cluster" {
role = aws_iam_role.cluster.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}
# EKS Cluster.
resource "aws_eks_cluster" "main" {
name = var.cluster_name
role_arn = aws_iam_role.cluster.arn
version = var.cluster_version
vpc_config {
subnet_ids = concat(var.public_subnets, var.private_subnets)
endpoint_private_access = true
endpoint_public_access = true # restrict to your CIDR in production
security_group_ids = [aws_security_group.cluster.id]
}
enabled_cluster_log_types = ["api","audit","authenticator","controllerManager","scheduler"]
depends_on = [aws_iam_role_policy_attachment.cluster]
tags = local.tags
}
# Cluster security group.
resource "aws_security_group" "cluster" {
name = "${var.cluster_name}-cluster-sg"
vpc_id = var.vpc_id
egress { from_port = 0; to_port = 0; protocol = "-1"; cidr_blocks = ["0.0.0.0/0"] }
tags = merge(local.tags, { Name = "${var.cluster_name}-cluster-sg" })
}
# Node Group IAM role — EC2 nodes assume this role.
data "aws_iam_policy_document" "node_assume" {
statement { effect = "Allow"; actions = ["sts:AssumeRole"]
principals { type = "Service"; identifiers = ["ec2.amazonaws.com"] } }
}
resource "aws_iam_role" "node" {
name = "${var.cluster_name}-node-role"
assume_role_policy = data.aws_iam_policy_document.node_assume.json
}
resource "aws_iam_role_policy_attachment" "node_worker" {
role = aws_iam_role.node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "node_cni" {
role = aws_iam_role.node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "node_ecr" {
role = aws_iam_role.node.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
# Managed Node Group.
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.cluster_name}-node-group"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.private_subnets
instance_types = ["t3.medium"]
ami_type = "AL2_x86_64"
disk_size = 50
scaling_config { desired_size = 2; min_size = 2; max_size = 5 }
update_config { max_unavailable = 1 }
depends_on = [aws_iam_role_policy_attachment.node_worker,
aws_iam_role_policy_attachment.node_cni,
aws_iam_role_policy_attachment.node_ecr]
tags = local.tags
}
# OIDC Provider — enables IRSA (IAM Roles for Service Accounts).
data "tls_certificate" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
tags = local.tags
}
# IAM Role for AWS Load Balancer Controller — uses IRSA.
locals {
oidc_id = replace(aws_iam_openid_connect_provider.eks.url, "https://", "")
}
data "aws_iam_policy_document" "lbc_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals { type = "Federated"; identifiers = [aws_iam_openid_connect_provider.eks.arn] }
condition {
test = "StringEquals"
variable = "${local.oidc_id}:sub"
values = ["system:serviceaccount:kube-system:aws-load-balancer-controller"]
}
condition {
test = "StringEquals"
variable = "${local.oidc_id}:aud"
values = ["sts.amazonaws.com"]
}
}
}
resource "aws_iam_role" "lbc" {
name = "${var.cluster_name}-lbc-role"
assume_role_policy = data.aws_iam_policy_document.lbc_assume.json
}
# Download the AWS-managed policy for the load balancer controller.
resource "aws_iam_policy" "lbc" {
name = "${var.cluster_name}-alb-controller-policy"
policy = file("${path.module}/alb-controller-policy.json") # download from AWS docs
}
resource "aws_iam_role_policy_attachment" "lbc" {
role = aws_iam_role.lbc.name
policy_arn = aws_iam_policy.lbc.arn
}outputs.tf
output "cluster_endpoint" { value = aws_eks_cluster.main.endpoint }
output "cluster_ca" { value = aws_eks_cluster.main.certificate_authority[0].data; sensitive = true }
output "oidc_provider_arn" { value = aws_iam_openid_connect_provider.eks.arn }
output "lbc_role_arn" { value = aws_iam_role.lbc.arn }
output "kubeconfig_command" {
value = "aws eks update-kubeconfig --region ${var.aws_region} --name ${var.cluster_name}"
}Workflow Commands
terraform init && terraform apply -var="vpc_id=vpc-xxxx" \
-var='private_subnets=["subnet-a","subnet-b"]' \
-var='public_subnets=["subnet-pub-a","subnet-pub-b"]'
# Configure kubectl
$(terraform output -raw kubeconfig_command)
# Verify nodes
kubectl get nodes -o wide
# Install AWS Load Balancer Controller via Helm (uses the IRSA role)
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=$(terraform output -raw cluster_name 2>/dev/null || echo 'prod-eks-cluster') \
--set serviceAccount.create=true \
--set serviceAccount.name=aws-load-balancer-controller \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=$(terraform output -raw lbc_role_arn)
terraform destroy -var="vpc_id=..." -var='private_subnets=["..."]' -var='public_subnets=["..."]'Apply complete! Resources: 12 added, 0 changed, 0 destroyed. Outputs: cluster_ca = <sensitive> cluster_endpoint = "https://ABCDEF1234567890.gr7.us-east-1.eks.amazonaws.com" kubeconfig_command = "aws eks update-kubeconfig --region us-east-1 --name prod-eks-cluster" lbc_role_arn = "arn:aws:iam::123456789012:role/prod-eks-cluster-lbc-role" oidc_provider_arn = "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/ABCDEF..."

