Alright, let’s talk about the actual compute power in your EKS cluster: the nodes. This is where your pods actually run, and AWS gives you three distinct flavors to choose from. Picking the right one isn’t just a technicality; it’s the difference between a smooth ride and a part-time job you never applied for.

Managed Node Groups: Your Default Choice

If you’re not a masochist, start here. An EKS Managed Node Group (MNG) is AWS saying, “Hey, we’ll handle the Kubernetes worker node boilerplate for you.” They provision the underlying EC2 instances, register them with your cluster, and—this is the killer feature—manage the node lifecycle, including automated rolling updates and terminations.

Why would you use this? Because you have better things to do than manually SSH into nodes to patch kubelet or drain them safely. When a new AMI version comes out, you trigger an update, and AWS gracefully cordons and drains each node, replacing it with a new one. It’s glorious. You also get automatic enrollment in the AWS security patch program for the underlying OS.

The catch? You’re limited to a subset of EC2 instance types (though it covers 95% of use cases) and you can’t customize the node bootstrap process after the initial deployment. You get what AWS gives you, which is usually exactly what you need.

Here’s how you create one with eksctl. This is the way. Writing CloudFormation by hand is for chumps.

# eksctl-config.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: my-awesome-cluster
  region: us-west-2
managedNodeGroups:
  - name: ng-1
    instanceType: m5.large
    minSize: 2
    maxSize: 10
    desiredCapacity: 3
    labels:
      role: workload
    tags:
      # This tag is crucial for the AWS Cloud Provider to handle ELB resources correctly
      # Not having this is a classic "why don't my LoadBalancers work?!" pitfall.
      k8s.io/cluster-autoreleaser/my-awesome-cluster: owned

Run eksctl create nodegroup -f eksctl-config.yaml and go get a coffee. It’s managed.

Self-Managed Nodes: For When You Need the Reins

Sometimes, MNGs are too, well, managed. You need a custom AMI baked with your own security agents, a specific kernel module, or perhaps an instance type so new it’s not yet supported in MNGs. Enter the self-managed node group.

This is essentially you saying, “I got this, AWS.” You’re responsible for the entire lifecycle: provisioning, scaling, updating, and terminating. You typically use a CloudFormation template (often the one AWS provides as a starting point) or the classic Auto Scaling Group.

Why would you subject yourself to this? Ultimate flexibility. You can use any AMI, any instance type, and run any bootstrap script you want. The downside? You’re on the hook for everything. Security patches? Your problem. kubelet upgrades? Your problem. Draining nodes correctly before termination? You better believe that’s your problem. It’s a solid choice, but it’s a commitment.

The biggest pitfall here is forgetting to properly integrate the node with the cluster. Your user data script must include the correct bootstrap.sh command with your cluster’s endpoint and the signed certificate. Mess this up, and the node will sit there, useless and lonely.

#!/bin/bash
# Example snippet for a self-managed node's UserData script
/etc/eks/bootstrap.sh my-awesome-cluster \
  --kubelet-extra-args '--node-labels=role=special-worker,env=prod' \
  --b64-cluster-ca <your-cluster's-ca-cert> \
  --apiserver-endpoint <your-cluster's-endpoint>

Fargate: No Nodes, No Problems (Mostly)

Fargate is the ultimate abstraction: Serverless for Kubernetes. You don’t see, manage, or pay for nodes. You define a pod, and AWS finds a place to run it on its own internal, ephemeral infrastructure. You’re billed per vCPU and GB of memory your pod requests, down to the second.

It’s magic… until it isn’t. The “why” is all about operational overhead (or lack thereof). No node security patches, no capacity planning, no dealing with noisy neighbors. It’s perfect for simple web services, batch jobs, or anything where you just want to define a pod spec and forget it.

But the designers made some questionable choices that absolutely neuter it for certain use cases. Ready?

  1. No DaemonSets: Want a service mesh sidecar? You have to manually include it in every single pod spec. No exceptions.
  2. No Privileged Pods: This means no hostNetwork, no hostPort, and no running Docker-in-Docker. Say goodbye to a lot of CI/CD tools that expect this.
  3. Cold Start Hell: A pod scheduled on Fargate can take significantly longer to start (sometimes 60+ seconds) than on an EC2 node. This is awful for horizontal pod autoscaling in response to traffic spikes.

You define a Fargate Profile, which is basically a set of rules (namespace + optional labels) that tell EKS, “Hey, any pod matching this selector should run on Fargate.”

# A Fargate Profile via eksctl
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: my-awesome-cluster
  region: us-west-2
fargateProfiles:
  - name: fp-default
    selectors:
      # This sends any pod in the 'default' namespace to Fargate. Be careful!
      - namespace: default
      # This is more precise and usually smarter
      - namespace: web-app
        labels:
          role: api

So, which one do you pick? Use Managed Node Groups for almost everything. Use Self-Managed when you have very specific needs that MNGs can’t satisfy. Use Fargate for low-complexity, bursty workloads where the operational benefits outweigh the massive limitations. And always, always read the fine print.