AWS EKS
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
AWS manages the control plane HA. You only pay for worker nodes
What AWS runs and what you run
| Layer | Managed by | What that means for you |
|---|---|---|
| API server, etcd, scheduler | AWS | You never SSH to it, can't tune etcd flags, can't install API server plugins |
| Worker capacity | You (or AWS, with Fargate/Auto Mode) | Instance types, AMIs, patching cadence, scaling |
| VPC, subnets, security groups | You | Pod IP capacity and load balancer placement depend on it |
| Core add-ons (CNI, CoreDNS, kube-proxy) | Shared | AWS publishes versions; you choose when to upgrade |
| Workload IAM | You | Via 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
| Option | Good for | The catch |
|---|---|---|
| Managed node groups | Most workloads | You still trigger AMI upgrades and handle drains |
| Self-managed nodes | Custom AMIs, GPU/specialised kernels | You own all patching |
| Fargate | Bursty, isolated, low-ops workloads | No DaemonSets, no privileged pods, no EBS volumes, higher per-pod cost |
| EKS Auto Mode | Teams that want AWS to run compute entirely | Less 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.
- IAM decides who can call the EKS API and who maps to which Kubernetes identity.
- 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:
| Tag | Value | Applied to |
|---|---|---|
kubernetes.io/role/elb | 1 | Public subnets (internet-facing) |
kubernetes.io/role/internal-elb | 1 | Private 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
| Task | Command |
|---|---|
| Configure kubectl | aws eks update-kubeconfig --region <r> --name <c> |
| List clusters | aws eks list-clusters |
| Cluster details | aws eks describe-cluster --name <c> |
| Who has access | aws eks list-access-entries --cluster-name <c> |
| Installed add-ons | aws eks list-addons --cluster-name <c> |
| Node group status | eksctl get nodegroup --cluster <c> |
| Delete everything | eksctl 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-authConfigMap. - 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
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?
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.