| 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 | */ |
| 17 | import * as Alchemy from "alchemy"; |
| 18 | import * as AWS from "alchemy/AWS"; |
| 19 | import * as Output from "alchemy/Output"; |
| 20 | import * as Effect from "effect/Effect"; |
| 21 | import ApiLive, { Api } from "./src/Api.ts"; |
| 22 | import { |
| 23 | OrdersCluster, |
| 24 | OrdersIngress, |
| 25 | OrdersNetwork, |
| 26 | OrdersTable, |
| 27 | } from "./src/infra.ts"; |
| 28 | |
| 29 | export 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 | ); |