This guide provisions a production-ready EKS cluster from scratch — VPC with public/private subnets, IAM roles, node groups, and required add-ons.
Prerequisites
AWS CLI v2 configured with appropriate credentials
kubectl installed
Helm v3 installed
An AWS account with permissions to create EKS clusters, VPCs, and IAM roles
1. Create a VPC
Create a VPC with public and private subnets across two availability zones.
# Create VPC
aws ec2 create-vpc \
--cidr-block 10.100.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=xpander-vpc},{Key=Project,Value=xpander}]' \
--region < REGIO N> --profile < PROFIL E>
# Enable DNS
aws ec2 modify-vpc-attribute --vpc-id < VPC_I D> --enable-dns-support '{"Value":true}' \
--region < REGIO N> --profile < PROFIL E>
aws ec2 modify-vpc-attribute --vpc-id < VPC_I D> --enable-dns-hostnames '{"Value":true}' \
--region < REGIO N> --profile < PROFIL E>
Create four subnets:
Subnet CIDR AZ Type Tag public-a 10.100.1.0/24AZ-a Public kubernetes.io/role/elb: 1public-b 10.100.2.0/24AZ-b Public kubernetes.io/role/elb: 1private-a 10.100.10.0/24AZ-a Private kubernetes.io/role/internal-elb: 1private-b 10.100.20.0/24AZ-b Private kubernetes.io/role/internal-elb: 1
# Create subnets (repeat for each)
aws ec2 create-subnet \
--vpc-id < VPC_I D> \
--cidr-block < CID R> \
--availability-zone < A Z> \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=<NAME>},{Key=kubernetes.io/role/elb,Value=1}]' \
--region < REGIO N> --profile < PROFIL E>
2. Create Internet Gateway and NAT Gateway
# Internet Gateway
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=xpander-igw}]' \
--region < REGIO N> --profile < PROFIL E>
aws ec2 attach-internet-gateway --internet-gateway-id < IGW_I D> --vpc-id < VPC_I D> \
--region < REGIO N> --profile < PROFIL E>
# Elastic IP for NAT
aws ec2 allocate-address --domain vpc --region < REGIO N> --profile < PROFIL E>
# NAT Gateway (in a public subnet)
aws ec2 create-nat-gateway \
--subnet-id < PUBLIC_SUBNET_A_I D> \
--allocation-id < EIP_ALLOC_I D> \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=xpander-nat}]' \
--region < REGIO N> --profile < PROFIL E>
3. Route Tables
# Public route table → Internet Gateway
aws ec2 create-route-table --vpc-id < VPC_I D> --region < REGIO N> --profile < PROFIL E>
aws ec2 create-route --route-table-id < PUBLIC_RT_I D> \
--destination-cidr-block 0.0.0.0/0 --gateway-id < IGW_I D> \
--region < REGIO N> --profile < PROFIL E>
aws ec2 associate-route-table --route-table-id < PUBLIC_RT_I D> \
--subnet-id < PUBLIC_SUBNET_A_I D> --region < REGIO N> --profile < PROFIL E>
aws ec2 associate-route-table --route-table-id < PUBLIC_RT_I D> \
--subnet-id < PUBLIC_SUBNET_B_I D> --region < REGIO N> --profile < PROFIL E>
# Private route table → NAT Gateway
aws ec2 create-route-table --vpc-id < VPC_I D> --region < REGIO N> --profile < PROFIL E>
aws ec2 create-route --route-table-id < PRIVATE_RT_I D> \
--destination-cidr-block 0.0.0.0/0 --nat-gateway-id < NAT_GW_I D> \
--region < REGIO N> --profile < PROFIL E>
aws ec2 associate-route-table --route-table-id < PRIVATE_RT_I D> \
--subnet-id < PRIVATE_SUBNET_A_I D> --region < REGIO N> --profile < PROFIL E>
aws ec2 associate-route-table --route-table-id < PRIVATE_RT_I D> \
--subnet-id < PRIVATE_SUBNET_B_I D> --region < REGIO N> --profile < PROFIL E>
4. IAM Roles
# Create cluster role
aws iam create-role \
--role-name xpander-eks-cluster-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}' \
--profile < PROFIL E>
# Attach policies
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSComputePolicy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSBlockStoragePolicy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSLoadBalancingPolicy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSNetworkingPolicy --profile < PROFIL E>
aws iam create-role \
--role-name xpander-node-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}' \
--profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-node-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-node-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-node-role \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-node-role \
--policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore --profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-node-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy --profile < PROFIL E>
5. Create EKS Cluster
aws eks create-cluster \
--name xpander-cluster \
--role-arn arn:aws:iam:: < ACCOUNT_I D> :role/xpander-eks-cluster-role \
--kubernetes-version 1.35 \
--resources-vpc-config "subnetIds=<ALL_SUBNET_IDS>,endpointPublicAccess=true,endpointPrivateAccess=true,publicAccessCidrs=<YOUR_IP>/32" \
--access-config "authenticationMode=API" \
--region < REGIO N> --profile < PROFIL E>
Wait for the cluster to become active (~10 minutes):
aws eks wait cluster-active --name xpander-cluster --region < REGIO N> --profile < PROFIL E>
6. Create Node Group
xpander container images are amd64 only . Do not use ARM/Graviton instances (t4g, m7g, c7g, etc.).
Minimum requirements:
Instance type: t3.large (2 vCPU, 8 GiB) or larger
Minimum 2 nodes, recommended 3+ nodes
The agent-worker pod requests 2 CPU — plan capacity accordingly
aws eks create-nodegroup \
--cluster-name xpander-cluster \
--nodegroup-name xpander-nodes \
--node-role arn:aws:iam:: < ACCOUNT_I D> :role/xpander-node-role \
--subnets < PRIVATE_SUBNET_A_I D> < PRIVATE_SUBNET_B_I D> \
--instance-types t3.large \
--ami-type AL2023_x86_64_STANDARD \
--scaling-config minSize=2,maxSize=4,desiredSize= 3 \
--capacity-type ON_DEMAND \
--region < REGIO N> --profile < PROFIL E>
Wait for the node group:
aws eks wait nodegroup-active \
--cluster-name xpander-cluster \
--nodegroup-name xpander-nodes \
--region < REGIO N> --profile < PROFIL E>
aws eks update-kubeconfig --name xpander-cluster --region < REGIO N> --profile < PROFIL E>
kubectl get nodes # Verify nodes are Ready
EKS Add-Ons
Install the required EKS add-ons for networking, DNS, storage, and pod identity.
# VPC CNI (pod networking)
aws eks create-addon --cluster-name xpander-cluster --addon-name vpc-cni \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
# CoreDNS (cluster DNS)
aws eks create-addon --cluster-name xpander-cluster --addon-name coredns \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
# kube-proxy
aws eks create-addon --cluster-name xpander-cluster --addon-name kube-proxy \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
# EBS CSI Driver (persistent volumes)
aws eks create-addon --cluster-name xpander-cluster --addon-name aws-ebs-csi-driver \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
# Pod Identity Agent
aws eks create-addon --cluster-name xpander-cluster --addon-name eks-pod-identity-agent \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
# Metrics Server
aws eks create-addon --cluster-name xpander-cluster --addon-name metrics-server \
--resolve-conflicts OVERWRITE --region < REGIO N> --profile < PROFIL E>
EBS CSI IAM Setup (Pod Identity)
The EBS CSI driver needs IAM permissions to provision volumes.
# Create IAM role for EBS CSI
aws iam create-role \
--role-name xpander-ebs-csi-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}' \
--profile < PROFIL E>
aws iam attach-role-policy --role-name xpander-ebs-csi-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy --profile < PROFIL E>
# Associate with service account
aws eks create-pod-identity-association \
--cluster-name xpander-cluster \
--namespace kube-system \
--service-account ebs-csi-controller-sa \
--role-arn arn:aws:iam:: < ACCOUNT_I D> :role/xpander-ebs-csi-role \
--region < REGIO N> --profile < PROFIL E>
Create Default StorageClass
kubectl apply -f - << 'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
fsType: ext4
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
EOF
Next Steps
Your EKS cluster is ready. Continue with:
Configure PrivateLink — If your security policy requires traffic to stay within the AWS network
Install the Helm Chart — SSL certificate, ingress, Helm chart installation, DNS, and verification