AWS.ECS reference
CapacityProvider
Section titled “CapacityProvider”Source:
src/AWS/ECS/CapacityProvider.ts
An Amazon ECS capacity provider backed by an EC2 Auto Scaling Group.
Capacity providers are associated with one or more ECS clusters via
Cluster#capacityProviders and are referenced by a service or task’s
capacity provider strategy.
Only EC2 Auto Scaling Group-backed capacity providers are currently
supported. The reserved AWS providers FARGATE and FARGATE_SPOT do not
need to be created and can be referenced by name on a Cluster directly.
CapacityProvider: Creating Capacity Providers
Section titled “CapacityProvider: Creating Capacity Providers”const provider = yield* CapacityProvider("AppCapacityProvider", { autoScalingGroupArn: asg.autoScalingGroupArn, managedScaling: { status: "ENABLED", targetCapacity: 80, minimumScalingStepSize: 1, maximumScalingStepSize: 10, }, managedTerminationProtection: "ENABLED",});
yield* Cluster("AppCluster", { capacityProviders: [provider.name], defaultCapacityProviderStrategy: [ { capacityProvider: provider.name, weight: 1 }, ],});CapacityProvider: Adopting Existing Capacity Providers
Section titled “CapacityProvider: Adopting Existing Capacity Providers”Foreign-tagged capacity providers (i.e. providers that exist in AWS but were
not created by this stack/stage/logical-id) are surfaced as Unowned by
read, and the engine fails with OwnedBySomeoneElse unless adoption is
explicitly opted in via --adopt or adopt.
import { adopt } from "alchemy/AdoptPolicy";
yield* CapacityProvider("AppCapacityProvider", { name: "existing-provider", autoScalingGroupArn: asg.autoScalingGroupArn,}).pipe(adopt());Cluster
Section titled “Cluster”Source:
src/AWS/ECS/Cluster.ts
An Amazon ECS cluster for running tasks and services.
Cluster: Creating Clusters
Section titled “Cluster: Creating Clusters”const cluster = yield* Cluster("AppCluster", {});ContinueServiceDeployment
Section titled “ContinueServiceDeployment”Source:
src/AWS/ECS/ContinueServiceDeployment.ts
Runtime binding for ecs:ContinueServiceDeployment.
Bind this operation to a Service inside a function runtime to get a
callable that resumes a blue/green deployment paused at a lifecycle-hook
stage — the canonical consumer is the deployment lifecycle-hook Lambda
that validates the green revision and then approves (or vetoes) the
traffic shift. The host is granted ecs:ContinueServiceDeployment on the
bound service’s deployments.
ContinueServiceDeployment: Service Deployments
Section titled “ContinueServiceDeployment: Service Deployments”const continueServiceDeployment = yield* AWS.ECS.ContinueServiceDeployment(service);
yield* continueServiceDeployment({ serviceDeploymentArn: deploymentArn, hookId,});DescribeContainerInstances
Section titled “DescribeContainerInstances”Source:
src/AWS/ECS/DescribeContainerInstances.ts
Runtime binding for ecs:DescribeContainerInstances.
Bind this operation to a Cluster inside a function runtime to get a
callable that describes container instances registered to the bound
cluster — remaining resources, running-task counts, agent status. The
cluster ARN is injected automatically and the host is granted
ecs:DescribeContainerInstances on the cluster’s container instances.
DescribeContainerInstances: Container Instances
Section titled “DescribeContainerInstances: Container Instances”const describeContainerInstances = yield* AWS.ECS.DescribeContainerInstances(cluster);
const response = yield* describeContainerInstances({ containerInstances: [containerInstanceArn],});const remaining = response.containerInstances?.[0]?.remainingResources;DescribeServiceDeployments
Section titled “DescribeServiceDeployments”Source:
src/AWS/ECS/DescribeServiceDeployments.ts
Runtime binding for ecs:DescribeServiceDeployments.
Bind this operation to a Service inside a function runtime to get a
callable that describes the bound service’s deployments — rollout state,
circuit-breaker status, target service revision. The host is granted
ecs:DescribeServiceDeployments on the service’s deployments (deployment
ARNs are only known at runtime, e.g. from ListServiceDeployments).
DescribeServiceDeployments: Service Deployments
Section titled “DescribeServiceDeployments: Service Deployments”const describeServiceDeployments = yield* AWS.ECS.DescribeServiceDeployments(service);
const response = yield* describeServiceDeployments({ serviceDeploymentArns: [deploymentArn],});const status = response.serviceDeployments?.[0]?.status;DescribeServiceRevisions
Section titled “DescribeServiceRevisions”Source:
src/AWS/ECS/DescribeServiceRevisions.ts
Runtime binding for ecs:DescribeServiceRevisions.
Bind this operation to a Service inside a function runtime to get a
callable that describes the bound service’s revisions — the immutable
task-definition + configuration snapshots that deployments roll between.
The host is granted ecs:DescribeServiceRevisions on the service’s
revisions (revision ARNs come from deployment describe/list responses).
DescribeServiceRevisions: Service Deployments
Section titled “DescribeServiceRevisions: Service Deployments”const describeServiceRevisions = yield* AWS.ECS.DescribeServiceRevisions(service);
const response = yield* describeServiceRevisions({ serviceRevisionArns: [revisionArn],});const taskDefinition = response.serviceRevisions?.[0]?.taskDefinition;DescribeServices
Section titled “DescribeServices”Source:
src/AWS/ECS/DescribeServices.ts
Runtime binding for ecs:DescribeServices.
Bind this operation to a Cluster inside a function runtime to get a
callable that describes services in the bound cluster. The cluster ARN is
injected automatically and the host is granted ecs:DescribeServices on
the cluster’s services.
DescribeServices: Describing Services
Section titled “DescribeServices: Describing Services”const describeServices = yield* AWS.ECS.DescribeServices(cluster);
const response = yield* describeServices({ services: [serviceName] });const runningCount = response.services?.[0]?.runningCount;DescribeTasks
Section titled “DescribeTasks”Source:
src/AWS/ECS/DescribeTasks.ts
Runtime binding for ecs:DescribeTasks.
Bind this operation to a Cluster inside a function runtime to get a
callable that describes tasks in the bound cluster. The cluster ARN is
injected automatically and the host is granted ecs:DescribeTasks on the
cluster’s tasks.
DescribeTasks: Describing Tasks
Section titled “DescribeTasks: Describing Tasks”const describeTasks = yield* AWS.ECS.DescribeTasks(cluster);
const response = yield* describeTasks({ tasks: [taskArn] });const status = response.tasks?.[0]?.lastStatus;const exitCode = response.tasks?.[0]?.containers?.[0]?.exitCode;ExecuteCommand
Section titled “ExecuteCommand”Source:
src/AWS/ECS/ExecuteCommand.ts
Runtime binding for ecs:ExecuteCommand (ECS Exec).
Bind this operation to a Cluster inside a function runtime to get a
callable that starts a command against a running container in the bound
cluster. The cluster ARN is injected automatically and the host is granted
ecs:ExecuteCommand on the cluster and its tasks.
The target task must have been launched with enableExecuteCommand and the
task role must allow the SSM messages channel. The response’s
session.tokenValue is a Redacted bearer token for the SSM WebSocket
stream (session.streamUrl).
ExecuteCommand: Executing Commands
Section titled “ExecuteCommand: Executing Commands”const executeCommand = yield* AWS.ECS.ExecuteCommand(cluster);
const response = yield* executeCommand({ task: taskArn, command: "ls -al /", interactive: true,});const streamUrl = response.session?.streamUrl;GetTaskProtection
Section titled “GetTaskProtection”Source:
src/AWS/ECS/GetTaskProtection.ts
Runtime binding for ecs:GetTaskProtection.
Bind this operation to a Cluster inside a function runtime to get a
callable that reads the scale-in protection status of service-managed tasks
in the bound cluster. The cluster ARN is injected automatically and the
host is granted ecs:GetTaskProtection on the cluster’s tasks.
GetTaskProtection: Task Protection
Section titled “GetTaskProtection: Task Protection”const getTaskProtection = yield* AWS.ECS.GetTaskProtection(cluster);
const response = yield* getTaskProtection({ tasks: [taskArn] });const protected_ = response.protectedTasks?.[0]?.protectionEnabled;ListContainerInstances
Section titled “ListContainerInstances”Source:
src/AWS/ECS/ListContainerInstances.ts
Runtime binding for ecs:ListContainerInstances.
Bind this operation to a Cluster inside a function runtime to get a
callable that lists container-instance ARNs registered to the bound
cluster (EC2/EXTERNAL launch types). The cluster ARN is injected
automatically and the host is granted ecs:ListContainerInstances on the
cluster.
ListContainerInstances: Container Instances
Section titled “ListContainerInstances: Container Instances”const listContainerInstances = yield* AWS.ECS.ListContainerInstances(cluster);
const response = yield* listContainerInstances({ status: "ACTIVE" });const instanceArns = response.containerInstanceArns ?? [];ListServiceDeployments
Section titled “ListServiceDeployments”Source:
src/AWS/ECS/ListServiceDeployments.ts
Runtime binding for ecs:ListServiceDeployments.
Bind this operation to a Service inside a function runtime to get a
callable that lists the bound service’s deployments (newest first). The
service and cluster ARNs are injected automatically and the host is
granted ecs:ListServiceDeployments on the service.
ListServiceDeployments: Service Deployments
Section titled “ListServiceDeployments: Service Deployments”const listServiceDeployments = yield* AWS.ECS.ListServiceDeployments(service);
const response = yield* listServiceDeployments({ status: ["IN_PROGRESS"],});const deploymentArn = response.serviceDeployments?.[0]?.serviceDeploymentArn;ListServices
Section titled “ListServices”Source:
src/AWS/ECS/ListServices.ts
Runtime binding for ecs:ListServices.
Bind this operation to a Cluster inside a function runtime to get a
callable that lists service ARNs in the bound cluster. The cluster ARN is
injected automatically and the grant is conditioned on the bound cluster.
ListServices: Listing Services
Section titled “ListServices: Listing Services”const listServices = yield* AWS.ECS.ListServices(cluster);
const response = yield* listServices({ launchType: "FARGATE" });const serviceArns = response.serviceArns ?? [];ListTasks
Section titled “ListTasks”Source:
src/AWS/ECS/ListTasks.ts
Runtime binding for ecs:ListTasks.
Bind this operation to a Cluster inside a function runtime to get a
callable that lists task ARNs in the bound cluster. The cluster ARN is
injected automatically and the grant is conditioned on the bound cluster.
ListTasks: Listing Tasks
Section titled “ListTasks: Listing Tasks”const listTasks = yield* AWS.ECS.ListTasks(cluster);
const response = yield* listTasks({ desiredStatus: "STOPPED",});const taskArns = response.taskArns ?? [];RunTask
Section titled “RunTask”Source:
src/AWS/ECS/RunTask.ts
Runtime binding for ecs:RunTask.
Bind this operation to a Cluster and Task inside a function runtime to
get a callable that starts a Fargate task from the bound task
definition. The cluster and task definition ARNs are injected automatically;
the host is granted ecs:RunTask on the task definition plus iam:PassRole
on the task and execution roles.
RunTask: Running Tasks
Section titled “RunTask: Running Tasks”const api = yield* AWS.Lambda.Function( "Api", { main: import.meta.url, functionUrl: true }, Effect.gen(function* () { // init: bind the launch (IAM grants happen here) const runTask = yield* AWS.ECS.RunTask(cluster, task);
return { fetch: Effect.gen(function* () { // runtime: launch a task per request const response = yield* runTask({ launchType: "FARGATE", networkConfiguration: { awsvpcConfiguration: { subnets: [subnetId], assignPublicIp: "ENABLED", }, }, }); return yield* HttpServerResponse.json({ taskArn: response.tasks?.[0]?.taskArn, }); }), }; }),);Source:
src/AWS/ECS/Schedule.ts
Creates a scheduled EventBridge rule that runs an ECS Fargate task.
every is the high-level ECS scheduling helper for phase 1. It provisions
the EventBridge rule plus the invoke role required to call ecs:RunTask and
iam:PassRole for the target task’s execution roles.
Plain English durations like "1 hour" are normalized to rate(...)
expressions automatically. Full rate(...) and cron(...) expressions are
also accepted as-is.
every: Examples
Section titled “every: Examples”Run a task every hour
yield* AWS.ECS.every("HourlyJob", "1 hour", { cluster, task: jobTask, subnets: [privateSubnet1.subnetId, privateSubnet2.subnetId], securityGroups: [jobSecurityGroup.groupId],});Use an explicit cron expression
yield* AWS.ECS.every("NightlyJob", "cron(0 3 * * ? *)", { cluster, task: nightlyTask, subnets: [privateSubnet1.subnetId, privateSubnet2.subnetId], securityGroups: [jobSecurityGroup.groupId],});Run multiple copies with static input
yield* AWS.ECS.every("BatchJob", "30 minutes", { cluster, task: batchTask, subnets: [privateSubnet1.subnetId, privateSubnet2.subnetId], securityGroups: [jobSecurityGroup.groupId], taskCount: 3, input: JSON.stringify({ source: "scheduler" }),});Service
Section titled “Service”Source:
src/AWS/ECS/Service.ts
An ECS service: N copies of a container kept alive, optionally behind a load balancer.
The service’s image comes from one of four sources:
image— run a pre-built registry image, mirrored into ECR.context— build your own Dockerfile.main— bundle an inline Effect program (servers return{ fetch }).task:— deploy an existingAWS.ECS.Task’s definition; the Service addsdesiredCount/ load balancing / deployment configuration.
With any of the first three the Service synthesizes its own task
definition (task + execution roles, log group, ECR repository).
loadBalancer: true wires a public ALB + target group + listener and
populates the url attribute. When vpcId/subnets are omitted the
account’s default VPC (and its per-AZ subnets) is used.
Most configuration is updated in place via updateService
(desiredCount, task definition, network, deployment config, placement,
exec, load balancers, tags). Only truly-immutable aspects — serviceName,
cluster, launchType↔capacityProviderStrategy switch, deploymentController
type, schedulingStrategy, enableECSManagedTags, role — replace the
service.
Service: Creating Services
Section titled “Service: Creating Services”Remote Image Behind a Load Balancer
const nginx = yield* Service("Edge", { cluster, image: "public.ecr.aws/nginx/nginx:1.27", port: 80, desiredCount: 2, loadBalancer: true, // ALB + target group + listener wiring});nginx.url; // http://<alb-dns-name>Run an Existing Task’s Definition
const api = yield* Service("Api", { cluster, task: apiTask, // shared image/roles/config; Service adds desiredCount: 2, // desiredCount / LB / deployment config loadBalancer: true,});Inline Effect Server
const api = yield* Service( "Api", { cluster, main: import.meta.url, port: 3000, desiredCount: 2, cpu: 256, memory: 512 }, Effect.gen(function* () { const putItem = yield* AWS.DynamoDB.PutItem(table); return { fetch: Effect.gen(function* () { return yield* HttpServerResponse.json({ ok: true }); }), }; }).pipe(Effect.provide(AWS.DynamoDB.PutItemHttp)),);Service: Bundling & Tree-shaking
Section titled “Service: Bundling & Tree-shaking”main is bundled with rolldown at deploy time. Unused code is
tree-shaken. effect, alchemy, and @distilled.cloud are marked
pure so unused parts prune more aggressively. Your app is not
marked pure.
Mark additional packages as pure
Only list packages with no top-level side effects.
{ main: import.meta.url, build: { pure: { packages: ["my-lib", "@my-scope/*"] }, },}Turn it off
{ main: import.meta.url, build: { pure: false },}Service: Shared Load Balancers
Section titled “Service: Shared Load Balancers”Two Services Sharing One Listener
// The ALB + listener are stack-level resources owned by neither service.const lb = yield* AWS.ELBv2.LoadBalancer("Alb", { subnets: [subnetA.subnetId, subnetB.subnetId], securityGroups: [sg.groupId],});const listener = yield* AWS.ELBv2.Listener("Http", { loadBalancerArn: lb.loadBalancerArn, port: 80, defaultActions: [ { type: "fixedResponse", statusCode: "404", messageBody: "no route" }, ],});
// Each service composes only its own TargetGroup + ListenerRule on the// shared listener. Destroying one service removes its rule + target// group; the ALB, listener, and the other service are untouched.const api = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, loadBalancer: { listener, rules: [{ path: "/api/*" }] },});const web = yield* Service("Web", { cluster, image: "my-org/web:latest", port: 8080, loadBalancer: { listener, rules: [{ path: "/*" }] },});Catch-All on a Shared Listener
// A bare listener reference adds a single `path: "/*"` rule.const svc = yield* Service("Svc", { cluster, image: "my-org/web:latest", port: 8080, loadBalancer: listener,});Owned ALB with Routing Rules and an HTTP → HTTPS Redirect
// `"80/http"`-style `listen` strings mean the service OWNS the ALB and// these listeners (mixing them with shared listener references is a// typed error).const svc = yield* Service("Svc", { cluster, image: "my-org/web:latest", port: 8080, certificateArn, loadBalancer: { rules: [ { listen: "80/http", redirect: "443/https" }, { listen: "443/https", forward: "8080/http" }, ], },});Service: Custom Domains
Section titled “Service: Custom Domains”Domain with a Composed Certificate
// Looks up the matching Route 53 hosted zone, composes a DNS-validated// ACM certificate in the service's region, wires it to the HTTPS// listener, and creates alias A/AAAA records. `url` becomes// https://api.example.com.const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, loadBalancer: { domain: "api.example.com" },});Domain with an Existing Certificate
const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, loadBalancer: { domain: { name: "api.example.com", aliases: ["www.api.example.com"], cert: certificateArn }, },});Service: Network Load Balancers
Section titled “Service: Network Load Balancers”// tcp/udp/tls/tcp_udp listen protocols compose a Network Load Balancer;// each rule's action becomes its listener's default forward (NLB// listeners route by port alone).const svc = yield* Service("Tcp", { cluster, image: "my-org/tcp-echo:latest", port: 9000, loadBalancer: { rules: [{ listen: "80/tcp" }] },});Service: Health Checks
Section titled “Service: Health Checks”Per-Target-Group Health Overrides
const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, loadBalancer: { rules: [{ listen: "80/http" }], health: { "3000/http": { path: "/healthz", interval: "15 seconds", healthyThreshold: 3, successCodes: "200-299", }, }, },});Container Health Check
const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, healthCheck: { command: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"], interval: "30 seconds", retries: 3, },});Service: Autoscaling
Section titled “Service: Autoscaling”// Composes a ScalableTarget (min/max) plus one target-tracking policy// per metric. Redeploys stop pinning desiredCount while scaling is set.const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, loadBalancer: true, scaling: { min: 1, max: 4, cpuUtilization: 70, requestCount: 200, scaleInCooldown: "5 minutes", },});Service: Secrets & Logging
Section titled “Service: Secrets & Logging”// Values are ARNs; the container gets them as env vars via `valueFrom`// and the execution role is granted read on exactly these ARNs.const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, secrets: { DB_PASSWORD: dbPasswordSecret.secretArn, API_KEY: apiKeyParameter.parameterArn, }, logging: { retention: "2 weeks" },});Service: Service Discovery
Section titled “Service: Service Discovery”const namespace = yield* AWS.CloudMap.PrivateDnsNamespace("AppNs", { name: "internal.example.com", vpc: vpc.vpcId,});const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, serviceRegistry: { namespace },});Service: Volumes
Section titled “Service: Volumes”const svc = yield* Service("Api", { cluster, image: "my-org/api:latest", port: 3000, volumes: [{ efs: fileSystem, path: "/mnt/data" }],});Service: Capacity
Section titled “Service: Capacity”// The cluster must have the Fargate capacity providers associated:// Cluster("C", { capacityProviders: ["FARGATE", "FARGATE_SPOT"] }).const svc = yield* Service("Worker", { cluster, image: "my-org/worker:latest", capacity: { fargate: { weight: 1, base: 1 }, spot: { weight: 4 } },});Service: Load Balancing
Section titled “Service: Load Balancing”const service = yield* Service("ApiService", { cluster, task: apiTask, vpcId: vpc.vpcId, subnets: [subnet1.subnetId, subnet2.subnetId], loadBalancers: [ { targetGroupArn, containerName: apiTask.containerName, containerPort: apiTask.port, }, ],});Service: Capacity & Placement
Section titled “Service: Capacity & Placement”const service = yield* Service("WorkerService", { cluster, task: workerTask, vpcId: vpc.vpcId, subnets: [subnet.subnetId], capacityProviderStrategy: [ { capacityProvider: "FARGATE_SPOT", weight: 4 }, { capacityProvider: "FARGATE", weight: 1, base: 1 }, ], placementStrategy: [{ type: "spread", field: "attribute:ecs.availability-zone" }],});Service: Deployment
Section titled “Service: Deployment”const service = yield* Service("ApiService", { cluster, task: apiTask, vpcId: vpc.vpcId, subnets: [subnet1.subnetId, subnet2.subnetId], desiredCount: 3, enableExecuteCommand: true, deploymentConfiguration: { minimumHealthyPercent: 100, maximumPercent: 200, deploymentCircuitBreaker: { enable: true, rollback: true }, }, healthCheckGracePeriod: "30 seconds",});StartTask
Section titled “StartTask”Source:
src/AWS/ECS/StartTask.ts
Runtime binding for ecs:StartTask.
Bind this operation to a Cluster and Task inside a function runtime to
get a callable that places the bound task definition on specific container
instances (EC2/EXTERNAL launch types — unlike RunTask, which lets ECS
pick placement). The cluster and task definition ARNs are injected
automatically; the host is granted ecs:StartTask on the task definition
plus iam:PassRole on the task and execution roles.
StartTask: Running Tasks
Section titled “StartTask: Running Tasks”const controller = yield* AWS.Lambda.Function( "PlacementController", { main: import.meta.url }, Effect.gen(function* () { // init: bind the launch (IAM grants happen here) const startTask = yield* AWS.ECS.StartTask(cluster, task);
return { fetch: Effect.gen(function* () { // runtime: place the task on a chosen instance const response = yield* startTask({ containerInstances: [containerInstanceArn], startedBy: "placement-controller", }); return yield* HttpServerResponse.json({ taskArn: response.tasks?.[0]?.taskArn, }); }), }; }),);StopServiceDeployment
Section titled “StopServiceDeployment”Source:
src/AWS/ECS/StopServiceDeployment.ts
Runtime binding for ecs:StopServiceDeployment.
Bind this operation to a Service inside a function runtime to get a
callable that stops an in-progress deployment of the bound service —
either abandoning it or rolling back to the last completed revision.
The host is granted ecs:StopServiceDeployment on the service’s
deployments.
StopServiceDeployment: Service Deployments
Section titled “StopServiceDeployment: Service Deployments”const stopServiceDeployment = yield* AWS.ECS.StopServiceDeployment(service);
yield* stopServiceDeployment({ serviceDeploymentArn: deploymentArn, stopType: "ROLLBACK",});StopTask
Section titled “StopTask”Source:
src/AWS/ECS/StopTask.ts
Runtime binding for ecs:StopTask.
Bind this operation to a Cluster inside a function runtime to get a
callable that stops a running task in the bound cluster. The cluster ARN is
injected automatically and the host is granted ecs:StopTask on the
cluster’s tasks.
StopTask: Stopping Tasks
Section titled “StopTask: Stopping Tasks”const stopTask = yield* AWS.ECS.StopTask(cluster);
const response = yield* stopTask({ task: taskArn, reason: "drained by worker",});Source:
src/AWS/ECS/Task.ts
A Fargate task definition with a container image from one of three sources, declared flat on the props:
main— bundle an inline Effect program into a generated image (compose withimageor an inlinedockerfileto pick the environment; defaults tooven/bun:1).context— build your own Dockerfile (dockerfileis a path relative to the cwd, defaulting to${context}/Dockerfile).image— run a pre-built registry image, mirrored into ECR.
Task provisions task + execution IAM roles, a CloudWatch log group, and
an ECR repository holding the built (or mirrored) image, then registers a
Fargate task definition. Each reconcile registers a new immutable
revision. A launched task runs until its process exits — it is the target
of AWS.ECS.RunTask / StopTask bindings and AWS.ECS.Schedule;
effectful impls return { run }, executed to completion when the
container starts.
Beyond the primary container you can declare task-level configuration
(volumes, runtime platform, ephemeral storage, IPC/PID mode, placement
constraints) and append additional sidecars for multi-container tasks.
Task: Creating a Task
Section titled “Task: Creating a Task”Remote Image
const migrate = yield* Task("DbMigrate", { image: "public.ecr.aws/docker/library/busybox:stable", command: ["sh", "-c", "echo done"], cpu: 256, memory: 512,});Build Your Own Dockerfile
const render = yield* Task("RenderJob", { context: "./render", // dockerfile defaults to ./render/Dockerfile dockerfile: "./render/Dockerfile.gpu", // always a PATH cpu: 1024, memory: 4096,});Inline Effect Program
const drainer = yield* Task( "QueueDrainer", { main: import.meta.url, image: "oven/bun:1", cpu: 256, memory: 512 }, Effect.gen(function* () { const receive = yield* AWS.SQS.ReceiveMessage(queue); return { run: Effect.gen(function* () { // runs to completion, then the container exits const batch = yield* receive({ MaxNumberOfMessages: 10 }); }), }; }),);Task: Multi-Container Tasks
Section titled “Task: Multi-Container Tasks”const task = yield* Task("ApiTask", { main: import.meta.url, port: 3000, sidecars: [ { name: "otel-collector", image: "public.ecr.aws/aws-observability/aws-otel-collector:latest", essential: false, portMappings: [{ containerPort: 4317, protocol: "tcp" }], }, ],});Task: Bundling & Tree-shaking
Section titled “Task: Bundling & Tree-shaking”main is bundled with rolldown at deploy time. Unused code is
tree-shaken. effect, alchemy, and @distilled.cloud are marked
pure so unused parts prune more aggressively. Your app is not
marked pure.
Mark additional packages as pure
Only list packages with no top-level side effects.
{ main: import.meta.url, build: { pure: { packages: ["my-lib", "@my-scope/*"] }, },}Turn it off
{ main: import.meta.url, build: { pure: false },}Task: Task-Level Configuration
Section titled “Task: Task-Level Configuration”ARM64 with EFS Volume and Ephemeral Storage
const task = yield* Task("WorkerTask", { main: import.meta.url, runtimePlatform: { cpuArchitecture: "ARM64", operatingSystemFamily: "LINUX" }, ephemeralStorage: { sizeInGiB: 40 }, volumes: [ { name: "data", efsVolumeConfiguration: { fileSystemId: fileSystem.fileSystemId }, }, ], container: { mountPoints: [{ sourceVolume: "data", containerPath: "/data" }], },});Environment Files from S3
const task = yield* Task("ApiTask", { main: import.meta.url, environmentFiles: [ { value: "arn:aws:s3:::my-config-bucket/app.env", type: "s3" }, ],});TaskDefinition
Section titled “TaskDefinition”Source:
src/AWS/ECS/TaskDefinition.ts
A standalone ECS task definition for bring-your-own-container workloads.
Unlike the Effect-native AWS.ECS.Task (which bundles an inline program and
builds/pushes a Docker image), TaskDefinition registers user-supplied
containerDefinitions — any image URI from ECR, public.ecr.aws, or an
external registry — with full control over Fargate/EC2 compatibility,
volumes, runtime platform, and IAM roles.
Task definitions are immutable revisions under a family:
- reconcile registers a new revision only when the definition content
changed (compared against the observed latest
ACTIVErevision), so a no-op redeploy keeps the same revision; - changing the
familyreplaces the resource; - destroy deregisters and hard-deletes every revision of the family. ECS
may retain referenced revisions in
DELETE_IN_PROGRESSuntil their tasks and services terminate; that is a successful terminal state.
Layering — why TaskDefinition is deliberately not a Platform. ECS
splits “what runs” from “how it runs”: a task definition is the immutable
container spec, while Task (runs to completion) and Service
(long-running) are
the execution vehicles. The effectful Platform abstraction requires
Alchemy to own the container image and entrypoint so it can bundle the
inline Effect program — that is exactly what AWS.ECS.Task does (bundle →
Docker build/push → register definition → serve the program). Making
TaskDefinition also a Platform would duplicate Task while
contradicting this resource’s purpose: user-supplied images whose
entrypoint Alchemy must not rewrite. So the effectful path is
AWS.ECS.Task; the bring-your-own-container path is TaskDefinition.
Both surface taskDefinitionArn / containerName / port, so either
plugs into AWS.ECS.Service’s task prop unchanged.
TaskDefinition: Creating a Task Definition
Section titled “TaskDefinition: Creating a Task Definition”Public Image on Fargate
const taskDef = yield* TaskDefinition("Nginx", { containerDefinitions: [ { name: "nginx", image: "public.ecr.aws/nginx/nginx:stable", essential: true, portMappings: [{ containerPort: 80, protocol: "tcp" }], }, ],});With IAM Roles and CloudWatch Logs
const taskDef = yield* TaskDefinition("Api", { cpu: 512, memory: 1024, taskRoleArn: taskRole, // AWS.IAM.Role resource or raw ARN executionRoleArn: executionRole, // needed for awslogs / private images awslogs: true, // creates /ecs/{family} and injects awslogs config containerDefinitions: [ { name: "api", image: image.imageUri, essential: true, portMappings: [{ containerPort: 8080 }], environment: [{ name: "STAGE", value: "prod" }], }, ],});TaskDefinition: Running with a Service
Section titled “TaskDefinition: Running with a Service”const service = yield* Service("ApiService", { cluster, task: taskDef, // exposes taskDefinitionArn / containerName / port vpcId: vpc.vpcId, subnets: [subnet.subnetId], assignPublicIp: true,});TaskDefinition: EC2 Launch Type
Section titled “TaskDefinition: EC2 Launch Type”const taskDef = yield* TaskDefinition("Agent", { requiresCompatibilities: ["EC2"], networkMode: "bridge", volumes: [{ name: "docker-sock", host: { sourcePath: "/var/run/docker.sock" } }], containerDefinitions: [ { name: "agent", image: "public.ecr.aws/docker/library/busybox:stable", memory: 128, essential: true, mountPoints: [{ sourceVolume: "docker-sock", containerPath: "/var/run/docker.sock" }], }, ],});UpdateContainerInstancesState
Section titled “UpdateContainerInstancesState”Source:
src/AWS/ECS/UpdateContainerInstancesState.ts
Runtime binding for ecs:UpdateContainerInstancesState.
Bind this operation to a Cluster inside a function runtime to get a
callable that transitions container instances between ACTIVE and
DRAINING — the canonical building block of an Auto Scaling
lifecycle-hook drain function that gracefully migrates tasks off an
instance before it terminates. The cluster ARN is injected automatically
and the host is granted ecs:UpdateContainerInstancesState on the
cluster’s container instances.
UpdateContainerInstancesState: Container Instances
Section titled “UpdateContainerInstancesState: Container Instances”const updateContainerInstancesState = yield* AWS.ECS.UpdateContainerInstancesState(cluster);
yield* updateContainerInstancesState({ containerInstances: [containerInstanceArn], status: "DRAINING",});UpdateTaskProtection
Section titled “UpdateTaskProtection”Source:
src/AWS/ECS/UpdateTaskProtection.ts
Runtime binding for ecs:UpdateTaskProtection.
Bind this operation to a Cluster inside a function runtime to get a
callable that toggles scale-in protection on service-managed tasks in the
bound cluster — the canonical pattern is a task protecting itself while
it processes long-running work so deployments and scale-in don’t terminate
it. The cluster ARN is injected automatically and the host is granted
ecs:UpdateTaskProtection on the cluster’s tasks.
UpdateTaskProtection: Task Protection
Section titled “UpdateTaskProtection: Task Protection”const updateTaskProtection = yield* AWS.ECS.UpdateTaskProtection(cluster);
yield* updateTaskProtection({ tasks: [taskArn], protectionEnabled: true, expiresIn: "30 minutes",});