@fjall/components-infrastructure 0.95.0 → 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/dist/lib/app.d.ts +90 -107
  2. package/dist/lib/app.js +149 -139
  3. package/dist/lib/config/aws/__t17fixture.d.ts +1 -0
  4. package/dist/lib/config/aws/__t17fixture.js +3 -0
  5. package/dist/lib/config/aws/__t17fixtureType.d.ts +2 -0
  6. package/dist/lib/config/aws/__t17fixtureType.js +1 -0
  7. package/dist/lib/config/aws/alarmTopic.js +8 -4
  8. package/dist/lib/config/aws/cloudTrail.js +1 -1
  9. package/dist/lib/config/aws/disasterRecovery.js +11 -16
  10. package/dist/lib/config/aws/ecrDefaultImage.d.ts +0 -1
  11. package/dist/lib/config/aws/ecrDefaultImage.js +13 -23
  12. package/dist/lib/config/aws/identityCenter.d.ts +10 -3
  13. package/dist/lib/config/aws/identityCenter.js +101 -37
  14. package/dist/lib/config/aws/identityCenterGroupMembership.js +8 -2
  15. package/dist/lib/config/aws/identityCenterMembership.d.ts +11 -0
  16. package/dist/lib/config/aws/identityCenterMembership.js +61 -0
  17. package/dist/lib/config/aws/index.d.ts +1 -1
  18. package/dist/lib/config/aws/index.js +1 -1
  19. package/dist/lib/config/aws/ipam.js +6 -11
  20. package/dist/lib/config/aws/oidcConnector.js +5 -1
  21. package/dist/lib/config/aws/scpPreset.js +4 -1
  22. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.d.ts +1 -0
  23. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.js +4 -0
  24. package/dist/lib/patterns/aws/account.js +7 -8
  25. package/dist/lib/patterns/aws/apexDomainPattern.js +10 -10
  26. package/dist/lib/patterns/aws/bastionFactory.d.ts +10 -0
  27. package/dist/lib/patterns/aws/bastionFactory.js +29 -0
  28. package/dist/lib/patterns/aws/buildkite.d.ts +2 -2
  29. package/dist/lib/patterns/aws/buildkite.js +51 -97
  30. package/dist/lib/patterns/aws/cdn.js +1 -1
  31. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +172 -0
  32. package/dist/lib/patterns/aws/clickhouseDatabase.js +600 -0
  33. package/dist/lib/patterns/aws/compute.d.ts +4 -6
  34. package/dist/lib/patterns/aws/compute.js +7 -13
  35. package/dist/lib/patterns/aws/computeEcs.d.ts +95 -396
  36. package/dist/lib/patterns/aws/computeEcs.js +880 -46
  37. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +889 -0
  38. package/dist/lib/patterns/aws/computeEcsTypes.js +12 -0
  39. package/dist/lib/patterns/aws/computeLambda.d.ts +0 -5
  40. package/dist/lib/patterns/aws/computeLambda.js +1 -2
  41. package/dist/lib/patterns/aws/database.d.ts +50 -8
  42. package/dist/lib/patterns/aws/database.js +183 -27
  43. package/dist/lib/patterns/aws/domain.js +8 -7
  44. package/dist/lib/patterns/aws/index.d.ts +3 -0
  45. package/dist/lib/patterns/aws/index.js +3 -0
  46. package/dist/lib/patterns/aws/interfaces/compute.d.ts +13 -1
  47. package/dist/lib/patterns/aws/interfaces/connector.d.ts +1 -1
  48. package/dist/lib/patterns/aws/interfaces/connector.js +1 -1
  49. package/dist/lib/patterns/aws/interfaces/database.d.ts +187 -8
  50. package/dist/lib/patterns/aws/interfaces/database.js +17 -3
  51. package/dist/lib/patterns/aws/interfaces/index.d.ts +4 -2
  52. package/dist/lib/patterns/aws/interfaces/index.js +4 -2
  53. package/dist/lib/patterns/aws/interfaces/messaging.d.ts +7 -0
  54. package/dist/lib/patterns/aws/interfaces/migrationContributor.d.ts +47 -0
  55. package/dist/lib/patterns/aws/interfaces/migrationContributor.js +9 -0
  56. package/dist/lib/patterns/aws/interfaces/vpcPeer.d.ts +7 -0
  57. package/dist/lib/patterns/aws/interfaces/vpcPeer.js +1 -0
  58. package/dist/lib/patterns/aws/messaging.d.ts +66 -10
  59. package/dist/lib/patterns/aws/messaging.js +115 -20
  60. package/dist/lib/patterns/aws/network.js +16 -7
  61. package/dist/lib/patterns/aws/organisation.d.ts +4 -0
  62. package/dist/lib/patterns/aws/organisation.js +24 -5
  63. package/dist/lib/patterns/aws/storage.d.ts +1 -2
  64. package/dist/lib/patterns/aws/storage.js +3 -2
  65. package/dist/lib/patterns/aws/vpcPeer.d.ts +34 -0
  66. package/dist/lib/patterns/aws/vpcPeer.js +38 -0
  67. package/dist/lib/patterns/aws/vpcPeerAccepter.d.ts +29 -0
  68. package/dist/lib/patterns/aws/vpcPeerAccepter.js +196 -0
  69. package/dist/lib/resources/aws/analytics/clickhouse.js +25 -7
  70. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +49 -0
  71. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +140 -0
  72. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +4 -4
  73. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +6 -4
  74. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +12 -0
  75. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -0
  76. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +56 -5
  77. package/dist/lib/resources/aws/analytics/index.d.ts +2 -0
  78. package/dist/lib/resources/aws/analytics/index.js +1 -0
  79. package/dist/lib/resources/aws/base/awsStack.js +4 -2
  80. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +2 -0
  81. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +11 -0
  82. package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.d.ts +52 -0
  83. package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.js +60 -0
  84. package/dist/lib/resources/aws/compute/blockDeviceVolume.d.ts +8 -0
  85. package/dist/lib/resources/aws/compute/blockDeviceVolume.js +10 -0
  86. package/dist/lib/resources/aws/compute/ec2.d.ts +132 -12
  87. package/dist/lib/resources/aws/compute/ec2.js +163 -23
  88. package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.d.ts +41 -0
  89. package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +194 -0
  90. package/dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs +458 -0
  91. package/dist/lib/resources/aws/compute/ecs.d.ts +27 -1
  92. package/dist/lib/resources/aws/compute/ecs.js +42 -2
  93. package/dist/lib/resources/aws/compute/ecsConstants.d.ts +9 -0
  94. package/dist/lib/resources/aws/compute/ecsConstants.js +16 -0
  95. package/dist/lib/resources/aws/compute/ecsImages.js +32 -20
  96. package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.d.ts +96 -0
  97. package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +113 -0
  98. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +2 -1
  99. package/dist/lib/resources/aws/compute/ecsNetworking.js +18 -6
  100. package/dist/lib/resources/aws/compute/ecsRemoteConnections.d.ts +38 -0
  101. package/dist/lib/resources/aws/compute/ecsRemoteConnections.js +80 -0
  102. package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +13 -4
  103. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +155 -33
  104. package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +31 -1
  105. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +110 -6
  106. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +180 -13
  107. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +9 -0
  108. package/dist/lib/resources/aws/compute/ecsValidation.js +63 -0
  109. package/dist/lib/resources/aws/compute/index.d.ts +2 -0
  110. package/dist/lib/resources/aws/compute/index.js +2 -0
  111. package/dist/lib/resources/aws/compute/lambda.d.ts +7 -13
  112. package/dist/lib/resources/aws/compute/lambda.js +30 -38
  113. package/dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs +192 -0
  114. package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +104 -0
  115. package/dist/lib/resources/aws/compute/persistentDataVolume.js +245 -0
  116. package/dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs +398 -0
  117. package/dist/lib/resources/aws/compute/samApplication.d.ts +15 -0
  118. package/dist/lib/resources/aws/compute/samApplication.js +27 -0
  119. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +159 -0
  120. package/dist/lib/resources/aws/database/clickhouseConstants.js +181 -0
  121. package/dist/lib/resources/aws/database/clickhouseSchemas.d.ts +71 -0
  122. package/dist/lib/resources/aws/database/clickhouseSchemas.js +157 -0
  123. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +14 -0
  124. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +23 -0
  125. package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +69 -0
  126. package/dist/lib/resources/aws/database/clickhouseUserData.js +371 -0
  127. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +56 -0
  128. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +112 -0
  129. package/dist/lib/resources/aws/database/rdsAurora.d.ts +8 -1
  130. package/dist/lib/resources/aws/database/rdsAurora.js +42 -32
  131. package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +15 -2
  132. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +39 -43
  133. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +6 -0
  134. package/dist/lib/resources/aws/database/rdsDefaults.js +7 -1
  135. package/dist/lib/resources/aws/database/rdsHelpers.d.ts +3 -3
  136. package/dist/lib/resources/aws/database/rdsHelpers.js +1 -0
  137. package/dist/lib/resources/aws/database/rdsInstance.d.ts +8 -1
  138. package/dist/lib/resources/aws/database/rdsInstance.js +51 -34
  139. package/dist/lib/resources/aws/database/rdsProxyOutput.d.ts +1 -1
  140. package/dist/lib/resources/aws/database/rdsProxyOutput.js +1 -1
  141. package/dist/lib/resources/aws/iam/delegationRole.js +12 -5
  142. package/dist/lib/resources/aws/iam/identityCenter/groupMembership.d.ts +9 -0
  143. package/dist/lib/resources/aws/iam/identityCenter/groupMembership.js +12 -0
  144. package/dist/lib/resources/aws/iam/identityCenter/index.d.ts +1 -0
  145. package/dist/lib/resources/aws/iam/identityCenter/index.js +1 -0
  146. package/dist/lib/resources/aws/iam/identityCenter/permissionSet.d.ts +1 -0
  147. package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +1 -0
  148. package/dist/lib/resources/aws/logging/logGroup.d.ts +0 -8
  149. package/dist/lib/resources/aws/logging/logGroup.js +0 -11
  150. package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +7 -0
  151. package/dist/lib/resources/aws/messaging/defaultEventBus.js +21 -0
  152. package/dist/lib/resources/aws/messaging/eventBridgeRule.d.ts +96 -0
  153. package/dist/lib/resources/aws/messaging/eventBridgeRule.js +110 -0
  154. package/dist/lib/resources/aws/messaging/eventTargets.d.ts +84 -0
  155. package/dist/lib/resources/aws/messaging/eventTargets.js +152 -0
  156. package/dist/lib/resources/aws/messaging/eventbridge.d.ts +25 -2
  157. package/dist/lib/resources/aws/messaging/eventbridge.js +22 -10
  158. package/dist/lib/resources/aws/messaging/index.d.ts +5 -0
  159. package/dist/lib/resources/aws/messaging/index.js +2 -0
  160. package/dist/lib/resources/aws/messaging/schedule.d.ts +118 -0
  161. package/dist/lib/resources/aws/messaging/schedule.js +64 -0
  162. package/dist/lib/resources/aws/messaging/sns.d.ts +2 -1
  163. package/dist/lib/resources/aws/messaging/sqs.d.ts +2 -1
  164. package/dist/lib/resources/aws/messaging/subscription.d.ts +112 -0
  165. package/dist/lib/resources/aws/messaging/subscription.js +67 -0
  166. package/dist/lib/resources/aws/messaging/utils.d.ts +6 -0
  167. package/dist/lib/resources/aws/messaging/utils.js +10 -0
  168. package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +60 -0
  169. package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +139 -0
  170. package/dist/lib/resources/aws/monitoring/index.d.ts +2 -0
  171. package/dist/lib/resources/aws/monitoring/index.js +2 -0
  172. package/dist/lib/resources/aws/monitoring/scheduleAlarms.d.ts +47 -0
  173. package/dist/lib/resources/aws/monitoring/scheduleAlarms.js +106 -0
  174. package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +6 -3
  175. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.d.ts +40 -0
  176. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +158 -0
  177. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +7 -4
  178. package/dist/lib/resources/aws/networking/domainCertificate.d.ts +2 -2
  179. package/dist/lib/resources/aws/networking/domainCertificate.js +6 -3
  180. package/dist/lib/resources/aws/networking/hostedZone.js +6 -4
  181. package/dist/lib/resources/aws/networking/index.d.ts +3 -0
  182. package/dist/lib/resources/aws/networking/index.js +3 -0
  183. package/dist/lib/resources/aws/networking/serviceDiscovery.d.ts +96 -0
  184. package/dist/lib/resources/aws/networking/serviceDiscovery.js +96 -0
  185. package/dist/lib/resources/aws/networking/vpc.d.ts +4 -1
  186. package/dist/lib/resources/aws/networking/vpc.js +10 -3
  187. package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.d.ts +18 -0
  188. package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.js +61 -0
  189. package/dist/lib/resources/aws/networking/vpcPeeringConnection.d.ts +49 -0
  190. package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +106 -0
  191. package/dist/lib/resources/aws/organisation/costAllocationTagActivator.d.ts +16 -5
  192. package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +17 -3
  193. package/dist/lib/resources/aws/organisation/index.d.ts +1 -1
  194. package/dist/lib/resources/aws/organisation/organisationPolicy.d.ts +2 -0
  195. package/dist/lib/resources/aws/organisation/organisationPolicy.js +3 -2
  196. package/dist/lib/resources/aws/secrets/secret.d.ts +7 -0
  197. package/dist/lib/resources/aws/secrets/secret.js +4 -3
  198. package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +16 -0
  199. package/dist/lib/resources/aws/storage/bucketDeployment.js +17 -0
  200. package/dist/lib/resources/aws/storage/ecr.js +5 -5
  201. package/dist/lib/resources/aws/storage/index.d.ts +1 -0
  202. package/dist/lib/resources/aws/storage/index.js +1 -0
  203. package/dist/lib/resources/aws/storage/s3.js +10 -3
  204. package/dist/lib/resources/aws/utilities/customResource.js +18 -9
  205. package/dist/lib/synth_dump.d.ts +1 -0
  206. package/dist/lib/synth_dump.js +42 -0
  207. package/dist/lib/utils/bastionFactory.d.ts +10 -0
  208. package/dist/lib/utils/bastionFactory.js +29 -0
  209. package/dist/lib/utils/capitaliseString.d.ts +1 -1
  210. package/dist/lib/utils/capitaliseString.js +1 -1
  211. package/dist/lib/utils/cdkContext.d.ts +10 -0
  212. package/dist/lib/utils/cdkContext.js +13 -0
  213. package/dist/lib/utils/connections.d.ts +7 -1
  214. package/dist/lib/utils/connections.js +21 -0
  215. package/dist/lib/utils/connector.d.ts +30 -2
  216. package/dist/lib/utils/connector.js +6 -1
  217. package/dist/lib/utils/costAllocationTags.d.ts +15 -0
  218. package/dist/lib/utils/costAllocationTags.js +16 -0
  219. package/dist/lib/utils/databaseTypes.d.ts +14 -0
  220. package/dist/lib/utils/getConfig.d.ts +2 -0
  221. package/dist/lib/utils/getConfig.js +2 -0
  222. package/dist/lib/utils/index.d.ts +4 -0
  223. package/dist/lib/utils/index.js +4 -0
  224. package/dist/lib/utils/manifestWriter.d.ts +6 -89
  225. package/dist/lib/utils/manifestWriter.js +36 -23
  226. package/dist/lib/utils/migrationVersionResolvers.d.ts +2 -0
  227. package/dist/lib/utils/migrationVersionResolvers.js +2 -0
  228. package/dist/lib/utils/orgConfigParser.js +2 -1
  229. package/dist/lib/utils/resolveAlertsTopic.d.ts +14 -0
  230. package/dist/lib/utils/resolveAlertsTopic.js +30 -0
  231. package/dist/lib/utils/validationLogger.js +6 -3
  232. package/dist/lib/utils/vpcPeerInterface.d.ts +22 -0
  233. package/dist/lib/utils/vpcPeerInterface.js +1 -0
  234. package/package.json +22 -18
@@ -1,2 +1,4 @@
1
1
  export { default as ClickHouse } from "./clickhouse.js";
2
2
  export type { ClickHouseProps, ClickHouseR2Config, ClickHouseOutputs } from "./clickhouseTypes.js";
3
+ export { createClickHouseAlarms } from "./clickhouseAlarms.js";
4
+ export type { ClickHouseAlarmThresholds, ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
@@ -1 +1,2 @@
1
1
  export { default as ClickHouse } from "./clickhouse.js";
2
+ export { createClickHouseAlarms } from "./clickhouseAlarms.js";
@@ -1,4 +1,4 @@
1
- import { Stack } from "aws-cdk-lib";
1
+ import { Annotations, Stack } from "aws-cdk-lib";
2
2
  import { Port } from "aws-cdk-lib/aws-ec2";
3
3
  import { Construct } from "constructs";
4
4
  import App from "../../../app.js";
@@ -27,7 +27,9 @@ export class AwsStack {
27
27
  }
28
28
  }
29
29
  getCdkStack(id, props) {
30
- return new Stack(App.getInstance(), id, this.getStackProps(props));
30
+ const stack = new Stack(App.getInstance(), id, this.getStackProps(props));
31
+ Annotations.of(stack).acknowledgeWarning("@aws-cdk/aws-ec2:ipv4IgnoreEgressRule");
32
+ return stack;
31
33
  }
32
34
  getStackProps(props) {
33
35
  // If no explicit props are provided, fall back to the account/region that
@@ -0,0 +1,2 @@
1
+ import { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
2
+ export declare function regression(asg: AutoScalingGroup, queue: any, id: string): void;
@@ -0,0 +1,11 @@
1
+ import { DefaultResult, LifecycleTransition } from "aws-cdk-lib/aws-autoscaling";
2
+ import { QueueHook } from "aws-cdk-lib/aws-autoscaling-hooktargets";
3
+ import { Duration } from "aws-cdk-lib";
4
+ export function regression(asg, queue, id) {
5
+ asg.addLifecycleHook(`${id}LaunchingHook`, {
6
+ lifecycleTransition: LifecycleTransition.INSTANCE_LAUNCHING,
7
+ defaultResult: DefaultResult.ABANDON,
8
+ heartbeatTimeout: Duration.seconds(300),
9
+ notificationTarget: new QueueHook(queue)
10
+ });
11
+ }
@@ -0,0 +1,52 @@
1
+ import { type AutoScalingGroup, type DefaultResult, type LifecycleTransition } from "aws-cdk-lib/aws-autoscaling";
2
+ import { type Construct } from "constructs";
3
+ export interface InlineAsgLifecycleHookProps {
4
+ /** ASG to attach the hook to. */
5
+ autoScalingGroup: AutoScalingGroup;
6
+ /** Lifecycle hook name — must be unique within the ASG. */
7
+ hookName: string;
8
+ /** EC2_INSTANCE_LAUNCHING or EC2_INSTANCE_TERMINATING. */
9
+ lifecycleTransition: LifecycleTransition;
10
+ /** Action when heartbeat elapses. */
11
+ defaultResult: DefaultResult;
12
+ /** Heartbeat window before defaultResult fires, in seconds. */
13
+ heartbeatTimeoutSeconds: number;
14
+ }
15
+ /**
16
+ * Atomically attach an ASG lifecycle hook by appending a
17
+ * `LifecycleHookSpecification` to the ASG's `LifecycleHookSpecificationList`
18
+ * (CFN property on `AWS::AutoScaling::AutoScalingGroup`) instead of emitting
19
+ * a standalone `AWS::AutoScaling::LifecycleHook` resource.
20
+ *
21
+ * Standalone `AWS::AutoScaling::LifecycleHook` resources are created AFTER
22
+ * the ASG. CFN starts the ASG's desiredCapacity ramp as part of ASG creation,
23
+ * not as a separate step, so on a fresh stack the first instance launches
24
+ * BEFORE the hook is attached and the hook fires zero notifications for that
25
+ * instance. `LifecycleHookSpecificationList` is part of the ASG's own CFN
26
+ * payload — the ASG is never in a state where it has instances but no hooks.
27
+ *
28
+ * No `NotificationTargetARN` / `RoleARN` is set. AWS rejects ASG creation when
29
+ * the inline `LifecycleHookSpecificationList` contains two entries with
30
+ * different `NotificationTargetARN` values:
31
+ *
32
+ * "NotificationTargetARN should be the same for all Lifecycle Hooks"
33
+ *
34
+ * The standalone-hook form permits per-hook targets, but inline does not. Two
35
+ * Fjall consumers (`PersistentDataVolume` LAUNCHING + `Ec2GracefulTerminationHandler`
36
+ * TERMINATING) each own their own SQS queue, so a shared target is impossible.
37
+ *
38
+ * Routing is therefore delegated to EventBridge. ASG natively emits
39
+ * `EC2 Instance-launch Lifecycle Action` / `EC2 Instance-terminate Lifecycle Action`
40
+ * events on the account+region default bus for every lifecycle hook regardless
41
+ * of whether a notification target is configured. Each consumer attaches a
42
+ * `Subscription` (from `lib/resources/aws/messaging/subscription.ts`) whose
43
+ * event pattern discriminates by `AutoScalingGroupName` + `LifecycleHookName`,
44
+ * targeting the consumer's own SQS queue. The Lambda sees the EventBridge event
45
+ * envelope and reads `detail.LifecycleActionToken` etc. from the unwrapped
46
+ * detail.
47
+ *
48
+ * Multiple consumers may call this helper against the same ASG — the existing
49
+ * spec list is read, the new entry appended, and the merged array assigned
50
+ * back. Synth is sequential per scope, so the merge is race-free.
51
+ */
52
+ export declare function attachInlineAsgLifecycleHook(_scope: Construct, _id: string, props: InlineAsgLifecycleHookProps): void;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Atomically attach an ASG lifecycle hook by appending a
3
+ * `LifecycleHookSpecification` to the ASG's `LifecycleHookSpecificationList`
4
+ * (CFN property on `AWS::AutoScaling::AutoScalingGroup`) instead of emitting
5
+ * a standalone `AWS::AutoScaling::LifecycleHook` resource.
6
+ *
7
+ * Standalone `AWS::AutoScaling::LifecycleHook` resources are created AFTER
8
+ * the ASG. CFN starts the ASG's desiredCapacity ramp as part of ASG creation,
9
+ * not as a separate step, so on a fresh stack the first instance launches
10
+ * BEFORE the hook is attached and the hook fires zero notifications for that
11
+ * instance. `LifecycleHookSpecificationList` is part of the ASG's own CFN
12
+ * payload — the ASG is never in a state where it has instances but no hooks.
13
+ *
14
+ * No `NotificationTargetARN` / `RoleARN` is set. AWS rejects ASG creation when
15
+ * the inline `LifecycleHookSpecificationList` contains two entries with
16
+ * different `NotificationTargetARN` values:
17
+ *
18
+ * "NotificationTargetARN should be the same for all Lifecycle Hooks"
19
+ *
20
+ * The standalone-hook form permits per-hook targets, but inline does not. Two
21
+ * Fjall consumers (`PersistentDataVolume` LAUNCHING + `Ec2GracefulTerminationHandler`
22
+ * TERMINATING) each own their own SQS queue, so a shared target is impossible.
23
+ *
24
+ * Routing is therefore delegated to EventBridge. ASG natively emits
25
+ * `EC2 Instance-launch Lifecycle Action` / `EC2 Instance-terminate Lifecycle Action`
26
+ * events on the account+region default bus for every lifecycle hook regardless
27
+ * of whether a notification target is configured. Each consumer attaches a
28
+ * `Subscription` (from `lib/resources/aws/messaging/subscription.ts`) whose
29
+ * event pattern discriminates by `AutoScalingGroupName` + `LifecycleHookName`,
30
+ * targeting the consumer's own SQS queue. The Lambda sees the EventBridge event
31
+ * envelope and reads `detail.LifecycleActionToken` etc. from the unwrapped
32
+ * detail.
33
+ *
34
+ * Multiple consumers may call this helper against the same ASG — the existing
35
+ * spec list is read, the new entry appended, and the merged array assigned
36
+ * back. Synth is sequential per scope, so the merge is race-free.
37
+ */
38
+ export function attachInlineAsgLifecycleHook(_scope, _id, props) {
39
+ const cfnAsg = props.autoScalingGroup.node
40
+ .defaultChild;
41
+ const existing = readLifecycleHookSpecList(cfnAsg);
42
+ cfnAsg.lifecycleHookSpecificationList = [
43
+ ...existing,
44
+ {
45
+ lifecycleHookName: props.hookName,
46
+ lifecycleTransition: props.lifecycleTransition,
47
+ defaultResult: props.defaultResult,
48
+ heartbeatTimeout: props.heartbeatTimeoutSeconds
49
+ }
50
+ ];
51
+ }
52
+ function readLifecycleHookSpecList(cfnAsg) {
53
+ const current = cfnAsg.lifecycleHookSpecificationList;
54
+ if (current === undefined)
55
+ return [];
56
+ if (Array.isArray(current)) {
57
+ return current;
58
+ }
59
+ throw new Error("Cannot append to lifecycleHookSpecificationList: existing value is an IResolvable — refactor the override to call attachInlineAsgLifecycleHook");
60
+ }
@@ -0,0 +1,8 @@
1
+ import { BlockDeviceVolume, EbsDeviceVolumeType } from "aws-cdk-lib/aws-ec2";
2
+ export interface SafeEbsOptions {
3
+ volumeType?: EbsDeviceVolumeType;
4
+ iops?: number;
5
+ throughput?: number;
6
+ deleteOnTermination?: boolean;
7
+ }
8
+ export declare function safeEbs(sizeGiB: number, opts?: SafeEbsOptions): BlockDeviceVolume;
@@ -0,0 +1,10 @@
1
+ import { BlockDeviceVolume, EbsDeviceVolumeType } from "aws-cdk-lib/aws-ec2";
2
+ export function safeEbs(sizeGiB, opts = {}) {
3
+ return BlockDeviceVolume.ebs(sizeGiB, {
4
+ encrypted: true,
5
+ volumeType: opts.volumeType ?? EbsDeviceVolumeType.GP3,
6
+ iops: opts.iops,
7
+ throughput: opts.throughput,
8
+ deleteOnTermination: opts.deleteOnTermination
9
+ });
10
+ }
@@ -1,9 +1,39 @@
1
- import { type BlockDevice, type IMachineImage, type IVpc, type UserData, SecurityGroup, type IConnectable, Connections, type SubnetConfiguration } from "aws-cdk-lib/aws-ec2";
1
+ import { type BlockDevice, type IMachineImage, type IVpc, type UserData, type ISecurityGroup, type IConnectable, Connections, type SubnetConfiguration, type SubnetSelection } from "aws-cdk-lib/aws-ec2";
2
2
  import { Construct } from "constructs";
3
- import { Stack, type StackProps } from "aws-cdk-lib";
3
+ import { Duration, Stack, type StackProps } from "aws-cdk-lib";
4
4
  import { type Role } from "aws-cdk-lib/aws-iam";
5
- import { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
6
- interface Ec2InstanceProps extends StackProps {
5
+ import { AutoScalingGroup, Monitoring, type WarmPoolOptions } from "aws-cdk-lib/aws-autoscaling";
6
+ import { type PersistentDataVolumeProps } from "./persistentDataVolume.js";
7
+ export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup">;
8
+ /**
9
+ * Caller-supplied ASG `UpdatePolicy` resolution. Default (`undefined`) →
10
+ * `rollingUpdate({ minInstancesInService: 0, maxBatchSize: 1, pauseTime:
11
+ * Duration.minutes(5) })`. The rolling shape propagates userdata mutations
12
+ * to running instances on every `LaunchTemplateVersion` change; data
13
+ * continuity for stateful patterns is preserved by `persistentDataVolume`.
14
+ *
15
+ * Variants:
16
+ * - `rollingUpdate` — single-instance roll; `pauseTime` overridable.
17
+ * `minInstancesInService` and `maxBatchSize` are fixed at `0` and `1`.
18
+ * - `replacingUpdate` — explicit opt-in to the legacy full-replacement shape.
19
+ * No Fjall consumer uses this today; retained for future bare-EC2 patterns.
20
+ * - `none` — no caller-driven `UpdatePolicy` (omits both
21
+ * `AutoScalingRollingUpdate` and `AutoScalingReplacingUpdate`). CDK still
22
+ * auto-emits `AutoScalingScheduledAction.IgnoreUnmodifiedGroupSizeProperties`
23
+ * regardless. For callers owning rollout externally.
24
+ *
25
+ * See `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
26
+ * ClickHouse-specific downtime expectations.
27
+ */
28
+ export type Ec2InstanceUpdatePolicyConfig = {
29
+ type: "rollingUpdate";
30
+ pauseTime?: Duration;
31
+ } | {
32
+ type: "replacingUpdate";
33
+ } | {
34
+ type: "none";
35
+ };
36
+ export interface Ec2InstanceProps extends StackProps {
7
37
  spotCapacityPercentage?: number;
8
38
  blockDevices?: BlockDevice[];
9
39
  accountId?: string;
@@ -12,32 +42,122 @@ interface Ec2InstanceProps extends StackProps {
12
42
  subnetConfiguration?: SubnetConfiguration[];
13
43
  minCapacity?: number;
14
44
  maxCapacity?: number;
45
+ /** CDK `AutoScalingGroupProps.desiredCapacity` — initial instance count. */
46
+ desiredCapacity?: number;
15
47
  instanceType: string;
16
48
  machineImage?: IMachineImage;
17
49
  userData?: UserData;
18
50
  role?: Role;
19
51
  enableSSH?: boolean;
20
52
  defaultPort?: number;
53
+ /**
54
+ * Caller-supplied EC2 instance monitoring resolution. Routes through the
55
+ * LaunchTemplate's `detailedMonitoring` field (the AWS-side source of truth
56
+ * for instances launched through a launch template — the ASG-level
57
+ * `instanceMonitoring` prop is silently ignored by CDK whenever a
58
+ * `launchTemplate` or `mixedInstancesPolicy` is set, which is always the
59
+ * case in this construct). Translation: `Monitoring.DETAILED` → `true`,
60
+ * `Monitoring.BASIC` → `false`. Absent → existing default of `true`
61
+ * (1-minute metrics) is preserved.
62
+ */
63
+ instanceMonitoring?: Monitoring;
64
+ /**
65
+ * Externally-supplied security group. When provided, `Ec2Instance` does not
66
+ * create its own `AsgSecurityGroup`; the supplied SG is used by the launch
67
+ * template, exposed via `this.asgSecurityGroup`, and threaded into the
68
+ * `IConnectable` view. Callers retain full ownership.
69
+ */
70
+ securityGroup?: ISecurityGroup;
71
+ /**
72
+ * CDK `AutoScalingGroupProps.vpcSubnets`. Overrides the default
73
+ * `enableSSH ? PUBLIC : resolvePrivateSubnetType(vpc)` inference. Use when
74
+ * the caller needs a precise subnet selection (e.g. AZ pinning).
75
+ */
76
+ vpcSubnets?: SubnetSelection;
77
+ /** CDK `AutoScalingGroupProps.capacityRebalance`. Absent → CDK default. */
78
+ capacityRebalance?: boolean;
79
+ /**
80
+ * CDK `aws-cdk-lib/aws-autoscaling.WarmPoolOptions` verbatim — `minSize`,
81
+ * `maxGroupPreparedCapacity`, `poolState`, `reuseOnScaleIn`. When present,
82
+ * `asg.addWarmPool(warmPool)` is called once after ASG construction.
83
+ */
84
+ warmPool?: WarmPoolOptions;
85
+ /**
86
+ * CDK `LaunchTemplateProps.associatePublicIpAddress`. When defined,
87
+ * overrides the `!!keyPair` auto-derivation; otherwise the existing
88
+ * keyPair-driven default applies.
89
+ */
90
+ associatePublicIpAddress?: boolean;
91
+ /**
92
+ * ECS cluster ARN — when set, the graceful-termination Lambda also drains
93
+ * and deregisters the container instance before generic cleanup. Empty
94
+ * string is treated as unset (rejecting Pitfall 9 / env-var-truthy traps).
95
+ * Bare-EC2 consumers (bastion, Fivetran) leave this unset.
96
+ */
97
+ ecsClusterArn?: string;
98
+ /**
99
+ * Pairs the ASG with a standalone EBS data volume that re-attaches across
100
+ * instance refreshes. When set, requires `vpcSubnets.availabilityZones` to
101
+ * be exactly one entry (matching `persistentDataVolume.availabilityZone`);
102
+ * the wrapper's volume is AZ-local and cannot follow a multi-AZ ASG.
103
+ * Forwards the wrapper's `ownerLogicalId` into the graceful-termination
104
+ * Lambda so the TERMINATING and LAUNCHING handlers locate the same volume.
105
+ */
106
+ persistentDataVolume?: Ec2InstancePersistentDataVolumeConfig;
107
+ /**
108
+ * ASG `UpdatePolicy` resolution. Absent →
109
+ * `UpdatePolicy.rollingUpdate({ minInstancesInService: 0, maxBatchSize: 1,
110
+ * pauseTime: Duration.minutes(5) })`. Userdata mutations propagate to
111
+ * running instances via a single-batch rolling roll. See
112
+ * `Ec2InstanceUpdatePolicyConfig` for the variant menu and
113
+ * `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
114
+ * downtime-window runbook.
115
+ */
116
+ updatePolicy?: Ec2InstanceUpdatePolicyConfig;
117
+ /**
118
+ * Tags applied to the underlying ASG with
119
+ * `applyToLaunchedInstances: true` so every launched EC2 instance carries
120
+ * the tags. Used for tag-based SSM `SendCommand` targeting
121
+ * (`Targets: [{ Key: "tag:<name>", Values: [<value>] }]`). Empty-string
122
+ * keys or values are rejected by `validateEc2InstanceProps`.
123
+ */
124
+ tags?: Record<string, string>;
21
125
  }
22
126
  export declare class Ec2Instance extends Construct implements IConnectable {
23
127
  private launchTemplate;
24
128
  vpc: IVpc;
25
- asgSecurityGroup: SecurityGroup;
129
+ asgSecurityGroup: ISecurityGroup;
26
130
  private autoScalingGroup;
27
131
  private keyPair;
28
- connections: Connections;
132
+ private persistentDataVolume?;
133
+ readonly connections: Connections;
29
134
  constructor(scope: Construct, id: string, props: Ec2InstanceProps);
30
- addVpc(props: Ec2InstanceProps): void;
31
- addKeyPair(props: Ec2InstanceProps): void;
32
- addLaunchTemplate(props: Ec2InstanceProps): void;
33
- addAutoScalingGroup(props: Ec2InstanceProps): void;
135
+ private addVpc;
136
+ private addKeyPair;
137
+ private addLaunchTemplate;
138
+ private addAutoScalingGroup;
139
+ /**
140
+ * Apply `props.tags` to the underlying ASG with
141
+ * `applyToLaunchedInstances: true` so the CFN ASG `Tags` array carries
142
+ * `{ Key, Value, PropagateAtLaunch: true }` for each entry. Enables
143
+ * tag-based SSM `SendCommand` targeting on the launched EC2 instances.
144
+ */
145
+ private applyInstanceTags;
34
146
  /**
35
147
  * Get the Auto Scaling Group.
36
148
  */
37
149
  getAutoScalingGroup(): AutoScalingGroup;
38
- suspendAutoScaling(_props: Ec2InstanceProps): void;
150
+ /**
151
+ * Wire an ASG `EC2_INSTANCE_TERMINATING` hook + Lambda so instances
152
+ * terminate cleanly during stack updates and stack deletes. Drain ownership
153
+ * lives at the EC2 layer; ECS-specific drain plugs in via `ecsClusterArn`
154
+ * so a single hook handles bare-EC2 (bastion, Fivetran) and ECS-wired
155
+ * ASGs (ClickHouse) without duplicate plumbing in the ECS layer.
156
+ */
157
+ private addGracefulTerminationHandler;
158
+ private addPersistentDataVolume;
159
+ private suspendAutoScaling;
39
160
  }
40
161
  export declare class Ec2InstanceStack extends Stack {
41
162
  constructor(scope: Construct, id: string, props: Ec2InstanceProps);
42
163
  }
43
- export {};
@@ -1,27 +1,75 @@
1
1
  import { InstanceType, LaunchTemplate, SubnetType, MachineImage, LaunchTemplateHttpTokens, KeyPair, SecurityGroup, Peer, Port, Connections } from "aws-cdk-lib/aws-ec2";
2
2
  import { Construct } from "constructs";
3
3
  import { resolvePrivateSubnetType } from "../../../utils/vpcUtils.js";
4
- import { Duration, Stack } from "aws-cdk-lib";
5
- import { AutoScalingGroup, GroupMetrics, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
4
+ import { safeEbs } from "./blockDeviceVolume.js";
5
+ import { Duration, Stack, Tags } from "aws-cdk-lib";
6
+ import { AutoScalingGroup, GroupMetrics, Monitoring, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
6
7
  import { AwsCustomResource } from "../utilities/awsCustomResource.js";
7
8
  import { PhysicalResourceId } from "aws-cdk-lib/custom-resources";
8
9
  import { Vpc } from "../networking/vpc.js";
10
+ import { Ec2GracefulTerminationHandler } from "./ec2GracefulTerminationHandler.js";
11
+ import { PersistentDataVolume } from "./persistentDataVolume.js";
12
+ /**
13
+ * SOC2-uniform default root device when callers omit `blockDevices`.
14
+ * `/dev/xvda` is the AL2023 / EcsOptimizedImage root device name and matches
15
+ * every Linux AMI consumed by `Ec2Instance` today (ECS, Buildkite, ClickHouse).
16
+ * `encrypted: true` enforces EBS encryption at the wrapper layer rather than
17
+ * relying on the AWS account-level "encryption by default" setting — the
18
+ * propagation contract documented in
19
+ * `.claude/rules/generator-standards.md § "Wrapper Routing Discipline"`.
20
+ */
21
+ const DEFAULT_ROOT_DEVICE_NAME = "/dev/xvda";
22
+ const DEFAULT_ROOT_VOLUME_SIZE_GIB = 30;
23
+ /**
24
+ * Resolve the caller's `updatePolicy` prop to a CDK `UpdatePolicy`. Default
25
+ * (`config` absent) → single-instance rolling roll on every
26
+ * `LaunchTemplateVersion` change. The `none` branch returns `undefined` so
27
+ * `addAutoScalingGroup` can conditionally spread the field — passing
28
+ * `updatePolicy: undefined` directly emits an empty `UpdatePolicy: {}` block
29
+ * on some CDK versions.
30
+ *
31
+ * Free function rather than a private method: `this` state is not read, and
32
+ * a free function is easier to reach from focused tests.
33
+ */
34
+ function resolveUpdatePolicy(config) {
35
+ const resolved = config ?? { type: "rollingUpdate" };
36
+ switch (resolved.type) {
37
+ case "none":
38
+ return undefined;
39
+ case "replacingUpdate":
40
+ return UpdatePolicy.replacingUpdate();
41
+ case "rollingUpdate":
42
+ return UpdatePolicy.rollingUpdate({
43
+ minInstancesInService: 0,
44
+ maxBatchSize: 1,
45
+ pauseTime: resolved.pauseTime ?? Duration.minutes(5)
46
+ });
47
+ default:
48
+ return assertNever(resolved);
49
+ }
50
+ }
51
+ function assertNever(value) {
52
+ throw new Error(`Ec2Instance.updatePolicy: unrecognised config ${JSON.stringify(value)}`);
53
+ }
9
54
  export class Ec2Instance extends Construct {
10
55
  launchTemplate;
11
56
  vpc;
12
57
  asgSecurityGroup;
13
58
  autoScalingGroup;
14
59
  keyPair;
60
+ persistentDataVolume;
15
61
  connections;
16
62
  constructor(scope, id, props) {
17
63
  super(scope, id);
64
+ validateEc2InstanceProps(props);
18
65
  this.addVpc(props);
19
66
  this.addKeyPair(props);
20
67
  this.addLaunchTemplate(props);
21
68
  this.addAutoScalingGroup(props);
69
+ this.applyInstanceTags(props);
22
70
  this.suspendAutoScaling(props);
23
- // Initialise connections with the security group and optional defaultPort
24
- // When defaultPort is set, other resources can use allowToDefaultPort(ec2Instance)
71
+ this.addPersistentDataVolume(props);
72
+ this.addGracefulTerminationHandler(props);
25
73
  this.connections = new Connections({
26
74
  securityGroups: [this.asgSecurityGroup],
27
75
  defaultPort: props.defaultPort ? Port.tcp(props.defaultPort) : undefined
@@ -43,52 +91,76 @@ export class Ec2Instance extends Construct {
43
91
  this.keyPair = undefined;
44
92
  }
45
93
  else {
46
- // TODO: Break out into a separate construct for use with better prop handling
47
94
  this.keyPair = new KeyPair(this, "KeyPair", {
48
95
  keyPairName: `${props.serviceName}KeyPair`
49
96
  });
50
97
  }
51
98
  }
52
99
  addLaunchTemplate(props) {
53
- this.asgSecurityGroup = new SecurityGroup(this, `AsgSecurityGroup`, {
54
- vpc: this.vpc,
55
- description: `Security group for the ${props.serviceName} auto scaling group`
56
- });
100
+ if (props.securityGroup !== undefined) {
101
+ this.asgSecurityGroup = props.securityGroup;
102
+ }
103
+ else {
104
+ this.asgSecurityGroup = new SecurityGroup(this, `AsgSecurityGroup`, {
105
+ vpc: this.vpc,
106
+ description: `Security group for the ${props.serviceName} auto scaling group`
107
+ });
108
+ }
57
109
  if (props.enableSSH) {
58
110
  this.asgSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(22), "Allow SSH");
59
111
  }
60
112
  this.launchTemplate = new LaunchTemplate(this, "LaunchTemplate", {
61
113
  launchTemplateName: `${props.serviceName}LaunchTemplate`,
62
- instanceType: new InstanceType(`${props.instanceType}`),
63
- machineImage: props.machineImage || MachineImage.latestAmazonLinux2023(),
114
+ instanceType: new InstanceType(props.instanceType),
115
+ machineImage: props.machineImage ?? MachineImage.latestAmazonLinux2023(),
64
116
  userData: props.userData,
65
117
  role: props.role,
66
- blockDevices: props?.blockDevices,
118
+ blockDevices: props.blockDevices ?? [
119
+ {
120
+ deviceName: DEFAULT_ROOT_DEVICE_NAME,
121
+ volume: safeEbs(DEFAULT_ROOT_VOLUME_SIZE_GIB)
122
+ }
123
+ ],
67
124
  securityGroup: this.asgSecurityGroup,
68
- detailedMonitoring: true,
125
+ detailedMonitoring: props.instanceMonitoring === undefined
126
+ ? true
127
+ : props.instanceMonitoring === Monitoring.DETAILED,
69
128
  requireImdsv2: true,
70
129
  httpPutResponseHopLimit: 2,
71
130
  httpTokens: LaunchTemplateHttpTokens.REQUIRED,
72
131
  instanceMetadataTags: true,
73
132
  keyPair: this.keyPair,
74
- associatePublicIpAddress: !!this.keyPair
133
+ associatePublicIpAddress: props.associatePublicIpAddress ?? !!this.keyPair
75
134
  });
76
135
  }
77
136
  addAutoScalingGroup(props) {
78
- // TODO: Handle terminating EC2 instances when updating, currently hangs.
137
+ const vpcSubnets = props.vpcSubnets ?? {
138
+ subnetType: props.enableSSH
139
+ ? SubnetType.PUBLIC
140
+ : resolvePrivateSubnetType(this.vpc)
141
+ };
142
+ const resolvedUpdatePolicy = resolveUpdatePolicy(props.updatePolicy);
79
143
  const baseAsgProps = {
80
144
  vpc: this.vpc,
81
- vpcSubnets: {
82
- subnetType: props.enableSSH
83
- ? SubnetType.PUBLIC
84
- : resolvePrivateSubnetType(this.vpc)
85
- },
145
+ vpcSubnets,
86
146
  minCapacity: props.minCapacity,
87
147
  maxCapacity: props.maxCapacity,
148
+ ...(props.desiredCapacity !== undefined && {
149
+ desiredCapacity: props.desiredCapacity
150
+ }),
151
+ ...(props.capacityRebalance !== undefined && {
152
+ capacityRebalance: props.capacityRebalance
153
+ }),
88
154
  cooldown: Duration.seconds(60),
89
155
  groupMetrics: [GroupMetrics.all()],
90
- updatePolicy: UpdatePolicy.replacingUpdate(),
91
- newInstancesProtectedFromScaleIn: true,
156
+ ...(resolvedUpdatePolicy !== undefined && {
157
+ updatePolicy: resolvedUpdatePolicy
158
+ }),
159
+ // ECS task drain on terminate is handled by the EC2_INSTANCE_TERMINATING
160
+ // lifecycle hook + Ec2GracefulTerminationHandler (300s heartbeat), not by
161
+ // managed termination protection. Leaving instances scale-in-protected
162
+ // wedges CFN rollback because ASG cannot terminate the instance.
163
+ newInstancesProtectedFromScaleIn: false,
92
164
  terminationPolicies: [
93
165
  TerminationPolicy.OLDEST_LAUNCH_CONFIGURATION,
94
166
  TerminationPolicy.CLOSEST_TO_NEXT_INSTANCE_HOUR
@@ -98,7 +170,7 @@ export class Ec2Instance extends Construct {
98
170
  // Support spot instances via mixed instances policy
99
171
  if (props.spotCapacityPercentage !== undefined &&
100
172
  props.spotCapacityPercentage > 0) {
101
- const spotPercentage = Math.min(100, Math.max(0, props.spotCapacityPercentage));
173
+ const spotPercentage = Math.min(100, props.spotCapacityPercentage);
102
174
  const onDemandPercentage = 100 - spotPercentage;
103
175
  this.autoScalingGroup = new AutoScalingGroup(this, "AutoScalingGroup", {
104
176
  ...baseAsgProps,
@@ -119,6 +191,24 @@ export class Ec2Instance extends Construct {
119
191
  launchTemplate: this.launchTemplate
120
192
  });
121
193
  }
194
+ if (props.warmPool !== undefined) {
195
+ this.autoScalingGroup.addWarmPool(props.warmPool);
196
+ }
197
+ }
198
+ /**
199
+ * Apply `props.tags` to the underlying ASG with
200
+ * `applyToLaunchedInstances: true` so the CFN ASG `Tags` array carries
201
+ * `{ Key, Value, PropagateAtLaunch: true }` for each entry. Enables
202
+ * tag-based SSM `SendCommand` targeting on the launched EC2 instances.
203
+ */
204
+ applyInstanceTags(props) {
205
+ if (props.tags === undefined)
206
+ return;
207
+ for (const [key, value] of Object.entries(props.tags)) {
208
+ Tags.of(this.autoScalingGroup).add(key, value, {
209
+ applyToLaunchedInstances: true
210
+ });
211
+ }
122
212
  }
123
213
  /**
124
214
  * Get the Auto Scaling Group.
@@ -126,6 +216,34 @@ export class Ec2Instance extends Construct {
126
216
  getAutoScalingGroup() {
127
217
  return this.autoScalingGroup;
128
218
  }
219
+ /**
220
+ * Wire an ASG `EC2_INSTANCE_TERMINATING` hook + Lambda so instances
221
+ * terminate cleanly during stack updates and stack deletes. Drain ownership
222
+ * lives at the EC2 layer; ECS-specific drain plugs in via `ecsClusterArn`
223
+ * so a single hook handles bare-EC2 (bastion, Fivetran) and ECS-wired
224
+ * ASGs (ClickHouse) without duplicate plumbing in the ECS layer.
225
+ */
226
+ addGracefulTerminationHandler(props) {
227
+ const dataVolumeOwnerLogicalId = this.persistentDataVolume?.ownerLogicalId;
228
+ new Ec2GracefulTerminationHandler(this, `${props.serviceName}GracefulTermination`, {
229
+ autoScalingGroup: this.autoScalingGroup,
230
+ ...(props.ecsClusterArn !== undefined &&
231
+ props.ecsClusterArn !== "" && {
232
+ ecsClusterArn: props.ecsClusterArn
233
+ }),
234
+ ...(dataVolumeOwnerLogicalId !== undefined && {
235
+ dataVolumeOwnerLogicalId
236
+ })
237
+ });
238
+ }
239
+ addPersistentDataVolume(props) {
240
+ if (props.persistentDataVolume === undefined)
241
+ return;
242
+ this.persistentDataVolume = new PersistentDataVolume(this, `${props.serviceName}PersistentDataVolume`, {
243
+ autoScalingGroup: this.autoScalingGroup,
244
+ ...props.persistentDataVolume
245
+ });
246
+ }
129
247
  suspendAutoScaling(_props) {
130
248
  const suspendCall = {
131
249
  service: "AutoScaling",
@@ -143,9 +261,31 @@ export class Ec2Instance extends Construct {
143
261
  });
144
262
  }
145
263
  }
264
+ function validateEc2InstanceProps(props) {
265
+ if (props.persistentDataVolume !== undefined) {
266
+ const azs = props.vpcSubnets?.availabilityZones;
267
+ if (azs === undefined || azs.length !== 1) {
268
+ throw new Error(`Ec2Instance.persistentDataVolume requires vpcSubnets.availabilityZones to be exactly 1 entry; got ${azs?.length ?? 0}`);
269
+ }
270
+ if (azs[0] !== props.persistentDataVolume.availabilityZone) {
271
+ throw new Error(`Ec2Instance.persistentDataVolume.availabilityZone must match vpcSubnets.availabilityZones[0]; got '${props.persistentDataVolume.availabilityZone}' vs '${azs[0]}'`);
272
+ }
273
+ }
274
+ if (props.tags !== undefined) {
275
+ for (const [key, value] of Object.entries(props.tags)) {
276
+ if (key === "") {
277
+ throw new Error("Ec2Instance.tags: empty-string keys are not permitted");
278
+ }
279
+ if (value === "") {
280
+ throw new Error(`Ec2Instance.tags['${key}']: empty-string values are not permitted`);
281
+ }
282
+ }
283
+ }
284
+ }
146
285
  export class Ec2InstanceStack extends Stack {
147
286
  constructor(scope, id, props) {
148
287
  super(scope, id, props);
288
+ validateEc2InstanceProps(props);
149
289
  new Ec2Instance(this, id, {
150
290
  ...props
151
291
  });