TL;DR

Most AWS Kubernetes Terraform work creates a VPC, private/public subnets, EKS control plane, managed node groups, IAM roles, security groups, OIDC/IRSA roles, and add-on dependencies. Keep cluster creation, add-on Helm releases, and app workloads clearly separated.

Provider And Backend

This block pins Terraform and AWS provider versions and configures S3 remote state with locking. Use it at the root of an AWS environment so plans are reproducible and multiple engineers do not apply against local state.

hclversions.tf
terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket         = "client-prod-terraform-state"
    key            = "aws/eks/prod.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Owner       = "platform"
    }
  }
}

VPC And Subnet Tags

EKS load balancers depend on subnet tags. Public subnets usually get kubernetes.io/role/elb=1; private subnets get kubernetes.io/role/internal-elb=1.

hclvpc.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.name}-${var.environment}"
  cidr = "10.40.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.40.1.0/24", "10.40.2.0/24", "10.40.3.0/24"]
  public_subnets  = ["10.40.101.0/24", "10.40.102.0/24", "10.40.103.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = false
  enable_dns_hostnames = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
  }
}

EKS Cluster And Node Groups

This module creates the EKS control plane and managed node groups. Use separate node groups for different workload classes such as general, spot, GPU, or isolated platform workloads.

hcleks.tf
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${var.name}-${var.environment}"
  cluster_version = var.cluster_version

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  enable_irsa                              = true
  cluster_endpoint_public_access           = true
  cluster_endpoint_private_access          = true
  enable_cluster_creator_admin_permissions = true

  eks_managed_node_groups = {
    general = {
      min_size       = 2
      max_size       = 6
      desired_size   = 3
      instance_types = ["m6i.large"]
      capacity_type  = "ON_DEMAND"
      labels = {
        role = "general"
      }
    }
    spot = {
      min_size       = 0
      max_size       = 10
      desired_size   = 2
      instance_types = ["m6i.large", "m5.large"]
      capacity_type  = "SPOT"
      taints = {
        spot = {
          key    = "capacity"
          value  = "spot"
          effect = "NO_SCHEDULE"
        }
      }
    }
  }
}

Security Group Pattern

This pattern creates an explicit security group plus separate ingress and egress rule resources. Use it for load balancers or platform endpoints where reviewers need to see exactly which ports and CIDRs are allowed.

hclsecurity-groups.tf
resource "aws_security_group" "app_lb" {
  name        = "${var.name}-${var.environment}-app-lb"
  description = "Ingress from internet to public ALB"
  vpc_id      = module.vpc.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "app_lb_https" {
  security_group_id = aws_security_group.app_lb.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "app_lb_all_egress" {
  security_group_id = aws_security_group.app_lb.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

IRSA Role For A Pod

IRSA maps a Kubernetes ServiceAccount to a narrow IAM role. Use this for ExternalDNS, AWS Load Balancer Controller, EBS CSI, External Secrets, or app pods that need AWS API access.

hclirsa-externaldns.tf
module "external_dns_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "${var.name}-${var.environment}-external-dns"

  attach_external_dns_policy    = true
  external_dns_hosted_zone_arns = ["arn:aws:route53:::hostedzone/Z1234567890"]

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["external-dns:external-dns"]
    }
  }
}

Custom IAM Policy

This policy grants narrow read access to one S3 bucket. Use this shape when an app, controller, or CI role needs a custom permission set that is smaller than a managed AWS policy.

hcliam-policy.tf
data "aws_iam_policy_document" "app_s3_read" {
  statement {
    actions   = ["s3:GetObject", "s3:ListBucket"]
    resources = [
      "arn:aws:s3:::client-prod-app-config",
      "arn:aws:s3:::client-prod-app-config/*"
    ]
  }
}

resource "aws_iam_policy" "app_s3_read" {
  name   = "${var.name}-${var.environment}-app-s3-read"
  policy = data.aws_iam_policy_document.app_s3_read.json
}

Add-On Ownership

Add-onTerraform ownsHelm/GitOps owns
AWS Load Balancer ControllerIRSA role and policyHelm release and values
ExternalDNSRoute53-scoped IRSA roleDeployment, flags, domain filters
EBS CSIIAM role, KMS policyEKS add-on or Helm chart
cert-managerRoute53 DNS-01 permissionscert-manager chart and Issuers