| 1 | import { createHash } from "node:crypto"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as IAM from "../IAM/index.ts"; |
| 4 | import type { Cluster } from "../ECS/Cluster.ts"; |
| 5 | import type { EventBus } from "./EventBus.ts"; |
| 6 | import { Rule, type RuleProps, type RuleTarget } from "./Rule.ts"; |
| 7 | |
| 8 | interface EventDescriptor { |
| 9 | id?: string; |
| 10 | bus?: EventBus; |
| 11 | pattern: Record<string, any>; |
| 12 | props?: Pick<RuleProps, "description" | "state">; |
| 13 | } |
| 14 | |
| 15 | export 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 | */ |
| 56 | export 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 | |
| 130 | const 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)}`; |