EKS Node Groups: Managed vs. Self-Managed, and the Production Architecture
In the last post, we walked through a production eksctl config file field by field. Buried in it were three choices we made without explaining them: managedNodeGroups, t3.medium, and a desiredCapacity of 2. This post is about those choices — because your worker nodes are where the application actually runs, and the decisions here affect cost, availability, performance, and how much operational work lands on your team.
What a node group actually is
A node group is a collection of EC2 worker nodes that share the same configuration — same instance type, same operating system, same IAM role, same scaling settings. Think of it as a fleet: you define it once, and every node in it follows the same rules. If it's a managed node group, AWS helps with the EC2 lifecycle. If it's self-managed, your team owns more of that lifecycle directly.
A single cluster can have multiple node groups — a general-purpose group for most workloads, a memory-optimized group for something like a cache or database. The Kubernetes scheduler sees every node across every group and places pods based on what each node can offer. But within one node group, every node is identical — that uniformity is what makes a group easy to scale, replace, and manage.
There are three practical ways to run worker capacity on EKS: managed node groups, self-managed node groups, and AWS Fargate.
Managed node groups — the practical default
With a managed node group, AWS handles the EC2 lifecycle. Ask for two nodes, and AWS provisions two EC2 instances, joins them to your cluster, and keeps them registered. When you need to update the nodes, AWS can perform a rolling update — draining one node, replacing it, waiting for the new node to become healthy, then moving to the next. With enough replicas and the right disruption settings on your application, this keeps it available through the update.
You still own the EC2 instances — they run in your account, and you pay for them. What AWS takes off your plate is the lifecycle work: node updates, rolling replacement, health monitoring. That's why managed node groups are the practical default for most teams, and what we use throughout this course. See the managed node groups docs for the full mechanics.
Self-managed node groups — full control, full responsibility
Self-managed means you provision and manage the EC2 instances yourself, typically through an Auto Scaling Group with a custom launch template. You get complete control — custom OS images, custom bootstrap scripts, specific kernel settings, hardware configurations a managed node group may not support.
Complete control means complete responsibility, though: manual node upgrades, your own drain-and-replace process, your own AMI maintenance, and every other operational task around the node lifecycle. Self-managed node groups make sense when you have a specific requirement a managed node group can't satisfy — a regulatory constraint, custom security tooling baked into the image, or unusual hardware. For this course, and for most production teams without one of those reasons, managed node groups are the right starting point.
Fargate — no nodes, but real limitations
Fargate is a different model entirely. There are no EC2 worker nodes for you to manage at all. You define a Fargate profile — which namespaces and pod selectors it applies to — and AWS runs the matching pods on infrastructure you never see. No nodes to patch, no node capacity to manage. You pay for the vCPU and memory the pods actually use, calculated per second.
That sounds appealing, but Fargate has real limitations for a production platform. DaemonSets don't run on Fargate — which affects node-level tools like log collectors and security agents, anything that normally needs one pod on every node. Storage differs too: EFS is supported, but EBS-backed Persistent Volumes that depend on node-attached storage aren't the same pattern. Pod startup can take longer, since there's no EC2 node already sitting there waiting. And for steady, predictable workloads, per-pod pricing can end up more expensive than EC2 worker nodes. For a platform running Prometheus, Grafana, and Loki — where node-level DaemonSets are a real part of the observability and security model — managed node groups are the right foundation. See the Fargate on EKS docs for the full list of constraints.
Choosing an instance type
AWS instance types follow a naming pattern: family, generation, size. t3.medium — the t family is general-purpose with burstable CPU, generation 3, medium size: 2 vCPUs, 4 GB RAM. m6i.large — the m family is general-purpose with consistent CPU, generation 6, Intel. c6g.xlarge — the c family is compute-optimized, generation 6, Graviton (ARM).
This course uses t3.medium as a low-cost starting point for the early labs; once the observability stack lands, we monitor actual capacity and scale if needed. For production, the general guidance:
- General workloads —
m6iorm7i: balanced CPU and memory for a wide range of applications. - CPU-intensive workloads —
c6iorc7i: a higher CPU-to-memory ratio for compute-heavy work. - Memory-intensive workloads —
r6iorr7i: a higher memory-to-CPU ratio. - Cost-conscious production —
m6gorc6g: Graviton (ARM), often a strong price-performance option if your container images support the architecture.
That last point is the catch worth checking before you commit: most modern container images ship as multi-architecture builds, but an image published only as amd64 simply won't run on a Graviton node. Verify your image architecture support before betting production on ARM. See the EC2 instance types reference and the Graviton processor page.
The production node architecture: a bootstrap group, plus Karpenter
In production, teams typically don't size one fixed node group to carry every workload — they split the responsibility. A small managed node group, often called the bootstrap group, gives critical platform components a stable place to run. Karpenter and CoreDNS shouldn't have to compete with application pods for a spot. Some components — the VPC CNI, the EKS Pod Identity agent — run as DaemonSets on every node regardless, but the bootstrap group gives the cluster a safe starting point.
Everything else runs on nodes that Karpenter provisions on demand: it watches for pods that can't be scheduled, picks the right instance type and size for what they actually need, launches new nodes in seconds, and terminates them when they're no longer needed.
Why split it this way? Karpenter is what creates the rest of your compute. If it has to compete with application pods for scheduling, or has no stable place to run at all, your autoscaling becomes fragile exactly when you need it most. The bootstrap group protects the infrastructure that creates the rest of the infrastructure. We'll go deep on Karpenter versus Cluster Autoscaler in a dedicated post — for now, the architecture to hold onto is: one small fixed group for critical platform components, Karpenter for everything else.
For the hands-on lab in this course, we run a single node group that handles everything — the right call for a learning cluster where simplicity matters. When you move to production, the bootstrap-plus-Karpenter split is the pattern to reach for.
Taints keep the split clean
If the bootstrap group is meant for critical add-ons only, what stops your application pods from landing there anyway? A taint. A taint is a signal on a node that says: don't schedule here unless you explicitly tolerate this. Taint the bootstrap nodes with CriticalAddonsOnly:NoSchedule, and regular workload pods — which don't carry a matching toleration — can't be placed there. Only pods that explicitly declare the CriticalAddonsOnly toleration, like Karpenter itself, can land on those nodes. Application pods usually don't have that toleration, so the split stays clean without any extra scheduling logic. In eksctl, you add the taint directly to the node group:
managedNodeGroups:
- name: bootstrap
instanceType: t3.medium
desiredCapacity: 2
minSize: 2
maxSize: 3
taints:
- key: CriticalAddonsOnly
effect: NoSchedule
See the Kubernetes taints and tolerations reference for the full set of effects (NoSchedule, PreferNoSchedule, NoExecute).
Node security: IMDSv2 and EBS encryption
Worker nodes are EC2 instances — they have metadata, IAM access, root volumes, OS-level configuration. Node group design isn't only about scaling and instance types; it's also about security, and two settings are worth deciding explicitly rather than leaving at their defaults.
The instance metadata service, at 169.254.169.254, is how a node retrieves its own metadata — including credentials for its IAM role. If IMDSv1 is allowed, a compromised workload may be able to make a metadata request and walk away with the node role's credentials, then do anything that role permits. IMDSv2 closes that gap by requiring a session token before metadata can be read at all. It isn't a substitute for least privilege, though — the safer EKS pattern is to require IMDSv2, keep the node IAM role minimal, and give application pods their own AWS permissions through Pod Identity or IRSA rather than leaning on the node role. You set this in the node group config:
instanceMetadata: httpTokens: required httpPutResponseHopLimit: 2
httpPutResponseHopLimit controls how far the IMDSv2 token response can travel. A value of 1 keeps it close to the instance itself, which in many EKS environments helps stop ordinary pods from reaching node metadata at all. A value of 2 may be justified if a node-level component genuinely needs IMDS access from inside a container — but don't reach for 2 casually; document why you need it. See the IMDSv2 mechanics and the EKS Best Practices Guide's take on node credential theft.
The second setting protects the node's disk rather than its credentials. Every worker node has a root EBS volume — where the OS, kubelet files, and container image layers live — and you should not assume it's encrypted by default; that depends on your account settings and node group configuration. The production rule is simple: verify or enforce encryption on worker-node root volumes, whether through your IaC tool or account-level EBS encryption defaults. The exact syntax matters less than the decision itself — an unencrypted root volume left to chance is exactly the kind of gap a compliance review finds. See Amazon EBS encryption for the defaults and KMS options.
The node IAM role — keep it minimal
Every EC2 worker node in a managed node group runs with an IAM role — the node IAM role. Application pods shouldn't use it for their own permissions, but if a pod can reach the metadata service, it may be able to obtain that role's credentials anyway. That's the direct reason to keep it as small as possible.
What the node actually needs is narrow: AmazonEKSWorkerNodePolicy lets it communicate with the cluster and register with the control plane; AmazonEC2ContainerRegistryPullOnly lets it pull images from ECR with read-only permissions. There's one common exception — the VPC CNI needs AWS permissions to manage pod networking, provided through AmazonEKS_CNI_Policy. You'll see that attached directly to the node role on simple lab clusters, but for production, attach it instead to a separate role used by the aws-node service account, not the node role itself. Then there's AmazonSSMManagedInstanceCore, which Kubernetes workloads don't need at all — it's only relevant if you want to reach the EC2 nodes through Systems Manager Session Manager instead of SSH, and should be added only if you actually need that access.
The production rule: keep the node IAM role small, move CNI permissions to the aws-node service account where possible, add SSM only if you need node access, and never attach anything broad like AmazonEC2FullAccess or administrator-level permissions. If a workload escapes its container boundary or reaches node metadata, it inherits whatever the node role allows — least privilege here directly reduces the blast radius, not just a checkbox for a security review. eksctl creates the node IAM role for you with managed node groups; for production, review exactly what's attached. Full reference: Amazon EKS node IAM role.
Pod density and VPC CNI prefix delegation
Here's something that surprises many teams the first time they run EKS in production: a node can still have CPU and memory to spare, and Kubernetes still can't schedule another pod on it. The reason is IP addresses, not compute. With the Amazon VPC CNI, every pod gets an IP from your VPC, and each EC2 instance type supports a limited number of network interfaces and IPs — so the number of pods a node can run is also an IP-address question, and on smaller instance types that limit can be lower than you'd expect. When you pick an instance type, check its EKS max-pods behavior alongside vCPU and RAM, not instead of it.
In a real cluster this shows up as pods stuck in Pending while the nodes don't look full on CPU or memory at all — the missing resource is pod IPs. One way to improve this is VPC CNI prefix delegation: instead of assigning individual secondary IPs to a node, the CNI assigns IPv4 prefixes — for IPv4 clusters, each prefix is a /28 block of 16 addresses — which can meaningfully raise pod density on supported instance types.
One design point matters here: prefix delegation doesn't create free IP addresses, it still consumes them from your VPC subnets. If your worker subnets are too small, or too fragmented to offer enough contiguous /28 blocks, you can still hit IP exhaustion with prefix delegation enabled. Before a production cluster exists, check three things: the instance types you plan to use, the max-pods behavior for those types, and whether your subnets actually have enough IP capacity for the nodes and pods you expect. Subnet design is painful to change after the fact — plan it before the cluster, not after. See increasing available IPs for EC2 nodes for the full prefix-delegation and subnet-sizing guide.
Right-sizing your nodes
Avoid choosing a node size just because it looks cheaper. Every node carries overhead before your application gets any usable capacity at all — kubelet, kube-proxy, the container runtime, OS processes, and system add-ons all consume CPU and memory first. On something as small as t3.micro, a node can technically join the cluster and still have very little practical capacity left over — it looks available and can't actually schedule much.
For this course's labs, two t3.medium nodes are a reasonable starting point, and we watch and scale as the observability stack gets added later. In production, many teams skip fixed-size sizing altogether and let Karpenter handle it: Karpenter looks at pending pods, their resource requests, and their scheduling constraints, then provisions nodes that actually match — often improving bin-packing and cost compared with running one fixed instance type for every workload. Node design isn't a decision you make once; it's about continuously matching capacity to the workloads you actually run.
Decisions to get right before you create a node group
- Node model. Managed node groups by default. Self-managed only for a specific requirement they can't satisfy. Fargate only where node-level DaemonSets aren't a hard requirement.
- Instance type. Match the family to the workload (general/CPU/memory-optimized), and verify multi-arch image support before committing to Graviton.
- Production topology. A small bootstrap group, tainted
CriticalAddonsOnly:NoSchedule, for platform components; Karpenter for everything else. - Node security.
httpTokens: requiredalways; keephttpPutResponseHopLimitat the lowest value that works; verify or enforce root EBS volume encryption. - Node IAM role. Only
AmazonEKSWorkerNodePolicyandAmazonEC2ContainerRegistryPullOnlyby default. CNI permissions onaws-node, not the node role. SSM only if you need it. Nothing broader. - Pod density. Check max-pods per instance type and subnet IP capacity before creating the cluster — prefix delegation raises density, it doesn't create IP addresses.
None of this is a one-time checkbox — instance type, node topology, and IAM scope all shape how the cluster behaves under real load, long after the CLI command that created it is forgotten. That's the kind of production judgment DMI's graded weekly loop is built to develop, one real decision at a time.
Want the fundamentals these decisions build on? Start with DMI Self-Paced →