1/**
2 * An "orders" app on ECS Fargate that exercises the full container surface:
3 *
4 * - a stack-owned SHARED ALB + HTTP listener (`src/infra.ts`) that two
5 * services attach path rules to,
6 * - `Api` — an effectful `AWS.ECS.Service` (bundled `main:` image source)
7 * serving `/api/*` with DynamoDB + `RunTask` bindings (`src/Api.ts`),
8 * - `Web` — an EXTERNAL `AWS.ECS.Service` (registry `image:` source, no
9 * Effect runtime in the container) serving the catch-all `/*`,
10 * - `SeedTask` — an inline-effect one-shot `AWS.ECS.Task` (`{ run }`) that
11 * seeds the orders table, launched from `POST /api/seed` (`src/SeedTask.ts`),
12 * - `ReportTask` — an external one-shot task built from a local Dockerfile
13 * (`context:` image source, `src/report/Dockerfile`),
14 * - `HeartbeatTask` — an external one-shot task on an EventBridge cron
15 * schedule via `AWS.ECS.every`.
16 */
17import * as Alchemy from "alchemy";
18import * as AWS from "alchemy/AWS";
19import * as Output from "alchemy/Output";
20import * as Effect from "effect/Effect";
21import ApiLive, { Api } from "./src/Api.ts";
22import {
23 OrdersCluster,
24 OrdersIngress,
25 OrdersNetwork,
26 OrdersTable,
27} from "./src/infra.ts";
28
29export default Alchemy.Stack(
30 "AwsEcsExample",
31 {
32 providers: AWS.providers(),
33 state: Alchemy.localState(),
34 },
35 Effect.gen(function* () {
36 const network = yield* OrdersNetwork;
37 const cluster = yield* OrdersCluster;
38 const table = yield* OrdersTable;
39 const { alb, listener } = yield* OrdersIngress;
40
41 // ── Api — effectful service in the TAGGED form (bundled `main:`),
42 // /api/* on the shared ALB. `ApiLive` (the `Api.make(...)` Layer,
43 // provided below) carries the props + init program; its init also
44 // declares SeedTask (for the RunTask binding) and the DynamoDB
45 // bindings; see src/Api.ts.
46 const api = yield* Api;
47
48 // ── Web — EXTERNAL service: a pre-built registry image (mirrored into
49 // ECR), no Effect runtime in the container. Attaches the catch-all `/*`
50 // rule to the shared listener; the explicit priority orders it AFTER
51 // Api's `/api/*` rule (lower number evaluates first).
52 const web = yield* AWS.ECS.Service("Web", {
53 cluster,
54 image: "nginxdemos/hello:plain-text",
55 port: 80,
56 desiredCount: 1,
57 vpcId: network.vpcId,
58 subnets: network.publicSubnetIds,
59 assignPublicIp: true,
60 loadBalancer: {
61 listener,
62 rules: [{ path: "/*", priority: 20000 }],
63 },
64 });
65
66 // ── ReportTask — EXTERNAL one-shot task built from the example's own
67 // Dockerfile (`context:` image source; `dockerfile` defaults to
68 // `${context}/Dockerfile`). Deploy builds + pushes the image and
69 // registers the task definition; run it with `RunTask` or a schedule.
70 const reportTask = yield* AWS.ECS.Task("ReportTask", {
71 context: `${import.meta.dirname}/src/report`,
72 cpu: 256,
73 memory: 512,
74 // Build/run on ARM64 so an image built on an Apple Silicon host
75 // matches the Fargate runtime architecture (Graviton).
76 runtimePlatform: {
77 cpuArchitecture: "ARM64",
78 operatingSystemFamily: "LINUX",
79 },
80 });
81
82 // ── HeartbeatTask — EXTERNAL one-shot task (registry `image:` source)
83 // run nightly by an EventBridge Scheduler cron via `AWS.ECS.every`
84 // (which provisions the schedule plus the ecs:RunTask invoke role).
85 const heartbeatTask = yield* AWS.ECS.Task("HeartbeatTask", {
86 image: "busybox:stable",
87 command: ["sh", "-c", "echo orders heartbeat"],
88 cpu: 256,
89 memory: 512,
90 });
91 yield* AWS.ECS.every("HeartbeatSchedule", "cron(0 3 * * ? *)", {
92 cluster,
93 task: heartbeatTask,
94 subnets: network.publicSubnetIds,
95 assignPublicIp: true,
96 });
97
98 return {
99 // Service-derived URLs, not `http://${alb.dnsName}`: in `alchemy dev`
100 // the emulated ALB is host-routed on the emulator gateway port, and
101 // the service attribute is the only place that port is known.
102 url: web.url,
103 apiUrl: Output.interpolate`${api.url}/api/orders`,
104 seedUrl: Output.interpolate`${api.url}/api/seed`,
105 albDnsName: alb.dnsName,
106 tableName: table.tableName,
107 apiServiceName: api.serviceName,
108 webServiceName: web.serviceName,
109 reportTaskDefinitionArn: reportTask.taskDefinitionArn,
110 heartbeatTaskDefinitionArn: heartbeatTask.taskDefinitionArn,
111 };
112 }).pipe(Effect.provide(ApiLive)),
113);