1---
2title: 2.0.0-beta.64 - Full AWS Coverage
3date: 2026-07-22T06:00:00Z
4category: release
5excerpt: v2.0.0-beta.64 is the AWS release — generated coverage of (nearly) every AWS service, unified container platforms on ECS and EKS, Durable Lambda Functions, and Helm charts on EKS. Plus Python Workers, Drizzle for D1, GitHub Enterprise, and a new Effect-native test runner.
6---
7
8beta.64 is the AWS release. Alchemy now covers **206 AWS
9services**, with every resource and binding live-tested against
10the real cloud. It also adds first-class **containers on ECS and
11EKS**, **Durable Lambda Functions**, and **Helm charts** on EKS.
12
13:::caution
14**Breaking changes**
15
16- **Container props are restructured**
17 ([#867](https://github.com/alchemy-run/alchemy/pull/867)):
18 `docker: { base }` → `image`, `AWS.EKS.ServerHost` →
19 `AWS.EKS.Deployment`, `alchemy/Kubernetes` is removed in favor
20 of `AWS.EKS.Manifest`, and one-shot ECS Task impls return
21 `{ run }` instead of `{ fetch }`.
22- **AWS prop data types are richer**
23 ([#797](https://github.com/alchemy-run/alchemy/pull/797)):
24 duration props take `Duration.Input` (`"1 hour"`,
25 `Duration.hours(1)`), and sensitive props (passwords, secrets,
26 keys) are `Redacted.Redacted<string>`.
27:::
28
29## "100%" AWS coverage
30
31One PR ([#797](https://github.com/alchemy-run/alchemy/pull/797))
32took the AWS provider from a handful of services to all of them,
33using the same [resource
34factory](/blog/2026-07-02-cloudflare-resource-factory) that built
35out the Cloudflare provider. Fleets of agents cataloged all 428
36generated SDK modules, then implemented and live-tested them in
37waves: serverless first, then ECS/Fargate, EKS, EC2, and the long
38tail. Everything left over is documented as out of scope
39(deprecated APIs, data-only endpoints, billing objects). The
40result:
41
42- **206 AWS services** with **683 resources** and **2,842
43 bindings** (plus event sources and sinks), from DynamoDB and S3
44 all the way out to MediaLive, IoT SiteWise, Bedrock, and
45 SageMaker.
46- **2,692 live tests green by default** against real AWS. 375
47 more are gated behind env vars because they are slow to
48 provision, expensive, or need special account entitlements.
49- **Zero-orphan guarantee**: each round wiped the test account
50 clean, ran the full suite, then audited the account for
51 leftover resources. The loop ran until two consecutive rounds
52 left nothing behind. A passing test that leaks a cloud resource
53 is a provider bug, and the loop caught dozens of them.
54
55Bindings are 1:1 with IAM actions. Each binding your code uses
56becomes a least-privilege policy statement on exactly the
57resources it touches, whether the code runs on Lambda, ECS, or
58EKS:
59
60```typescript
61const api = yield* AWS.ECS.Service(
62 "Api",
63 { cluster, main: import.meta.url, port: 3000, desiredCount: 2 },
64 Effect.gen(function* () {
65 // init: each binding grants one IAM action on one ARN
66 const getItem = yield* AWS.DynamoDB.GetItem(table);
67 const send = yield* AWS.SQS.SendMessage(queue);
68
69 return {
70 fetch: Effect.gen(function* () {
71 const item = yield* getItem({ Key: { pk: { S: "user#1" } } });
72 yield* send({ MessageBody: JSON.stringify(item.Item) });
73 return yield* HttpServerResponse.json({ ok: true });
74 }),
75 };
76 }).pipe(
77 Effect.provide([AWS.DynamoDB.GetItemHttp, AWS.SQS.SendMessageHttp]),
78 ),
79);
80```
81
82You never write an IAM policy, and every error is typed — the
83compiler tells you what can fail and you handle it like any other
84Effect error. See [Bindings](/infrastructure-as-effects/binding)
85for how it works.
86
87All of it is documented in the [API reference](/providers), with
88usage examples on every resource and binding.
89
90## ECS: Tasks and Services
91
92ECS gets the full platform treatment
93([#867](https://github.com/alchemy-run/alchemy/pull/867)):
94[`Task`](/providers/aws/ecs/task) for run-to-completion work and
95[`Service`](/providers/aws/ecs/service) for long-running servers.
96Both are authored the same three ways as Workers and Lambda: an
97external image, an inline effect, or a tagged class.
98
99Every container platform takes one image source, flat on props:
100
101```typescript
102{ main } // bundle this Effect program
103{ main, image } // bundle FROM your base image
104{ context, dockerfile? } // build your own Dockerfile
105{ dockerfile: Dockerfile.inline`...` } // inline Dockerfile content
106{ image } // registry ref, mirrored into ECR
107```
108
109The simplest `Service` runs a registry image behind a provisioned
110Application Load Balancer:
111
112```typescript
113const cluster = yield* AWS.ECS.Cluster("AppCluster", {});
114
115const nginx = yield* AWS.ECS.Service("Edge", {
116 cluster,
117 image: "public.ecr.aws/nginx/nginx:1.27",
118 port: 80,
119 desiredCount: 2,
120 loadBalancer: true,
121});
122
123return { url: nginx.url }; // http://<alb-dns-name>
124```
125
126A `Service` can also run an Effect program. The impl returns
127`{ fetch }`, and bindings grant IAM to the task role:
128
129```typescript
130const api = yield* AWS.ECS.Service(
131 "Api",
132 { cluster, main: import.meta.url, port: 3000, desiredCount: 2 },
133 Effect.gen(function* () {
134 const putItem = yield* AWS.DynamoDB.PutItem(table);
135 return {
136 fetch: Effect.gen(function* () {
137 const request = yield* HttpServerRequest;
138 const order = (yield* request.json) as { orderId: string };
139 yield* putItem({ Item: { pk: { S: order.orderId } } });
140 return yield* HttpServerResponse.json({ ok: true });
141 }),
142 };
143 }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
144);
145```
146
147A one-shot `Task` is the run-to-completion counterpart: the impl
148returns `{ run }`, and the container exits when the effect
149completes. Here it composes `main` with `Dockerfile.inline` — the
150inline content builds the environment, and the bundled program is
151layered on top:
152
153```typescript
154import { Dockerfile } from "alchemy/Docker";
155
156const thumbnailer = yield* AWS.ECS.Task(
157 "Thumbnailer",
158 {
159 main: import.meta.url,
160 dockerfile: Dockerfile.inline`
161 FROM oven/bun:1
162 RUN apt-get update && apt-get install -y ffmpeg
163 `,
164 cpu: 1024,
165 memory: 2048,
166 },
167 Effect.gen(function* () {
168 const getObject = yield* AWS.S3.GetObject(bucket);
169 return {
170 run: Effect.gen(function* () {
171 // pull the video from S3, shell out to ffmpeg, then exit
172 }),
173 };
174 }).pipe(Effect.provide(AWS.S3.GetObjectHttp)),
175);
176```
177
178Tasks are launched from other compute with the
179[`RunTask`](/providers/aws/ecs/runtask) binding, which grants
180`ecs:RunTask` and `iam:PassRole` automatically — here a Lambda
181function launches a task per request:
182
183```typescript
184const api = yield* AWS.Lambda.Function(
185 "Api",
186 { main: import.meta.url, url: true },
187 Effect.gen(function* () {
188 const runTask = yield* AWS.ECS.RunTask(cluster, thumbnailer);
189
190 return {
191 fetch: Effect.gen(function* () {
192 const response = yield* runTask({
193 launchType: "FARGATE",
194 networkConfiguration: {
195 awsvpcConfiguration: { subnets: [subnetId] },
196 },
197 });
198 return yield* HttpServerResponse.json({
199 taskArn: response.tasks?.[0]?.taskArn,
200 });
201 }),
202 };
203 }),
204);
205```
206
207Or run a task on a schedule with `AWS.ECS.every`, which
208provisions the EventBridge rule and invoke role:
209
210```typescript
211yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", {
212 cluster,
213 task: nightlyTask,
214 subnets: [privateSubnet.subnetId],
215});
216```
217
218Services go well past `loadBalancer: true`: multiple services can
219share one load balancer, and props cover custom domains,
220auto-scaling on CPU or request count, Fargate Spot capacity, EFS
221volumes, secrets, and health checks. Docs:
222[ECS](/aws/compute/ecs).
223
224## EKS: Deployments, Jobs, Manifests, and Helm
225
226Kubernetes workloads live in the same TypeScript program as the
227cluster itself — no YAML, no `kubectl`. `compute: "auto"` stands
228up an EKS Auto Mode cluster where AWS manages the nodes:
229
230```typescript
231const network = yield* AWS.EC2.Network("Network", {
232 cidrBlock: "10.42.0.0/16",
233 availabilityZones: 2,
234 nat: "single",
235});
236
237const cluster = yield* AWS.EKS.Cluster("Cluster", {
238 compute: "auto",
239 resourcesVpcConfig: { subnetIds: network.privateSubnetIds },
240});
241
242const api = yield* AWS.EKS.Deployment(
243 "Api",
244 { cluster, main: import.meta.url, port: 3000, replicas: 2, serviceType: "LoadBalancer" },
245 Effect.gen(function* () {
246 const putItem = yield* AWS.DynamoDB.PutItem(table); // → pod-identity IAM
247 return { fetch: Effect.gen(function* () { /* ... */ }) };
248 }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
249);
250```
251
252[`Deployment`](/providers/kubernetes/deployment) synthesizes a
253Kubernetes `Deployment`, `Service`, and `ServiceAccount`, with
254bindings landing as pod-identity IAM.
255
256[`Job`](/providers/kubernetes/job) is the run-to-completion
257counterpart — same image sources, and the impl returns `{ run }`:
258
259```typescript
260const seed = yield* AWS.EKS.Job(
261 "SeedData",
262 { cluster, main: import.meta.url },
263 Effect.gen(function* () {
264 const putItem = yield* AWS.DynamoDB.PutItem(table);
265 return {
266 run: Effect.gen(function* () {
267 // runs to completion, then the pod exits
268 }),
269 };
270 }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
271);
272```
273
274Add `schedule` and a Job becomes a Kubernetes CronJob — here in
275the external form, running a pre-built image:
276
277```typescript
278const nightly = yield* AWS.EKS.Job("NightlyBackfill", {
279 cluster,
280 image: "ghcr.io/acme/backfill:v3",
281 schedule: "0 3 * * *",
282});
283```
284
285[`Manifest`](/providers/kubernetes/manifest) applies any raw
286Kubernetes object via server-side apply, CRDs included — the
287manifest is a literal object, exactly what you'd write in YAML.
288No kubeconfig required:
289
290```typescript
291const sts = yield* AWS.EKS.Manifest("Cache", {
292 cluster,
293 manifest: {
294 apiVersion: "apps/v1",
295 kind: "StatefulSet",
296 metadata: { name: "cache", namespace: "apps" },
297 spec: { replicas: 3 /* ... */ },
298 },
299});
300```
301
302[`HelmChart`](/providers/kubernetes/helmchart) installs Helm charts
303([#891](https://github.com/alchemy-run/alchemy/pull/891)). It
304renders the chart locally with `helm template` and applies the
305objects to the cluster, so Alchemy owns the lifecycle: drift is
306corrected on every deploy, dropped objects are pruned, and
307destroy deletes everything:
308
309```typescript
310const ingress = yield* AWS.EKS.HelmChart("IngressNginx", {
311 cluster,
312 chart: "ingress-nginx",
313 repo: "https://kubernetes.github.io/ingress-nginx",
314 version: "4.11.2",
315 namespace: "ingress-nginx",
316 createNamespace: true,
317 values: { controller: { replicaCount: 2 } },
318});
319```
320
321Docs: [EKS](/aws/compute/eks).
322
323## Durable Lambda Functions
324
325`AWS.Lambda.DurableFunction` writes long-running orchestrations
326as plain code, built on AWS Lambda's durable execution model
327([#797](https://github.com/alchemy-run/alchemy/pull/797)). It
328**is** a Lambda Function, with the same props and bindings, but
329its body is checkpointed. `Durable.step` records each result so
330it isn't re-run on replay. `Durable.sleep` suspends the execution
331entirely; Lambda re-invokes the function to resume it. And
332`Durable.waitForCallback` pauses on an external signal for up to
333a year:
334
335```typescript
336export class OrderFlow extends AWS.Lambda.DurableFunction<OrderFlow>()(
337 "OrderFlow",
338) {}
339
340export default OrderFlow.make(
341 { main: import.meta.url, executionTimeout: "1 hour" },
342 Effect.gen(function* () {
343 // init: resolve typed binding clients (IAM lands on this role)
344 const putItem = yield* AWS.DynamoDB.PutItem(table);
345
346 return Effect.fn(function* (input: { orderId: string }) {
347 const reserved = yield* AWS.Lambda.Durable.step(
348 "reserve",
349 putItem({ Item: { pk: { S: input.orderId } } }).pipe(Effect.orDie),
350 { retry: { limit: 3, delay: "5 seconds" } },
351 );
352 yield* AWS.Lambda.Durable.sleep("cooldown", "10 minutes");
353 return { orderId: input.orderId, reserved };
354 });
355 }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),
356);
357```
358
359Replay requires the body to be deterministic, and the type system
360enforces it: the run body has no access to cloud credentials, so
361the only way to do I/O is through a binding client resolved in
362init and called inside a `Durable.step`.
363
364`yield* OrderFlow` returns a typed handle to start, inspect, and
365stop executions, and to complete callbacks from the outside:
366
367```typescript
368const orders = yield* OrderFlow;
369const ref = yield* orders.start({
370 name: "order-123", // idempotent start
371 params: { orderId: "123" },
372});
373const execution = yield* orders.get(ref.executionArn!);
374// execution.Status: "RUNNING" | "SUCCEEDED" | "FAILED" | ...
375```
376
377The vocabulary mirrors Cloudflare
378[Workflows](/cloudflare/compute/workflows), so orchestration code
379reads the same on both clouds. Reference:
380[DurableFunction](/providers/aws/lambda/durablefunction).
381
382## Effect SQL and Drizzle for D1
383
384Drizzle now works end to end on Cloudflare D1
385([#887](https://github.com/alchemy-run/alchemy/pull/887)),
386mirroring the Neon and PlanetScale integrations. At deploy time,
387`Drizzle.Schema` diffs your schema module and writes pending
388migration SQL, and passing `schema.out` as the database's
389`migrationsDir` makes the database apply it — generate first,
390apply second, in one deploy:
391
392```typescript
393// src/db.ts
394export const Database = Effect.gen(function* () {
395 const schema = yield* Drizzle.Schema("app-schema", {
396 schema: "./src/schema.ts",
397 out: "./migrations",
398 dialect: "sqlite",
399 });
400
401 return yield* Cloudflare.D1.Database("app-db", {
402 migrationsDir: schema.out,
403 });
404});
405```
406
407At runtime, a Worker binds the database with
408`Cloudflare.D1.QueryDatabase` and hands the client to
409`Drizzle.D1`. Every query is an Effect with `SqlError` in the
410typed error channel:
411
412```typescript
413// src/api.ts
414export default class Api extends Cloudflare.Worker<Api>()(
415 "Api",
416 { main: import.meta.url },
417 Effect.gen(function* () {
418 const database = yield* Database;
419 const d1 = yield* Cloudflare.D1.QueryDatabase(database);
420 const db = yield* Drizzle.D1(d1, { relations });
421
422 return {
423 fetch: Effect.gen(function* () {
424 const users = yield* db.query.Users.findMany({
425 with: { posts: true },
426 });
427 return yield* HttpServerResponse.json({ users });
428 }),
429 };
430 }).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding)),
431) {}
432```
433
434The low-level effect-sql clients also get one home: the new
435`alchemy/SQL/*` subpaths, with a raw tagged-template client for
436each Drizzle integration. It binds inside a Worker the same way:
437
438```typescript
439import * as SQL from "alchemy/SQL/D1";
440
441Effect.gen(function* () {
442 const d1 = yield* Cloudflare.D1.QueryDatabase(database);
443 const sql = yield* SQL.D1(d1);
444
445 return {
446 fetch: Effect.gen(function* () {
447 const rows = yield* sql`SELECT id, name FROM users`;
448 return yield* HttpServerResponse.json({ rows });
449 }),
450 };
451}).pipe(Effect.provide(Cloudflare.D1.QueryDatabaseBinding));
452```
453
454All clients share the same lifecycle: nothing connects at plan or
455deploy time, the client is built lazily on the first query, and
456it's torn down when the request settles. The website's new
457[SQL hub](/sql) collects effect-sql, Drizzle, and migrations in
458one place
459([#903](https://github.com/alchemy-run/alchemy/pull/903)).
460
461## Also in this release
462
463- **Python Workers**
464 ([#861](https://github.com/alchemy-run/alchemy/pull/861)) —
465 point `main` at a `.py` file and `Cloudflare.Worker` deploys a
466 Python Worker, with dependencies vendored from `pyproject.toml`
467 via uv. Docs:
468 [Python Workers](/cloudflare/compute/python-workers).
469- **Workers AI binding**
470 ([#850](https://github.com/alchemy-run/alchemy/pull/850)) —
471 `Cloudflare.Workers.AI` runs models on Cloudflare's GPU fleet
472 from a Worker.
473- **Durable Objects pin to a region**
474 ([#851](https://github.com/alchemy-run/alchemy/pull/851)) with
475 `locationHint`, and DO classes **transfer between workers**
476 declaratively
477 ([#803](https://github.com/alchemy-run/alchemy/pull/803)).
478- **GitHub Enterprise + deployment environments**
479 ([#845](https://github.com/alchemy-run/alchemy/pull/845)) —
480 `alchemy login` accepts an enterprise host (GHES and data
481 residency), and the new `GitHub.Environment` resource manages
482 deployment environments.
483- **Effect-native test runner**
484 ([#848](https://github.com/alchemy-run/alchemy/pull/848)) —
485 the whole alchemy test suite now runs in a single bun process
486 on `alchemy-test`, with output designed for agents. Full story:
487 [A Test Runner Built for
488 Agents](/blog/2026-07-22-a-test-runner-for-agents). The live
489 suite converged to green: 4,458 passed
490 ([#916](https://github.com/alchemy-run/alchemy/pull/916)).
491- **RDS safety**: `MasterUserPassword` is fingerprint-guarded so
492 it's no longer re-sent on every reconcile
493 ([#876](https://github.com/alchemy-run/alchemy/pull/876)), and
494 `skipFinalSnapshot: false` takes a final snapshot on deliberate
495 teardown
496 ([#877](https://github.com/alchemy-run/alchemy/pull/877)).
497- **`AWS_REGION` overrides the SSO profile's stored region**
498 ([#895](https://github.com/alchemy-run/alchemy/pull/895)).
499- **Engine reliability** — destroy aggregates GC failures and
500 refuses to silently orphan zombie rows
501 ([#899](https://github.com/alchemy-run/alchemy/pull/899),
502 [#862](https://github.com/alchemy-run/alchemy/pull/862)),
503 `--force` refreshes stale bound attributes
504 ([#900](https://github.com/alchemy-run/alchemy/pull/900)),
505 binding diffs are stable across runs
506 ([#898](https://github.com/alchemy-run/alchemy/pull/898)),
507 unresolved Outputs are never persisted into state
508 ([#835](https://github.com/alchemy-run/alchemy/pull/835)), and
509 truncated physical names stay unique
510 ([#868](https://github.com/alchemy-run/alchemy/pull/868)).
511- **Actions get local bindings and Output accessors**
512 ([#807](https://github.com/alchemy-run/alchemy/pull/807)), and
513 `alchemy login` works without an `alchemy.run.ts`
514 ([#842](https://github.com/alchemy-run/alchemy/pull/842)).
515- **Migrating from v1** now adopts existing resources instead of
516 destroying and recreating them
517 ([#819](https://github.com/alchemy-run/alchemy/pull/819)) —
518 see [Migrating from v1](/migrating-from-v1).
519- **Cloudflare fixes**: queue-consumer wiring surviving mid-start
520 reconciles
521 ([#917](https://github.com/alchemy-run/alchemy/pull/917)),
522 asset-upload retries
523 ([#914](https://github.com/alchemy-run/alchemy/pull/914)),
524 wasm in `alchemy dev`
525 ([#881](https://github.com/alchemy-run/alchemy/pull/881)), and
526 Effect-valued env bindings converging in plans
527 ([#890](https://github.com/alchemy-run/alchemy/pull/890)).
528- **The website now lives at
529 [alchemy.run](https://alchemy.run)**
530 ([#806](https://github.com/alchemy-run/alchemy/pull/806)),
531 builds 8x faster on Astro 7
532 ([#873](https://github.com/alchemy-run/alchemy/pull/873)), and
533 the [Layers](/infrastructure-as-effects/layers),
534 [Bindings](/infrastructure-as-effects/binding), and
535 [Runtime](/infrastructure-as-effects/runtime)
536 concept pages are rewritten.
537
538## Where to go next
539
540- [AWS](/aws) — the provider hub: setup, tutorial, and guides
541- [ECS](/aws/compute/ecs) and [EKS](/aws/compute/eks)
542- [Choosing a runtime](/aws/compute/choosing-a-runtime)
543- [SQL](/sql)
544- [Bindings](/infrastructure-as-effects/binding)
545- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta64)
546- [Compare v2.0.0-beta.63 → v2.0.0-beta.64](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.63...v2.0.0-beta.64)