| 1 | import type * as Effect from "effect/Effect"; |
| 2 | import type * as Stream from "effect/Stream"; |
| 3 | import { |
| 4 | consumeBusEvents, |
| 5 | type EventRecord, |
| 6 | } from "../EventBridge/EventSource.ts"; |
| 7 | import type { AutoScalingGroup } from "./AutoScalingGroup.ts"; |
| 8 | |
| 9 | /** |
| 10 | * EventBridge `detail-type`s EC2 Auto Scaling emits when a scaling activity |
| 11 | * finishes launching or terminating an instance. |
| 12 | */ |
| 13 | export type InstanceEventType = |
| 14 | | "EC2 Instance Launch Successful" |
| 15 | | "EC2 Instance Launch Unsuccessful" |
| 16 | | "EC2 Instance Terminate Successful" |
| 17 | | "EC2 Instance Terminate Unsuccessful"; |
| 18 | |
| 19 | /** |
| 20 | * The `detail` payload EC2 Auto Scaling delivers to EventBridge when a |
| 21 | * scaling activity launches or terminates an instance. |
| 22 | */ |
| 23 | export interface InstanceEventDetail { |
| 24 | /** |
| 25 | * ID of the scaling activity that produced the event. |
| 26 | */ |
| 27 | ActivityId: string; |
| 28 | /** |
| 29 | * Name of the Auto Scaling Group the instance belongs to. |
| 30 | */ |
| 31 | AutoScalingGroupName: string; |
| 32 | /** |
| 33 | * ID of the launched/terminated EC2 instance. |
| 34 | */ |
| 35 | EC2InstanceId: string; |
| 36 | /** |
| 37 | * Final status of the activity (e.g. `InProgress`, `Successful`, `Failed`). |
| 38 | */ |
| 39 | StatusCode: string; |
| 40 | /** |
| 41 | * Human-readable status message for unsuccessful activities. |
| 42 | */ |
| 43 | StatusMessage?: string; |
| 44 | /** |
| 45 | * What initiated the activity (e.g. a policy execution or capacity change). |
| 46 | */ |
| 47 | Cause?: string; |
| 48 | /** |
| 49 | * ISO-8601 time the activity started. |
| 50 | */ |
| 51 | StartTime?: string; |
| 52 | /** |
| 53 | * ISO-8601 time the activity ended. |
| 54 | */ |
| 55 | EndTime?: string; |
| 56 | /** |
| 57 | * Additional context, e.g. `{ "Availability Zone": "...", "Subnet ID": "..." }`. |
| 58 | */ |
| 59 | Details?: Record<string, string>; |
| 60 | /** |
| 61 | * Where the instance is coming from (e.g. `EC2` or `WarmPool`). |
| 62 | */ |
| 63 | Origin?: string; |
| 64 | /** |
| 65 | * Where the instance is headed (e.g. `AutoScalingGroup` or `WarmPool`). |
| 66 | */ |
| 67 | Destination?: string; |
| 68 | } |
| 69 | |
| 70 | /** An instance launch/terminate EventBridge event delivered to the handler. */ |
| 71 | export type InstanceEvent = EventRecord<InstanceEventDetail>; |
| 72 | |
| 73 | export interface InstanceEventSourceProps { |
| 74 | /** |
| 75 | * Logical id prefix for the EventBridge rule. Defaults to the Auto Scaling |
| 76 | * Group's logical id. |
| 77 | */ |
| 78 | id?: string; |
| 79 | /** |
| 80 | * Which instance events to deliver. |
| 81 | * @default all four launch/terminate success/failure events |
| 82 | */ |
| 83 | events?: InstanceEventType[]; |
| 84 | } |
| 85 | |
| 86 | const allInstanceEvents: InstanceEventType[] = [ |
| 87 | "EC2 Instance Launch Successful", |
| 88 | "EC2 Instance Launch Unsuccessful", |
| 89 | "EC2 Instance Terminate Successful", |
| 90 | "EC2 Instance Terminate Unsuccessful", |
| 91 | ]; |
| 92 | |
| 93 | /** |
| 94 | * Deliver EC2 Auto Scaling instance launch/terminate events to the host |
| 95 | * Function via EventBridge. Unlike {@link consumeLifecycleActions} this does |
| 96 | * not pause the instance transition — it observes completed scaling |
| 97 | * activities, e.g. to deregister an instance from an external system after |
| 98 | * termination or alert on launch failures. |
| 99 | * |
| 100 | * The EventBridge pattern matches every event of the chosen detail-types in |
| 101 | * the account; inspect `event.detail.AutoScalingGroupName` in the handler if |
| 102 | * multiple groups share the Function. |
| 103 | * |
| 104 | * ### Observing the Fleet |
| 105 | * **Example:** React to instance launches and terminations |
| 106 | * ```typescript |
| 107 | * yield* consumeInstanceEvents( |
| 108 | * group, |
| 109 | * { |
| 110 | * events: [ |
| 111 | * "EC2 Instance Launch Successful", |
| 112 | * "EC2 Instance Terminate Successful", |
| 113 | * ], |
| 114 | * }, |
| 115 | * (events) => |
| 116 | * Stream.runForEach(events, (event) => |
| 117 | * Effect.log( |
| 118 | * `${event["detail-type"]}: ${event.detail.EC2InstanceId} in ${event.detail.AutoScalingGroupName}`, |
| 119 | * ), |
| 120 | * ), |
| 121 | * ); |
| 122 | * ``` |
| 123 | * |
| 124 | * **Example:** Alert on launch failures |
| 125 | * ```typescript |
| 126 | * yield* consumeInstanceEvents( |
| 127 | * group, |
| 128 | * { events: ["EC2 Instance Launch Unsuccessful"] }, |
| 129 | * (events) => |
| 130 | * Stream.runForEach(events, (event) => |
| 131 | * notify(`Launch failed: ${event.detail.StatusMessage}`), |
| 132 | * ), |
| 133 | * ); |
| 134 | * ``` |
| 135 | */ |
| 136 | export const consumeInstanceEvents = <StreamReq = never, Req = never>( |
| 137 | group: AutoScalingGroup, |
| 138 | props: InstanceEventSourceProps, |
| 139 | process: ( |
| 140 | events: Stream.Stream<InstanceEvent, never, StreamReq>, |
| 141 | ) => Effect.Effect<void, never, Req>, |
| 142 | ) => |
| 143 | // The pattern uses only literal `source` + `detail-type` so it round-trips |
| 144 | // through both the deploy-time rule and the runtime matcher (Output values |
| 145 | // cannot appear in the pattern — they don't resolve inside the deployed |
| 146 | // bundle). |
| 147 | consumeBusEvents( |
| 148 | `${props.id ?? group.LogicalId}-InstanceEvents`, |
| 149 | { |
| 150 | source: ["aws.autoscaling"], |
| 151 | "detail-type": props.events ?? allInstanceEvents, |
| 152 | }, |
| 153 | process, |
| 154 | ); |