# EKS Module - Kubernetes Cluster with Karpenter # RFC 0039: ADR-Compliant Foundation Infrastructure # ADR 0004: "Set It and Forget It" Auto-Scaling Architecture # # Architecture: # - EKS control plane (~$73/mo) # - Fargate profile for Karpenter (chicken-egg problem) # - Karpenter for auto-scaling nodes # - No managed node groups (Karpenter handles all compute) # - FIPS-compliant node configuration locals { cluster_name = var.cluster_name } # EKS Cluster resource "aws_eks_cluster" "main" { name = local.cluster_name version = var.cluster_version role_arn = aws_iam_role.cluster.arn vpc_config { subnet_ids = concat(var.private_subnet_ids, var.public_subnet_ids) endpoint_private_access = true endpoint_public_access = var.enable_public_access public_access_cidrs = var.public_access_cidrs security_group_ids = [aws_security_group.cluster.id] } # Enable all log types for security compliance enabled_cluster_log_types = [ "api", "audit", "authenticator", "controllerManager", "scheduler" ] # Encryption configuration for secrets encryption_config { provider { key_arn = aws_kms_key.eks.arn } resources = ["secrets"] } tags = merge(var.tags, { Name = local.cluster_name }) depends_on = [ aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy, aws_iam_role_policy_attachment.cluster_AmazonEKSVPCResourceController, aws_cloudwatch_log_group.cluster, ] } # CloudWatch Log Group for EKS resource "aws_cloudwatch_log_group" "cluster" { name = "/aws/eks/${local.cluster_name}/cluster" retention_in_days = 30 tags = var.tags } # KMS Key for EKS secrets encryption resource "aws_kms_key" "eks" { description = "EKS cluster secrets encryption key" deletion_window_in_days = 7 enable_key_rotation = true tags = merge(var.tags, { Name = "${local.cluster_name}-eks-secrets" }) } resource "aws_kms_alias" "eks" { name = "alias/${local.cluster_name}-eks-secrets" target_key_id = aws_kms_key.eks.key_id } # EKS Cluster IAM Role resource "aws_iam_role" "cluster" { name = "${local.cluster_name}-cluster" 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_AmazonEKSClusterPolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" role = aws_iam_role.cluster.name } resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSVPCResourceController" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController" role = aws_iam_role.cluster.name } # Cluster Security Group resource "aws_security_group" "cluster" { name = "${local.cluster_name}-cluster" description = "EKS cluster security group" vpc_id = var.vpc_id tags = merge(var.tags, { Name = "${local.cluster_name}-cluster" "kubernetes.io/cluster/${local.cluster_name}" = "owned" }) } resource "aws_security_group_rule" "cluster_egress" { type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.cluster.id description = "Allow all outbound traffic" } # Node Security Group (for Karpenter-managed nodes) resource "aws_security_group" "node" { name = "${local.cluster_name}-node" description = "Security group for EKS nodes" vpc_id = var.vpc_id tags = merge(var.tags, { Name = "${local.cluster_name}-node" "kubernetes.io/cluster/${local.cluster_name}" = "owned" "karpenter.sh/discovery" = local.cluster_name }) } resource "aws_security_group_rule" "node_egress" { type = "egress" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.node.id description = "Allow all outbound traffic" } resource "aws_security_group_rule" "node_ingress_self" { type = "ingress" from_port = 0 to_port = 65535 protocol = "-1" source_security_group_id = aws_security_group.node.id security_group_id = aws_security_group.node.id description = "Allow nodes to communicate with each other" } resource "aws_security_group_rule" "node_ingress_cluster" { type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" source_security_group_id = aws_security_group.cluster.id security_group_id = aws_security_group.node.id description = "Allow cluster API to communicate with nodes" } resource "aws_security_group_rule" "node_ingress_cluster_kubelet" { type = "ingress" from_port = 10250 to_port = 10250 protocol = "tcp" source_security_group_id = aws_security_group.cluster.id security_group_id = aws_security_group.node.id description = "Allow cluster API to communicate with kubelet" } resource "aws_security_group_rule" "cluster_ingress_node" { type = "ingress" from_port = 443 to_port = 443 protocol = "tcp" source_security_group_id = aws_security_group.node.id security_group_id = aws_security_group.cluster.id description = "Allow nodes to communicate with cluster API" } # Fargate Profile for Karpenter # Karpenter needs to run on Fargate to avoid chicken-egg problem resource "aws_eks_fargate_profile" "karpenter" { count = var.enable_karpenter ? 1 : 0 cluster_name = aws_eks_cluster.main.name fargate_profile_name = "karpenter" pod_execution_role_arn = aws_iam_role.fargate.arn subnet_ids = var.private_subnet_ids selector { namespace = "karpenter" } tags = var.tags } # Fargate Profile for kube-system (CoreDNS, etc) resource "aws_eks_fargate_profile" "kube_system" { cluster_name = aws_eks_cluster.main.name fargate_profile_name = "kube-system" pod_execution_role_arn = aws_iam_role.fargate.arn subnet_ids = var.private_subnet_ids selector { namespace = "kube-system" } tags = var.tags } # Fargate IAM Role resource "aws_iam_role" "fargate" { name = "${local.cluster_name}-fargate" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "eks-fargate-pods.amazonaws.com" } } ] }) tags = var.tags } resource "aws_iam_role_policy_attachment" "fargate_AmazonEKSFargatePodExecutionRolePolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" role = aws_iam_role.fargate.name } # OIDC Provider for IRSA data "tls_certificate" "cluster" { url = aws_eks_cluster.main.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.main.identity[0].oidc[0].issuer tags = var.tags } # Node IAM Role (for Karpenter-managed nodes) resource "aws_iam_role" "node" { name = "${local.cluster_name}-node" 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_AmazonEKSWorkerNodePolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "node_AmazonEKS_CNI_Policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "node_AmazonEC2ContainerRegistryReadOnly" { policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" role = aws_iam_role.node.name } resource "aws_iam_role_policy_attachment" "node_AmazonSSMManagedInstanceCore" { policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" role = aws_iam_role.node.name } resource "aws_iam_instance_profile" "node" { name = "${local.cluster_name}-node" role = aws_iam_role.node.name tags = var.tags } # EKS Addons resource "aws_eks_addon" "vpc_cni" { cluster_name = aws_eks_cluster.main.name addon_name = "vpc-cni" addon_version = var.vpc_cni_version resolve_conflicts_on_create = "OVERWRITE" resolve_conflicts_on_update = "OVERWRITE" tags = var.tags } resource "aws_eks_addon" "coredns" { cluster_name = aws_eks_cluster.main.name addon_name = "coredns" addon_version = var.coredns_version resolve_conflicts_on_create = "OVERWRITE" resolve_conflicts_on_update = "OVERWRITE" # CoreDNS needs compute to run depends_on = [aws_eks_fargate_profile.kube_system] tags = var.tags } resource "aws_eks_addon" "kube_proxy" { cluster_name = aws_eks_cluster.main.name addon_name = "kube-proxy" addon_version = var.kube_proxy_version resolve_conflicts_on_create = "OVERWRITE" resolve_conflicts_on_update = "OVERWRITE" tags = var.tags } resource "aws_eks_addon" "ebs_csi_driver" { cluster_name = aws_eks_cluster.main.name addon_name = "aws-ebs-csi-driver" addon_version = var.ebs_csi_version service_account_role_arn = var.ebs_csi_role_arn resolve_conflicts_on_create = "OVERWRITE" resolve_conflicts_on_update = "OVERWRITE" tags = var.tags } # aws-auth ConfigMap for Karpenter nodes resource "kubernetes_config_map_v1_data" "aws_auth" { metadata { name = "aws-auth" namespace = "kube-system" } data = { mapRoles = yamlencode([ { rolearn = aws_iam_role.node.arn username = "system:node:{{EC2PrivateDNSName}}" groups = [ "system:bootstrappers", "system:nodes", ] }, { rolearn = aws_iam_role.fargate.arn username = "system:node:{{SessionName}}" groups = [ "system:bootstrappers", "system:nodes", "system:node-proxier", ] } ]) } force = true depends_on = [aws_eks_cluster.main] } # Data source for current region data "aws_region" "current" {} # Data source for current account data "aws_caller_identity" "current" {}