AWS EKS

By Vishvesh Patel · DevOps EngineerUpdated September 11, 2026

How Amazon EKS runs managed Kubernetes on AWS. Control plane, node options, access entries, IRSA and Pod Identity, add-ons, and what each choice costs you.

Amazon EKS

Amazon EKS runs the Kubernetes control plane for you. AWS operates the API server and etcd across three Availability Zones, patches them, and backs them with an SLA. You keep everything above that line: worker capacity, networking, add-ons, and access control.

That split is the whole point, and it's also where most of the surprises live. A cluster comes up in fifteen minutes; making it production-ready takes considerably longer, because the pieces EKS doesn't manage are the ones that cause outages.

EKS: Managed Kubernetes Architecture

kubectl / eksctl / AWS Console
EKS Control Plane (AWS managed)
API server · etcd · scheduler
manages
Managed Node Group
EC2 workers
Fargate Profile
serverless pods
Pods / Services / Deployments

AWS manages the control plane HA. You only pay for worker nodes

What AWS runs and what you run

LayerManaged byWhat that means for you
API server, etcd, schedulerAWSYou never SSH to it, can't tune etcd flags, can't install API server plugins
Worker capacityYou (or AWS, with Fargate/Auto Mode)Instance types, AMIs, patching cadence, scaling
VPC, subnets, security groupsYouPod IP capacity and load balancer placement depend on it
Core add-ons (CNI, CoreDNS, kube-proxy)SharedAWS publishes versions; you choose when to upgrade
Workload IAMYouVia IRSA or EKS Pod Identity

The control plane costs $0.10/hour per cluster, about $73/month, before you run a single pod. That's per cluster, so the "one cluster per team per environment" pattern gets expensive quickly. Nodes, load balancers, and data transfer are all billed separately.

Creating a cluster

eksctl is the fastest path. It's a community tool, not an AWS product, but it's the de facto standard and it provisions the VPC, IAM roles, and node group in one command:

# Simplest possible cluster. Fine for learning, not for production
eksctl create cluster \
  --name demo \
  --region us-east-1 \
  --nodes 2 \
  --node-type t3.medium

# Point kubectl at it (eksctl does this automatically; this is the manual form)
aws eks update-kubeconfig --region us-east-1 --name demo

kubectl get nodes -o wide
NAME                          STATUS   ROLES    AGE   VERSION
ip-192-168-12-34.ec2.internal Ready    <none>   3m    v1.31.4-eks-a1b2c3d
ip-192-168-56-78.ec2.internal Ready    <none>   3m    v1.31.4-eks-a1b2c3d

Expect cluster creation to take 15 to 20 minutes. The control plane alone is roughly 10 of those.

For anything real, define the cluster in a config file so it's reviewable and reproducible:

# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: production
  region: us-east-1
  version: "1.31"

vpc:
  cidr: 10.0.0.0/16
  nat:
    gateway: HighlyAvailable   # one NAT per AZ; Single is cheaper but a SPOF

managedNodeGroups:
  - name: general
    instanceTypes: ["m6i.large", "m6a.large"]  # mixed types survive capacity crunches
    minSize: 2
    maxSize: 10
    desiredCapacity: 3
    privateNetworking: true
    volumeSize: 50
    volumeType: gp3

addons:
  - name: vpc-cni
  - name: coredns
  - name: kube-proxy
  - name: aws-ebs-csi-driver

cloudWatch:
  clusterLogging:
    enableTypes: ["api", "audit", "authenticator"]
eksctl create cluster -f cluster.yaml

Two things in that file matter more than they look. privateNetworking: true keeps nodes off public subnets. Nodes should never have public IPs. And enabling audit logging from day one means you have the logs when you need them; turning it on after an incident is too late.

Choosing how nodes run

OptionGood forThe catch
Managed node groupsMost workloadsYou still trigger AMI upgrades and handle drains
Self-managed nodesCustom AMIs, GPU/specialised kernelsYou own all patching
FargateBursty, isolated, low-ops workloadsNo DaemonSets, no privileged pods, no EBS volumes, higher per-pod cost
EKS Auto ModeTeams that want AWS to run compute entirelyLess control, newer, priced above raw EC2

Managed node groups are the default answer. Fargate is genuinely useful for CI runners and one-off jobs, but the "no DaemonSets" limitation catches teams out. Most observability agents ship as DaemonSets and simply won't run there.

For scaling, Karpenter has largely replaced the Cluster Autoscaler on new clusters. Cluster Autoscaler adjusts node group sizes; Karpenter provisions right-sized instances directly from the EC2 API, which packs workloads better and responds faster.

Access control: two systems, not one

This is the part that confuses nearly everyone. Getting a pod or a person into an EKS cluster involves two separate permission systems, and both must allow the action.

  1. IAM decides who can call the EKS API and who maps to which Kubernetes identity.
  2. Kubernetes RBAC decides what that identity can do once inside.

An IAM administrator with AdministratorAccess is not automatically a cluster admin. They need a mapping.

Historically that mapping lived in the aws-auth ConfigMap: a single YAML object in kube-system that, if you corrupted it, locked everyone out of the cluster with no recovery path short of AWS Support. Access entries replaced it and are now the recommended approach:

# Grant an IAM role cluster-admin via the EKS API, no ConfigMap editing
aws eks create-access-entry \
  --cluster-name production \
  --principal-arn arn:aws:iam::111122223333:role/PlatformTeam \
  --type STANDARD

aws eks associate-access-policy \
  --cluster-name production \
  --principal-arn arn:aws:iam::111122223333:role/PlatformTeam \
  --access-scope type=cluster \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
# See who currently has access
aws eks list-access-entries --cluster-name production

If you're on a cluster still using aws-auth, migrating is worth doing. Set the cluster's authentication mode to API_AND_CONFIG_MAP, create access entries alongside the existing ConfigMap, verify, then switch to API.

Giving pods AWS permissions

Never attach application permissions to the node's instance profile. Every pod on that node inherits them, so one compromised container gets whatever the node has.

There are two correct approaches:

IRSA (IAM Roles for Service Accounts) uses an OIDC provider on the cluster. The service account carries an annotation, and the pod receives a scoped token:

eksctl create iamserviceaccount \
  --cluster production \
  --namespace payments \
  --name payments-api \
  --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
  --approve
apiVersion: v1
kind: ServiceAccount
metadata:
  name: payments-api
  namespace: payments
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/payments-api-role

EKS Pod Identity is the newer option and simpler to operate. It needs the eks-pod-identity-agent add-on, but it drops the per-cluster OIDC trust-policy setup, which makes roles reusable across clusters:

aws eks create-pod-identity-association \
  --cluster-name production \
  --namespace payments \
  --service-account payments-api \
  --role-arn arn:aws:iam::111122223333:role/payments-api-role

For a new cluster, prefer Pod Identity. IRSA remains the right choice if you need to reuse roles outside EKS or you're on a region or version where Pod Identity isn't available.

Networking, and the mistake everyone makes once

The AWS VPC CNI gives every pod a real VPC IP address from your subnets. That's excellent for observability and security groups, and it means you can run out of IP addresses in a way you never would on other Kubernetes distributions.

A t3.medium supports 17 pods. Not because of CPU or memory, because of ENI limits. Teams routinely provision a /24 subnet, scale to 40 pods, and hit failed to assign an IP address to container.

# Check what's actually available before you scale
aws ec2 describe-subnets \
  --filters "Name=tag:kubernetes.io/role/internal-elb,Values=1" \
  --query 'Subnets[].{ID:SubnetId,AZ:AvailabilityZone,Free:AvailableIpAddressCount}' \
  --output table

Two fixes: size subnets generously from the start (a /20 per AZ is not excessive), and enable prefix delegation, which assigns /28 blocks per ENI and raises that t3.medium from 17 pods to 110.

Load balancer placement depends on subnet tags. Miss these and your Ingress will sit in pending with no useful error:

TagValueApplied to
kubernetes.io/role/elb1Public subnets (internet-facing)
kubernetes.io/role/internal-elb1Private subnets (internal)

Upgrades

EKS supports each Kubernetes version for about 14 months, then moves it to extended support at a significantly higher hourly rate. Clusters left unattended eventually get force-upgraded.

Upgrade order matters:

# 1. Control plane first. One minor version at a time, no skipping
eksctl upgrade cluster --name production --version 1.32 --approve

# 2. Core add-ons
eksctl utils update-coredns --cluster production --approve
eksctl utils update-kube-proxy --cluster production --approve

# 3. Nodes last
eksctl upgrade nodegroup --cluster production --name general

Before any upgrade, check for removed APIs. This is the single most common cause of a broken upgrade:

kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

Set a PodDisruptionBudget on anything that matters, or a node drain will take all your replicas down at once.

Quick reference

TaskCommand
Configure kubectlaws eks update-kubeconfig --region <r> --name <c>
List clustersaws eks list-clusters
Cluster detailsaws eks describe-cluster --name <c>
Who has accessaws eks list-access-entries --cluster-name <c>
Installed add-onsaws eks list-addons --cluster-name <c>
Node group statuseksctl get nodegroup --cluster <c>
Delete everythingeksctl delete cluster --name <c>

Key takeaways

  • AWS manages the control plane; you own everything above it: nodes, networking, add-ons, and access. The unmanaged parts cause the outages.
  • $0.10/hour per cluster before compute, so resist a cluster per team per environment without a reason.
  • Access needs both IAM and Kubernetes RBAC. Use access entries, not the aws-auth ConfigMap.
  • Give pods permissions with Pod Identity or IRSA, never the node instance profile.
  • Pods consume real VPC IPs. Size subnets generously and turn on prefix delegation before you scale.
  • Upgrade control plane, then add-ons, then nodes, one minor version at a time, after checking for deprecated APIs.

Check your understanding

Exercise

Cluster access

An engineer has the IAM AdministratorAccess policy but gets "error: You must be logged in to the server (Unauthorized)" when running kubectl. Why?

Exercise

Pod scheduling failure

A deployment scales from 10 to 40 replicas. New pods stay in ContainerCreating with "failed to assign an IP address to container", but nodes show plenty of free CPU and memory. What is the most likely cause?


Frequently Asked Questions

How much does an EKS cluster cost?

The control plane is $0.10 per hour, roughly $73 per month, per cluster. On top of that you pay for worker nodes (EC2 or Fargate), EBS volumes, load balancers, NAT gateway data processing, and cross-AZ traffic. A small production cluster with three m6i.large nodes and a NAT gateway typically lands somewhere around $300 to 400 per month. Clusters on extended support versions cost considerably more per hour, which is a deliberate nudge to keep current.

What is the difference between IRSA and EKS Pod Identity?

Both give pods scoped AWS permissions without using the node's instance profile. IRSA uses an OIDC provider per cluster, so each IAM role's trust policy references that specific cluster. Reusing a role across clusters means editing trust policies. Pod Identity uses an agent add-on and a simple association API instead, so the same role works across clusters unchanged. Pod Identity is simpler to operate and is the better default for new clusters; IRSA is still needed for some cross-account patterns and in places where Pod Identity isn't yet available.

Why do my pods fail with "failed to assign an IP address to container"?

The VPC CNI allocates each pod a real IP from your subnets, so you've exhausted either the subnet's addresses or the instance's ENI limit. Check free addresses with aws ec2 describe-subnets. Fix it by enabling prefix delegation on the VPC CNI, which assigns /28 blocks per ENI and raises pod density dramatically, and by using larger subnets. A /20 per Availability Zone is a reasonable starting point.

Why is my Ingress stuck in pending?

Almost always subnet tags. The AWS Load Balancer Controller discovers where to place a load balancer by looking for kubernetes.io/role/elb=1 on public subnets and kubernetes.io/role/internal-elb=1 on private ones. Without those tags it has nowhere to put the load balancer and fails quietly. Check the controller's logs, kubectl logs -n kube-system deploy/aws-load-balancer-controller, which will say it couldn't discover subnets. The other common cause is that the controller isn't installed at all; EKS does not ship it by default.

Do I need eksctl, or can I use Terraform?

Either works, and for anything long-lived Terraform is usually the better answer because the cluster then lives alongside the rest of your infrastructure in the same state and review process. eksctl is faster for learning and for throwaway clusters, and it handles a lot of IAM and VPC wiring for you. Many teams use eksctl to explore and Terraform to run production.

Can I run an EKS cluster without any EC2 instances?

Yes. Fargate profiles run pods on AWS-managed capacity with no nodes to patch, and EKS Auto Mode goes further by managing compute for you. The Fargate constraints are real, though: no DaemonSets, no privileged containers, no EBS volume mounts, and a per-pod cost that's higher than equivalent EC2 for steady workloads. Because most monitoring and logging agents are DaemonSets, fully serverless clusters usually need a different observability approach.

Continue Learning

Explore Related Topics

When it breaks

Related Resources