Docs / EKS

EKS

LocalEmu Amazon EKS implements all 64 operations. The control plane is served by the moto-ext metadata backend; the behavior plane is a real Kubernetes cluster started via k3d (k3s in Docker) when the k3d binary is on PATH. Four of the 64 operations are LocalEmu-custom: CreateCluster boots a real k3s cluster and waits until the API server accepts connections; DescribeCluster reports the live endpoint and base64 CA certificate; DeleteCluster tears the cluster down; CreateNodegroup spawns real k3d agent nodes that join the cluster. kubectl works end-to-end: apply, wait, exec, logs all round-trip through the cluster.

Operation-level coverage: see the EKS coverage matrix.

Prerequisites

Terminal
# Install k3d on the host (one-time). LocalEmu auto-detects it.
$ brew install k3d                                       # macOS
$ curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash   # Linux

$ k3d version | head -1
k3d version v5.7.4

LocalEmu auto-detects the k3d binary at provider startup (services/eks/provider.py:60). If it is missing, EKS silently stays in metadata-only mode (the API still works; CreateCluster returns immediately with status=ACTIVE but no cluster runs). Set EKS_K8S_PROVIDER=k3d to make CreateCluster error out when k3d is unavailable, or EKS_K8S_PROVIDER=off (or none, metadata, moto) to force metadata-only mode even with k3d installed.

Quick start

Terminal
$ awsemu eks create-cluster --name dev \
    --kubernetes-version 1.29 \
    --role-arn  arn:aws:iam::000000000000:role/EksClusterRole \
    --resources-vpc-config subnetIds=subnet-1,subnet-2 \
    --query 'cluster.{Name:name,Status:status}'
{"Name": "dev", "Status": "CREATING"}

# Pull + boot of the k3s image takes 60-120 s on first run.
$ while [ "$(awsemu eks describe-cluster --name dev \
    --query 'cluster.status' --output text)" != "ACTIVE" ]; do sleep 5; done

$ awsemu eks describe-cluster --name dev \
    --query 'cluster.{Status:status,Endpoint:endpoint,Version:version}'
{
  "Status":   "ACTIVE",
  "Endpoint": "https://127.0.0.1:54321",
  "Version":  "1.29"
}

Get a kubeconfig

Terminal
# Option 1 (easiest): ask k3d directly.
$ k3d kubeconfig get localemu-eks-dev > ~/.kube/localemu-eks-dev
$ export KUBECONFIG=~/.kube/localemu-eks-dev

# Option 2: LocalEmu exposes the same kubeconfig at an HTTP route.
$ curl -s http://localhost:4566/_localemu_eks/dev/kubeconfig > ~/.kube/localemu-eks-dev

$ kubectl get nodes
NAME                      STATUS   ROLES                  AGE   VERSION
k3d-localemu-eks-dev-server-0   Ready    control-plane,master   45s   v1.29.8+k3s1

Deploy and exec into a Pod

Terminal
$ kubectl run nginx --image=nginx:alpine --port=80
pod/nginx created

$ kubectl wait --for=condition=Ready pod/nginx --timeout=60s
pod/nginx condition met

# Real exec into the Pod proves the cluster is fully functional.
$ kubectl exec nginx -- nginx -v
nginx version: nginx/1.27.0

$ kubectl exec nginx -- wget -qO- http://127.0.0.1/ | head -1
<!DOCTYPE html>

Architecture

Code lives at services/eks/. The entry point is create_eks_service() at provider.py:445, registered as eks:default in plux.ini via services/providers.py. The four custom handlers are intercepted; everything else proxies to moto.

FileRole
provider.pyHandlers (_handle_create_cluster, _handle_describe_cluster, _handle_delete_cluster, _handle_create_nodegroup), kubeconfig HTTP route, lifecycle hooks.
cluster_manager.pyK3dClusterManager singleton, k3d subprocess invocations, kubeconfig extraction, persistence reconciliation.
plugins.pyStartup/shutdown hooks: PERSISTENCE=1 stops k3d clusters without deleting them; otherwise destroys them.

k3s image version map

The kubernetesVersion field is mapped to a pinned k3s image at cluster_manager.py:31-40:

EKS versionk3s image
1.24rancher/k3s:v1.24.17-k3s1
1.25rancher/k3s:v1.25.16-k3s4
1.26rancher/k3s:v1.26.15-k3s1
1.27rancher/k3s:v1.27.16-k3s1
1.28rancher/k3s:v1.28.13-k3s1
1.29rancher/k3s:v1.29.8-k3s1
1.30rancher/k3s:v1.30.4-k3s1
1.31rancher/k3s:v1.31.1-k3s1

CreateCluster flow

  1. Moto writes the cluster metadata record (provider.py:91).
  2. The API returns immediately with status=CREATING.
  3. A background thread allocates an ephemeral host port, then runs:
    k3d cluster create localemu-eks-<name> \
      --api-port 127.0.0.1:<port> \
      --wait --timeout 120s \
      --no-lb \
      --k3s-arg --disable=traefik@server:0
  4. k3d's --wait flag holds until the API server is reachable (120 s timeout). LocalEmu's outer budget is 180 s.
  5. The kubeconfig is extracted via k3d kubeconfig get; the endpoint is rewritten to https://127.0.0.1:<port>; the CA cert is base64-encoded.
  6. The moto record is updated with endpoint, certificateAuthority.data, status=ACTIVE. If k3d fails, status flips to CREATE_FAILED.

Features supported

FeatureNotes
Cluster lifecycleCreateCluster (real k3d boot + readiness gate), DeleteCluster (async k3d cluster delete), DescribeCluster (live endpoint + base64 CA cert), ListClusters.
Kubernetes versions1.24 through 1.31, each pinned to an upstream rancher/k3s tag.
kubectlFull round-trip: apply, get, describe, wait, exec, logs, port-forward against the live cluster.
Node groupsCreateNodegroup spawns real k3d node create --role agent containers (one per scalingConfig.desiredSize) that join the cluster.
Endpoint shapeDescribeCluster returns endpoint=https://127.0.0.1:<port>, certificateAuthority.data=<base64>, platformVersion="eks.local", kubernetesNetworkConfig={serviceIpv4Cidr:10.100.0.0/16,ipFamily:ipv4}, identity.oidc.issuer.
Kubeconfig deliveryThree options: k3d kubeconfig get localemu-eks-<name>, GET /_localemu_eks/<name>/kubeconfig, or read cluster.kubeconfig from DescribeCluster (non-standard; botocore may strip).
Persistencemoto cluster records survive PERSISTENCE=1 restart; k3d clusters are stopped (not deleted) so PVCs and etcd survive; on next startup, reattach_from_disk calls k3d cluster start for each persisted cluster.

Node groups

CreateNodegroup is the rare AWS-API call that produces a real Docker container per worker node. The handler at provider.py:255 calls mgr.add_agent_nodes(), which runs k3d node create <name>-agent-<i> --cluster <k3d-name> --role agent --wait --timeout 60s once per scalingConfig.desiredSize. The new nodes join the cluster and show up immediately in kubectl get nodes.

Terminal
$ awsemu eks create-nodegroup --cluster-name dev \
    --nodegroup-name workers \
    --node-role  arn:aws:iam::000000000000:role/EksNodeRole \
    --subnets    subnet-1 subnet-2 \
    --scaling-config minSize=1,maxSize=3,desiredSize=2 \
    --query 'nodegroup.{Name:nodegroupName,Status:status,Desired:scalingConfig.desiredSize}'
{"Name": "workers", "Status": "ACTIVE", "Desired": 2}

# Two real k3d agent containers were spawned and joined the cluster.
$ kubectl get nodes
NAME                                STATUS   ROLES                  AGE   VERSION
k3d-localemu-eks-dev-server-0       Ready    control-plane,master   3m    v1.29.8+k3s1
k3d-localemu-eks-dev-workers-agent-0   Ready    <none>              30s   v1.29.8+k3s1
k3d-localemu-eks-dev-workers-agent-1   Ready    <none>              30s   v1.29.8+k3s1

Teardown

Terminal
$ awsemu eks delete-cluster --name dev \
    --query 'cluster.status' --output text
DELETING

# Async: the k3d cluster is torn down in a background thread.
$ while k3d cluster list -o json | grep -q localemu-eks-dev; do sleep 2; done
$ awsemu eks list-clusters --query 'clusters'
[]

Persistence

With PERSISTENCE=1, on shutdown LocalEmu runs k3d cluster stop (not delete) so the k3s SQLite/etcd state, PVC contents, and CA keys all live in the stopped container's writable layer. On the next startup, reattach_from_disk(moto_clusters) walks every persisted moto cluster: if the k3d cluster exists, it is started and the kubeconfig is rehydrated into the in-memory ClusterInfo; if it does not exist, it is recreated in a background thread; orphan k3d clusters (no matching moto record) are deleted.

Configuration

VariableDefaultPurpose
EKS_K8S_PROVIDERauto-detectUnset: enable k3d when its binary is on PATH, else metadata-only. k3d: require k3d (error if missing). off / none / metadata / moto / disabled: force metadata-only. Read at provider.py:46.

Integration points

ServiceHow it touches EKS
k3d / k3sReal cluster process. The k3d binary on the host shells into Docker to run the k3s control plane and agents.
Dockerk3d uses the same Docker daemon LocalEmu uses for ECS, EC2, RDS Docker, etc. Image pulls are de-duplicated per host.
CloudFormationAWS::EKS::Cluster, AWS::EKS::Nodegroup, AWS::EKS::Addon, AWS::EKS::FargateProfile resource providers route through this provider.
CloudTrailAll EKS API calls appear in the CloudTrail event store and the dashboard.

Known limitations