EKS Kubernetes Clusters
Run a real Kubernetes cluster from the EKS API. Create clusters backed by k3s in Docker, deploy pods with kubectl, scale deployments, and manage the full lifecycle through standard AWS APIs.
eks create-cluster which spins up a
real k3s cluster in Docker via k3d, waits for the cluster to report
ACTIVE with at least one node
Ready, fetches the kubeconfig back from k3d
and writes it to a local file, then drives the cluster with stock
kubectl: lists nodes and namespaces, applies
an nginx:alpine Deployment with 2 replicas,
exposes it as a NodePort Service, scales to 3 replicas, verifies the rollout, tails the
pod logs, runs kubectl get all, deletes the
Kubernetes resources, and finally eks delete-cluster
which tears the k3d cluster down. The cluster typically reaches
ACTIVE in 15-30 seconds once the k3s image
is cached locally (60-90 s on first run); teardown asserts the k3d cluster is gone.
The k3d backend is on by default when the k3d
binary is on PATH (along with kubectl);
set EKS_K8S_PROVIDER=off to force the
metadata-only path.
Source: 17-eks-demo/ in the examples repo.
k3s control plane in Docker
Each EKS cluster brings up an actual k3s control plane (API server, scheduler, controller-manager, kubelet, embedded etcd) via k3d.
kubectl access
DescribeCluster returns the real https://127.0.0.1:<port> endpoint plus a CA cert. Any standard kubectl or Kubernetes client works against it.
Full Lifecycle
Create clusters, deploy pods, scale, monitor, and delete. Complete EKS workflow with clean teardown.
Step-by-Step Walkthrough
Step 1: Prerequisites
$ brew install k3d
$ k3d version
k3d version v5.8.3
k3s version v1.33.6-k3s1 (default)
# Docker must be running Install k3d via Homebrew or the curl install script. Docker must be running on your machine.
Step 2: Start LocalEmu
$ localemu start
# The k3d backend turns on automatically when the k3d binary is on PATH.
# Force metadata-only mode with EKS_K8S_PROVIDER=off if you do not want
# real Kubernetes clusters for this LocalEmu instance. The k3d backend turns on automatically when LocalEmu finds the k3d binary on PATH. Force the legacy metadata-only path with EKS_K8S_PROVIDER=off when you want EKS to return ARNs and statuses without paying the cost of a real k3s cluster (CI smoke tests, IaC dry-runs).
Step 3: Create an EKS cluster
$ awsemu eks create-cluster \
--name my-cluster \
--role-arn arn:aws:iam::000000000000:role/eks-role \
--resources-vpc-config subnetIds=subnet-12345,securityGroupIds=sg-12345
cluster:
name: my-cluster
arn: arn:aws:eks:us-east-1:000000000000:cluster/my-cluster
status: CREATING
# CreateCluster returns CREATING immediately (matching real EKS); the
# k3d cluster spins up in the background. Wait for ACTIVE:
$ awsemu eks wait cluster-active --name my-cluster
$ awsemu eks describe-cluster --name my-cluster --query 'cluster.{name:name,status:status,endpoint:endpoint}'
name: my-cluster
status: ACTIVE
endpoint: https://127.0.0.1:57070
# 15-30 s once the k3s image is cached locally, 60-90 s on first run
# while k3d pulls rancher/k3s. CreateCluster returns CREATING immediately, the same way real EKS does. The k3d cluster spins up in a background thread, so wait on eks wait cluster-active (or poll describe-cluster) before fetching the kubeconfig. Time-to-Active is 15-30 s once the rancher/k3s image is cached, 60-90 s on the very first run.
Step 4: Connect with kubectl
$ k3d kubeconfig get localemu-eks-my-cluster > /tmp/eks-kubeconfig.yaml
$ export KUBECONFIG=/tmp/eks-kubeconfig.yaml
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3d-localemu-eks-my-cluster-server-0 Ready control-plane,master 15s v1.33.6+k3s1
$ kubectl get namespaces
NAME STATUS AGE
default Active 18s
kube-system Active 18s
kube-public Active 18s
kube-node-lease Active 18s Export the kubeconfig from k3d and you have full kubectl access. The node shows Ready with v1.33.6+k3s1. All standard namespaces are present.
Step 5: Deploy an application
$ kubectl create deployment nginx --image=nginx:alpine --replicas=2
deployment.apps/nginx created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-7c5ddbdf54-k8m2x 1/1 Running 0 6s
nginx-7c5ddbdf54-qr9tl 1/1 Running 0 6s
$ kubectl expose deployment nginx --port=80 --type=NodePort
service/nginx exposed
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 45s
nginx NodePort 10.43.217.89 <none> 80:30676/TCP 3s Deploy nginx with 2 replicas and expose it as a NodePort service on port 30676. From here on the workflow is plain Kubernetes: anything you can kubectl apply against a k3s cluster (Deployments, StatefulSets, ConfigMaps, Secrets, CRDs, ingress controllers you install yourself, ...) works here.
Step 6: Scale and monitor
$ kubectl scale deployment nginx --replicas=3
deployment.apps/nginx scaled
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-7c5ddbdf54-k8m2x 1/1 Running 0 22s
nginx-7c5ddbdf54-qr9tl 1/1 Running 0 22s
nginx-7c5ddbdf54-v4f7n 1/1 Running 0 4s
$ kubectl logs deployment/nginx --tail=5
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/nginx-7c5ddbdf54-k8m2x 1/1 Running 0 35s
pod/nginx-7c5ddbdf54-qr9tl 1/1 Running 0 35s
pod/nginx-7c5ddbdf54-v4f7n 1/1 Running 0 17s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 58s
service/nginx NodePort 10.43.217.89 <none> 80:30676/TCP 16s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/nginx 3/3 3 3 35s
NAME DESIRED CURRENT READY AGE
replicaset.apps/nginx-7c5ddbdf54 3 3 3 35s Scale to 3 replicas, tail the nginx logs, and see the full picture with kubectl get all. The pods, ReplicaSet and Deployment are k3s objects, so they reschedule, restart and roll over the same way they would in any Kubernetes cluster.
Step 7: Cleanup
$ kubectl delete service nginx
service "nginx" deleted
$ kubectl delete deployment nginx
deployment.apps "nginx" deleted
$ awsemu eks delete-cluster --name my-cluster
cluster:
name: my-cluster
arn: arn:aws:eks:us-east-1:000000000000:cluster/my-cluster
status: DELETING
$ k3d cluster list
NAME SERVERS AGENTS LOADBALANCER
(empty - cluster deleted)
$ awsemu eks list-clusters
clusters: [] Delete Kubernetes resources, then delete the EKS cluster. The k3d cluster and all Docker containers are removed. No orphaned resources.
How It Works
Metadata layer
The EKS control-plane shape (cluster records, ARNs, nodegroups, addon status, tags) lives in LocalEmu's in-process EKS metadata store, so describe-cluster, list-clusters and friends return the same JSON shape AWS does.
Behavior layer (k3d)
CreateCluster hands off to K3dClusterManager, which runs k3d cluster create localemu-eks-<name> in a background thread. That brings up the k3s control plane (API server, scheduler, controller-manager) plus an etcd-equivalent and a kubelet, all in Docker containers on a dedicated network. Budget ~500 MB of RAM per cluster.
kubectl-usable endpoint
describe-cluster returns the actual https://127.0.0.1:<port> the k3s API server bound to, plus the cluster's CA cert in certificateAuthority.data. Fetch the kubeconfig with k3d kubeconfig get localemu-eks-<name> and any Kubernetes client works.
Lifecycle
DeleteCluster calls k3d cluster delete, which removes every container on the cluster's Docker network and the network itself. Under PERSISTENCE=1, LocalEmu shutdown uses k3d cluster stop instead, so the k3s state and PVC data are preserved and the same cluster boots back next start.
Supported Operations
- ●CreateCluster with real k3s Kubernetes cluster
- ●DeleteCluster with full Docker cleanup
- ●DescribeCluster with real endpoint and CA cert
- ●ListClusters
- ●kubectl access with real kubeconfig
- ●Pod deployments, services, scaling
- ●Any Kubernetes workload (pods, deployments, statefulsets, configmaps, secrets, etc.)
- ●15-30 second cluster creation (warm image cache); 60-90 s on first run