Skip to main content
EKS managed node groups (MNG) let AWS handle the provisioning, patching, and graceful draining of EC2 worker nodes. You retain full control over instance types, AMI selection, sizing, and node-level configuration while AWS owns the underlying Auto Scaling group lifecycle. This example demonstrates two separate clusters — one using the Amazon Linux 2023 (AL2023) AMI and one using Bottlerocket — to show how the eks_managed_node_groups map works with different operating systems.

Prerequisites

  • AWS credentials with permissions to create EKS, EC2, IAM, and VPC resources
  • Terraform >= 1.5.7
  • AWS provider >= 6.28
  • The example provisions its own VPC — no pre-existing VPC is required

VPC and shared locals

Both clusters share a common VPC defined in main.tf.
provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {
  # Exclude local zones
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

locals {
  name   = "ex-eks-mng"
  region = "eu-west-1"

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-eks"
    GithubOrg  = "terraform-aws-modules"
  }
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 6.0"

  name = local.name
  cidr = local.vpc_cidr

  azs             = local.azs
  private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
  public_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)]
  intra_subnets   = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 52)]

  enable_nat_gateway = true
  single_nat_gateway = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
  }

  tags = local.tags
}

Amazon Linux 2023 cluster (eks-al2023.tf)

AL2023 is the default AMI type for EKS managed node groups starting with Kubernetes 1.30. It uses nodeadm for node initialization, and additional configuration can be injected via cloudinit_pre_nodeadm.
module "eks_al2023" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 21.0"

  name               = "${local.name}-al2023"
  kubernetes_version = "1.33"

  # EKS Addons
  addons = {
    coredns = {}
    eks-pod-identity-agent = {
      before_compute = true
    }
    kube-proxy = {}
    vpc-cni = {
      before_compute = true
    }
  }

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  eks_managed_node_groups = {
    example = {
      # Starting on 1.30, AL2023 is the default AMI type for EKS managed node groups
      instance_types = ["m6i.large"]
      ami_type       = "AL2023_x86_64_STANDARD"

      min_size = 2
      max_size = 5
      # This value is ignored after the initial creation
      # https://github.com/bryantbiggs/eks-desired-size-hack
      desired_size = 2

      # This is not required - demonstrates how to pass additional configuration to nodeadm
      # Ref https://awslabs.github.io/amazon-eks-ami/nodeadm/doc/api/
      cloudinit_pre_nodeadm = [
        {
          content_type = "application/node.eks.aws"
          content      = <<-EOT
            ---
            apiVersion: node.eks.aws/v1alpha1
            kind: NodeConfig
            spec:
              kubelet:
                config:
                  shutdownGracePeriod: 30s
          EOT
        }
      ]
    }
  }

  tags = local.tags
}

Bottlerocket cluster (eks-bottlerocket.tf)

Bottlerocket is a Linux-based OS purpose-built for container workloads. Node configuration is injected via the bootstrap_extra_args field using TOML syntax.
module "eks_bottlerocket" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 21.0"

  name               = "${local.name}-bottlerocket"
  kubernetes_version = "1.33"

  # EKS Addons
  addons = {
    coredns = {}
    eks-pod-identity-agent = {
      before_compute = true
    }
    kube-proxy = {}
    vpc-cni = {
      before_compute = true
    }
  }

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  eks_managed_node_groups = {
    example = {
      ami_type       = "BOTTLEROCKET_x86_64"
      instance_types = ["m6i.large"]

      min_size = 2
      max_size = 5
      # This value is ignored after the initial creation
      # https://github.com/bryantbiggs/eks-desired-size-hack
      desired_size = 2

      # This is not required - demonstrates how to pass additional configuration
      # Ref https://bottlerocket.dev/en/os/1.19.x/api/settings/
      bootstrap_extra_args = <<-EOT
        # The admin host container provides SSH access and runs with "superpowers".
        # It is disabled by default, but can be disabled explicitly.
        [settings.host-containers.admin]
        enabled = false

        # The control host container provides out-of-band access via SSM.
        # It is enabled by default, and can be disabled if you do not expect to use SSM.
        # This could leave you with no way to access the API and change settings on an existing node!
        [settings.host-containers.control]
        enabled = true

        # extra args added
        [settings.kernel]
        lockdown = "integrity"
      EOT
    }
  }

  tags = local.tags
}

Key configuration options

AMI types

ami_type valueOSNotes
AL2023_x86_64_STANDARDAmazon Linux 2023 (x86)Default from Kubernetes 1.30+
AL2023_ARM_64_STANDARDAmazon Linux 2023 (ARM)For Graviton instances
BOTTLEROCKET_x86_64Bottlerocket (x86)TOML-based configuration
BOTTLEROCKET_ARM_64Bottlerocket (ARM)For Graviton instances

before_compute addon flag

Setting before_compute = true on vpc-cni and eks-pod-identity-agent ensures those addons are installed before the first node joins the cluster, preventing race conditions where pods start before the CNI is ready.

desired_size and drift

desired_size is only respected on the initial creation of the node group. After creation, the Auto Scaling group’s actual desired capacity is the source of truth. See eks-desired-size-hack for a workaround pattern.
Use use_latest_ami_release_version = true on a node group to automatically track the latest patched AMI release for the configured AMI type and Kubernetes version.

Deploy

1

Initialize Terraform

terraform init
2

Review the plan

terraform plan
3

Apply

Both clusters are created together. Expect 15–20 minutes for all resources.
terraform apply
4

Configure kubectl

# For the AL2023 cluster
aws eks update-kubeconfig --region eu-west-1 --name ex-eks-mng-al2023

# For the Bottlerocket cluster
aws eks update-kubeconfig --region eu-west-1 --name ex-eks-mng-bottlerocket

Key outputs

OutputDescription
cluster_nameName of the EKS cluster
cluster_endpointKubernetes API server endpoint
eks_managed_node_groupsMap of attributes for all managed node groups
eks_managed_node_groups_autoscaling_group_namesAuto Scaling group names for each node group

Full example on GitHub

View the complete example including outputs.tf, variables.tf, and versions.tf.

Build docs developers (and LLMs) love