Skip to content

AWS.ECS reference

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());

Source: src/AWS/ECS/Cluster.ts

An Amazon ECS cluster for running tasks and services.

const cluster = yield* Cluster("AppCluster", {});

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,
});

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;

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;

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;

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.

const describeServices = yield* AWS.ECS.DescribeServices(cluster);
const response = yield* describeServices({ services: [serviceName] });
const runningCount = response.services?.[0]?.runningCount;

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.

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;

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).

const executeCommand = yield* AWS.ECS.ExecuteCommand(cluster);
const response = yield* executeCommand({
task: taskArn,
command: "ls -al /",
interactive: true,
});
const streamUrl = response.session?.streamUrl;

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.

const getTaskProtection = yield* AWS.ECS.GetTaskProtection(cluster);
const response = yield* getTaskProtection({ tasks: [taskArn] });
const protected_ = response.protectedTasks?.[0]?.protectionEnabled;

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 ?? [];

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;

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.

const listServices = yield* AWS.ECS.ListServices(cluster);
const response = yield* listServices({ launchType: "FARGATE" });
const serviceArns = response.serviceArns ?? [];

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.

const listTasks = yield* AWS.ECS.ListTasks(cluster);
const response = yield* listTasks({
desiredStatus: "STOPPED",
});
const taskArns = response.taskArns ?? [];

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.

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.

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" }),
});

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 existing AWS.ECS.Task’s definition; the Service adds desiredCount / 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.

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)),
);

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 },
}

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" },
],
},
});

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 },
},
});
// 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" }] },
});

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,
},
});
// 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",
},
});
// 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" },
});
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 },
});
const svc = yield* Service("Api", {
cluster,
image: "my-org/api:latest",
port: 3000,
volumes: [{ efs: fileSystem, path: "/mnt/data" }],
});
// 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 } },
});
const service = yield* Service("ApiService", {
cluster,
task: apiTask,
vpcId: vpc.vpcId,
subnets: [subnet1.subnetId, subnet2.subnetId],
loadBalancers: [
{
targetGroupArn,
containerName: apiTask.containerName,
containerPort: apiTask.port,
},
],
});
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" }],
});
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",
});

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.

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,
});
}),
};
}),
);

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",
});

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.

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 with image or an inline dockerfile to pick the environment; defaults to oven/bun:1).
  • context — build your own Dockerfile (dockerfile is 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.

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 });
}),
};
}),
);
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" }],
},
],
});

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 },
}

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" },
],
});

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 ACTIVE revision), so a no-op redeploy keeps the same revision;
  • changing the family replaces the resource;
  • destroy deregisters and hard-deletes every revision of the family. ECS may retain referenced revisions in DELETE_IN_PROGRESS until 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" }],
},
],
});
const service = yield* Service("ApiService", {
cluster,
task: taskDef, // exposes taskDefinitionArn / containerName / port
vpcId: vpc.vpcId,
subnets: [subnet.subnetId],
assignPublicIp: true,
});
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" }],
},
],
});

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",
});

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.

const updateTaskProtection = yield* AWS.ECS.UpdateTaskProtection(cluster);
yield* updateTaskProtection({
tasks: [taskArn],
protectionEnabled: true,
expiresIn: "30 minutes",
});