Skip to main content
Skip this guide if you already have an EKS cluster with sufficient CPU/memory and the ability to create LoadBalancer services. Proceed directly to Configure PrivateLink or Install the Helm Chart.
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 <REGION> --profile <PROFILE>

# Enable DNS
aws ec2 modify-vpc-attribute --vpc-id <VPC_ID> --enable-dns-support '{"Value":true}' \
  --region <REGION> --profile <PROFILE>
aws ec2 modify-vpc-attribute --vpc-id <VPC_ID> --enable-dns-hostnames '{"Value":true}' \
  --region <REGION> --profile <PROFILE>
Create four subnets:
SubnetCIDRAZTypeTag
public-a10.100.1.0/24AZ-aPublickubernetes.io/role/elb: 1
public-b10.100.2.0/24AZ-bPublickubernetes.io/role/elb: 1
private-a10.100.10.0/24AZ-aPrivatekubernetes.io/role/internal-elb: 1
private-b10.100.20.0/24AZ-bPrivatekubernetes.io/role/internal-elb: 1
# Create subnets (repeat for each)
aws ec2 create-subnet \
  --vpc-id <VPC_ID> \
  --cidr-block <CIDR> \
  --availability-zone <AZ> \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=<NAME>},{Key=kubernetes.io/role/elb,Value=1}]' \
  --region <REGION> --profile <PROFILE>

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 <REGION> --profile <PROFILE>
aws ec2 attach-internet-gateway --internet-gateway-id <IGW_ID> --vpc-id <VPC_ID> \
  --region <REGION> --profile <PROFILE>

# Elastic IP for NAT
aws ec2 allocate-address --domain vpc --region <REGION> --profile <PROFILE>

# NAT Gateway (in a public subnet)
aws ec2 create-nat-gateway \
  --subnet-id <PUBLIC_SUBNET_A_ID> \
  --allocation-id <EIP_ALLOC_ID> \
  --tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=xpander-nat}]' \
  --region <REGION> --profile <PROFILE>

3. Route Tables

# Public route table → Internet Gateway
aws ec2 create-route-table --vpc-id <VPC_ID> --region <REGION> --profile <PROFILE>
aws ec2 create-route --route-table-id <PUBLIC_RT_ID> \
  --destination-cidr-block 0.0.0.0/0 --gateway-id <IGW_ID> \
  --region <REGION> --profile <PROFILE>
aws ec2 associate-route-table --route-table-id <PUBLIC_RT_ID> \
  --subnet-id <PUBLIC_SUBNET_A_ID> --region <REGION> --profile <PROFILE>
aws ec2 associate-route-table --route-table-id <PUBLIC_RT_ID> \
  --subnet-id <PUBLIC_SUBNET_B_ID> --region <REGION> --profile <PROFILE>

# Private route table → NAT Gateway
aws ec2 create-route-table --vpc-id <VPC_ID> --region <REGION> --profile <PROFILE>
aws ec2 create-route --route-table-id <PRIVATE_RT_ID> \
  --destination-cidr-block 0.0.0.0/0 --nat-gateway-id <NAT_GW_ID> \
  --region <REGION> --profile <PROFILE>
aws ec2 associate-route-table --route-table-id <PRIVATE_RT_ID> \
  --subnet-id <PRIVATE_SUBNET_A_ID> --region <REGION> --profile <PROFILE>
aws ec2 associate-route-table --route-table-id <PRIVATE_RT_ID> \
  --subnet-id <PRIVATE_SUBNET_B_ID> --region <REGION> --profile <PROFILE>

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 <PROFILE>

# Attach policies
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSComputePolicy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSBlockStoragePolicy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSLoadBalancingPolicy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-eks-cluster-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSNetworkingPolicy --profile <PROFILE>
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 <PROFILE>

aws iam attach-role-policy --role-name xpander-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-node-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore --profile <PROFILE>
aws iam attach-role-policy --role-name xpander-node-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy --profile <PROFILE>

5. Create EKS Cluster

aws eks create-cluster \
  --name xpander-cluster \
  --role-arn arn:aws:iam::<ACCOUNT_ID>: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 <REGION> --profile <PROFILE>
Wait for the cluster to become active (~10 minutes):
aws eks wait cluster-active --name xpander-cluster --region <REGION> --profile <PROFILE>

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_ID>:role/xpander-node-role \
  --subnets <PRIVATE_SUBNET_A_ID> <PRIVATE_SUBNET_B_ID> \
  --instance-types t3.large \
  --ami-type AL2023_x86_64_STANDARD \
  --scaling-config minSize=2,maxSize=4,desiredSize=3 \
  --capacity-type ON_DEMAND \
  --region <REGION> --profile <PROFILE>
Wait for the node group:
aws eks wait nodegroup-active \
  --cluster-name xpander-cluster \
  --nodegroup-name xpander-nodes \
  --region <REGION> --profile <PROFILE>

7. Configure kubectl

aws eks update-kubeconfig --name xpander-cluster --region <REGION> --profile <PROFILE>
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 <REGION> --profile <PROFILE>

# CoreDNS (cluster DNS)
aws eks create-addon --cluster-name xpander-cluster --addon-name coredns \
  --resolve-conflicts OVERWRITE --region <REGION> --profile <PROFILE>

# kube-proxy
aws eks create-addon --cluster-name xpander-cluster --addon-name kube-proxy \
  --resolve-conflicts OVERWRITE --region <REGION> --profile <PROFILE>

# EBS CSI Driver (persistent volumes)
aws eks create-addon --cluster-name xpander-cluster --addon-name aws-ebs-csi-driver \
  --resolve-conflicts OVERWRITE --region <REGION> --profile <PROFILE>

# Pod Identity Agent
aws eks create-addon --cluster-name xpander-cluster --addon-name eks-pod-identity-agent \
  --resolve-conflicts OVERWRITE --region <REGION> --profile <PROFILE>

# Metrics Server
aws eks create-addon --cluster-name xpander-cluster --addon-name metrics-server \
  --resolve-conflicts OVERWRITE --region <REGION> --profile <PROFILE>

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 <PROFILE>

aws iam attach-role-policy --role-name xpander-ebs-csi-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy --profile <PROFILE>

# 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_ID>:role/xpander-ebs-csi-role \
  --region <REGION> --profile <PROFILE>

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:
  1. Configure PrivateLink — If your security policy requires traffic to stay within the AWS network
  2. Install the Helm Chart — SSL certificate, ingress, Helm chart installation, DNS, and verification