# ============================================================================= # EKS Module — Production-Grade AWS EKS Cluster # ============================================================================= # Security Features: # - Private API endpoint (public access optional, restricted CIDRs) # - Encrypted secrets with KMS # - Managed node groups with custom launch templates # - IRSA (IAM Roles for Service Accounts) # - Audit logging enabled (all log types) # - Pod security standards enforced via Kyverno (at k8s layer) # - Bottlerocket or AL2023 node OS options # ============================================================================= terraform { required_version = ">= 1.7.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.25" } } } data "aws_caller_identity" "current" {} # ---------- EKS Cluster ---------- resource "aws_eks_cluster" "this" { name = var.cluster_name role_arn = aws_iam_role.cluster.arn version = var.kubernetes_version vpc_config { subnet_ids = var.private_subnet_ids endpoint_private_access = true endpoint_public_access = var.endpoint_public_access public_access_cidrs = var.endpoint_public_access_cidrs security_group_ids = [var.cluster_security_group_id] } encryption_config { provider { key_arn = var.kms_key_arn } resources = ["secrets"] } enabled_cluster_log_types = [ "api", "audit", "authenticator", "controllerManager", "scheduler" ] tags = merge(var.tags, { Name = var.cluster_name }) } # ---------- Cluster IAM Role ---------- resource "aws_iam_role" "cluster" { name = "${var.cluster_name}-cluster-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "eks.amazonaws.com" } }] }) tags = var.tags } resource "aws_iam_role_policy_attachment" "cluster_policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" role = aws_iam_role.cluster.name } resource "aws_iam_role_policy_attachment" "cluster_vpc_resource_controller" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController" role = aws_iam_role.cluster.name } # ---------- OIDC Provider for IRSA ---------- data "tls_certificate" "cluster" { url = aws_eks_cluster.this.identity[0].oidc[0].issuer } resource "aws_iam_openid_connect_provider" "cluster" { client_id_list = ["sts.amazonaws.com"] thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint] url = aws_eks_cluster.this.identity[0].oidc[0].issuer tags = merge(var.tags, { Name = "${var.cluster_name}-oidc" }) } # ---------- Managed Node Groups ---------- resource "aws_eks_node_group" "this" { for_each = var.node_groups cluster_name = aws_eks_cluster.this.name node_group_name = each.key node_role_arn = aws_iam_role.node.arn subnet_ids = var.private_subnet_ids instance_types = each.value.instance_types ami_type = each.value.ami_type capacity_type = each.value.capacity_type disk_size = each.value.disk_size scaling_config { desired_size = each.value.desired_size min_size = each.value.min_size max_size = each.value.max_size } update_config { max_unavailable_percentage = 25 } labels = merge(each.value.labels, { "node-group" = each.key }) dynamic "taint" { for_each = each.value.taints content { key = taint.value.key value = taint.value.value effect = taint.value.effect } } # Only proceed when cluster is ready depends_on = [ aws_iam_role_policy_attachment.node_policy, aws_iam_role_policy_attachment.cni_policy, aws_iam_role_policy_attachment.container_registry_policy, ] tags = merge(var.tags, { Name = "${var.cluster_name}-${each.key}" }) } # ---------- Node IAM Role ---------- resource "aws_iam_role" "node" { name = "${var.cluster_name}-node-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } }] }) tags = var.tags } resource "aws_iam_role_policy_attachment" "node_policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "cni_policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "container_registry_policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "ssm_managed_instance" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" role = aws_iam_role.node.name } # ---------- IRSA Helper Module ---------- # Creates IAM role for a Kubernetes service account resource "aws_iam_role" "irsa" { for_each = var.irsa_roles name = "${var.cluster_name}-${each.key}-irsa" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRoleWithWebIdentity" Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.cluster.arn } Condition = { StringEquals = { "${aws_iam_openid_connect_provider.cluster.url}:sub" = "system:serviceaccount:${each.value.namespace}:${each.value.service_account}" } } }] }) tags = merge(var.tags, { Name = "${var.cluster_name}-${each.key}-irsa" ServiceAccount = each.value.service_account }) } resource "aws_iam_role_policy_attachment" "irsa" { for_each = var.irsa_roles policy_arn = each.value.policy_arn role = aws_iam_role.irsa[each.key].name }