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.
- 🚀 Provisions an
aws_eks_node_group(the keystone) plus a module-ownedaws_launch_templateso 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 providerdefault_tagscannot reach on ASG-launched resources). - 🎛️ Deeply-typed
objectschemas 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_changespattern ondesired_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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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
| 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.
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:CreateServiceLinkedRoleis not exercised.
- Existing EKS cluster. The node group joins a cluster created by
terraform-aws-eks(wirecluster_name). The cluster must beACTIVE. - Node IAM role authorized on the cluster. The role passed as
node_role_arnmust be authorized via an EKS access entry of typeEC2_LINUX/EC2(or, on legacy clusters, mapped inaws-auth). Without this, nodes launch but never reachReady. - Node role runtime policies.
node_role_arnmust carry, at minimum: AmazonEKSWorkerNodePolicy—kubeletdescribes EC2/VPC resources (also backs EKS Pod Identity).AmazonEC2ContainerRegistryPullOnly(orAmazonEC2ContainerRegistryReadOnly) — pull container images (required for the built-in networking add-ons).AmazonEKS_CNI_Policy— only 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_arnis set, the key policy must allow the node role andAWSServiceRoleForAutoScalingtokms:CreateGrant,kms:Decrypt, andkms: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.
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
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"
}
}| 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) |
| 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 |
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_changesblock must live in the callingmoduleblock'slifecycle {}, 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"
}
}| 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.tffor the full schema.
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.
- 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_versionare integers. - FORCE-NEW / immutable fields.
cluster_name,node_group_name(_prefix),node_role_arn,subnet_ids,ami_type, andcapacity_typeare 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 bumpslatest_version, and becauseupdate_default_version = trueby default, the new hardened version becomes default — triggering a managed rolling node update governed byupdate_config. Pinlaunch_template_versionto a number string for strict change control. The EKS API resolves$Latest/$Defaultto a concrete number on read, so always pass a numeric string to avoid plan churn. tags↔tags_all↔default_tags.var.tagsis applied to the node group and the launch template;tags_allis the computed merge of resource tags over providerdefault_tags, with resource tags winning on key conflict. Crucially,default_tagsdoes not propagate to ASG-launched instances/volumes/ENIs — the launch template'stag_specifications(driven byvar.tagsandtag_resource_types) is how governance tags actually reach running nodes. Node group tags are not Kubernetes labels — usevar.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.
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 encryption — encrypted = 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 |
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire 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.
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 nodesshows the nodesReady. - Verify hardening: confirm instances report
HttpTokens=required(aws ec2 describe-instances) and volumes showEncrypted=true.
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-..."]
| 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) |
- Create a managed node group
- Amazon EKS node IAM role
- Delete a managed node group
- IAM roles for service accounts (IRSA)
- Terraform:
aws_eks_node_group·aws_launch_template - Sibling modules:
terraform-aws-eks,terraform-aws-iam-role,terraform-aws-vpc,terraform-aws-kms,terraform-aws-security-group,terraform-aws-key-pair
🧡 "Infrastructure as Code should be standardized, consistent, and secure."