feat(aws)!: unified container platforms — ECS Task/Service, EKS Deployment/Job/Manifest, Dockerfile.inline image composition - #867
Conversation
…e platform
- New internal ECR/ImageSource: presence-discriminated { main, baseImage? } |
{ context, dockerfile-as-path } | { image } (remote refs mirrored into ECR,
content-addressed tags, crash-safe resolve, bounded pulls, progress notes)
- ECS.Task: docker:{base,dockerfile} removed; main optional; external forms
take no impl (busybox fixtures are now one-line externals); one-shot impls
return { run } and exit on completion
- ECS.Service becomes a platform: image-owning or task:-referencing;
loadBalancer:true wires ALB/TG/listener with owned ingress SG and
default-VPC fallback; isBindingHost includes AWS.ECS.Service
Live: test/AWS/ECS 22 passed / 0 failed, zero-orphan census clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… types+builders
- EKS.Deployment replaces ServerHost (no alias): image triad via shared
ImageSource, podTemplate DeepPartial merge, content-drift diffing; same
Deployment+Service+SA+pod-identity semantics
- New EKS.Job: { run } impls, full triad, backoffLimit, content-addressed
one-shot Jobs (immutable-template updates apply new + reap old);
schedule: synthesizes a CronJob
- New EKS.Manifest: server-side apply of any manifest incl. CRDs (API
discovery resolves arbitrary kinds); absorbs the former src/Kubernetes
apply machinery as un-exported EKS internals
- src/Kubernetes is now 1:1 K8s types + zero-runtime builders only
- Deleted dead surface: Workload/LoadBalancedWorkload/PodIdentityWorkload;
PodIdentityServiceAccount internal; AutoCluster folds into
Cluster({ compute: 'auto' }) with optional self-managed IAM roles
Live: test/AWS/EKS 18 passed / 0 failed / skip-clean gated E2E.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The effectful (main) variant's dockerfile prop was inline Dockerfile content used as the base image; it is now baseImage: a plain registry ref (the provider synthesizes FROM). dockerfile remains only on the external variant as a path. The props union is now strictly typed per variant (the old misuse no longer typechecks); a runtime guard turns accidental Dockerfile content in baseImage into a clear error. Migration is hash-stable: FROM x -> baseImage x produces an identical generated Dockerfile, so no rebuilds. Live: test/Cloudflare/Container 12/12 standalone green (one pre-existing cross-file concurrency flake with LocalContainer noted separately). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four hand-written guide pages documented the deleted surface (ServerHost, Workload family, AutoCluster, Kubernetes resource providers). Rewritten against the shipped code: Cluster compute:'auto', Deployment/Job/Manifest platforms, types+builders Kubernetes package, server-side-apply mechanics. docs:check clean (4097 pages). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Install the packages built from this commit: alchemy bun add alchemy@https://pkg.ing/alchemy/a5a5336@alchemy.run/better-auth bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/a5a5336@alchemy.run/pr-package bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/a5a5336 |
…tes sidebar group
ecs.mdx now documents the Task one-shot triad (image/context/main,
{ run } impls, RunTask + Schedule) and the Service platform
(image-owning + task:-referencing, loadBalancer → ALB + url) instead of
docker.base/dockerfile-as-content and public:true. The
providerResourcesEntry("Kubernetes") sidebar entry pointed at a
now-deleted generated dir. docs:check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Services can now attach routing rules to a shared listener instead of
owning an ALB:
loadBalancer: true | ELBv2.Listener | { listener?, rules?, public? }
Rules are flat ({ listen, forward, redirect, container, path, host,
header, query, priority }); string listen = owned (service creates the
listener), Listener ref = shared (service owns only its TargetGroups +
ListenerRules); mixing is a typed error. Priorities auto-derive via a
pinned FNV-1a hash of the rule's logical id (1..50000); a live
collision is a typed ListenerRulePriorityInUse suggesting an explicit
override.
LB wiring is now COMPOSED resources (SecurityGroup/LoadBalancer/
Listener/TargetGroup/ListenerRule under Namespace.push) instead of
inline elbv2 calls — enabled by an additive transformProps hook on the
Platform factory that preserves all three authoring forms. Destroying
a service removes exactly its rules/TGs; shared infra is never touched.
Legacy inline-created ingress is reaped on first reconcile under the
new shape (proven by a live migration test).
Live: shared-ALB e2e (2 services, path routing, partial destroy leaves
sibling + ALB intact) + migration + priority determinism green; full
ECS suite green; zero-orphan account scan clean.
Note: the managed SG now admits the container port from 0.0.0.0/0
(tasks sit in public subnets; self-referencing rules can't precede
group creation); composed TGs default to fast health checks (10s/2)
and 30s deregistration delay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…example
Found by deploying examples/aws-ecs end-to-end; each fix verified live
(full deploy -> curl -> seed -> destroy -> zero-orphan) from a worktree.
- ECS createContainerRuntimeContext: capture base.serve before
Object.assign overwrites it — the wrapper called itself at call time,
recursing unboundedly (JSC MemoryExhaustion) the moment any Task/
Service impl declared { fetch }
- Http.serve: park behind Effect.never after registering the server —
HttpServer.serve registers on the ambient Scope and returns, so a
pure { fetch } host program completed, closed its scope, tore down
the server, and the container exited 0 in a crash-loop
- ECS bootstrap: Credentials.fromChain() instead of fromEnv() — Fargate
task-role creds come from the container-credentials endpoint, not env
vars; every Http binding inside a container failed
- Scheduler builders: remove the prohibited Resource field from the
scheduler trust policy (MalformedPolicyDocumentException on every
AWS.ECS.every / Scheduler.every target)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared stack-level ALB + listener; Api (effectful Service, main:
bundled, DynamoDB Scan/GetItem + RunTask bindings, /api/* rule) and Web
(external image:, catch-all rule) sharing it; SeedTask (inline { run }
one-shot, PutItem); ReportTask (context: Dockerfile); HeartbeatTask
(image: busybox) on an AWS.ECS.every cron. All three image sources and
authoring forms appear.
Verified live: deploy, GET / and /api/orders (0 -> 3 after RunTask-
launched seed), GetItem + 404 paths, schedule ENABLED; destroy +
zero-orphan census clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ets, logging, serviceRegistry
SST-parity features, all additive props with live fixture tests:
- domain (owned LB): composes ACM Certificate (DNS-validated via zone
lookup; explicit cert: skips) + Route53 alias records; url prefers
the domain. ACM gains a region prop (in-region ALB certs); reads/
deletes derive region from the ARN.
- NLB: tcp/udp/tls/tcp_udp rule protocols derive a network LB; typed
errors for mixed layers/conditions/redirects on network rules.
- health: per-'{port}/{proto}' target-group overrides (Duration.Input).
- scaling: composes ScalableTarget + per-metric ScalingPolicies via a
new onCreate hook (resourceId/ResourceLabel derive from the service's
own outputs); reconcile stops pinning desiredCount while set.
- capacity: 'spot' | fargate/spot base+weight -> capacityProviderStrategy.
- secrets: SSM/SecretsManager valueFrom + exact-ARN execution-role
policy. logging.retention on the managed log group. serviceRegistry
composes CloudMap.Service into serviceRegistries. healthCheck +
volumes ({ efs, path }) sugar.
- Service diff adopts imageSource.hash so main: source drift updates
(same fix as Task).
Also: Docker push pins --platform (containerd store shipped whatever
arch was cached — arm64 busybox crashed x86 Fargate); HostedZone delete
retries HostedZoneNotEmpty (name-looked-up records race the zone).
Live: full ECS suite green; zero-orphan sweep clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edule Input types
- ECS bun bootstrap now folds the entrypoint via makeEntrypointLayer
(the Lambda/Container bridge pattern): a tagged X.make Layer default
export boots identically to an inline Effect export. Proven live by
a tagged { run } one-shot (RunTask -> exit 0) and by converting
examples/aws-ecs Api to the tagged form and redeploying end-to-end.
- Task diff hashes main: sources at plan time via the new
imageSource.hash (bundle covers the bootstrap entry) — bootstrap or
code-only changes no longer silently no-op until --force.
- Scheduler builder args take Input<> (they are function args, not
resource Props): a yielded Task and Output-valued subnets now
typecheck; example casts removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The inline-class-with-Effect-props OOM report was the (already fixed) createContainerRuntimeContext serve self-recursion — props were a red herring; pre-fix, plain-object props OOM'd identically. Proven by cherry-picking the serveBase capture into a pre-fix worktree and watching the hermetic repro flip. New offline tests fail fast (2s timeout guard, no cloud, bounded memory) instead of an 8GB MemoryExhaustion: unit serve guard, plan/apply of the inline class form with Effect-valued props, and repeated-yield plan stability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two compounding bugs made EKS cluster creation look like a harness
deadlock:
- Cluster.ts/Addon.ts waited on an UNCAPPED exponential
(Schedule.exponential('1 second') x recurs(120)) — late attempts
sleep 4/8/17/34 minutes, parking the process at 0% CPU mid-create
and overshooting hook budgets. Now flat bounded polls
(spaced 10s x 180 / 5s x 120), matching Nodegroup/FargateProfile.
- alchemy-test buffered ALL file-hook output until FileEnd and the 10s
stall report ignored hooks, so a beforeAll deploy was invisible by
design. Hook log lines now stream to the run log as they happen and
hooks appear in the stall report; hermetic regression test asserts
mid-hook visibility.
Fallout of the illusion: timeout-killed runs had silently issued
createCluster during the invisible sleeps, orphaning 4 ACTIVE clusters
+ 4 VPCs (account hit the 5-VPC quota).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tags The EKS Deployment E2E's readiness poll failed with transport errors for five consecutive runs because the provider returned http://<nlb-hostname> while the NLB listener is the K8s Service port (3000) — every GET hit :80 where nothing ever listened. The pods were healthy the whole time. url is now http://<hostname>[:port] (port elided when 80) and the fixture uses it verbatim. Supporting fixes proven in the same green runs: - EC2.Network tags public/private subnets with kubernetes.io/role/elb and /internal-elb so EKS Auto Mode's LB controller can discover them - Deployment detects Auto Mode LB capability (loadBalancerClass eks.amazonaws.com/nlb) and defaults the NLB to internet-facing; user serviceAnnotations win; delete path tolerates a gone/DELETING cluster - ImageSource default baseImage oven/bun:1 (docker/library/bun does not exist); Job.ts aligned with the shared machinery - E2E budget 45 min with the measured timeline documented; NO_DESTROY gate on afterAll for live debugging Full EKS suite with AWS_TEST_SLOW=1: 0 failed | 19 passed | 7 todo (19 min) — including the complete cluster-create -> serialized-config -> image -> Deployment -> NLB -> in-pod DynamoDB write -> destroy cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const appendHookLine: FileLog["appendHookLine"] = (file, entry) => { | ||
| try { | ||
| const prefixed = entry.message | ||
| .split("\n") | ||
| .map((line) => `[hook ${file}] ${line}`) | ||
| .join("\n"); | ||
| NodeFs.appendFileSync(logFile, `${prefixed}\n`); | ||
| } catch { | ||
| // Never let log streaming break a hook. | ||
| } | ||
| }; | ||
| return { append, appendHookLine } satisfies FileLog; |
There was a problem hiding this comment.
Never do synchronous I/O. appendHookLike should probably return an Effect
There was a problem hiding this comment.
Fixed in a180ede: the capture site now only does Queue.offerUnsafe (in-memory enqueue, no I/O — it's called from the synchronous array-push interception so it can't yield an Effect itself); a forked writer fiber drains the queue with Effect FileSystem appends, and fileLog.close ends + awaits it at RunEnd so tail lines flush. The mid-hook streaming regression test still passes.
|
|
||
| const scan = yield* AWS.DynamoDB.Scan(table); | ||
| const getItem = yield* AWS.DynamoDB.GetItem(table); | ||
| const runSeedTask = yield* AWS.ECS.RunTask(cluster, seedTask); |
There was a problem hiding this comment.
Why do we need to pass cluster here? Don't we know the cluster from the task? Or is it because we need the LogicalId of the cluster?
There was a problem hiding this comment.
Great catch — two answers: (1) ECS task definitions are cluster-independent (any taskdef can run on any cluster), which is why the AWS API — and our binding — takes a cluster. (2) But since this Task declares its cluster, making callers repeat it was redundant. Fixed in 79a7b1b: Task attributes now record clusterArn when declared, and RunTask(task) / StartTask(task) infer it (explicit RunTask(cluster, task) remains as override; a cluster-less Task fails the single-arg form at bind time with typed RunTaskRequiresCluster). The example now reads RunTask(seedTask). One DX note: the task-only form carries the typed error in its channel, so inside a Platform init (which constrains errors to ConfigError) it needs .pipe(Effect.orDie) — happy to switch to a defect-carrying clean channel if you prefer bare yield* RunTask(task).
| * entry as it is captured keeps the log tail-able mid-hook. Sync (node:fs | ||
| * append) because it is called from a plain array-push interception; | ||
| * append-mode writes interleave safely with the Effect-based appends. | ||
| */ | ||
| readonly appendHookLine: (file: string, entry: LogEntry) => void; |
There was a problem hiding this comment.
Can we take another look at this and see if we can solve the problem without a synchronous call?
There was a problem hiding this comment.
Same fix — no synchronous calls remain in FileLog (NodeFs import is gone entirely); all disk I/O moved to the async writer fiber.
Review feedback on #867: appendHookLine used NodeFs.appendFileSync because it is called from the synchronous array-push interception that captures hook output. It now performs NO I/O at the capture site — Queue.offerUnsafe onto an unbounded queue (plain in-memory enqueue) — and a forked writer fiber drains the queue with Effect FileSystem appends. fileLog.close ends the queue and awaits the writer at RunEnd so tail lines are flushed. Hermetic mid-hook streaming test still green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on #867: RunTask(cluster, task) forced callers to repeat a cluster the Task already declared. Task attributes now record clusterArn when props.cluster is set, and RunTask/StartTask accept the task-only form — RunTask(task) resolves the declared cluster (works for Task.ref too); RunTask(cluster, task) stays as the explicit override with an unchanged error channel. A task with no declared cluster fails the single-arg form at BIND time with typed RunTaskRequiresCluster/StartTaskRequiresCluster. ECS task definitions remain cluster-independent — this is inference from the declaration, not a change to the ECS model. examples/aws-ecs: SeedTask declares its cluster; Api uses RunTask(seedTask). Live: ECS suite 33/0; example deployed, task-only seed ran on Fargate, destroyed, zero-orphan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/infra.ts (Network, Cluster compute:'auto', table, namespace via
EKS.Manifest + Kubernetes.namespace builder), src/Api.ts (tagged
effectful EKS.Deployment with DynamoDB Http bindings, LoadBalancer,
podTemplate escape hatch), src/SeedJob.ts (inline-effect EKS.Job
{ run } seeding via PutItem), thin alchemy.run.ts adding an external
nginx Deployment. Verified live: 38 resources, namespace applied,
seed Job pod ran to completion with pod-identity credentials (3 items
scanned), external nginx NLB served 200, destroy + zero-orphan sweep
clean. The tagged Api pods crash-loop on a platform bug fixed
separately (EKS bootstraps missing Layer-entrypoint normalization).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The EKS Deployment/Job bootstraps assumed the module's default export is an Effect (inline-class form); the tagged form (X.make(props, impl)) exports a Layer, so tagged EKS workloads crash-looped with 'Not a valid effect'. Both bootstraps now fold the entrypoint through makeEntrypointLayer(tag, entrypoint) — the same normalization the ECS/Lambda/Container bridges use. Proven live via examples/aws-eks: tagged Api Deployment served the guestbook over its NLB (GET /entries returned the seed Job's items through the Scan binding; POST wrote), external nginx 200, full destroy after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, Dockerfile.inline
- Remove `baseImage` everywhere: `{ main, image }` now means "bundle the
Effect program FROM <image>"; `{ image }` alone stays the verbatim
pre-built source
- `dockerfile?: string | Dockerfile.inline` — a string is always a PATH;
`Dockerfile.inline` is inline content ({ content: Input<string> },
structural discrimination, state-serializable, Output interpolation =
dependency edge, unresolved content defers plan-time hashing)
- With `main`: inline content replaces the generated FROM preamble; on AWS a
path dockerfile builds a local env stage the bundle layers on top of
- Without `main`: inline content is the whole Dockerfile, built in a stable
generated context (live + local Cloudflare providers share it)
- Exclusivity defects: image+dockerfile, image+context, inline+context
- Fix: `{ main, dockerfile: path }` without `context` crashed on
path.resolve(undefined); context now defaults to "."
- Live-verified: Fargate one-shot with inline-env RUN artifact exits 0;
ECS suite 33/0; CF Container suite green modulo the known pre-existing
RemoteContainer cross-file race; 12 new unit tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itly A Task maps to AWS::ECS::TaskDefinition, which is cluster-independent — the cluster belongs to the launch, not the definition. The task-only RunTask(task) inference forced a RunTaskRequiresCluster error channel (and .pipe(Effect.orDie) at every bind site) to cover tasks with no declared cluster. - Task props lose `cluster`; attributes lose `clusterArn` - RunTask/StartTask restored to the single two-arg form RunTask(cluster, task) with a clean error channel - examples/aws-ecs: SeedTask props are a plain object again; Api binds RunTask(cluster, seedTask) - ecs.mdx: stale baseImage copy updated to image/Dockerfile.inline Live: ECS suite 33/0 post-revert (Bindings suite exercises the two-arg form through a deployed Lambda). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ECS intro now says what ECS is (managed container orchestrator, Fargate = no servers to manage) and introduces the four primitives (cluster, task definition, task, service) in AWS's own terminology before mapping them to Alchemy resources — no more defining ECS relative to Lambda - "one-shot" replaced with plain "runs to completion" phrasing across ecs.mdx, eks.mdx, and the user-facing Task/TaskDefinition/RunTask/ EKS.Job JSDoc (matching Kubernetes' own run-to-completion language for Jobs) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…; test(ecs): cover every()
- The Apple Silicon tip wrongly implied the host machine dictates the
image architecture. Corrected: runtimePlatform.cpuArchitecture picks
the Fargate architecture and Alchemy builds/mirrors for whatever the
task definition declares; ARM64 = Graviton (cheaper), which happens
to build natively on Apple Silicon
- RunTask/StartTask examples (ecs.mdx + JSDoc) now show the binding
yielded in the init phase and called from a { fetch } handler at
runtime — yielding and calling a binding in the same scope is always
wrong and the old snippets modeled exactly that
- EKS intro rewritten like the ECS one: what EKS is, then the
Kubernetes primitives (cluster, pod, Deployment, Service, Job,
manifest) in K8s terminology before the Alchemy mapping
- New live test for AWS.ECS.every: deploys cluster + busybox task +
cron schedule, verifies expression/ECS target/network/role via the
Scheduler API out-of-band, destroys, verifies the schedule is gone
(previously only exercised by the examples/aws-ecs deploy)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pod, Deployment, Service, Job, CronJob are always capitalized (matching Kubernetes' own convention for API kinds); cluster, node, and manifest stay lowercase (they're not kinds). "Pod Identity" capitalized as the EKS feature name. Shell commands and builder function names keep their literal code casing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cluster, Task Definition, Task, Service (ECS) and Cluster, Node, Pod, Deployment, Service, Job, CronJob (EKS) — every named primitive gets a capital first letter; only descriptive words (manifest, load balancer) stay lowercase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the variant-union pattern ContainerApplicationProps was a flat everything-bag that Container.ts carved strict views out of with Omit-and-redeclare. Restructured to match the AWS side (TaskPropsBase + variant interfaces + union): - ContainerApplicationPropsBase — the shared naming/scaling/runtime config - EffectfulContainerProps (main + image/inline-dockerfile environment + bundling options), ExternalContainerProps (context + dockerfile), RemoteContainerProps (image, required) — defined once next to the resource; Container.ts just re-exports them - ContainerApplicationProps = the union of the three - AnyContainerApplicationProps — INTERNAL loose view (every variant field optional at its widest type) that provider/bundle helpers annotate their params with, since each union member is assignable to it Bundling options (handler/runtime/external/autoInstallExternals) now live only on the effectful variant where they have meaning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lare containers
- validateContainerImageProps / containerEnvPreamble return Effects and
surface invalid props via Effect.die — no raw throws (call sites
yield* them; unit tests use an Effect-based dies() helper)
- delete src/Kubernetes (types + builders) and the alchemy/Kubernetes
package export: EKS is the only Kubernetes surface. AWS.EKS.Manifest
takes a literal KubernetesManifest object ({ apiVersion, kind,
metadata, ...rest }; CRDs via API discovery); Deployment/Job
podTemplate is a literal deep-partial object. Apply machinery stays
internal to AWS.EKS
- delete website /kubernetes section (+ sidebar entries); eks.mdx
Manifest examples are literal objects
- examples/aws-eks writes literal manifests / pod templates
Suites: Cloudflare Workers 177/0, Cloudflare Container 16/0
(sequential), EKS fast 17/0, unit 12/12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One consistent container interface across ECS, EKS, and Cloudflare, modeled on the Worker authoring triad (external / inline effect / tagged effect). Design doc:
processes/AWS/design/container-platform-redesign.md(local).Every platform takes one image source, flat on props —
imagecomposes withmain(there is nobaseImage):Servers return
{ fetch, ...rpc }; one-shots return{ run, ...rpc }. Tagged forms are alwaysP<Self, Shape>().AWS.ECS.Task— one-shotAWS.ECS.Service— long-running server (new platform)AWS.EKS.Deployment— replicated K8s server (wasServerHost)AWS.EKS.Job— run-to-completion (new)AWS.EKS.Manifest— raw manifests (new)The manifest is a literal object, exactly as you would write it in YAML (CRDs resolve via API discovery):
Cloudflare.ContainerShared load balancers (
ECS.Service)Rules attach to listeners; ALBs own listeners. String
listen⇒ owned;Listenerref ⇒ shared (the service owns only its TargetGroups + ListenerRules); mixing is a typed error.ListenerRulePriorityInUsesuggesting an explicitpriority.SecurityGroup/LoadBalancer/Listener/TargetGroup/ListenerRuleunder the service's namespace) instead of inline API calls; destroying a service removes exactly its rules/TGs and never touches shared infra. Legacy inline-created ingress is reaped on first reconcile (live migration test).ECS.ServicePhase 2 (SST parity) + live-example hardeningNLB via
tcp/udp/tlsrule protocols; ACM gainsregion; scaling composes ApplicationAutoScaling resources via a newonCreatehook. All features have live fixture tests (DNS-validateddomainpath env-gated).examples/aws-ecsis rewritten as an orders app exercising every authoring form and image source (shared ALB, tagged-form Api + external Web with path rules,{ run }seed Task launched via theRunTaskbinding, cronSchedule,context:build) — deployed/curled/destroyed with a zero-orphan sweep. Deploying it live surfaced and fixed six real bugs the suites missed: serve self-recursion OOM on{ fetch }impls,{ fetch }containers exiting into a crash-loop, Fargate credential-chain resolution, an invalid Scheduler trust policy, multi-arch image mirroring (docker push --platform), and tagged-form (X.makeLayer) boot on ECS +main:-source drift never invalidating images.Breaking changes & how to migrate
Scan your code for these; each has a detailed diff below.
docker: { base: "..." }on ECS/EKS propsimage: "..."next tomaindocker: { dockerfile: "<content>" }(ECS/EKS)dockerfile: Dockerfile.inlinecontent — or the external{ image, command }form when the bundled program was a placeholderdockerfile: "FROM ..."(content string)image: "..."for a bare base, ordockerfile: Dockerfile.inlinefor content{ fetch }{ run }AWS.EKS.ServerHostAWS.EKS.Deployment(tagged form takes two type args)AWS.EKS.Workload/LoadBalancedWorkload/PodIdentityWorkloadAWS.EKS.Deployment(orAWS.EKS.Manifestfor raw specs)AWS.EKS.AutoClusterAWS.EKS.Cluster("...", { compute: "auto" })alchemy/Kubernetes(resource providers, types, builders)AWS.EKS.Manifestwith a literal objectclusteronAWS.ECS.TaskpropsRunTask(cluster, task)/StartTask(cluster, task);Scheduletakes its ownclusterRedeploy impact (state, not code):
loadBalancer: trueservices redeploy onto composed resources — new ALB/TG/listener/SG physical names and a new URL; the old inline-created ingress is reaped automatically on first reconcile. The managed SG now admits the container port from0.0.0.0/0(tasks sit in public subnets); composed TargetGroups default to fast health checks (10s interval / threshold 2) and 30s deregistration delay.ServerHost→Deployment): existing state rows no longer match, so the next deploy replaces those resources (new physical names; NLB/ELB URLs change). Plan a maintenance window if the URL is consumed externally.docker: { base } → imageand CFdockerfile-content →imageproduce the same generated Dockerfile, so no image rebuild or task-definition churn on redeploy.ECS.Task:
docker: {}removed; external forms replace the empty-program hackAWS.ECS.Task("Api", { main: import.meta.url, - docker: { base: "oven/bun:1" }, + image: "oven/bun:1", }, impl)For environments that need extra build steps, compose inline content instead of a bare base:
Dockerfile.inlineproduces{ content }— a plain state-serializable object (structural discrimination, no symbol brand). Interpolations rideOutput.interpolate, soFROM ${base.imageUri}is a real dependency edge; unresolved content defers plan-time hashing to reconcile. On AWS, a pathdockerfilealso composes withmain(the env Dockerfile builds as a local stage; the bundle layersFROMit). Exclusivity is enforced with typed defects:image+dockerfile,image+context, inline+context.ECS.Task does not accept
cluster;RunTask/StartTasktake it explicitlyA
Taskmaps toAWS::ECS::TaskDefinition, which is cluster-independent — the cluster belongs to the launch, not the definition:const seedTask = yield* AWS.ECS.Task("SeedTask", { - cluster, main: import.meta.url, image: "oven/bun:1", }, impl); -const runSeedTask = yield* AWS.ECS.RunTask(seedTask).pipe(Effect.orDie); +const runSeedTask = yield* AWS.ECS.RunTask(cluster, seedTask);The two-argument form is the only form (no bind-time error channel, no
Effect.orDie);AWS.ECS.Schedule/everyalready take their owncluster.ECS.Task one-shot impls return
{ run }(and the container exits when it completes)Effect.gen(function* () { - return { fetch: Effect.gen(function* () { /* pseudo-handler */ }) }; + return { run: Effect.gen(function* () { /* runs to completion */ }) }; }){ fetch }remains for server tasks referenced byService({ task }).EKS:
ServerHost→Deployment(no alias); resource Type string changesEKS:
Workloadfamily deletedEKS:
AutoCluster→Cluster({ compute: "auto" });roleArnoptional under autoalchemy/Kubernetesremoved entirely — EKS is the only Kubernetes surfaceThe module (resource providers, hand-bridged types, zero-runtime builders) and the website's
/kubernetessection are gone.AWS.EKS.Manifesttakes a literal object;podTemplateonDeployment/Jobis a literal deep-partial object. The apply machinery (server-side apply, discovery, ordering) is internal toAWS.EKS.Cloudflare.Container:
dockerfile-as-content string →image(orDockerfile.inline); hash-stable, no rebuildsRunner.make( { main: import.meta.url, - dockerfile: "FROM oven/bun:latest", + image: "oven/bun:latest", instanceType: "standard", }, impl, );A
dockerfilestring is now always a path (externalcontextvariant). Inline content is explicit:dockerfile: Dockerfile.inline\FROM oven/bun:latest\nRUN apk add ffmpeg`— withmainit becomes the environment preamble the bundle layers onto; withoutmain` it is the whole Dockerfile. The variant-strict props union makes the old string-content misuse a type error.Verification
Live suites green with ALL slow paths enabled:
test/AWS/ECS32/0 (AWS_TEST_SLOW=1; Fargate smoke, tagged bootstrap, shared-ALB, Phase 2) andtest/AWS/EKS19/0 (AWS_TEST_SLOW=1; full cluster-create → serialized config → image → Deployment → NLB → in-pod DynamoDB write → destroy, 19 min).test/Cloudflare/Container12/12. Zero-orphan censuses clean after every round. The gated EKS path surfaced and fixed six more real bugs on its first true exercise: updateClusterConfig category batching, uncapped exponential cluster polls, hook-output invisibility in alchemy-test, NLB url missing the Service port, missing kubernetes.io/role/elb subnet tags, and a nonexistent default base-image ref.Image-source finalization re-verified: ECS 33/0 fast sweep post-rework; a live Fargate one-shot whose inline environment bakes a marker via
RUNand whose bundled{ run }reads it back exited 0 (InlineDockerfileTask.test.ts); 12 unit tests cover the composition/exclusivity matrix (test/Docker/Dockerfile.test.ts). Also fixed in passing:{ main, dockerfile: <path> }withoutcontextcrashed onpath.resolve(undefined)— context defaults to".".🤖 Generated with Claude Code