LAB42_AWS_Amazon EKS: Managed Node Group, IRSA, and ALB Ingress Controller

Amazon EKS: Managed Node Group, IRSA, and ALB Ingress Controller

☁ AWS 🔥 Advanced EKS Managed Node Groups IRSA OIDC Provider AWS Load Balancer 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.

Your Objectives
  • 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=["..."]'
✓ Expected Output After Apply
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..."