EKS
EKS (Elastic Kubernetes Service) is AWS’s managed Kubernetes: AWS runs the Kubernetes control plane, and your containers run on it as Kubernetes objects. Alchemy targets Auto Mode, where AWS also manages the nodes, storage, and load-balancer integration — no machines to operate.
The Kubernetes primitives you’ll meet here:
- A Cluster is one Kubernetes installation: the API server plus the Nodes that run your containers.
- A Pod is one or more containers running together — the unit Kubernetes schedules onto Nodes.
- A Deployment keeps a set number of identical Pods running,
replacing ones that fail. A Service gives them one stable
address — and, as type
LoadBalancer, a public one. - A Job runs a Pod to completion; a CronJob does that on a schedule.
- Everything in Kubernetes is an object described by a manifest you apply to the Cluster.
Alchemy models these directly:
Cluster with compute: "auto"
stands up the control plane from a VPC, and the cluster-agnostic
alchemy/Kubernetes workloads target it by passing the cluster
resource as their cluster prop:
Kubernetes.Deployment
synthesizes a Kubernetes Deployment + Service;
Kubernetes.Job a Job or CronJob;
and Kubernetes.Manifest applies
any raw Kubernetes object. The workloads live in the same TypeScript
program as the cluster, with no YAML and no kubectl apply step —
and the same workloads run on any other cluster your kubeconfig can
reach (Kubernetes.KubeConfig(...)).
The workload providers ship in Kubernetes.providers() — compose it
with AWS.providers() in the Stack:
import * as AWS from "alchemy/AWS";import * as Kubernetes from "alchemy/Kubernetes";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "my-app", { providers: Layer.mergeAll(AWS.providers(), Kubernetes.providers()), state: Alchemy.localState(), }, // ...);Create an Auto Mode cluster
Section titled “Create an Auto Mode cluster”compute: "auto" turns on EKS Auto Mode with sensible defaults —
managed compute (system and general-purpose node pools), block
storage, elastic load balancing, and API authentication — and,
when you don’t pass roleArn, provisions and owns the cluster and
node IAM roles with the standard Auto Mode managed policies:
import * as AWS from "alchemy/AWS";
const network = yield* AWS.EC2.Network("Network", { cidrBlock: "10.42.0.0/16", availabilityZones: 2, nat: "single",});
const cluster = yield* AWS.EKS.Cluster("Cluster", { compute: "auto", resourcesVpcConfig: { subnetIds: network.privateSubnetIds, endpointPublicAccess: true, endpointPrivateAccess: true, },});The cluster lands in the network’s private subnets, built on a
Network. Budget for the create: an
EKS control plane takes ~10 minutes to provision.
Grant access and install add-ons
Section titled “Grant access and install add-ons”Auto Mode provisions with authenticationMode: "API", so cluster
access is granted through
AccessEntry resources rather than
the aws-auth ConfigMap, and
Addon installs EKS add-ons (EKS picks
the default compatible version when you don’t pin one):
const admin = yield* AWS.EKS.AccessEntry("ClusterAdmin", { clusterName: cluster.clusterName, principalArn: "arn:aws:iam::123456789012:role/YourAdminRole", accessPolicies: [ { policyArn: "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy", accessScope: { type: "cluster" }, }, ],});
const metricsServer = yield* AWS.EKS.Addon("MetricsServer", { clusterName: cluster.clusterName, addonName: "metrics-server",});The deploying principal is bootstrapped as cluster admin
(bootstrapClusterCreatorAdminPermissions: true is part of the Auto
Mode defaults), so these are for everyone (and everything) else that
needs in.
Deploy a server
Section titled “Deploy a server”Kubernetes.Deployment is a
replicated Kubernetes server — the Kubernetes analog of
AWS.ECS.Service. It
synthesizes a Kubernetes Deployment + Service (+
ServiceAccount) and applies them via server-side apply, with the
container image coming from exactly one of three sources flat on
props: image (a registry reference, mirrored into ECR), context
(build your own Dockerfile), or main (bundle an inline Effect
program). The simplest form runs a remote image with no Effect
runtime in the container:
const echo = yield* Kubernetes.Deployment("EchoServer", { cluster, image: "registry.k8s.io/echoserver:1.10", namespace: "default", replicas: 2, port: 8080, serviceType: "LoadBalancer",});
echo.url; // LoadBalancer hostname (an NLB on Auto Mode)echo.deploymentName; // K8s-native attrs: deploymentName, serviceName, ...serviceType: "LoadBalancer" provisions a cloud load balancer and
exposes its hostname as url. Swap image for
context: "./legacy" to build your own Dockerfile (dockerfile is
a path, defaulting to ${context}/Dockerfile).
Effect servers with bindings
Section titled “Effect servers with bindings”Pass main: import.meta.url and an init Effect and the program is
bundled into a generated image instead — the same authoring model as
Lambda. Bindings work identically too:
Deployment accepts the same { env, policyStatements } binding
contract as a Lambda Function, so every AWS Binding.Service
attaches environment variables to the Pod spec and IAM policy
statements to a generated Pod Identity role:
const api = yield* Kubernetes.Deployment( "Api", { cluster, main: import.meta.url, port: 3000, replicas: 2 }, Effect.gen(function* () { const putItem = yield* AWS.DynamoDB.PutItem(table); return { fetch: Effect.gen(function* () { yield* putItem({ Item: { id: { S: "1" } } }); return HttpServerResponse.text("ok"); }), }; }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);Every Kubernetes workload on EKS gets Pod Identity as standard:
Alchemy creates
the IAM role, wires it to the workload’s ServiceAccount with a
PodIdentityAssociation,
and Pods resolve credentials through the EKS Pod Identity
container-credentials chain — no OIDC provider or IRSA annotation
ceremony. The tagged form
(class Api extends Kubernetes.Deployment<Api, Shape>()("Api") {} +
Api.make(props, impl)) works exactly as it does on Lambda and
Cloudflare Workers.
Run-to-completion work with Job
Section titled “Run-to-completion work with Job”Kubernetes.Job runs a container to
completion — the Kubernetes analog of AWS.ECS.Task. Same three image sources,
same bindings and Pod Identity; an Effect impl returns { run }
instead of { fetch }, executing to completion inside the Pod:
const migrate = yield* Kubernetes.Job("DbMigrate", { cluster, image: "ghcr.io/acme/migrator:v3", backoffLimit: 2,});Set schedule (standard 5-field cron) and a Kubernetes CronJob is
synthesized instead of a plain Job:
const nightly = yield* Kubernetes.Job("NightlyBackfill", { cluster, main: import.meta.url, schedule: "0 3 * * *",});Everything else is a Manifest
Section titled “Everything else is a Manifest”Kubernetes.Manifest applies any
raw Kubernetes object — StatefulSets, Namespaces, CRDs — via server-side apply.
The manifest is a literal object, exactly as you would write it in
YAML:
const namespace = yield* Kubernetes.Manifest("DemoNamespace", { cluster, manifest: { apiVersion: "v1", kind: "Namespace", metadata: { name: "demo" }, },});There is no kubeconfig step: Alchemy authenticates to the cluster’s
API with your AWS credentials (a presigned STS token) and applies
objects via server-side apply under the alchemy field manager,
so deploys converge the live objects the same way the rest of your
Stack converges cloud resources. Unknown kinds resolve through the
Kubernetes API discovery endpoint, so CRDs work without any
registration.
Install a Helm chart
Section titled “Install a Helm chart”Kubernetes.HelmChart renders a
chart with the
local helm CLI (helm template — install helm on your machine,
like Docker for image builds) and applies the rendered objects
through the same server-side-apply path as Manifest:
const ingress = yield* Kubernetes.HelmChart("IngressNginx", { cluster, chart: "ingress-nginx", repo: "https://kubernetes.github.io/ingress-nginx", version: "4.11.2", namespace: "ingress-nginx", createNamespace: true, values: { controller: { replicaCount: 2 }, },});chart also accepts oci:// references and local chart directories,
and values is a literal object — the same shape as a values.yaml
file. Because the objects are applied (not helm installed), Alchemy
owns their lifecycle: drift is corrected on every deploy, objects
that drop out of the render are pruned, and destroy deletes them —
there is no in-cluster Helm release record. Charts that rely on
install/upgrade hooks for correctness should be installed with Helm
directly.
Run on SageMaker HyperPod
Section titled “Run on SageMaker HyperPod”An EKS cluster can also orchestrate a
SageMaker HyperPod fleet — persistent,
health-checked ML compute whose nodes join the cluster as
ordinary Kubernetes nodes.
AWS.SageMaker.Cluster
attaches the fleet to the EKS control plane:
const hyperpod = yield* AWS.SageMaker.Cluster("HyperPod", { orchestrator: { Eks: { ClusterArn: cluster.clusterArn } }, vpcConfig: { SecurityGroupIds: [clusterSecurityGroupId], Subnets: network.privateSubnetIds, }, instanceGroups: { workers: { InstanceType: "ml.g5.xlarge", InstanceCount: 4, ExecutionRole: role.roleArn, LifeCycleConfig: { SourceS3Uri: script.sourceS3Uri, OnCreate: script.onCreate, }, }, },});Kubernetes.Deployment and Kubernetes.Job then opt onto those
nodes in plain Kubernetes vocabulary — the HyperPod resources
expose the derived values as attributes. The instance-group keys
carry through to the cluster’s attributes as types —
hyperpod.instanceGroups.workers is typed per key and a typo’d
name is a compile error — so the workload is connected to the
fleet through the resource graph, and a team’s ComputeQuota
materializes the governed namespace and Kueue queue:
const train = yield* Kubernetes.Job("Train", { cluster, main: import.meta.url, namespace: researchQuota.namespace, // hyperpod-ns-research labels: { [AWS.SageMaker.KUEUE_QUEUE_NAME_LABEL]: researchQuota.queueName, [AWS.SageMaker.KUEUE_PRIORITY_CLASS_LABEL]: "training-priority", }, podTemplate: { spec: { nodeSelector: hyperpod.instanceGroups.workers.nodeSelector }, },});See HyperPod for the fleet’s prerequisites (lifecycle scripts, the dependencies Helm chart, EKS auth-mode and version constraints) and the full governance surface.
Where next
Section titled “Where next”- VPC & networking — what the
Networkhelper builds under the cluster. - HyperPod — run training fleets on this cluster’s nodes.
- Choosing a runtime — when Lambda or ECS is the better fit than running your own Kubernetes workloads.
Clusterreference,Deploymentreference,Jobreference,Manifestreference — every prop and attribute.