1import { createHash } from "node:crypto";
2import * as Effect from "effect/Effect";
3import * as IAM from "../IAM/index.ts";
4import type { Cluster } from "../ECS/Cluster.ts";
5import type { EventBus } from "./EventBus.ts";
6import { Rule, type RuleProps, type RuleTarget } from "./Rule.ts";
7
8interface EventDescriptor {
9 id?: string;
10 bus?: EventBus;
11 pattern: Record<string, any>;
12 props?: Pick<RuleProps, "description" | "state">;
13}
14
15export interface EcsRouteTargetProps extends Pick<
16 RuleTarget,
17 | "Input"
18 | "InputPath"
19 | "InputTransformer"
20 | "RetryPolicy"
21 | "DeadLetterConfig"
22> {
23 task: {
24 taskDefinitionArn: string;
25 taskRoleArn: string;
26 executionRoleArn: string;
27 };
28 subnets: string[];
29 securityGroups?: string[];
30 assignPublicIp?: boolean;
31 taskCount?: number;
32}
33
34/**
35 * Routes matching events from an EventBridge bus to an ECS task run.
36 *
37 * Creates a {@link Rule} targeting the ECS cluster plus an IAM role that lets
38 * EventBridge call `ecs:RunTask` with the given task definition (Fargate
39 * launch type). Usually reached through the `events(...)` builder rather than
40 * called directly.
41 * **Example:** Run a Fargate Task for Matching Events
42 * ```typescript
43 * yield* AWS.EventBridge.events(bus, { source: ["my.app"] }).toEcsTask(cluster, {
44 * task: {
45 * taskDefinitionArn: yield* taskDefinition.taskDefinitionArn,
46 * taskRoleArn: yield* taskRole.roleArn,
47 * executionRoleArn: yield* executionRole.roleArn,
48 * },
49 * subnets: subnetIds,
50 * assignPublicIp: true,
51 * });
52 * ```
53 *
54 * @binding
55 */
56export const toEcsTask = (
57 descriptor: EventDescriptor,
58 cluster: Cluster,
59 props: EcsRouteTargetProps,
60) =>
61 Effect.gen(function* () {
62 const routeId =
63 descriptor.id ?? createRouteId(descriptor, `${cluster.LogicalId}Ecs`);
64
65 const role = yield* IAM.Role(`${routeId}${cluster.LogicalId}Role`, {
66 assumeRolePolicyDocument: {
67 Version: "2012-10-17",
68 Statement: [
69 {
70 Effect: "Allow",
71 Principal: {
72 Service: "events.amazonaws.com",
73 },
74 Action: ["sts:AssumeRole"],
75 Resource: ["*"],
76 },
77 ],
78 },
79 inlinePolicies: {
80 RunTask: {
81 Version: "2012-10-17",
82 Statement: [
83 {
84 Effect: "Allow",
85 Action: ["ecs:RunTask"],
86 Resource: [props.task.taskDefinitionArn],
87 },
88 {
89 Effect: "Allow",
90 Action: ["iam:PassRole"],
91 Resource: [props.task.taskRoleArn, props.task.executionRoleArn],
92 },
93 ],
94 },
95 },
96 });
97
98 return yield* Rule(routeId, {
99 description: descriptor.props?.description,
100 state: descriptor.props?.state,
101 eventBusName: descriptor.bus?.eventBusName,
102 eventPattern: descriptor.pattern,
103 targets: [
104 {
105 Id: `${cluster.LogicalId}Target`,
106 Arn: cluster.clusterArn as any,
107 RoleArn: role.roleArn as any,
108 Input: props.Input,
109 InputPath: props.InputPath,
110 InputTransformer: props.InputTransformer,
111 RetryPolicy: props.RetryPolicy,
112 DeadLetterConfig: props.DeadLetterConfig,
113 EcsParameters: {
114 TaskDefinitionArn: props.task.taskDefinitionArn,
115 TaskCount: props.taskCount ?? 1,
116 LaunchType: "FARGATE",
117 NetworkConfiguration: {
118 awsvpcConfiguration: {
119 Subnets: props.subnets,
120 SecurityGroups: props.securityGroups,
121 AssignPublicIp: props.assignPublicIp ? "ENABLED" : "DISABLED",
122 },
123 },
124 },
125 },
126 ],
127 });
128 });
129
130const createRouteId = (descriptor: EventDescriptor, suffix: string) =>
131 `EventBridge${createHash("sha1")
132 .update(
133 JSON.stringify({
134 bus: descriptor.bus?.LogicalId ?? "default",
135 pattern: descriptor.pattern,
136 suffix,
137 }),
138 )
139 .digest("hex")
140 .slice(0, 10)}`;