AWS EKS Cluster Setup

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Create an EKS cluster that works. The IAM mapping that locks people out, access entries vs aws-auth, subnet tagging and the CNI IP limit.

EKS runs the Kubernetes control plane for you. Highly available across three availability zones, patched and backed by an SLA. For a fixed hourly charge per cluster. You supply the worker nodes and everything above them.

What makes EKS different from other managed Kubernetes is the IAM-to-Kubernetes boundary. AWS authenticates you; Kubernetes RBAC authorises you; and the mapping between the two is where almost every new cluster goes wrong.

Create the cluster

eksctl is the pragmatic choice. It creates the VPC, subnets, IAM roles and node groups that a cluster needs, which by hand is a long afternoon:

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

metadata:
  name: prod
  region: eu-west-1
  version: "1.31"

iam:
  withOIDC: true                 # required for IRSA

vpc:
  cidr: 10.0.0.0/16
  clusterEndpoints:
    publicAccess: true
    privateAccess: true

addons:
  - name: vpc-cni
    version: latest
    configurationValues: '{"env":{"ENABLE_PREFIX_DELEGATION":"true"}}'
  - name: coredns
  - name: kube-proxy
  - name: aws-ebs-csi-driver
    wellKnownPolicies:
      ebsCSIController: true

managedNodeGroups:
  - name: general
    instanceTypes: ["m6i.large", "m6a.large"]
    minSize: 2
    maxSize: 10
    desiredCapacity: 3
    privateNetworking: true
    volumeSize: 50
    volumeType: gp3
eksctl create cluster -f cluster.yaml

Expect 15 to 20 minutes. The equivalent with the AWS CLI is aws eks create-cluster plus separately creating the VPC, both IAM roles, the OIDC provider and the node groups.

aws eks update-kubeconfig --name prod --region eu-west-1
kubectl get nodes

withOIDC: true matters more than it looks. It creates the OIDC identity provider that IRSA depends on, and without it pods cannot assume IAM roles. You end up putting access keys in secrets, which is the thing IRSA exists to prevent. Adding it later is possible and easy to forget.

The access problem

The most common EKS experience: create a cluster, hand it to a colleague, and they get

error: You must be logged in to the server (Unauthorized)

The cluster creator is granted admin implicitly. Nobody else has any access at all until you grant it, and the error gives no hint as to why.

There are two mechanisms. Access entries are the current approach and far better:

aws eks create-access-entry --cluster-name prod \
  --principal-arn arn:aws:iam::123456789012:role/PlatformEngineer \
  --type STANDARD

aws eks associate-access-policy --cluster-name prod \
  --principal-arn arn:aws:iam::123456789012:role/PlatformEngineer \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope type=cluster
aws eks list-access-entries --cluster-name prod

The aws-auth ConfigMap is the older mechanism, still present on many clusters:

kubectl -n kube-system get configmap aws-auth -o yaml

Editing it is genuinely dangerous. A malformed entry locks everyone out of the cluster, including you, and recovery means the original creator identity or AWS Support. Access entries exist precisely because of that failure mode. Set the cluster's authentication mode to API_AND_CONFIG_MAP and migrate.

Note a subtlety that wastes a lot of time: IAM roles are matched by the role ARN, not the assumed-role session ARN. Mapping arn:aws:sts::123456789012:assumed-role/Admin/user does not work; map arn:aws:iam::123456789012:role/Admin.

Subnet tagging

EKS discovers subnets by tag. Missing tags produce a cluster that works until you create a LoadBalancer service or an Ingress, which then never gets an address:

TagValueOn
kubernetes.io/role/elb1Public subnets
kubernetes.io/role/internal-elb1Private subnets
kubernetes.io/cluster/<name>shared or ownedAll (older versions)
aws ec2 create-tags --resources subnet-0a1b2c3d \
  --tags Key=kubernetes.io/role/elb,Value=1

eksctl applies these. A cluster built into a pre-existing VPC by hand frequently does not have them, and the symptom, a service stuck in <pending> with no events explaining it, points nowhere useful.

The CNI IP address limit

The constraint that catches teams at exactly the wrong moment.

The AWS VPC CNI gives every pod a real VPC IP address, which is excellent for networking and means pod density is capped by how many IPs an instance type supports. An m6i.large supports 3 network interfaces with 10 addresses each: 29 usable, minus some for the node itself.

kubectl get nodes -o custom-columns=NAME:.metadata.name,PODS:.status.allocatable.pods
NAME                         PODS
ip-10-0-1-42.ec2.internal    29

Pods then sit in Pending with:

0/3 nodes are available: 3 Too many pods.

Two fixes. Prefix delegation assigns /28 prefixes instead of individual addresses, raising an m6i.large from 29 pods to around 110:

kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true

Or size your subnets properly. A /24 per subnet runs out quickly when every pod consumes an address. Plan the VPC CIDR around peak pod count, not node count.

Endpoint access

aws eks update-cluster-config --name prod \
  --resources-vpc-config endpointPublicAccess=true,endpointPrivateAccess=true,publicAccessCidrs=203.0.113.0/24

Public and private both enabled, with the public side restricted to your office and CI ranges, is the arrangement that works for most teams. Private-only is a genuine trap: kubectl then only works from inside the VPC or over a VPN, and it is easy to enable while working from an EC2 host and lose access from your laptop.

Add-ons

Managed add-ons let AWS handle version compatibility:

aws eks describe-addon-versions --addon-name vpc-cni \
  --kubernetes-version 1.31 --query 'addons[0].addonVersions[0].addonVersion'

aws eks create-addon --cluster-name prod --addon-name aws-ebs-csi-driver \
  --service-account-role-arn "$EBS_CSI_ROLE_ARN" \
  --resolve-conflicts OVERWRITE

Four are effectively required. vpc-cni, coredns and kube-proxy come with the cluster. aws-ebs-csi-driver does not, and since Kubernetes 1.23 it is what actually provisions EBS volumes, so without it every PersistentVolumeClaim sits Pending forever with no obvious cause. It is the single most common "my cluster is broken" after a fresh install.

Worth adding beyond those: AWS Load Balancer Controller for Ingress, and either Karpenter or Cluster Autoscaler for node scaling.

Upgrades

aws eks update-cluster-version --name prod --kubernetes-version 1.32

The order is fixed: control plane, then add-ons, then nodes. You cannot skip minor versions, so 1.29 to 1.32 is three sequential upgrades.

Before each one, check for deprecated APIs. An upgrade that removes an API version your manifests use breaks them with no warning:

kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

EKS supports each version for a limited window and then force-upgrades you, with extended support billed at a much higher hourly rate. Falling behind is expensive as well as risky.

Common errors

error: You must be logged in to the server (Unauthorized)

No access entry or aws-auth mapping for your principal. Also check you are in the right account and region.

0/3 nodes are available: 3 Too many pods.

CNI IP exhaustion. Enable prefix delegation.

Warning  FailedScheduling  ... persistentvolumeclaim "data" not bound

The EBS CSI driver add-on is missing.

A LoadBalancer service stuck at <pending>. Subnet tags missing, or no AWS Load Balancer Controller.

kubectl times out. Private-only endpoint, and you are outside the VPC.

Quick reference

TaskCommand
Createeksctl create cluster -f cluster.yaml
Configure kubectlaws eks update-kubeconfig --name <c> --region <r>
Grant accessaws eks create-access-entry + associate-access-policy
List who has accessaws eks list-access-entries --cluster-name <c>
Add an add-onaws eks create-addon --addon-name <a>
Upgrade the control planeaws eks update-cluster-version --kubernetes-version <v>
Restrict the endpointupdate-cluster-config --resources-vpc-config publicAccessCidrs=…
Check pod capacitykubectl get nodes -o custom-columns=…allocatable.pods

Key takeaways

  • Only the cluster creator has access by default. Everyone else is Unauthorized until granted.
  • Use access entries, not aws-auth: a malformed ConfigMap locks out the entire cluster.
  • Map the IAM role ARN, not the assumed-role session ARN.
  • Enable OIDC at creation or IRSA is unavailable and you end up with static keys.
  • Tag subnets or load balancers never get provisioned, with no useful error.
  • The VPC CNI caps pods per node by IP count. Prefix delegation raises it roughly 4x.
  • Install the EBS CSI driver add-on, or every PVC stays Pending.
  • Upgrade control plane, add-ons, then nodes: and you cannot skip minor versions.

Check your understanding

Exercise

A colleague who cannot reach the cluster

A new EKS cluster works for the engineer who created it. A teammate with AdministratorAccess in the same account runs kubectl get nodes and receives "You must be logged in to the server (Unauthorized)". Why?

Exercise

Pods that will not schedule on idle nodes

Pods are Pending with "0/4 nodes are available: 4 Too many pods." The nodes are m6i.large with CPU and memory largely unused. What is the constraint?


Frequently Asked Questions

Why do I get "You must be logged in to the server (Unauthorized)" on EKS?

Because your IAM principal is not mapped to anything in Kubernetes. EKS uses IAM for authentication and Kubernetes RBAC for authorisation, and only the identity that created the cluster is granted admin automatically, even an account administrator is unauthorised until mapped. Create an access entry for the principal and associate a cluster access policy. Two gotchas: map the IAM role ARN rather than the assumed-role session ARN, and confirm you are targeting the right account and region.

What is the difference between access entries and the aws-auth ConfigMap?

aws-auth is the original mechanism: a ConfigMap in kube-system mapping IAM ARNs to Kubernetes users and groups. It is edited with kubectl, has no validation, and a malformed entry can lock every user out of the cluster with recovery requiring the original creator identity or AWS Support. Access entries are the API-based replacement. Managed with aws eks commands, validated, auditable through CloudTrail, and manageable before anyone can reach the cluster. Use access entries; migrate existing clusters by setting authentication mode to API_AND_CONFIG_MAP.

Why are my pods stuck in Pending with "Too many pods"?

The AWS VPC CNI assigns each pod a real VPC IP address, so pods per node is limited by the instance type's network interface capacity rather than by CPU or memory. An m6i.large allows roughly 29. Enable prefix delegation with ENABLE_PREFIX_DELEGATION=true on the aws-node DaemonSet, which allocates /28 prefixes and raises the figure to around 110. Also check subnet sizing: with an address per pod, a /24 exhausts far sooner than node counts suggest.

Why is my PersistentVolumeClaim stuck in Pending on EKS?

Almost certainly the EBS CSI driver is not installed. Since Kubernetes 1.23 the in-tree EBS provisioner has been removed, so the aws-ebs-csi-driver add-on is what actually creates volumes, and a new cluster does not include it. Add it with aws eks create-addon --addon-name aws-ebs-csi-driver, giving it an IRSA role with the necessary EC2 permissions. This is the most frequent "my fresh cluster is broken" cause after access problems.

Why is my LoadBalancer service stuck in pending?

Usually subnet tags. EKS discovers which subnets to place load balancers in by tag, kubernetes.io/role/elb on public subnets and kubernetes.io/role/internal-elb on private ones, and without them nothing is provisioned and the events say very little. eksctl applies these automatically, so it mainly affects clusters built into an existing VPC. The second cause is the AWS Load Balancer Controller not being installed, which is required for Ingress resources and for LoadBalancer services of type NLB.

How do I upgrade an EKS cluster safely?

In order: control plane, then add-ons, then nodes, and one minor version at a time, since skipping is not permitted. Before each step, check for deprecated API usage with the apiserver_requested_deprecated_apis metric, because an upgrade that removes an API your manifests still use breaks them without warning. Test in a non-production cluster first. Falling behind is costly as well as risky: EKS force-upgrades unsupported versions, and extended support is billed at a substantially higher hourly rate.

Continue Learning

Explore Related Topics

When it breaks

Related Resources