> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# EKS Cluster Setup

> Provision an AWS EKS cluster with VPC, networking, IAM roles, node groups, and add-ons for xpander.ai

<Info>
  **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](/self-hosted/privatelink) or [Install the Helm Chart](/self-hosted/deployment).
</Info>

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.

```bash theme={"dark"}
# 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:

| Subnet    | CIDR             | AZ   | Type    | Tag                                  |
| --------- | ---------------- | ---- | ------- | ------------------------------------ |
| public-a  | `10.100.1.0/24`  | AZ-a | Public  | `kubernetes.io/role/elb: 1`          |
| public-b  | `10.100.2.0/24`  | AZ-b | Public  | `kubernetes.io/role/elb: 1`          |
| private-a | `10.100.10.0/24` | AZ-a | Private | `kubernetes.io/role/internal-elb: 1` |
| private-b | `10.100.20.0/24` | AZ-b | Private | `kubernetes.io/role/internal-elb: 1` |

```bash theme={"dark"}
# 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

```bash theme={"dark"}
# 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

```bash theme={"dark"}
# 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

<AccordionGroup>
  <Accordion title="EKS Cluster Role">
    ```bash theme={"dark"}
    # 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>
    ```
  </Accordion>

  <Accordion title="Node Role">
    ```bash theme={"dark"}
    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>
    ```
  </Accordion>
</AccordionGroup>

## 5. Create EKS Cluster

```bash theme={"dark"}
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):

```bash theme={"dark"}
aws eks wait cluster-active --name xpander-cluster --region <REGION> --profile <PROFILE>
```

## 6. Create Node Group

<Warning>
  xpander container images are **amd64 only**. Do not use ARM/Graviton instances (`t4g`, `m7g`, `c7g`, etc.).
</Warning>

**Minimum requirements:**

* Recommended minimum: 2 × t3x.large nodes, or equivalent capacity
* xpander workload requests: \~7 vCPU / \~10 GiB memory
* xpander configured limits: \~14 vCPU / \~13 GiB memory
* Agent Worker runs 2 replicas by default; each requests 2 vCPU / 2.25 GiB
* Plan additional headroom for Kubernetes system pods, DaemonSets, upgrades, and burst capacity
* Production deployments with significant agent runtime concurrency would require more capacity.

```bash theme={"dark"}
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:

```bash theme={"dark"}
aws eks wait nodegroup-active \
  --cluster-name xpander-cluster \
  --nodegroup-name xpander-nodes \
  --region <REGION> --profile <PROFILE>
```

## 7. Configure kubectl

```bash theme={"dark"}
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.

```bash theme={"dark"}
# 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.

```bash theme={"dark"}
# 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

```bash theme={"dark"}
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](/self-hosted/privatelink)** — If your security policy requires traffic to stay within the AWS network
2. **[Install the Helm Chart](/self-hosted/deployment)** — SSL certificate, ingress, Helm chart installation, DNS, and verification
