Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS EKS Managed Node Group Terraform Module

Attaches a secure, encrypted, IMDSv2-hardened EKS managed node group — backed by a module-owned launch template — to an existing EKS cluster. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🚀 Provisions an aws_eks_node_group (the keystone) plus a module-owned aws_launch_template so the data-plane capacity for an EKS cluster is created with one call.
  • 🔐 Secure by default: IMDSv2 required, metadata hop limit 1, EBS root and data volumes always encrypted (AWS-managed key, or a caller-supplied CMK), private-subnet placement, conservative rolling updates.
  • 🧱 The launch template is always present — IMDSv2, EBS encryption, and metadata hardening are guaranteed rather than left to EKS AMI defaults.
  • 🏷️ Universal tags flow to the node group, the launch template, and — via launch-template tag_specifications — to the launched instances, volumes, and ENIs (which provider default_tags cannot reach on ASG-launched resources).
  • 🎛️ Deeply-typed object schemas for scaling, updates, metadata, root and additional volumes; map(object) collections for taints and extra data volumes.
  • 🔁 Plays nicely with Cluster Autoscaler / Karpenter via a documented ignore_changes pattern on desired_size.

💡 Why it matters: an EKS node group is where pods actually run — the blast radius of an un-encrypted volume, an IMDSv1-reachable instance profile, or a public node is large in an PII/privacy-regulation estate. This module makes the secure posture the path of least resistance.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart LR
 iam["terraform-aws-iam-role<br/>(node role ARN)"]
 vpc["terraform-aws-vpc<br/>(private subnet ids)"]
 kms["terraform-aws-kms<br/>(EBS CMK ARN)"]
 sg["terraform-aws-security-group<br/>(extra node SGs)"]
 kp["terraform-aws-key-pair<br/>(optional SSH key)"]
 eks["terraform-aws-eks<br/>(cluster name + access entry)"]
 ng["terraform-aws-eks-node-group<br/>(THIS MODULE)"]

 iam --> ng
 vpc --> ng
 kms --> ng
 sg --> ng
 kp --> ng
 eks --> ng

 style ng fill:#FF9900,color:#fff
Loading

🧬 What this module builds

flowchart TB
 subgraph M["terraform-aws-eks-node-group"]
 lt["aws_launch_template.this<br/>• IMDSv2 required, hop limit 1<br/>• encrypted root + data volumes<br/>• tag_specifications (instance/volume/eni)<br/>• create_before_destroy"]
 ng["aws_eks_node_group.this<br/>• scaling_config (min/max/desired)<br/>• update_config (rolling)<br/>• taints / labels<br/>• node_repair_config<br/>• pins launch_template version"]
 lt -->|"launch_template { id, version }"| ng
 end

 role["node_role_arn (consumed)"] --> ng
 subs["subnet_ids (consumed)"] --> ng
 cmk["kms_key_arn (consumed)"] --> lt
 asg["EKS-managed Auto Scaling group<br/>(created by EKS, not Terraform)"]
 ng -.->|provisions| asg

 style ng fill:#FF9900,color:#fff
 style lt fill:#FFE0B2,color:#000
Loading

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0 (current GA v6.51.0)

No provider {} block is declared inside the module — it inherits the caller's configured provider and region.


🔑 Required IAM Permissions

Least-privilege actions the Terraform identity needs to manage this module. (These are deploy-time permissions — distinct from the runtime policies on node_role_arn, covered under Prerequisites.)

Action Required for Notes
eks:CreateNodegroup, eks:DeleteNodegroup, eks:DescribeNodegroup Node group lifecycle
eks:UpdateNodegroupConfig, eks:UpdateNodegroupVersion Scaling/label/taint and version updates Version updates trigger managed rolling node replacement
eks:TagResource, eks:UntagResource, eks:ListTagsForResource Node group tagging
ec2:CreateLaunchTemplate, ec2:DeleteLaunchTemplate Launch template lifecycle
ec2:CreateLaunchTemplateVersion, ec2:ModifyLaunchTemplate New hardened LT versions (rolls nodes)
ec2:DescribeLaunchTemplates, ec2:DescribeLaunchTemplateVersions Plan/read
ec2:CreateTags, ec2:DescribeTags Launch-template & launch-time tagging
iam:PassRole Pass the node IAM role to EKS/EC2 Scope to node_role_arn only — this is the highest-value permission to constrain
iam:CreateServiceLinkedRole AWSServiceRoleForAutoScaling Managed node groups run on an ASG under the hood; needed only on first use in the account
ec2:RunInstances, autoscaling:CreateOrUpdateTags Node provisioning / tag propagation Exercised by EKS on your behalf during scale-out
kms:DescribeKey, kms:CreateGrant CMK-encrypted EBS volumes Only when kms_key_arn is set; on the key, scoped to kms_key_arn

Service-linked role auto-creation: the first managed node group in an account triggers creation of AWSServiceRoleForAutoScaling. If the SLR already exists, iam:CreateServiceLinkedRole is not exercised.


📋 AWS Prerequisites

  • Existing EKS cluster. The node group joins a cluster created by terraform-aws-eks (wire cluster_name). The cluster must be ACTIVE.
  • Node IAM role authorized on the cluster. The role passed as node_role_arn must be authorized via an EKS access entry of type EC2_LINUX/EC2 (or, on legacy clusters, mapped in aws-auth). Without this, nodes launch but never reach Ready.
  • Node role runtime policies. node_role_arn must carry, at minimum:
  • AmazonEKSWorkerNodePolicykubelet describes EC2/VPC resources (also backs EKS Pod Identity).
  • AmazonEC2ContainerRegistryPullOnly (or AmazonEC2ContainerRegistryReadOnly) — pull container images (required for the built-in networking add-ons).
  • AmazonEKS_CNI_Policyonly if the VPC CNI uses the node instance role; the recommended pattern is to move CNI permissions to a dedicated IRSA/EKS Pod Identity role instead.
  • Service-linked role: AWSServiceRoleForAutoScaling (auto-created on first node group).
  • Networking: subnets (typically private) tagged for EKS discovery — kubernetes.io/cluster/<cluster_name> = shared|owned — and spread across multiple AZs.
  • KMS (optional, CMK path): if kms_key_arn is set, the key policy must allow the node role and AWSServiceRoleForAutoScaling to kms:CreateGrant, kms:Decrypt, and kms:GenerateDataKeyWithoutPlaintext, or instance launches fail with an encryption error.
  • Region: provider-inherited. No us-east-1 global-service constraint applies to EKS node groups.
  • Service quotas (soft, raisable): node groups per cluster, nodes per node group, and the account-level EC2 vCPU / launch-template / EBS quotas the running nodes consume.

📁 Module Structure

terraform-aws-eks-node-group/
├── providers.tf # terraform >= 1.12, aws >= 6.0 < 7.0 (no provider block)
├── variables.tf # identity → required → optional → tags → timeouts
├── main.tf # aws_launch_template.this + aws_eks_node_group.this
├── outputs.tf # id, arn, name, status, launch_template_*, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, gotchas

⚙️ Quick Start

Smallest working call, wiring upstream module outputs:

module "general_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = {
    min_size     = 2
    max_size     = 6
    desired_size = 3
  }

  instance_types = ["m6i.large"]

  tags = {
    Environment = "prod"
    CostCenter  = "platform"
  }
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
cluster_name string terraform-aws-eks (name)
node_role_arn string (IAM role ARN) terraform-aws-iam-role (arn)
subnet_ids list(string) terraform-aws-vpc (private subnet ids)
security_group_ids list(string) terraform-aws-security-group
kms_key_arn string (KMS key ARN) terraform-aws-kms (arn)
key_name string terraform-aws-key-pair (optional)

Emits

Output Description Consumed by
id Node group id (<cluster>:<nodegroup>) most consumers
arn Node group ARN (cross-resource reference type) IAM policies, monitoring, EventBridge
name Node group name scaling, kubectl drains
cluster_name Cluster the group attaches to downstream wiring
status ACTIVE / CREATING / UPDATING CI/CD gating
resources_autoscaling_group_names Underlying EKS-managed ASG name(s) scaling integrations
launch_template_id / launch_template_arn Module-owned LT identifiers audit
launch_template_latest_version / _default_version LT version numbers change control
tags_all / launch_template_tags_all Merged tags incl. default_tags governance/audit

📚 Example Library

1 · Minimal node group
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = {
    min_size     = 1
    max_size     = 3
    desired_size = 1
  }
}
2 · With governance tags

tags reach the node group, the launch template, and (via launch-template tag_specifications) the launched instances, volumes, and ENIs. They merge with provider default_tags; resource tags win on key conflict.

module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Owner       = "platform-team"
  }
}
3 · Customer-managed KMS key for EBS encryption (preferred for PII)

Volumes are always encrypted; supplying a CMK makes the key auditable and revocable.

module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }

  kms_key_arn = module.ebs_cmk.arn # the key policy must allow the node role
  # and AWSServiceRoleForAutoScaling to use it
}
4 · Larger root volume + additional encrypted data volume
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 2, max_size = 8, desired_size = 3 }

  root_volume = {
    volume_size = 100
    volume_type = "gp3"
    iops        = 4000
    throughput  = 250
  }

  additional_block_device_mappings = {
    "/dev/xvdb" = {
      volume_size = 200
      volume_type = "gp3"
    }
  }
}
5 · SPOT capacity with diversified instance types
module "spot_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  capacity_type  = "SPOT"
  instance_types = ["m6i.large", "m5.large", "m5a.large"] # diversify for SPOT

  scaling_config = { min_size = 0, max_size = 10, desired_size = 2 }

  labels = { lifecycle = "spot" }
}
6 · Dedicated node group with taints (reserve for specific workloads)
module "gpu_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  ami_type       = "AL2023_x86_64_NVIDIA"
  instance_types = ["g5.xlarge"]
  scaling_config = { min_size = 0, max_size = 4, desired_size = 0 }

  labels = { "nvidia.com/gpu" = "true" }

  taints = {
    gpu_only = {
      key    = "nvidia.com/gpu"
      value  = "true"
      effect = "NO_SCHEDULE"
    }
  }
}
7 · Bottlerocket nodes (minimal, IMDSv2-native host OS)
module "bottlerocket_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  ami_type       = "BOTTLEROCKET_x86_64"
  instance_types = ["m6i.large"]
  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }
}
8 · Co-existing with Cluster Autoscaler / Karpenter (ignore desired_size)

Let the autoscaler own desired_size; Terraform owns the bounds.

module "autoscaled_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 2, max_size = 20, desired_size = 2 }

  tags = {
    "k8s.io/cluster-autoscaler/enabled"            = "true"
    "k8s.io/cluster-autoscaler/${module.eks.name}" = "owned"
  }
}

# At the call site, stop Terraform fighting the autoscaler:
# lifecycle { ignore_changes = [scaling_config[0].desired_size] }

The ignore_changes block must live in the calling module block's lifecycle {}, not inside the module.

9 · Aggressive rolling update by percentage
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 4, max_size = 12, desired_size = 6 }

  update_config = {
    max_unavailable_percentage = 33 # exactly one of count/percentage
    update_strategy            = "DEFAULT"
  }
}
10 · Pin Kubernetes version + AMI release for change control
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  kubernetes_version   = "1.30"
  ami_release_version  = "1.30.0-20240625" # example release string
  force_update_version = false             # respect Pod Disruption Budgets

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }
}
11 · Node auto-repair enabled
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }

  node_repair_config = { enabled = true } # EKS replaces unhealthy nodes
}
12 · Extra security groups + detailed monitoring
module "nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name       = module.eks.name
  node_role_arn      = module.eks_node_role.arn
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.node_extra_sg.id]

  enable_detailed_monitoring = true
  ebs_optimized              = true

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }
}
13 · Secure-by-default opt-out (⚠️ exception — SSH key + relaxed metadata hop)

Documented exception only. Prefer SSM Session Manager and IRSA/Pod Identity over SSH and a raised hop limit.

module "debug_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  key_name = module.bastion_key.id # SSH access (discouraged)

  metadata_options = {
    http_tokens                 = "required" # keep IMDSv2 — do NOT relax this
    http_put_response_hop_limit = 2          # ⚠️ lets pods reach node IMDS creds
  }

  scaling_config = { min_size = 1, max_size = 2, desired_size = 1 }
}
14 · Custom AMI (CUSTOM ami_type + image_id + user_data)

You own the bootstrap when you go custom.

module "custom_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids

  ami_type  = "CUSTOM"
  image_id  = module.hardened_ami.id
  user_data = base64encode(local.nodeadm_bootstrap) # must join the cluster

  root_volume = { device_name = "/dev/xvda", volume_size = 50 } # match the AMI

  scaling_config = { min_size = 2, max_size = 6, desired_size = 2 }
}
15 · 🏁 Cross-module composition finale (VPC → IAM → KMS → EKS → node group)

End-to-end wiring of the family.

module "vpc" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-vpc?ref=v1.0.0"
  #...
}

module "ebs_cmk" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  # key policy allows the node role + AWSServiceRoleForAutoScaling to use the key
}

module "eks_node_role" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"

  assume_role_policy = data.aws_iam_policy_document.node_trust.json # ec2.amazonaws.com
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly",
    "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
  ]
}

module "eks" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks?ref=v1.0.0"

  # authorize the node role via an EC2_LINUX access entry on the cluster
  # access_entries = { nodes = { principal_arn = module.eks_node_role.arn, type = "EC2_LINUX" } }
}

module "general_nodes" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-eks-node-group?ref=v1.0.0"

  cluster_name  = module.eks.name
  node_role_arn = module.eks_node_role.arn
  subnet_ids    = module.vpc.private_subnet_ids
  kms_key_arn   = module.ebs_cmk.arn

  instance_types = ["m6i.large"]
  scaling_config = { min_size = 3, max_size = 12, desired_size = 3 }

  update_config = { max_unavailable = 1 }

  tags = {
    Environment = "prod"
    DataClass   = "PII"
  }
}

📥 Inputs

Name Type Default Description
cluster_name string — (required) EKS cluster to attach to. FORCE-NEW.
node_role_arn string — (required) Worker node IAM role ARN. FORCE-NEW.
subnet_ids list(string) — (required) Subnets for the nodes. FORCE-NEW.
scaling_config object — (required) min_size / max_size / desired_size.
node_group_name / node_group_name_prefix string null Mutually exclusive name controls. FORCE-NEW.
ami_type string null EKS AMI family (validated enum). FORCE-NEW.
capacity_type string null ON_DEMAND / SPOT / CAPACITY_BLOCK. FORCE-NEW.
instance_types list(string) null Node instance types.
kubernetes_version / ami_release_version string null Version pinning.
force_update_version bool false Force version update past PDBs.
labels map(string) {} Kubernetes labels.
taints map(object) {} Kubernetes taints (≤ 50).
update_config object { max_unavailable = 1 } Rolling-update aggressiveness.
node_repair_config object null Node auto-repair toggle.
kms_key_arn string null EBS CMK (else AWS-managed key).
root_volume object {} (gp3, 20 GiB, encrypted) Root EBS volume.
additional_block_device_mappings map(object) {} Extra encrypted data volumes.
metadata_options object IMDSv2 required, hop 1 IMDS hardening.
Launch-template knobs various secure defaults image_id, key_name, security_group_ids, user_data, ebs_optimized, enable_detailed_monitoring, launch_template_*, update_default_version, tag_resource_types.
tags map(string) {} Universal tags.
timeouts object {} create / update / delete.

Every variable carries a heredoc description and validation where a closed value set applies — see variables.tf for the full schema.


🧾 Outputs

See the Emits table under Cross-Module Contract. Primary outputs are id and arn; tags_all and launch_template_tags_all surface the merged tag sets.


🧠 Architecture Notes

  • ID format. id = <cluster_name>:<node_group_name> (e.g. prod-eks:general-nodes).
  • ARN format. arn:aws:eks:<region>:<account>:nodegroup/<cluster>/<name>/<uuid> — the cross-resource reference type for IAM conditions, EventBridge rules, and monitoring.
  • Launch template ARN/ID. lt-... id and a standard EC2 launch-template ARN; latest_version / default_version are integers.
  • FORCE-NEW / immutable fields. cluster_name, node_group_name(_prefix), node_role_arn, subnet_ids, ami_type, and capacity_type are replacement-only. Changing any of them destroys and recreates the node group (a full rolling node replacement).
  • Launch-template version coupling. The node group pins coalesce(var.launch_template_version, latest_version). Any module-driven LT change bumps latest_version, and because update_default_version = true by default, the new hardened version becomes default — triggering a managed rolling node update governed by update_config. Pin launch_template_version to a number string for strict change control. The EKS API resolves $Latest/$Default to a concrete number on read, so always pass a numeric string to avoid plan churn.
  • tagstags_alldefault_tags. var.tags is applied to the node group and the launch template; tags_all is the computed merge of resource tags over provider default_tags, with resource tags winning on key conflict. Crucially, default_tags does not propagate to ASG-launched instances/volumes/ENIs — the launch template's tag_specifications (driven by var.tags and tag_resource_types) is how governance tags actually reach running nodes. Node group tags are not Kubernetes labels — use var.labels.
  • Eventual consistency. A freshly created/updated node role may not be immediately usable; node group creation can transiently fail and EKS retries. Access-entry authorization must be in place before nodes can register.
  • Destroy ordering. Destroy the node group before the cluster. Nodes must drain and their ENIs detach before the cluster, subnets, or the node security group can be removed. With a separately-managed CNI/SG, dangling ENIs are the classic cause of a stuck destroy.
  • us-east-1 globals. Not applicable — EKS node groups are regional; no CloudFront/WAF/ACM coupling.

🧱 Design Principles

Secure-by-default posture and the explicit opt-out for each:

Posture Default Opt-out
Instance metadata IMDSv2 required (http_tokens = "required"), hop limit 1 Raise metadata_options.http_put_response_hop_limit to 2 (lets pods reach node creds — discouraged; relaxing http_tokens is strongly discouraged)
EBS encryption Always on (root + data volumes); AWS-managed key by default Supply kms_key_arn for a CMK. No opt-out from encryptionencrypted = true is validation-enforced
Node placement Private subnets, no public IP Pass public subnets (discouraged)
SSH access None (use SSM Session Manager) Set key_name (discouraged)
Rolling updates Conservative max_unavailable = 1 Larger surge via update_config (audited)
Version updates Respect Pod Disruption Budgets (force_update_version = false) force_update_version = true (may forcibly evict pods)
Launch template Module-owned — guarantees the hardening above n/a (intentional; not configurable away)
Launch-time tagging Tags instances/volumes/ENIs Narrow or empty tag_resource_types

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

plan/apply require valid AWS credentials (profile / SSO / OIDC) and a region, plus an existing EKS cluster and an authorized node role. The gold-standard CI pattern is OIDC federation — no static keys.


🧪 Testing

  • terraform validate — schema and type correctness (offline).
  • terraform fmt -check — formatting.
  • After apply: aws eks describe-nodegroup --cluster-name <c> --nodegroup-name <n>status: ACTIVE; kubectl get nodes shows the nodes Ready.
  • Verify hardening: confirm instances report HttpTokens=required (aws ec2 describe-instances) and volumes show Encrypted=true.

💬 Example Output

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:eks:us-east-2:123456789012:nodegroup/prod-eks/general-nodes/a1b2c3d4-..."
id = "prod-eks:general-nodes"
name = "general-nodes"
status = "ACTIVE"
launch_template_id = "lt-0a1b2c3d4e5f67890"
launch_template_latest_version = 1
resources_autoscaling_group_names = ["eks-general-nodes-a1b2c3d4-..."]

🔍 Troubleshooting

Symptom Likely cause Fix
Nodes launch but never become Ready Node role not authorized on the cluster Add an EC2_LINUX access entry (or aws-auth mapping) for node_role_arn
AccessDenied on iam:PassRole Terraform identity lacks PassRole on the node role Grant iam:PassRole scoped to node_role_arn
Instance launch fails with KMS error CMK policy doesn't allow the node role / ASG SLR Add kms:CreateGrant, kms:Decrypt, kms:GenerateDataKeyWithoutPlaintext for both principals on kms_key_arn
Perpetual desired_size diff An autoscaler owns the count Add lifecycle { ignore_changes = [scaling_config[0].desired_size] } at the call site
Plan churn on launch_template.version $Latest/$Default resolving to a number Pin launch_template_version to a numeric string
Running nodes/volumes missing governance tags default_tags doesn't reach ASG-launched resources Ensure var.tags is set and tag_resource_types includes the resource types
Destroy hangs / DependencyViolation on subnet/SG ENIs from un-drained nodes still attached Destroy the node group first; let nodes drain and ENIs detach
Credential-chain failure on plan/apply No resolved AWS credentials/region Configure profile/SSO/OIDC and a region in the caller's provider
InvalidParameterException: launch template... instance type Instance type set on both the node group and the LT Set instance types only via instance_types (the module never sets one on the LT)

🔗 Related Docs


🧡 "Infrastructure as Code should be standardized, consistent, and secure."