Docs / Use Cases / EKS Kubernetes Clusters

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.

What the demo does. Calls 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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