1---
2title: EKS
3description: Stand up an EKS Auto Mode cluster on a Network and run containers on it — Deployments for servers, Jobs for run-to-completion work, Manifests for everything else. No YAML, no kubectl.
4---
5
6**EKS** (Elastic Kubernetes Service) is AWS's managed Kubernetes:
7AWS runs the Kubernetes control plane, and your containers run on
8it as Kubernetes objects. Alchemy targets **Auto Mode**, where AWS
9also manages the nodes, storage, and load-balancer integration —
10no machines to operate.
11
12The Kubernetes primitives you'll meet here:
13
14- A **Cluster** is one Kubernetes installation: the API server
15 plus the Nodes that run your containers.
16- A **Pod** is one or more containers running together — the unit
17 Kubernetes schedules onto Nodes.
18- A **Deployment** keeps a set number of identical Pods running,
19 replacing ones that fail. A **Service** gives them one stable
20 address — and, as type `LoadBalancer`, a public one.
21- A **Job** runs a Pod to completion; a **CronJob** does that on a
22 schedule.
23- Everything in Kubernetes is an object described by a
24 **manifest** you apply to the Cluster.
25
26Alchemy models these directly:
27[`Cluster`](/providers/aws/eks/cluster) with `compute: "auto"`
28stands up the control plane from a VPC, and the cluster-agnostic
29`alchemy/Kubernetes` workloads target it by passing the cluster
30resource as their `cluster` prop:
31[`Kubernetes.Deployment`](/providers/kubernetes/deployment)
32synthesizes a Kubernetes `Deployment` + `Service`;
33[`Kubernetes.Job`](/providers/kubernetes/job) a `Job` or `CronJob`;
34and [`Kubernetes.Manifest`](/providers/kubernetes/manifest) applies
35any raw Kubernetes object. The workloads live in the same TypeScript
36program as the cluster, with no YAML and no `kubectl apply` step —
37and the same workloads run on any other cluster your kubeconfig can
38reach (`Kubernetes.KubeConfig(...)`).
39
40The workload providers ship in `Kubernetes.providers()` — compose it
41with `AWS.providers()` in the Stack:
42
43```typescript
44import * as AWS from "alchemy/AWS";
45import * as Kubernetes from "alchemy/Kubernetes";
46import * as Layer from "effect/Layer";
47
48export default Alchemy.Stack(
49 "my-app",
50 {
51 providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()),
52 state: Alchemy.localState(),
53 },
54 // ...
55);
56```
57
58## Create an Auto Mode cluster
59
60`compute: "auto"` turns on EKS Auto Mode with sensible defaults —
61managed compute (`system` and `general-purpose` node pools), block
62storage, elastic load balancing, and `API` authentication — and,
63when you don't pass `roleArn`, provisions and owns the cluster and
64node IAM roles with the standard Auto Mode managed policies:
65
66```typescript
67// alchemy.run.ts
68import * as AWS from "alchemy/AWS";
69
70const network = yield* AWS.EC2.Network("Network", {
71 cidrBlock: "10.42.0.0/16",
72 availabilityZones: 2,
73 nat: "single",
74});
75
76const cluster = yield* AWS.EKS.Cluster("Cluster", {
77 compute: "auto",
78 resourcesVpcConfig: {
79 subnetIds: network.privateSubnetIds,
80 endpointPublicAccess: true,
81 endpointPrivateAccess: true,
82 },
83});
84```
85
86The cluster lands in the network's private subnets, built on a
87[`Network`](/providers/aws/ec2/network). Budget for the create: an
88EKS control plane takes ~10 minutes to provision.
89
90## Grant access and install add-ons
91
92Auto Mode provisions with `authenticationMode: "API"`, so cluster
93access is granted through
94[`AccessEntry`](/providers/aws/eks/accessentry) resources rather than
95the `aws-auth` ConfigMap, and
96[`Addon`](/providers/aws/eks/addon) installs EKS add-ons (EKS picks
97the default compatible version when you don't pin one):
98
99```typescript
100const admin = yield* AWS.EKS.AccessEntry("ClusterAdmin", {
101 clusterName: cluster.clusterName,
102 principalArn: "arn:aws:iam::123456789012:role/YourAdminRole",
103 accessPolicies: [
104 {
105 policyArn:
106 "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy",
107 accessScope: { type: "cluster" },
108 },
109 ],
110});
111
112const metricsServer = yield* AWS.EKS.Addon("MetricsServer", {
113 clusterName: cluster.clusterName,
114 addonName: "metrics-server",
115});
116```
117
118The deploying principal is bootstrapped as cluster admin
119(`bootstrapClusterCreatorAdminPermissions: true` is part of the Auto
120Mode defaults), so these are for everyone (and everything) else that
121needs in.
122
123## Deploy a server
124
125[`Kubernetes.Deployment`](/providers/kubernetes/deployment) is a
126replicated Kubernetes server — the Kubernetes analog of
127`AWS.ECS.Service`. It
128synthesizes a Kubernetes `Deployment` + `Service` (+
129`ServiceAccount`) and applies them via server-side apply, with the
130container image coming from exactly one of three sources flat on
131props: `image` (a registry reference, mirrored into ECR), `context`
132(build your own Dockerfile), or `main` (bundle an inline Effect
133program). The simplest form runs a remote image with no Effect
134runtime in the container:
135
136```typescript
137const echo = yield* Kubernetes.Deployment("EchoServer", {
138 cluster,
139 image: "registry.k8s.io/echoserver:1.10",
140 namespace: "default",
141 replicas: 2,
142 port: 8080,
143 serviceType: "LoadBalancer",
144});
145
146echo.url; // LoadBalancer hostname (an NLB on Auto Mode)
147echo.deploymentName; // K8s-native attrs: deploymentName, serviceName, ...
148```
149
150`serviceType: "LoadBalancer"` provisions a cloud load balancer and
151exposes its hostname as `url`. Swap `image` for
152`context: "./legacy"` to build your own Dockerfile (`dockerfile` is
153a path, defaulting to `${context}/Dockerfile`).
154
155## Effect servers with bindings
156
157Pass `main: import.meta.url` and a constructor Effect and the program is
158bundled into a generated image instead — the same authoring model as
159[Lambda](/aws/compute/lambda). Bindings work identically too:
160`Deployment` accepts the same `{ env, policyStatements }` binding
161contract as a Lambda `Function`, so every AWS `Binding.Service`
162attaches environment variables to the Pod spec and IAM policy
163statements to a generated **Pod Identity role**:
164
165```typescript
166const api = yield* Kubernetes.Deployment(
167 "Api",
168 { cluster, main: import.meta.url, port: 3000, replicas: 2 },
169 Effect.gen(function* () {
170 const putItem = yield* AWS.DynamoDB.PutItem(table);
171 return {
172 fetch: Effect.gen(function* () {
173 yield* putItem({ Item: { id: { S: "1" } } });
174 return HttpServerResponse.text("ok");
175 }),
176 };
177 }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
178);
179```
180
181Every Kubernetes workload on EKS gets Pod Identity as standard:
182Alchemy creates
183the IAM role, wires it to the workload's ServiceAccount with a
184[`PodIdentityAssociation`](/providers/aws/eks/podidentityassociation),
185and Pods resolve credentials through the EKS Pod Identity
186container-credentials chain — no OIDC provider or IRSA annotation
187ceremony. The tagged form
188(`class Api extends Kubernetes.Deployment<Api, Shape>()("Api") {}` +
189`Api.make(props, impl)`) works exactly as it does on Lambda and
190Cloudflare Workers.
191
192## Run-to-completion work with Job
193
194[`Kubernetes.Job`](/providers/kubernetes/job) runs a container to
195completion — the Kubernetes analog of `AWS.ECS.Task`. Same three image sources,
196same bindings and Pod Identity; an Effect impl returns `{ run }`
197instead of `{ fetch }`, executing to completion inside the Pod:
198
199```typescript
200const migrate = yield* Kubernetes.Job("DbMigrate", {
201 cluster,
202 image: "ghcr.io/acme/migrator:v3",
203 backoffLimit: 2,
204});
205```
206
207Set `schedule` (standard 5-field cron) and a Kubernetes `CronJob` is
208synthesized instead of a plain `Job`:
209
210```typescript
211const nightly = yield* Kubernetes.Job("NightlyBackfill", {
212 cluster,
213 main: import.meta.url,
214 schedule: "0 3 * * *",
215});
216```
217
218## Everything else is a Manifest
219
220[`Kubernetes.Manifest`](/providers/kubernetes/manifest) applies any
221raw Kubernetes object — StatefulSets, Namespaces, CRDs — via server-side apply.
222The manifest is a literal object, exactly as you would write it in
223YAML:
224
225```typescript
226const namespace = yield* Kubernetes.Manifest("DemoNamespace", {
227 cluster,
228 manifest: {
229 apiVersion: "v1",
230 kind: "Namespace",
231 metadata: { name: "demo" },
232 },
233});
234```
235
236There is no kubeconfig step: Alchemy authenticates to the cluster's
237API with your AWS credentials (a presigned STS token) and applies
238objects via **server-side apply** under the `alchemy` field manager,
239so deploys converge the live objects the same way the rest of your
240Stack converges cloud resources. Unknown kinds resolve through the
241Kubernetes API discovery endpoint, so CRDs work without any
242registration.
243
244## Install a Helm chart
245
246[`Kubernetes.HelmChart`](/providers/kubernetes/helmchart) renders a
247chart with the
248local `helm` CLI (`helm template` — install helm on your machine,
249like Docker for image builds) and applies the rendered objects
250through the same server-side-apply path as `Manifest`:
251
252```typescript
253const ingress = yield* Kubernetes.HelmChart("IngressNginx", {
254 cluster,
255 chart: "ingress-nginx",
256 repo: "https://kubernetes.github.io/ingress-nginx",
257 version: "4.11.2",
258 namespace: "ingress-nginx",
259 createNamespace: true,
260 values: {
261 controller: { replicaCount: 2 },
262 },
263});
264```
265
266`chart` also accepts `oci://` references and local chart directories,
267and `values` is a literal object — the same shape as a `values.yaml`
268file. Because the objects are applied (not `helm install`ed), Alchemy
269owns their lifecycle: the full render is re-applied whenever the
270chart's inputs change, objects that drop out of the render are
271pruned, and destroy deletes them —
272there is no in-cluster Helm release record. Charts that rely on
273install/upgrade hooks for correctness should be installed with Helm
274directly.
275
276## What EKS adds to Kubernetes workloads
277
278The `Kubernetes.*` resources are cluster-agnostic. When their
279`cluster` is an EKS cluster, `AWS.providers()` contributes an
280adapter that handles the AWS-specific parts.
281
282### Authentication
283
284Alchemy signs a short-lived bearer token with your AWS credentials
285for every request, so there is no kubeconfig to manage. It's the
286same token `aws eks get-token` produces. The principal that created the
287cluster is its admin. Grant anyone else access with
288[`AccessEntry`](/providers/aws/eks/accessentry).
289
290### Images in ECR
291
292Each `Deployment` and `Job` gets its own private ECR repository.
293`main` programs are bundled and built into an image, `context`
294Dockerfiles are built, and `image` references are pulled and pushed
295into the repository, so pods only pull from your account. Set
296`architecture: "arm64"` for Graviton nodes. The default builds
297`amd64`. The repository is deleted when the workload is destroyed.
298
299### Pod Identity
300
301Each workload gets an IAM role and a
302[`PodIdentityAssociation`](/providers/aws/eks/podidentityassociation)
303for its ServiceAccount. AWS bindings used by the workload add their
304IAM statements to that role, and the pod receives temporary
305credentials for it through the EKS Pod Identity agent. Attach
306AWS-managed or customer-managed policies with
307`identity.managedPolicyArns`:
308
309```typescript
310const exporter = yield* Kubernetes.Job("Export", {
311 cluster,
312 main: import.meta.url,
313 identity: {
314 managedPolicyArns: ["arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"],
315 },
316});
317```
318
319### Load balancers
320
321Auto Mode's built-in load balancer controller only handles Services
322with `loadBalancerClass: eks.amazonaws.com/nlb`, and creates internal
323load balancers by default. On `LoadBalancer` Services Alchemy sets
324that class (when the cluster has Auto Mode load balancing enabled)
325and the `service.beta.kubernetes.io/aws-load-balancer-scheme:
326internet-facing` annotation, so `url` is reachable from the
327internet. Your own `serviceAnnotations` take precedence. Set the
328scheme to `internal` to keep a service private:
329
330```typescript
331const internal = yield* Kubernetes.Deployment("Internal", {
332 cluster,
333 image: "ghcr.io/acme/internal:v1",
334 port: 8080,
335 serviceAnnotations: {
336 "service.beta.kubernetes.io/aws-load-balancer-scheme": "internal",
337 },
338});
339```
340
341### Cleanup
342
343The ECR repository and Pod Identity role outlive the cluster's own
344objects. On destroy Alchemy deletes the in-cluster objects if the
345cluster is still reachable, then deletes the repository and role
346whether or not the cluster still exists, so destroying a cluster and
347its workloads together leaves nothing behind in AWS.
348
349## Target an EKS cluster you don't manage
350
351To deploy onto a cluster created elsewhere, name it in a connection.
352The adapter looks up the endpoint and certificate authority from the
353cluster name:
354
355```typescript
356const cluster: Kubernetes.Connection = {
357 auth: { kind: "aws-eks", clusterName: "shared-prod", region: "us-west-2" },
358};
359```
360
361`region` defaults to the Stack's AWS region. The `aws-eks` auth kind
362is added to the `Kubernetes.Connection` type by importing
363`alchemy/AWS`, and it requires `AWS.providers()` in the Stack. Your
364AWS principal needs an access entry on the cluster. Workloads get
365the same ECR, Pod Identity, and load balancer behavior as on a
366Stack-managed cluster.
367
368## Run on SageMaker HyperPod
369
370An EKS cluster can also orchestrate a
371[SageMaker HyperPod](/aws/compute/hyperpod) fleet — persistent,
372health-checked ML compute whose nodes join the cluster as
373ordinary Kubernetes nodes.
374[`AWS.SageMaker.Cluster`](/providers/aws/sagemaker/cluster)
375attaches the fleet to the EKS control plane:
376
377```typescript
378const hyperpod = yield* AWS.SageMaker.Cluster("HyperPod", {
379 orchestrator: { Eks: { ClusterArn: cluster.clusterArn } },
380 vpcConfig: {
381 SecurityGroupIds: [clusterSecurityGroupId],
382 Subnets: network.privateSubnetIds,
383 },
384 instanceGroups: {
385 workers: {
386 InstanceType: "ml.g5.xlarge",
387 InstanceCount: 4,
388 ExecutionRole: role.roleArn,
389 LifeCycleConfig: {
390 SourceS3Uri: script.sourceS3Uri,
391 OnCreate: script.onCreate,
392 },
393 },
394 },
395});
396```
397
398`Kubernetes.Deployment` and `Kubernetes.Job` then opt onto those
399nodes in plain Kubernetes vocabulary — the HyperPod resources
400expose the derived values as attributes. The instance-group keys
401carry through to the cluster's attributes as types —
402`hyperpod.instanceGroups.workers` is typed per key and a typo'd
403name is a compile error — so the workload is connected to the
404fleet through the resource graph, and a team's `ComputeQuota`
405materializes the governed namespace and Kueue queue:
406
407```typescript
408const train = yield* Kubernetes.Job("Train", {
409 cluster,
410 main: import.meta.url,
411 namespace: researchQuota.namespace, // hyperpod-ns-research
412 labels: {
413 [AWS.SageMaker.KUEUE_QUEUE_NAME_LABEL]: researchQuota.queueName,
414 [AWS.SageMaker.KUEUE_PRIORITY_CLASS_LABEL]: "training-priority",
415 },
416 podTemplate: {
417 spec: { nodeSelector: hyperpod.instanceGroups.workers.nodeSelector },
418 },
419});
420```
421
422See [HyperPod](/aws/compute/hyperpod) for the fleet's
423prerequisites (lifecycle scripts, the dependencies Helm chart,
424EKS auth-mode and version constraints) and the full governance
425surface.
426
427## Where next
428
429- [Kubernetes](/kubernetes) — the full guide to `Kubernetes.*`
430 workloads, manifests, and Helm charts, with a
431 [tutorial](/kubernetes/tutorial/part-1) you can run on a local
432 cluster before deploying to EKS.
433- [VPC & networking](/aws/networking) — what the `Network` helper
434 builds under the cluster.
435- [HyperPod](/aws/compute/hyperpod) — run training fleets on this
436 cluster's nodes.
437- [Choosing a runtime](/aws/compute/choosing-a-runtime) — when Lambda or ECS
438 is the better fit than running your own Kubernetes workloads.
439- [`Cluster` reference](/providers/aws/eks/cluster),
440 [`Deployment` reference](/providers/kubernetes/deployment),
441 [`Job` reference](/providers/kubernetes/job),
442 [`Manifest` reference](/providers/kubernetes/manifest) — every prop
443 and attribute.