diff --git a/README.md b/README.md index ca14e6f..ef60eea 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # terraform-aws-eks-basic -> **⚠️ Note**: This module supports **EC2 managed node groups** and **EKS Auto Mode**. Fargate is not available in this version. - -A Terraform module for creating and managing Amazon EKS (Elastic Kubernetes Service) clusters with EC2 managed node groups and optional EKS Auto Mode. +A Terraform module for creating and managing Amazon EKS (Elastic Kubernetes Service) clusters with EC2 managed node groups, optional EKS Auto Mode, and optional Fargate profiles. ## Features - **EC2 Managed Node Groups**: Full support with customizable launch templates and auto-scaling - **EKS Auto Mode**: Optional; compute is auto-provisioned by AWS (no managed node groups; built-in node pools). Mutually exclusive with `eks_managed_node_groups`. +- **Fargate profiles**: Optional via `fargate_profiles`; shared pod execution role (create or bring-your-own ARN), profiles, and optional API access entry (`create_fargate_access_entry`, `fargate_access_entry_type`). IAM options: `fargate_pod_execution_role_name`, `path`, `permissions_boundary`. See `examples/eks-fargate`. - **Dual-Stack Support**: IPv4 and IPv6 cluster support (IPv6 service CIDR auto-assigned by AWS) - **Modern EKS Access Entries**: Native EKS authentication via access entries (no aws-auth ConfigMap) - **IRSA and Pod Identity**: OIDC provider for IAM Roles for Service Accounts (IRSA); optional EKS Pod Identity per component (ALB controller, External DNS, addons, Secrets Manager). Choose per component via `*_identity_type` variables (`"irsa"` or `"pod_identity"`). When using Pod Identity, enable the **eks-pod-identity-agent** addon. @@ -177,6 +176,7 @@ EKS automatically creates an access entry for each capability role with default - **[examples/eks-capabilities](examples/eks-capabilities/)** - Platform engineering example with EKS capabilities (ACK, KRO, Argo CD) - **[examples/eks-capabilities-private](examples/eks-capabilities-private/)** - Private-only EKS and Argo CD (VPC endpoints; access from within VPC) - **[examples/eks-auto-mode](examples/eks-auto-mode/)** - EKS cluster with Auto Mode (compute auto-provisioned; no managed node groups) +- **[examples/eks-fargate](examples/eks-fargate/)** - EKS with Fargate only (no EC2 nodes); CoreDNS on Fargate; IRSA for workload AWS credentials - **[examples/eks-auto-mode-keda-workload](examples/eks-auto-mode-keda-workload/)** - Auto Mode + workload IAM wiring example (SQS; suitable for KEDA-managed workers) - **[examples/private-endpoint](examples/private-endpoint/)** - EKS with private API endpoint @@ -449,8 +449,8 @@ terraform test ```plaintext terraform-aws-eks-basic/ -├── main.tf # Core EKS cluster, node groups, addons, OIDC provider -├── access-entries.tf # EKS access entries for authentication +├── main.tf # Core EKS cluster, node groups, addons, access entries, OIDC provider +├── fargate.tf # Optional Fargate execution role (create/BYO), profiles, access entry ├── capabilities.tf # EKS Capabilities (ACK, KRO, ArgoCD) ├── capabilities-iam.tf # IAM roles for EKS Capabilities ├── capabilities-access-entries.tf # Optional access entry policy associations (e.g. ACK Secret Reader) @@ -468,6 +468,7 @@ terraform-aws-eks-basic/ ├── eks-capabilities/ # Platform engineering with capabilities ├── eks-capabilities-private/ # Private-only EKS and Argo CD ├── eks-auto-mode/ # EKS Auto Mode (no managed node groups) + ├── eks-fargate/ # Fargate (kube-system + app) + small MNG for Pod Identity agent ├── pod-identity/ # Pod Identity for ALB, External DNS, EBS CSI, Secrets Manager └── private-endpoint/ # EKS with private API endpoint ``` diff --git a/access-entries.tf b/access-entries.tf deleted file mode 100644 index 84759ea..0000000 --- a/access-entries.tf +++ /dev/null @@ -1,101 +0,0 @@ -# # ============================================================================= -# # User Access Entries -# # Grant cluster admin access to specified IAM users/roles -# # ============================================================================= - -# locals { -# # Determine if user access entries should be created -# # Only create when: -# # 1. cluster_admin_arns is not empty, AND -# # 2. Either capabilities are enabled OR authentication mode is not CONFIG_MAP -# create_user_access_entries = length(var.cluster_admin_arns) > 0 && ( -# var.enable_ack_capability || -# var.enable_kro_capability || -# var.enable_argocd_capability || -# var.cluster_authentication_mode != "CONFIG_MAP" -# ) -# } - -# resource "aws_eks_access_entry" "cluster_admins" { -# for_each = local.create_user_access_entries ? toset(var.cluster_admin_arns) : [] - -# cluster_name = aws_eks_cluster.this.name -# principal_arn = each.value -# type = "STANDARD" - -# depends_on = [ -# aws_eks_cluster.this -# ] -# } - -# resource "aws_eks_access_policy_association" "cluster_admin_policy" { -# for_each = local.create_user_access_entries ? toset(var.cluster_admin_arns) : [] - -# cluster_name = aws_eks_cluster.this.name -# principal_arn = each.value -# policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" - -# access_scope { -# type = "cluster" -# } - -# depends_on = [ -# aws_eks_access_entry.cluster_admins -# ] -# } - -# # ============================================================================= -# # EC2 Node Access Entry -# # Required when using API or API_AND_CONFIG_MAP authentication mode -# # ============================================================================= - -# locals { -# # Determine if access entries are needed for EC2 nodes -# ec2_needs_access_entry = contains(var.compute_mode, "ec2") && ( -# var.enable_ack_capability || -# var.enable_kro_capability || -# var.enable_argocd_capability || -# var.cluster_authentication_mode != "CONFIG_MAP" -# ) -# } - -# resource "aws_eks_access_entry" "ec2_nodes" { -# count = local.ec2_needs_access_entry ? 1 : 0 - -# cluster_name = aws_eks_cluster.this.name -# principal_arn = aws_iam_role.eks_nodes[0].arn -# type = "EC2_LINUX" - -# depends_on = [ -# aws_eks_cluster.this, -# aws_iam_role.eks_nodes[0] -# ] -# } - -# # ============================================================================= -# # Fargate Pod Access Entry -# # Required when using API or API_AND_CONFIG_MAP authentication mode -# # ============================================================================= - -# locals { -# # Determine if access entries are needed for Fargate pods -# fargate_needs_access_entry = contains(var.compute_mode, "fargate") && ( -# var.enable_ack_capability || -# var.enable_kro_capability || -# var.enable_argocd_capability || -# var.cluster_authentication_mode != "CONFIG_MAP" -# ) -# } - -# resource "aws_eks_access_entry" "fargate_pods" { -# count = local.fargate_needs_access_entry ? 1 : 0 - -# cluster_name = aws_eks_cluster.this.name -# principal_arn = aws_iam_role.eks_fargate[0].arn -# type = "FARGATE_LINUX" - -# depends_on = [ -# aws_eks_cluster.this, -# aws_iam_role.eks_fargate[0] -# ] -# } diff --git a/docs/README.md b/docs/README.md index fb8a136..a521d53 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,8 +29,8 @@ This directory contains detailed documentation for the module's design decisions The module is organized into focused files: -- `main.tf` - Core cluster, node groups, addons, and OIDC provider -- `access-entries.tf` - EKS access entries for authentication +- `main.tf` - Core cluster, node groups, addons, access entries, and OIDC provider +- `fargate.tf` - Optional Fargate pod execution role (create or BYO ARN), profiles, access entry; related variables in `variables.tf` - `capabilities.tf` - EKS Capabilities (ACK, KRO, ArgoCD) - `capabilities-iam.tf` - IAM roles for capabilities - `addons-iam.tf` - IAM roles for addons (IRSA) diff --git a/examples/eks-auto-mode/.terraform.lock.hcl b/examples/eks-auto-mode/.terraform.lock.hcl index 88f74d2..5741225 100644 --- a/examples/eks-auto-mode/.terraform.lock.hcl +++ b/examples/eks-auto-mode/.terraform.lock.hcl @@ -2,25 +2,25 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "6.35.1" + version = "6.39.0" constraints = ">= 6.0.0" hashes = [ - "h1:0/uXxSpL98lpRqjRhjAvvWZVnJZnbOehfAlTrcPXURI=", - "zh:0a16d1b0ba9379e5c5295e6b3caa42f0b8ba6b9f0a7cc9dbe58c232cf995db2d", - "zh:4b2e69907a1a2c557e45ef590f9fd6187ab5bf90378346ba9f723535e49ce908", - "zh:56bdafda0d629e15dc3dd9275b54f1fb953e2e09a3bc1a34e027da9d03ea4893", - "zh:5b84e933989150249036f84faad221dce0daa9d3043ff24401547e18f00b121e", - "zh:70bac98c27a14cb2cedabd741a1f7f1bab074c127efdcf02b54dbcf0d03db3cc", - "zh:7184f48bd077eaf68e184fd44f97e2d971cb77c59a68aedb95a0f8dc01b134fe", - "zh:7367589ae8b584bfcd83c973f5003e15010a453349c017a0d2cca8772d4fcfd9", - "zh:7ec9699dee49dd31bbc2d0e50fa1fff451eee5c1d9fd59bca7412acb49ce6594", - "zh:92dd139b96977a64af0e976cd06e84921033678ab97550f1b687c0ea54a8e82c", + "h1:jweey4Iefm/DuuBg84saQ8vz5IO3vC6hDFTU/eGdmBI=", + "zh:00c3e3c38063ff629d6fdbce04e9ac2e241566e0f5ad5399c335f0abdefd7bff", + "zh:148f95b62791080537d926b9d2f5d8457cca45921d9b1019d03ceb3ab93bf9db", + "zh:203da629ed5191dd5d7aa3427a5d1d1a83eed5c1b0114166897206973f0d0fd0", + "zh:21923eedbc60b4f68c8d717b951d16b0b1bbf31d66330c7be228869bec18f7ce", + "zh:26226f02e3661b3d071c01601b654a308b29d21758b75692bec66f70c6f6b945", + "zh:271c7c6fadcd8ac7ed37c11e61c0f374773eaaa5293703499f8a0f75830060e0", + "zh:46e319a8888dc50ed8d26a1cbee9637f529112a88f5d44decc8f1d10ef968ffe", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9f2df575a5b010db60068668c48806595a3d617a2c0305035283fe8b72f07b19", - "zh:a4602b7602c75c8f726bdc7e706dc5c26736e47cc8381be01386aa8d8d998403", - "zh:bc25fefeeee10425df7aebfc21dc6532d19acdf03fa97b9e6d8c113adffd0a1d", - "zh:f445592040b5fc368a12e6edeffc951b2eb41e86413c4074638a13376e25a9cc", - "zh:ff43962a48bd8f85e17188736bbd3c145b6a1320bd8303221f6b4f9ec861e1e6", + "zh:a3c3ca09cdbf3b9a3f892a23c000ff04772bdf19f626959ea83d0803c8fd2350", + "zh:a5fa6515ffc3c815e0d2204d67e838f5bad8635009dab85211d166c7ae729d2c", + "zh:c0807566b4ddde8390f50c5475464103f066bc7f511a6c0be762d75cb6d1a078", + "zh:da754a529fd0e06ac372f62d88566f85a8c4bcec7ee9a231b65e0a0148165e63", + "zh:dcb768e48363a9f4dffaf2dc7d01f1877285528925ec50de6335286298e37e1d", + "zh:eac9de9d123c679ea3035199fb9c588a08cda281cbabf948dc696e2a1a1b9063", + "zh:fef276b6331c663ca0e60dc7f637b2b8244825b8c9bc721481957e58f74ffb4f", ] } @@ -46,7 +46,7 @@ provider "registry.terraform.io/hashicorp/helm" { provider "registry.terraform.io/hashicorp/kubernetes" { version = "2.38.0" - constraints = ">= 2.30.0, ~> 2.30" + constraints = "~> 2.30" hashes = [ "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=", "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0", diff --git a/examples/eks-fargate/.terraform.lock.hcl b/examples/eks-fargate/.terraform.lock.hcl new file mode 100644 index 0000000..4dde3cb --- /dev/null +++ b/examples/eks-fargate/.terraform.lock.hcl @@ -0,0 +1,85 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.39.0" + constraints = ">= 6.0.0" + hashes = [ + "h1:jweey4Iefm/DuuBg84saQ8vz5IO3vC6hDFTU/eGdmBI=", + "zh:00c3e3c38063ff629d6fdbce04e9ac2e241566e0f5ad5399c335f0abdefd7bff", + "zh:148f95b62791080537d926b9d2f5d8457cca45921d9b1019d03ceb3ab93bf9db", + "zh:203da629ed5191dd5d7aa3427a5d1d1a83eed5c1b0114166897206973f0d0fd0", + "zh:21923eedbc60b4f68c8d717b951d16b0b1bbf31d66330c7be228869bec18f7ce", + "zh:26226f02e3661b3d071c01601b654a308b29d21758b75692bec66f70c6f6b945", + "zh:271c7c6fadcd8ac7ed37c11e61c0f374773eaaa5293703499f8a0f75830060e0", + "zh:46e319a8888dc50ed8d26a1cbee9637f529112a88f5d44decc8f1d10ef968ffe", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a3c3ca09cdbf3b9a3f892a23c000ff04772bdf19f626959ea83d0803c8fd2350", + "zh:a5fa6515ffc3c815e0d2204d67e838f5bad8635009dab85211d166c7ae729d2c", + "zh:c0807566b4ddde8390f50c5475464103f066bc7f511a6c0be762d75cb6d1a078", + "zh:da754a529fd0e06ac372f62d88566f85a8c4bcec7ee9a231b65e0a0148165e63", + "zh:dcb768e48363a9f4dffaf2dc7d01f1877285528925ec50de6335286298e37e1d", + "zh:eac9de9d123c679ea3035199fb9c588a08cda281cbabf948dc696e2a1a1b9063", + "zh:fef276b6331c663ca0e60dc7f637b2b8244825b8c9bc721481957e58f74ffb4f", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = ">= 2.13.0, ~> 2.13" + hashes = [ + "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:+W+DMrVoVnoXo3f3M4W+OpZbkCrUn6PnqDF33D2Cuf0=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 4.0.0, ~> 4.0" + hashes = [ + "h1:F5d6bQY8UlBo0D71Sv7CsV+3aZOFz0yeNF+vufog7h4=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/examples/eks-fargate/README.md b/examples/eks-fargate/README.md new file mode 100644 index 0000000..7ccdfa9 --- /dev/null +++ b/examples/eks-fargate/README.md @@ -0,0 +1,50 @@ +# EKS Fargate Example + +This example creates an EKS cluster with **Fargate profiles only** (no EC2 managed node groups). Pods in `kube-system` and in the configured application namespace run on AWS Fargate. + +## Requirements + +- Terraform >= 1.6.0 +- AWS provider >= 6.0 + +## Usage + +1. Configure AWS credentials. +2. Edit `terraform.tfvars` and set `access_entries` (required for kubectl access). +3. Run: + + ```bash + terraform init + terraform plan + terraform apply + ``` + +## What's created + +- A VPC (via `cloudbuildlab/vpc/aws`) with public and private subnets and a NAT gateway +- An EKS cluster using the root module with: + - **CoreDNS** and **vpc-cni** addons (CoreDNS uses `computeType = "fargate"`) + - **Fargate profiles** for `kube-system` and the namespace from `fargate_namespace` (default `app`) + - No `eks_managed_node_groups` and no `enable_automode` + +## Variables + +- `cluster_name`: EKS cluster name (default: `eks-fargate`) +- `aws_region`: Region (default: `ap-southeast-2`) +- `cluster_version`: Kubernetes version (default: `1.35`) +- `fargate_namespace`: Namespace whose pods are scheduled on Fargate via the `default` profile (default: `app`) +- `access_entries`: Map of IAM principals for cluster access +- `tags`: Tags for resources + +## Connecting to the cluster + +```bash +aws eks update-kubeconfig --name $(terraform output -raw cluster_name) --region $(terraform output -raw aws_region) +kubectl get pods -A +``` + +## Notes + +- Fargate workloads need **private subnets with outbound internet** (NAT). This example uses private subnets for Fargate profile `subnet_ids`. +- For AWS API access from application pods, use **IRSA** (IAM Roles for Service Accounts). EKS Pod Identity is not supported on Fargate; see [EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html). +- You can combine Fargate profiles with managed node groups or Auto Mode in the module for hybrid clusters; this example stays Fargate-only. diff --git a/examples/eks-fargate/main.tf b/examples/eks-fargate/main.tf new file mode 100644 index 0000000..420cc3e --- /dev/null +++ b/examples/eks-fargate/main.tf @@ -0,0 +1,85 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +module "vpc" { + source = "cloudbuildlab/vpc/aws" + + vpc_name = var.cluster_name + vpc_cidr = "10.0.0.0/16" + availability_zones = ["${var.aws_region}a", "${var.aws_region}b"] + + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24"] + + create_igw = true + nat_gateway_type = "single" + + enable_eks_tags = true + eks_cluster_name = var.cluster_name + tags = var.tags +} + +# Pure Fargate EKS cluster — no EC2 nodes. All pods run on AWS Fargate. +# +# AWS credential delivery for Fargate pods: +# - Use IRSA (IAM Roles for Service Accounts) — annotate the service account with +# an IAM role ARN; the pod exchanges its OIDC token with STS directly. +# - EKS Pod Identity is NOT supported on Fargate. The Pod Identity agent is a +# hostNetwork DaemonSet that only runs on EC2 nodes and cannot reach Fargate pods. +# See: https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html +# +# CoreDNS note: the addon is configured with computeType = "fargate" so EKS schedules +# it on Fargate. A kube-system Fargate profile is required for this to work. +module "eks" { + source = "../../" + + name = var.cluster_name + kubernetes_version = var.cluster_version + vpc_id = module.vpc.vpc_id + subnet_ids = concat(module.vpc.public_subnet_ids, module.vpc.private_subnet_ids) + + endpoint_public_access = true + access_entries = var.access_entries + + cloudwatch_log_group_force_destroy = true + + addons = { + coredns = { + addon_version = "v1.13.2-eksbuild.4" + configuration_values = jsonencode({ + computeType = "fargate" + }) + } + vpc-cni = { + before_compute = true + addon_version = "v1.21.1-eksbuild.3" + } + } + + fargate_profiles = { + # Required so CoreDNS (and other kube-system pods) can schedule on Fargate. + kube-system = { + selectors = [{ namespace = "kube-system" }] + subnet_ids = module.vpc.private_subnet_ids + } + # Application namespace — add more selectors or profiles as needed. + default = { + selectors = [{ namespace = var.fargate_namespace }] + subnet_ids = module.vpc.private_subnet_ids + } + } + + tags = var.tags +} diff --git a/examples/eks-fargate/outputs.tf b/examples/eks-fargate/outputs.tf new file mode 100644 index 0000000..025cc85 --- /dev/null +++ b/examples/eks-fargate/outputs.tf @@ -0,0 +1,54 @@ +output "aws_region" { + description = "AWS region (for update-kubeconfig)" + value = var.aws_region +} + +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "public_subnet_ids" { + description = "IDs of the public subnets" + value = module.vpc.public_subnet_ids +} + +output "private_subnet_ids" { + description = "IDs of the private subnets" + value = module.vpc.private_subnet_ids +} + +output "cluster_name" { + description = "Name of the EKS cluster" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "Endpoint for EKS control plane" + value = module.eks.cluster_endpoint +} + +output "cluster_version" { + description = "Kubernetes version of the EKS cluster" + value = module.eks.cluster_version +} + +output "oidc_provider_arn" { + description = "ARN of the EKS OIDC provider" + value = module.eks.oidc_provider_arn +} + +output "fargate_role_arn" { + description = "ARN of the Fargate pod execution IAM role" + value = module.eks.fargate_role_arn +} + +output "fargate_profiles" { + description = "Map of EKS Fargate profiles created" + value = module.eks.fargate_profiles +} + +output "fargate_access_entry_arn" { + description = "ARN of the module-managed Fargate access entry (when created)" + value = module.eks.fargate_access_entry_arn +} diff --git a/examples/eks-fargate/variables.tf b/examples/eks-fargate/variables.tf new file mode 100644 index 0000000..90244bd --- /dev/null +++ b/examples/eks-fargate/variables.tf @@ -0,0 +1,51 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-southeast-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "eks-fargate" +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS cluster" + type = string + default = "1.35" +} + +variable "fargate_namespace" { + description = "Kubernetes namespace selector for the Fargate profile — pods in this namespace are scheduled on Fargate" + type = string + default = "app" +} + +variable "access_entries" { + description = "Map of access entries to add to the cluster" + type = map(object({ + kubernetes_groups = optional(list(string)) + principal_arn = string + type = optional(string, "STANDARD") + user_name = optional(string) + tags = optional(map(string), {}) + policy_associations = optional(map(object({ + policy_arn = string + access_scope = object({ + namespaces = optional(list(string)) + type = string + }) + })), {}) + })) + default = {} +} + +variable "tags" { + description = "Map of tags to apply to all resources" + type = map(string) + default = { + Environment = "dev" + ManagedBy = "terraform" + } +} diff --git a/fargate.tf b/fargate.tf index e9d0f12..0733b93 100644 --- a/fargate.tf +++ b/fargate.tf @@ -1,60 +1,96 @@ -# # ============================================================================= -# # Fargate Profile IAM Role -# # ============================================================================= - -# data "aws_iam_policy_document" "eks_fargate_assume_role" { -# count = contains(var.compute_mode, "fargate") ? 1 : 0 - -# statement { -# effect = "Allow" - -# principals { -# type = "Service" -# identifiers = ["eks-fargate-pods.amazonaws.com"] -# } - -# actions = ["sts:AssumeRole"] -# } -# } - -# resource "aws_iam_role" "eks_fargate" { -# count = contains(var.compute_mode, "fargate") ? 1 : 0 - -# name = "${var.cluster_name}-eks-fargate-role" -# assume_role_policy = data.aws_iam_policy_document.eks_fargate_assume_role[0].json -# tags = var.tags -# } - -# resource "aws_iam_role_policy_attachment" "eks_fargate_pod_execution_role" { -# count = contains(var.compute_mode, "fargate") ? 1 : 0 - -# role = aws_iam_role.eks_fargate[0].name -# policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" -# } - -# # ============================================================================= -# # Fargate Profiles -# # ============================================================================= - -# resource "aws_eks_fargate_profile" "default" { -# for_each = contains(var.compute_mode, "fargate") ? var.fargate_profiles : {} - -# cluster_name = aws_eks_cluster.this.name -# fargate_profile_name = each.key -# pod_execution_role_arn = aws_iam_role.eks_fargate[0].arn -# subnet_ids = each.value.subnet_ids != null ? each.value.subnet_ids : var.subnet_ids - -# dynamic "selector" { -# for_each = each.value.selectors != null ? each.value.selectors : [] -# content { -# namespace = selector.value.namespace -# labels = selector.value.labels -# } -# } - -# tags = merge(var.tags, each.value.tags != null ? each.value.tags : {}) - -# depends_on = [ -# aws_iam_role_policy_attachment.eks_fargate_pod_execution_role[0] -# ] -# } +locals { + fargate_create_execution_role = length(var.fargate_profiles) > 0 && var.create_fargate_pod_execution_role + + fargate_pod_execution_role_arn_effective = length(var.fargate_profiles) == 0 ? null : ( + var.create_fargate_pod_execution_role ? aws_iam_role.eks_fargate[0].arn : var.fargate_pod_execution_role_arn + ) + + fargate_access_entry_count = ( + length(var.fargate_profiles) > 0 + && var.create_fargate_access_entry + && var.cluster_authentication_mode != "CONFIG_MAP" + ) ? 1 : 0 +} + +################################################################################ +# Fargate Pod Execution IAM Role +################################################################################ + +data "aws_iam_policy_document" "eks_fargate_assume_role" { + count = local.fargate_create_execution_role ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["eks-fargate-pods.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "eks_fargate" { + count = local.fargate_create_execution_role ? 1 : 0 + + name = coalesce(var.fargate_pod_execution_role_name, "${var.name}-fargate-pod-execution") + path = var.fargate_pod_execution_role_path + assume_role_policy = data.aws_iam_policy_document.eks_fargate_assume_role[0].json + permissions_boundary = var.fargate_pod_execution_role_permissions_boundary + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "eks_fargate" { + count = local.fargate_create_execution_role ? 1 : 0 + + role = aws_iam_role.eks_fargate[0].name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" +} + +################################################################################ +# Fargate Profiles +################################################################################ + +resource "aws_eks_fargate_profile" "this" { + for_each = var.fargate_profiles + + region = var.region + + cluster_name = aws_eks_cluster.this.name + fargate_profile_name = each.key + pod_execution_role_arn = local.fargate_pod_execution_role_arn_effective + subnet_ids = each.value.subnet_ids != null ? each.value.subnet_ids : var.subnet_ids + + dynamic "selector" { + for_each = each.value.selectors + content { + namespace = selector.value.namespace + labels = try(selector.value.labels, null) + } + } + + tags = merge(var.tags, each.value.tags) + + depends_on = [aws_eks_cluster.this] +} + +################################################################################ +# Fargate access entry (API / API_AND_CONFIG_MAP only) +################################################################################ + +resource "aws_eks_access_entry" "fargate" { + count = local.fargate_access_entry_count + + region = var.region + + cluster_name = aws_eks_cluster.this.id + principal_arn = local.fargate_pod_execution_role_arn_effective + type = var.fargate_access_entry_type + + tags = var.tags + + depends_on = [ + aws_eks_cluster.this, + ] +} diff --git a/main.tf b/main.tf index aaa7ecb..34909da 100644 --- a/main.tf +++ b/main.tf @@ -344,13 +344,13 @@ resource "aws_eks_cluster" "this" { } } - dynamic "compute_config" { - for_each = var.enable_automode ? [1] : [] - content { - enabled = true - node_pools = var.automode_node_pools - node_role_arn = aws_iam_role.eks_automode_nodes[0].arn - } + # AWS requires compute_config.enabled, elastic_load_balancing.enabled, and + # block_storage.enabled to be aligned (all true or all false). Omitting + # compute_config when Auto Mode is off violates that and fails plan/apply. + compute_config { + enabled = var.enable_automode + node_pools = var.enable_automode ? var.automode_node_pools : null + node_role_arn = length(aws_iam_role.eks_automode_nodes) > 0 ? aws_iam_role.eks_automode_nodes[0].arn : null } enabled_cluster_log_types = var.enabled_cluster_log_types diff --git a/outputs.tf b/outputs.tf index 9c7044d..f909500 100644 --- a/outputs.tf +++ b/outputs.tf @@ -226,6 +226,25 @@ output "cluster_addons" { value = merge(aws_eks_addon.before_compute, aws_eks_addon.this) } +################################################################################ +# Fargate +################################################################################ + +output "fargate_role_arn" { + description = "Fargate pod execution IAM role ARN in use (module-created or supplied via fargate_pod_execution_role_arn when fargate_profiles is non-empty)" + value = local.fargate_pod_execution_role_arn_effective +} + +output "fargate_profiles" { + description = "Map of EKS Fargate profiles created" + value = aws_eks_fargate_profile.this +} + +output "fargate_access_entry_arn" { + description = "ARN of the module-managed Fargate access entry (when created)" + value = try(aws_eks_access_entry.fargate[0].access_entry_arn, null) +} + ################################################################################ # EKS Managed Node Groups ################################################################################ diff --git a/tests/eks_test.tftest.hcl b/tests/eks_test.tftest.hcl index e6415e2..cce1dc7 100644 --- a/tests/eks_test.tftest.hcl +++ b/tests/eks_test.tftest.hcl @@ -457,3 +457,94 @@ run "eks_automode" { error_message = "EC2 node IAM role should not be created when using Auto Mode (no managed node groups)" } } + +run "eks_fargate_profiles" { + command = plan + + variables { + name = "test-eks-fargate" + kubernetes_version = "1.35" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-private-a", "subnet-private-b"] + fargate_profiles = { + kube-system = { + selectors = [{ namespace = "kube-system" }] + } + } + } + + assert { + condition = length(aws_iam_role.eks_fargate) == 1 + error_message = "Fargate pod execution IAM role should be created when fargate_profiles is set and create_fargate_pod_execution_role is true" + } + + assert { + condition = length(aws_eks_fargate_profile.this) == 1 + error_message = "One Fargate profile should be created for kube-system" + } + + assert { + condition = length(aws_eks_access_entry.fargate) == 1 + error_message = "Fargate access entry should exist for API_AND_CONFIG_MAP default auth" + } +} + +run "eks_fargate_byo_execution_role" { + command = plan + + variables { + name = "test-eks-fargate-byo" + kubernetes_version = "1.35" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-private-a", "subnet-private-b"] + create_fargate_pod_execution_role = false + fargate_pod_execution_role_arn = "arn:aws:iam::123456789012:role/existing-fargate-pod-exec" + fargate_profiles = { + app = { + selectors = [{ namespace = "app" }] + } + } + } + + assert { + condition = length(aws_iam_role.eks_fargate) == 0 + error_message = "Module should not create Fargate execution role when create_fargate_pod_execution_role is false" + } + + assert { + condition = aws_eks_fargate_profile.this["app"].pod_execution_role_arn == "arn:aws:iam::123456789012:role/existing-fargate-pod-exec" + error_message = "Fargate profile should use the supplied pod execution role ARN" + } + + assert { + condition = length(aws_eks_access_entry.fargate) == 1 + error_message = "Fargate access entry should still be created for the supplied execution role ARN" + } +} + +run "eks_fargate_skip_access_entry" { + command = plan + + variables { + name = "test-eks-fargate-no-ae" + kubernetes_version = "1.35" + vpc_id = "vpc-12345678" + subnet_ids = ["subnet-private-a", "subnet-private-b"] + create_fargate_access_entry = false + fargate_profiles = { + app = { + selectors = [{ namespace = "app" }] + } + } + } + + assert { + condition = length(aws_eks_access_entry.fargate) == 0 + error_message = "No Fargate access entry when create_fargate_access_entry is false" + } + + assert { + condition = length(aws_iam_role.eks_fargate) == 1 + error_message = "Pod execution role still created when only access entry is skipped" + } +} diff --git a/variables.tf b/variables.tf index 4262361..9561f35 100644 --- a/variables.tf +++ b/variables.tf @@ -539,6 +539,89 @@ variable "automode_node_pools" { default = ["system", "general-purpose"] } +################################################################################ +# Fargate Configuration +################################################################################ + +variable "fargate_profiles" { + description = "Map of EKS Fargate profile configurations (key = profile name). Fargate pods require private subnets with NAT gateway access. Per-profile subnet_ids override the module-level subnet_ids. Pod execution IAM is controlled by create_fargate_pod_execution_role / fargate_pod_execution_role_arn. Access entry behavior: create_fargate_access_entry and fargate_access_entry_type when cluster_authentication_mode is API or API_AND_CONFIG_MAP; for CONFIG_MAP-only clusters you must grant the pod execution role access yourself (e.g. aws-auth)." + type = map(object({ + selectors = list(object({ + namespace = string + labels = optional(map(string)) + })) + subnet_ids = optional(list(string)) + tags = optional(map(string), {}) + })) + default = {} + + validation { + condition = alltrue([for _, p in var.fargate_profiles : length(p.selectors) > 0]) + error_message = "Each fargate_profiles entry must include at least one selector." + } +} + +variable "create_fargate_pod_execution_role" { + description = "Whether to create the shared IAM role for Fargate pod execution. If false, set fargate_pod_execution_role_arn to an existing role ARN." + type = bool + default = true +} + +variable "fargate_pod_execution_role_arn" { + description = "Existing IAM role ARN for Fargate pod execution when create_fargate_pod_execution_role is false. Leave null when the module creates the role." + type = string + default = null + + validation { + condition = ( + length(var.fargate_profiles) == 0 + || var.create_fargate_pod_execution_role + || (try(var.fargate_pod_execution_role_arn, null) != null && var.fargate_pod_execution_role_arn != "") + ) + error_message = "When fargate_profiles is non-empty and create_fargate_pod_execution_role is false, fargate_pod_execution_role_arn must be set to an existing IAM role ARN." + } + + validation { + condition = var.create_fargate_pod_execution_role ? try(var.fargate_pod_execution_role_arn, null) == null : true + error_message = "Do not set fargate_pod_execution_role_arn when create_fargate_pod_execution_role is true." + } +} + +variable "fargate_pod_execution_role_name" { + description = "Name of the created Fargate pod execution IAM role (when create_fargate_pod_execution_role is true). Defaults to {name}-fargate-pod-execution." + type = string + default = null +} + +variable "fargate_pod_execution_role_path" { + description = "IAM path for the created Fargate pod execution role (when create_fargate_pod_execution_role is true)." + type = string + default = "/" +} + +variable "fargate_pod_execution_role_permissions_boundary" { + description = "ARN of a permissions boundary to attach to the created Fargate pod execution role (when create_fargate_pod_execution_role is true)." + type = string + default = null +} + +variable "create_fargate_access_entry" { + description = "Whether to create an EKS access entry for the Fargate pod execution principal. Ignored when cluster_authentication_mode is CONFIG_MAP (no entry is created)." + type = bool + default = true +} + +variable "fargate_access_entry_type" { + description = "Access entry type for the Fargate pod execution principal (when create_fargate_access_entry is true and auth is API-based). Must be a Fargate-compatible type accepted by AWS CreateAccessEntry." + type = string + default = "FARGATE_LINUX" + + validation { + condition = contains(["FARGATE_LINUX"], var.fargate_access_entry_type) + error_message = "fargate_access_entry_type must be FARGATE_LINUX." + } +} + ################################################################################ # Node Group Configuration ################################################################################