Amazon EKS — Terraform Add-ons & IRSA Module
EKS add-ons and IAM Roles for Service Accounts (IRSA) are managed together because add-ons often need IRSA for AWS API access.
Add-ons Module — variables.tf
# modules/addons/variables.tf
variable "cluster_name" {
description = "EKS cluster name"
type = string
}
variable "oidc_provider" {
description = "OIDC provider (without https://)"
type = string
}
variable "oidc_issuer_url" {
description = "OIDC issuer URL"
type = string
}
Add-ons Module — main.tf
# modules/addons/main.tf
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
region = data.aws_region.current.name
}
# ── Core EKS Add-ons ──
resource "aws_eks_addon" "vpc_cni" {
cluster_name = var.cluster_name
addon_name = "vpc-cni"
resolve_conflicts_on_update = "OVERWRITE"
service_account_role_arn = aws_iam_role.vpc_cni.arn
}
resource "aws_eks_addon" "coredns" {
cluster_name = var.cluster_name
addon_name = "coredns"
resolve_conflicts_on_update = "OVERWRITE"
}
resource "aws_eks_addon" "kube_proxy" {
cluster_name = var.cluster_name
addon_name = "kube-proxy"
resolve_conflicts_on_update = "OVERWRITE"
}
resource "aws_eks_addon" "ebs_csi" {
cluster_name = var.cluster_name
addon_name = "aws-ebs-csi-driver"
resolve_conflicts_on_update = "OVERWRITE"
service_account_role_arn = aws_iam_role.ebs_csi.arn
}
resource "aws_eks_addon" "pod_identity" {
cluster_name = var.cluster_name
addon_name = "eks-pod-identity-agent"
resolve_conflicts_on_update = "OVERWRITE"
}
# ── VPC CNI IRSA ──
resource "aws_iam_role" "vpc_cni" {
name = "${var.cluster_name}-vpc-cni"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${local.account_id}:oidc-provider/${var.oidc_provider}"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_provider}:aud" = "sts.amazonaws.com"
"${var.oidc_provider}:sub" = "system:serviceaccount:kube-system:aws-node"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "vpc_cni" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.vpc_cni.name
}
# ── EBS CSI IRSA ──
resource "aws_iam_role" "ebs_csi" {
name = "${var.cluster_name}-ebs-csi"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${local.account_id}:oidc-provider/${var.oidc_provider}"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_provider}:aud" = "sts.amazonaws.com"
"${var.oidc_provider}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
role = aws_iam_role.ebs_csi.name
}
# ── AWS Load Balancer Controller IRSA ──
resource "aws_iam_role" "lb_controller" {
name = "${var.cluster_name}-lb-controller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${local.account_id}:oidc-provider/${var.oidc_provider}"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_provider}:aud" = "sts.amazonaws.com"
"${var.oidc_provider}:sub" = "system:serviceaccount:kube-system:aws-load-balancer-controller"
}
}
}]
})
}
# Load Balancer Controller needs a custom policy
# Download from: https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json
resource "aws_iam_policy" "lb_controller" {
name = "${var.cluster_name}-lb-controller-policy"
policy = file("${path.module}/policies/lb-controller-policy.json")
}
resource "aws_iam_role_policy_attachment" "lb_controller" {
policy_arn = aws_iam_policy.lb_controller.arn
role = aws_iam_role.lb_controller.name
}
Reusable IRSA Module Pattern
Create a generic IRSA module for any service account:
# modules/addons/irsa.tf
module "irsa" {
source = "./irsa"
for_each = {
external-dns = {
namespace = "kube-system"
service_account = "external-dns"
policy_arns = ["arn:aws:iam::${local.account_id}:policy/ExternalDNSPolicy"]
}
cert-manager = {
namespace = "cert-manager"
service_account = "cert-manager"
policy_arns = ["arn:aws:iam::${local.account_id}:policy/CertManagerPolicy"]
}
}
cluster_name = var.cluster_name
oidc_provider = var.oidc_provider
account_id = local.account_id
namespace = each.value.namespace
service_account = each.value.service_account
policy_arns = each.value.policy_arns
}
# modules/addons/irsa/main.tf
variable "cluster_name" { type = string }
variable "oidc_provider" { type = string }
variable "account_id" { type = string }
variable "namespace" { type = string }
variable "service_account" { type = string }
variable "policy_arns" { type = list(string) }
resource "aws_iam_role" "this" {
name = "${var.cluster_name}-${var.service_account}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${var.account_id}:oidc-provider/${var.oidc_provider}"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_provider}:aud" = "sts.amazonaws.com"
"${var.oidc_provider}:sub" = "system:serviceaccount:${var.namespace}:${var.service_account}"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "this" {
count = length(var.policy_arns)
policy_arn = var.policy_arns[count.index]
role = aws_iam_role.this.name
}
output "role_arn" {
value = aws_iam_role.this.arn
}
Add-ons Module — outputs.tf
# modules/addons/outputs.tf
output "vpc_cni_role_arn" {
value = aws_iam_role.vpc_cni.arn
}
output "ebs_csi_role_arn" {
value = aws_iam_role.ebs_csi.arn
}
output "lb_controller_role_arn" {
value = aws_iam_role.lb_controller.arn
}
Add-on Version Pinning
# Pin add-on versions for stability
data "aws_eks_addon_version" "vpc_cni" {
addon_name = "vpc-cni"
kubernetes_version = aws_eks_cluster.main.version
most_recent = true
}
resource "aws_eks_addon" "vpc_cni" {
cluster_name = var.cluster_name
addon_name = "vpc-cni"
addon_version = data.aws_eks_addon_version.vpc_cni.version
# ...
}