Skip to content

2.0.0-beta.64 - Full AWS Coverage

beta.64 is the AWS release. Alchemy now covers 206 AWS services, with every resource and binding live-tested against the real cloud. It also adds first-class containers on ECS and EKS, Durable Lambda Functions, and Helm charts on EKS.

One PR (#797) took the AWS provider from a handful of services to all of them, using the same resource factory that built out the Cloudflare provider. Fleets of agents cataloged all 428 generated SDK modules, then implemented and live-tested them in waves: serverless first, then ECS/Fargate, EKS, EC2, and the long tail. Everything left over is documented as out of scope (deprecated APIs, data-only endpoints, billing objects). The result:

  • 206 AWS services with 683 resources and 2,842 bindings (plus event sources and sinks), from DynamoDB and S3 all the way out to MediaLive, IoT SiteWise, Bedrock, and SageMaker.
  • 2,692 live tests green by default against real AWS. 375 more are gated behind env vars because they are slow to provision, expensive, or need special account entitlements.
  • Zero-orphan guarantee: each round wiped the test account clean, ran the full suite, then audited the account for leftover resources. The loop ran until two consecutive rounds left nothing behind. A passing test that leaks a cloud resource is a provider bug, and the loop caught dozens of them.

Bindings are 1:1 with IAM actions. Each binding your code uses becomes a least-privilege policy statement on exactly the resources it touches, whether the code runs on Lambda, ECS, or EKS:

const api = yield* AWS.ECS.Service(
"Api",
{ cluster, main: import.meta.url, port: 3000, desiredCount: 2 },
Effect.gen(function* () {
// init: each binding grants one IAM action on one ARN
const getItem = yield* AWS.DynamoDB.GetItem(table);
const send = yield* AWS.SQS.SendMessage(queue);
return {
fetch: Effect.gen(function* () {
const item = yield* getItem({ Key: { pk: { S: "user#1" } } });
yield* send({ MessageBody: JSON.stringify(item.Item) });
return yield* HttpServerResponse.json({ ok: true });
}),
};
}).pipe(
Effect.provide([AWS.DynamoDB.GetItemHttp, AWS.SQS.SendMessageHttp]),
),
);

You never write an IAM policy, and every error is typed — the compiler tells you what can fail and you handle it like any other Effect error. See Bindings for how it works.

All of it is documented in the API reference, with usage examples on every resource and binding.

ECS gets the full platform treatment (#867): Task for run-to-completion work and Service for long-running servers. Both are authored the same three ways as Workers and Lambda: an external image, an inline effect, or a tagged class.

Every container platform takes one image source, flat on props:

{ main } // bundle this Effect program
{ main, image } // bundle FROM your base image
{ context, dockerfile? } // build your own Dockerfile
{ dockerfile: Dockerfile.inline`...` } // inline Dockerfile content
{ image } // registry ref, mirrored into ECR

The simplest Service runs a registry image behind a provisioned Application Load Balancer:

const cluster = yield* AWS.ECS.Cluster("AppCluster", {});
const nginx = yield* AWS.ECS.Service("Edge", {
cluster,
image: "public.ecr.aws/nginx/nginx:1.27",
port: 80,
desiredCount: 2,
loadBalancer: true,
});
return { url: nginx.url }; // http://<alb-dns-name>

A Service can also run an Effect program. The impl returns { fetch }, and bindings grant IAM to the task role:

const api = yield* AWS.ECS.Service(
"Api",
{ cluster, main: import.meta.url, port: 3000, desiredCount: 2 },
Effect.gen(function* () {
const putItem = yield* AWS.DynamoDB.PutItem(table);
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const order = (yield* request.json) as { orderId: string };
yield* putItem({ Item: { pk: { S: order.orderId } } });
return yield* HttpServerResponse.json({ ok: true });
}),
};
}).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
);

A one-shot Task is the run-to-completion counterpart: the impl returns { run }, and the container exits when the effect completes. Here it composes main with Dockerfile.inline — the inline content builds the environment, and the bundled program is layered on top:

import { Dockerfile } from "alchemy/Docker";
const thumbnailer = yield* AWS.ECS.Task(
"Thumbnailer",
{
main: import.meta.url,
dockerfile: Dockerfile.inline`
FROM oven/bun:1
RUN apt-get update && apt-get install -y ffmpeg
`,
cpu: 1024,
memory: 2048,
},
Effect.gen(function* () {
const getObject = yield* AWS.S3.GetObject(bucket);
return {
run: Effect.gen(function* () {
// pull the video from S3, shell out to ffmpeg, then exit
}),
};
}).pipe(Effect.provide(AWS.S3.GetObjectHttp)),
);

Tasks are launched from other compute with the RunTask binding, which grants ecs:RunTask and iam:PassRole automatically — here a Lambda function launches a task per request:

const api = yield* AWS.Lambda.Function(
"Api",
{ main: import.meta.url, url: true },
Effect.gen(function* () {
const runTask = yield* AWS.ECS.RunTask(cluster, thumbnailer);
return {
fetch: Effect.gen(function* () {
const response = yield* runTask({
launchType: "FARGATE",
networkConfiguration: {
awsvpcConfiguration: { subnets: [subnetId] },
},
});
return yield* HttpServerResponse.json({
taskArn: response.tasks?.[0]?.taskArn,
});
}),
};
}),
);

Or run a task on a schedule with AWS.ECS.every, which provisions the EventBridge rule and invoke role:

yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", {
cluster,
task: nightlyTask,
subnets: [privateSubnet.subnetId],
});

Services go well past loadBalancer: true: multiple services can share one load balancer, and props cover custom domains, auto-scaling on CPU or request count, Fargate Spot capacity, EFS volumes, secrets, and health checks. Docs: ECS.

EKS: Deployments, Jobs, Manifests, and Helm

Section titled “EKS: Deployments, Jobs, Manifests, and Helm”

Kubernetes workloads live in the same TypeScript program as the cluster itself — no YAML, no kubectl. compute: "auto" stands up an EKS Auto Mode cluster where AWS manages the nodes:

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 },
});
const api = yield* AWS.EKS.Deployment(
"Api",
{ cluster, main: import.meta.url, port: 3000, replicas: 2, serviceType: "LoadBalancer" },
Effect.gen(function* () {
const putItem = yield* AWS.DynamoDB.PutItem(table); // → pod-identity IAM
return { fetch: Effect.gen(function* () { /* ... */ }) };
}).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
);

Deployment synthesizes a Kubernetes Deployment, Service, and ServiceAccount, with bindings landing as pod-identity IAM.

Job is the run-to-completion counterpart — same image sources, and the impl returns { run }:

const seed = yield* AWS.EKS.Job(
"SeedData",
{ cluster, main: import.meta.url },
Effect.gen(function* () {
const putItem = yield* AWS.DynamoDB.PutItem(table);
return {
run: Effect.gen(function* () {
// runs to completion, then the pod exits
}),
};
}).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
);

Add schedule and a Job becomes a Kubernetes CronJob — here in the external form, running a pre-built image:

const nightly = yield* AWS.EKS.Job("NightlyBackfill", {
cluster,
image: "ghcr.io/acme/backfill:v3",
schedule: "0 3 * * *",
});

Manifest applies any raw Kubernetes object via server-side apply, CRDs included — the manifest is a literal object, exactly what you’d write in YAML. No kubeconfig required:

const sts = yield* AWS.EKS.Manifest("Cache", {
cluster,
manifest: {
apiVersion: "apps/v1",
kind: "StatefulSet",
metadata: { name: "cache", namespace: "apps" },
spec: { replicas: 3 /* ... */ },
},
});

HelmChart installs Helm charts (#891). It renders the chart locally with helm template and applies the objects to the cluster, so Alchemy owns the lifecycle: drift is corrected on every deploy, dropped objects are pruned, and destroy deletes everything:

const ingress = yield* AWS.EKS.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 } },
});

Docs: EKS.

AWS.Lambda.DurableFunction writes long-running orchestrations as plain code, built on AWS Lambda’s durable execution model (#797). It is a Lambda Function, with the same props and bindings, but its body is checkpointed. Durable.step records each result so it isn’t re-run on replay. Durable.sleep suspends the execution entirely; Lambda re-invokes the function to resume it. And Durable.waitForCallback pauses on an external signal for up to a year:

export class OrderFlow extends AWS.Lambda.DurableFunction<OrderFlow>()(
"OrderFlow",
) {}
export default OrderFlow.make(
{ main: import.meta.url, executionTimeout: "1 hour" },
Effect.gen(function* () {
// init: resolve typed binding clients (IAM lands on this role)
const putItem = yield* AWS.DynamoDB.PutItem(table);
return Effect.fn(function* (input: { orderId: string }) {
const reserved = yield* AWS.Lambda.Durable.step(
"reserve",
putItem({ Item: { pk: { S: input.orderId } } }).pipe(Effect.orDie),
{ retry: { limit: 3, delay: "5 seconds" } },
);
yield* AWS.Lambda.Durable.sleep("cooldown", "10 minutes");
return { orderId: input.orderId, reserved };
});
}).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
);

Replay requires the body to be deterministic, and the type system enforces it: the run body has no access to cloud credentials, so the only way to do I/O is through a binding client resolved in init and called inside a Durable.step.

yield* OrderFlow returns a typed handle to start, inspect, and stop executions, and to complete callbacks from the outside:

const orders = yield* OrderFlow;
const ref = yield* orders.start({
name: "order-123", // idempotent start
params: { orderId: "123" },
});
const execution = yield* orders.get(ref.executionArn!);
// execution.Status: "RUNNING" | "SUCCEEDED" | "FAILED" | ...

The vocabulary mirrors Cloudflare Workflows, so orchestration code reads the same on both clouds. Reference: DurableFunction.

Drizzle now works end to end on Cloudflare D1 (#887), mirroring the Neon and PlanetScale integrations. At deploy time, Drizzle.Schema diffs your schema module and writes pending migration SQL, and passing schema.out as the database’s migrationsDir makes the database apply it — generate first, apply second, in one deploy:

src/db.ts
export const Database = Effect.gen(function* () {
const schema = yield* Drizzle.Schema("app-schema", {
schema: "./src/schema.ts",
out: "./migrations",
dialect: "sqlite",
});
return yield* Cloudflare.D1.Database("app-db", {
migrationsDir: schema.out,
});
});

At runtime, a Worker binds the database with Cloudflare.D1.QueryDatabase and hands the client to Drizzle.D1. Every query is an Effect with SqlError in the typed error channel:

src/api.ts
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const database = yield* Database;
const d1 = yield* Cloudflare.D1.QueryDatabase(database);
const db = yield* Drizzle.D1(d1, { relations });
return {
fetch: Effect.gen(function* () {
const users = yield* db.query.Users.findMany({
with: { posts: true },
});
return yield* HttpServerResponse.json({ users });
}),
};
}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding)),
) {}

The low-level effect-sql clients also get one home: the new alchemy/SQL namespace, with a raw tagged-template client for each Drizzle integration. It binds inside a Worker the same way:

import * as SQL from "alchemy/SQL";
Effect.gen(function* () {
const d1 = yield* Cloudflare.D1.QueryDatabase(database);
const sql = yield* SQL.D1(d1);
return {
fetch: Effect.gen(function* () {
const rows = yield* sql`SELECT id, name FROM users`;
return yield* HttpServerResponse.json({ rows });
}),
};
}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding));

All clients share the same lifecycle: nothing connects at plan or deploy time, the client is built lazily on the first query, and it’s torn down when the request settles. The website’s new SQL hub collects effect-sql, Drizzle, and migrations in one place (#903).

  • Python Workers (#861) — point main at a .py file and Cloudflare.Worker deploys a Python Worker, with dependencies vendored from pyproject.toml via uv. Docs: Python Workers.
  • Workers AI binding (#850) — Cloudflare.Workers.AI runs models on Cloudflare’s GPU fleet from a Worker.
  • Durable Objects pin to a region (#851) with locationHint, and DO classes transfer between workers declaratively (#803).
  • GitHub Enterprise + deployment environments (#845) — alchemy login accepts an enterprise host (GHES and data residency), and the new GitHub.Environment resource manages deployment environments.
  • Effect-native test runner (#848) — the whole alchemy test suite now runs in a single bun process on alchemy-test, with output designed for agents. Full story: A Test Runner Built for Agents. The live suite converged to green: 4,458 passed (#916).
  • RDS safety: MasterUserPassword is fingerprint-guarded so it’s no longer re-sent on every reconcile (#876), and skipFinalSnapshot: false takes a final snapshot on deliberate teardown (#877).
  • AWS_REGION overrides the SSO profile’s stored region (#895).
  • Engine reliability — destroy aggregates GC failures and refuses to silently orphan zombie rows (#899, #862), --force refreshes stale bound attributes (#900), binding diffs are stable across runs (#898), unresolved Outputs are never persisted into state (#835), and truncated physical names stay unique (#868).
  • Actions get local bindings and Output accessors (#807), and alchemy login works without an alchemy.run.ts (#842).
  • Migrating from v1 now adopts existing resources instead of destroying and recreating them (#819) — see Migrating from v1.
  • Cloudflare fixes: queue-consumer wiring surviving mid-start reconciles (#917), asset-upload retries (#914), wasm in alchemy dev (#881), and Effect-valued env bindings converging in plans (#890).
  • The website now lives at alchemy.run (#806), builds 8x faster on Astro 7 (#873), and the Layers, Bindings, and Functions & Servers concept pages are rewritten.