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
# 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
$ 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
# 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
$ 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.
| File | Role |
|---|---|
provider.py | Handlers (_handle_create_cluster, _handle_describe_cluster, _handle_delete_cluster, _handle_create_nodegroup), kubeconfig HTTP route, lifecycle hooks. |
cluster_manager.py | K3dClusterManager singleton, k3d subprocess invocations, kubeconfig extraction, persistence reconciliation. |
plugins.py | Startup/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 version | k3s image |
|---|---|
| 1.24 | rancher/k3s:v1.24.17-k3s1 |
| 1.25 | rancher/k3s:v1.25.16-k3s4 |
| 1.26 | rancher/k3s:v1.26.15-k3s1 |
| 1.27 | rancher/k3s:v1.27.16-k3s1 |
| 1.28 | rancher/k3s:v1.28.13-k3s1 |
| 1.29 | rancher/k3s:v1.29.8-k3s1 |
| 1.30 | rancher/k3s:v1.30.4-k3s1 |
| 1.31 | rancher/k3s:v1.31.1-k3s1 |
CreateCluster flow
- Moto writes the cluster metadata record (
provider.py:91). - The API returns immediately with
status=CREATING. - 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 - k3d's
--waitflag holds until the API server is reachable (120 s timeout). LocalEmu's outer budget is 180 s. - The kubeconfig is extracted via
k3d kubeconfig get; the endpoint is rewritten tohttps://127.0.0.1:<port>; the CA cert is base64-encoded. - The moto record is updated with
endpoint,certificateAuthority.data,status=ACTIVE. If k3d fails,statusflips toCREATE_FAILED.
Features supported
| Feature | Notes |
|---|---|
| Cluster lifecycle | CreateCluster (real k3d boot + readiness gate), DeleteCluster (async k3d cluster delete), DescribeCluster (live endpoint + base64 CA cert), ListClusters. |
| Kubernetes versions | 1.24 through 1.31, each pinned to an upstream rancher/k3s tag. |
| kubectl | Full round-trip: apply, get, describe, wait, exec, logs, port-forward against the live cluster. |
| Node groups | CreateNodegroup spawns real k3d node create --role agent containers (one per scalingConfig.desiredSize) that join the cluster. |
| Endpoint shape | DescribeCluster 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 delivery | Three options: k3d kubeconfig get localemu-eks-<name>, GET /_localemu_eks/<name>/kubeconfig, or read cluster.kubeconfig from DescribeCluster (non-standard; botocore may strip). |
| Persistence | moto 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.
$ 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
$ 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
| Variable | Default | Purpose |
|---|---|---|
EKS_K8S_PROVIDER | auto-detect | Unset: 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
| Service | How it touches EKS |
|---|---|
| k3d / k3s | Real cluster process. The k3d binary on the host shells into Docker to run the k3s control plane and agents. |
| Docker | k3d uses the same Docker daemon LocalEmu uses for ECS, EC2, RDS Docker, etc. Image pulls are de-duplicated per host. |
| CloudFormation | AWS::EKS::Cluster, AWS::EKS::Nodegroup, AWS::EKS::Addon, AWS::EKS::FargateProfile resource providers route through this provider. |
| CloudTrail | All EKS API calls appear in the CloudTrail event store and the dashboard. |
Known limitations
- •Endpoint is loopback-only. The API server is bound to
127.0.0.1:<port>on the host. From inside another LocalEmu container (Lambda, ECS task, EC2 docker instance),127.0.0.1resolves to that container itself. To reach the cluster from inside a container, usehost.docker.internal(Docker Desktop) or the host-bridge IP (Linux) and rewrite the kubeconfigserver:field accordingly. - •No LocalEmu VPC attachment. The k3d cluster runs on its own Docker network, not on the LocalEmu VPC bridge. Pods cannot reach the LocalEmu gateway by VPC endpoint or by VPC-internal DNS. Workloads that call S3/DynamoDB/etc need an env var pointing at
host.docker.internal:4566. - •No ECR integration. k3d pulls images via its own containerd from external registries or pre-loaded Docker images on the host; LocalEmu's ECR is not wired as a registry mirror. Workaround:
k3d image import <image> --cluster localemu-eks-<name>. - •IRSA (IAM Roles for Service Accounts) is metadata-only. The OIDC issuer URL is constructed in
DescribeClusterbut no HTTP endpoint serves JWKS. ServiceAccount JWTs are k3s-issued, not AWS-backed. IRSA-aware workloads will fail to assume their pod role. - •aws-auth ConfigMap is not provisioned. The cluster's RBAC is k3d default; the kubeconfig grants cluster-admin via static token. Multi-user RBAC modelled on the AWS API will not be reflected in the cluster.
- •
aws eks update-kubeconfigis not wired. The AWS CLI'saws eks get-tokenexec-credentials are not honored (the kubeconfig has a static bearer token, not AWS STS). Fetch the kubeconfig via the HTTP route ork3d kubeconfig get. - •
UpdateClusterVersionandUpdateClusterConfigare metadata-only. The running k3s cluster is not upgraded or reconfigured. - •
DeleteNodegroupis metadata-only. Removes the moto record but leaves the k3d agent containers running. Clean them up withk3d node delete <name>. - •Fargate profiles are stubs.
CreateFargateProfilestores metadata; no real Fargate-style scheduler is wired and Pods matching the selector still schedule on k3d nodes. - •Addons are metadata-only.
CreateAddondoes not install Helm charts or operators. CoreDNS, kube-proxy, VPC CNI, etc., are whatever k3s ships by default. - •No AWS Load Balancer Controller. The cluster is created with
--no-lb;Service type=LoadBalancerstays Pending unless you bring your own ingress. - •Cluster autoscaler is not implemented. Node groups do not scale on Pod pressure.
- •Single control-plane server. Multi-AZ / HA control planes are not supported;
CreateNodegrouponly adds agent (worker) nodes.