@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
@@ -0,0 +1,41 @@
1
+ import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
2
+ import { Construct } from "constructs";
3
+ import { LambdaFunction } from "./lambda.js";
4
+ import { SQSQueue } from "../messaging/sqs.js";
5
+ /**
6
+ * Description prefix baked into the Lambda + execution-role descriptions.
7
+ * Tests grep CFN output for this needle to locate the function across
8
+ * stacks that may contain unrelated Lambdas — keep it here as the single
9
+ * source of truth so a rename does not silently break the test query.
10
+ */
11
+ export declare const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
12
+ export interface Ec2GracefulTerminationHandlerProps {
13
+ /** ASG to attach the EC2_INSTANCE_TERMINATING hook to. */
14
+ autoScalingGroup: AutoScalingGroup;
15
+ /**
16
+ * ECS cluster ARN — when set, the Lambda drains and deregisters the
17
+ * container instance before generic cleanup. Empty string is normalised
18
+ * to `undefined`; consumers should pass `undefined` for bare-EC2
19
+ * deployments (bastion, Fivetran).
20
+ */
21
+ ecsClusterArn?: string;
22
+ /**
23
+ * `fjall:OwnerLogicalId` of a paired `PersistentDataVolume`. When set, the
24
+ * Lambda detaches the tagged volume from the terminating instance before
25
+ * `CompleteLifecycleAction`. Empty string is normalised to `undefined`.
26
+ * Pass the `PersistentDataVolume.ownerLogicalId` so the TERMINATING and
27
+ * LAUNCHING handlers locate the same volume.
28
+ */
29
+ dataVolumeOwnerLogicalId?: string;
30
+ }
31
+ /**
32
+ * ASG `EC2_INSTANCE_TERMINATING` hook + Lambda that drains the instance
33
+ * cleanly. Drain ownership lives at the EC2 layer; ECS-specific drain plugs
34
+ * in via `ecsClusterArn` so a single hook handles both bare-EC2 (bastion,
35
+ * Fivetran) and ECS-wired ASGs (ClickHouse).
36
+ */
37
+ export declare class Ec2GracefulTerminationHandler extends Construct {
38
+ readonly lambda: LambdaFunction;
39
+ readonly queue: SQSQueue;
40
+ constructor(scope: Construct, id: string, props: Ec2GracefulTerminationHandlerProps);
41
+ }
@@ -0,0 +1,194 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Aws, Stack } from "aws-cdk-lib";
5
+ import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
6
+ import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
7
+ import { RetentionDays } from "aws-cdk-lib/aws-logs";
8
+ import { LifecycleTransition, DefaultResult } from "aws-cdk-lib/aws-autoscaling";
9
+ import { Construct } from "constructs";
10
+ import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
11
+ import { LambdaFunction } from "./lambda.js";
12
+ import { PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE, PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE, PERSISTENT_DATA_VOLUME_TAG_STACK_ID } from "./persistentDataVolume.js";
13
+ import { SQSQueue } from "../messaging/sqs.js";
14
+ import { Subscription } from "../messaging/subscription.js";
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const LAMBDA_SOURCE_FILE = "ec2GracefulTerminationLambda.source.cjs";
17
+ /**
18
+ * Description prefix baked into the Lambda + execution-role descriptions.
19
+ * Tests grep CFN output for this needle to locate the function across
20
+ * stacks that may contain unrelated Lambdas — keep it here as the single
21
+ * source of truth so a rename does not silently break the test query.
22
+ */
23
+ export const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
24
+ const LAMBDA_TIMEOUT_SECONDS = 300;
25
+ // Heartbeat must comfortably exceed the Lambda's drain (240s) + detach (60s)
26
+ // budget so the hook does not time out mid-cleanup when PDV is wired.
27
+ const HOOK_HEARTBEAT_SECONDS = 600;
28
+ const QUEUE_VISIBILITY_TIMEOUT_SECONDS = 360;
29
+ /**
30
+ * ASG `EC2_INSTANCE_TERMINATING` hook + Lambda that drains the instance
31
+ * cleanly. Drain ownership lives at the EC2 layer; ECS-specific drain plugs
32
+ * in via `ecsClusterArn` so a single hook handles both bare-EC2 (bastion,
33
+ * Fivetran) and ECS-wired ASGs (ClickHouse).
34
+ */
35
+ export class Ec2GracefulTerminationHandler extends Construct {
36
+ lambda;
37
+ queue;
38
+ constructor(scope, id, props) {
39
+ super(scope, id);
40
+ const sourcePath = path.resolve(__dirname, LAMBDA_SOURCE_FILE);
41
+ const source = readFileSync(sourcePath, "utf-8");
42
+ const ecsClusterArn = resolveOptionalString(props.ecsClusterArn);
43
+ const dataVolumeOwnerLogicalId = resolveOptionalString(props.dataVolumeOwnerLogicalId);
44
+ this.queue = new SQSQueue(this, `${id}Queue`, {
45
+ visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
46
+ deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
47
+ });
48
+ const ecsPolicies = ecsClusterArn !== undefined
49
+ ? [
50
+ new PolicyStatement({
51
+ effect: Effect.ALLOW,
52
+ actions: ["ecs:ListContainerInstances"],
53
+ resources: [ecsClusterArn]
54
+ }),
55
+ new PolicyStatement({
56
+ effect: Effect.ALLOW,
57
+ actions: [
58
+ "ecs:DescribeContainerInstances",
59
+ "ecs:UpdateContainerInstancesState",
60
+ "ecs:DeregisterContainerInstance"
61
+ ],
62
+ resources: ["*"],
63
+ conditions: {
64
+ ArnEquals: { "ecs:cluster": ecsClusterArn }
65
+ }
66
+ })
67
+ ]
68
+ : [];
69
+ const ec2Policies = new PolicyStatement({
70
+ effect: Effect.ALLOW,
71
+ actions: [
72
+ "ec2:DescribeAddresses",
73
+ "ec2:DisassociateAddress",
74
+ "ec2:DescribeNetworkInterfaces",
75
+ "ec2:DetachNetworkInterface"
76
+ ],
77
+ resources: ["*"]
78
+ });
79
+ const elbReadPolicy = new PolicyStatement({
80
+ effect: Effect.ALLOW,
81
+ actions: [
82
+ "elasticloadbalancing:DescribeTargetGroups",
83
+ "elasticloadbalancing:DescribeTargetHealth"
84
+ ],
85
+ resources: ["*"]
86
+ });
87
+ const elbDeregisterPolicy = new PolicyStatement({
88
+ effect: Effect.ALLOW,
89
+ actions: ["elasticloadbalancing:DeregisterTargets"],
90
+ resources: ["*"],
91
+ conditions: {
92
+ StringEquals: {
93
+ "aws:ResourceTag/aws:cloudformation:stack-id": Aws.STACK_ID
94
+ }
95
+ }
96
+ });
97
+ const stack = Stack.of(this);
98
+ // Account/region-scoped wildcard rather than the specific ASG ARN —
99
+ // see PersistentDataVolume for the full deadlock writeup. Same gotcha:
100
+ // a specific ARN creates a CFN Ref to the ASG, the Lambda's IAM Policy
101
+ // waits for ASG CREATE_COMPLETE, the ASG can't complete without this
102
+ // Lambda's CompleteLifecycleAction call on its first terminating instance.
103
+ const asgArnWildcard = `arn:${stack.partition}:autoscaling:${stack.region}:${stack.account}:autoScalingGroup:*:autoScalingGroupName/*`;
104
+ const asgPolicy = new PolicyStatement({
105
+ effect: Effect.ALLOW,
106
+ actions: ["autoscaling:CompleteLifecycleAction"],
107
+ resources: [asgArnWildcard]
108
+ });
109
+ const volumeArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/*`;
110
+ const instanceArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance/*`;
111
+ const dataVolumePolicies = dataVolumeOwnerLogicalId !== undefined
112
+ ? [
113
+ new PolicyStatement({
114
+ effect: Effect.ALLOW,
115
+ actions: ["ec2:DescribeVolumes"],
116
+ resources: ["*"]
117
+ }),
118
+ new PolicyStatement({
119
+ effect: Effect.ALLOW,
120
+ actions: ["ec2:DetachVolume"],
121
+ resources: [volumeArnPattern],
122
+ conditions: {
123
+ StringEquals: {
124
+ [`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE}`]: PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE,
125
+ [`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_STACK_ID}`]: Aws.STACK_ID
126
+ }
127
+ }
128
+ }),
129
+ new PolicyStatement({
130
+ effect: Effect.ALLOW,
131
+ actions: ["ec2:DetachVolume"],
132
+ resources: [instanceArnPattern]
133
+ })
134
+ ]
135
+ : [];
136
+ this.lambda = new LambdaFunction(this, `${id}Fn`, {
137
+ runtime: Runtime.NODEJS_22_X,
138
+ handler: "index.handler",
139
+ code: Code.fromInline(source),
140
+ lambdaDescription: `${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
141
+ roleDescription: `Execution role for ${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
142
+ timeout: LAMBDA_TIMEOUT_SECONDS,
143
+ memorySize: 256,
144
+ logGroupRetention: RetentionDays.ONE_MONTH,
145
+ environment: {
146
+ ...(ecsClusterArn !== undefined && { ECS_CLUSTER_ARN: ecsClusterArn }),
147
+ ...(dataVolumeOwnerLogicalId !== undefined && {
148
+ DATA_VOLUME_OWNER_LOGICAL_ID: dataVolumeOwnerLogicalId,
149
+ DATA_VOLUME_STACK_ID: Aws.STACK_ID
150
+ })
151
+ },
152
+ inlinePolicy: [
153
+ ...ecsPolicies,
154
+ ec2Policies,
155
+ elbReadPolicy,
156
+ elbDeregisterPolicy,
157
+ asgPolicy,
158
+ ...dataVolumePolicies
159
+ ]
160
+ });
161
+ this.lambda.addSqsEventSource(this.queue.getQueue());
162
+ // Hook name embeds Aws.STACK_NAME (pseudo-parameter Ref, NOT a resource
163
+ // Ref — no CFN dep) so LifecycleHookName alone discriminates across
164
+ // stacks in the EB pattern. Sibling LAUNCHING hook uses "-launching".
165
+ const terminatingHookName = `${Aws.STACK_NAME}-${id}-terminating`;
166
+ attachInlineAsgLifecycleHook(this, `${id}TerminatingHook`, {
167
+ autoScalingGroup: props.autoScalingGroup,
168
+ hookName: terminatingHookName,
169
+ lifecycleTransition: LifecycleTransition.INSTANCE_TERMINATING,
170
+ defaultResult: DefaultResult.CONTINUE,
171
+ heartbeatTimeoutSeconds: HOOK_HEARTBEAT_SECONDS
172
+ });
173
+ // No AutoScalingGroupName in the pattern — that would create a CFN Ref to
174
+ // the ASG and block the EB rule on ASG CREATE_COMPLETE (deadlock — see
175
+ // wildcard ARN comment above and the PersistentDataVolume sibling).
176
+ new Subscription(this, `${id}TerminatingSub`, {
177
+ pattern: {
178
+ source: ["aws.autoscaling"],
179
+ detailType: ["EC2 Instance-terminate Lifecycle Action"],
180
+ detail: {
181
+ LifecycleHookName: [terminatingHookName]
182
+ }
183
+ },
184
+ target: this.queue
185
+ });
186
+ }
187
+ }
188
+ function resolveOptionalString(value) {
189
+ if (value === undefined)
190
+ return undefined;
191
+ if (value === "")
192
+ return undefined;
193
+ return value;
194
+ }
@@ -0,0 +1,458 @@
1
+ /**
2
+ * EC2 graceful termination handler. Inlined into a Lambda at synth time by
3
+ * ec2GracefulTerminationHandler.ts. Triggered by an SQS queue fed by an
4
+ * EventBridge rule subscribed to the ASG EC2_INSTANCE_TERMINATING lifecycle
5
+ * event on the default bus.
6
+ *
7
+ * Wire format: the SQS message body is the EventBridge event envelope
8
+ * `{ version, "detail-type", source, account, time, region, resources,
9
+ * detail: { LifecycleActionToken, AutoScalingGroupName,
10
+ * LifecycleHookName, EC2InstanceId, LifecycleTransition,
11
+ * NotificationMetadata, ... } }`.
12
+ * The lifecycle payload lives at `body.detail`. Records whose envelope does
13
+ * not match (wrong source, missing detail, non-Lifecycle Action detail-type)
14
+ * are logged and skipped.
15
+ *
16
+ * Behaviour, in order:
17
+ * 1. (Conditional) ECS_CLUSTER_ARN set + non-empty → drain container
18
+ * instance, wait for runningTasksCount=0, deregister.
19
+ * 2. (Always) Dissociate EIPs, deregister from any target groups,
20
+ * detach non-primary ENIs.
21
+ * 3. CompleteLifecycleAction CONTINUE — even on partial failure, so the
22
+ * ASG never stalls indefinitely. Better to terminate dirty than hang
23
+ * the whole stack delete.
24
+ *
25
+ * CommonJS rather than ESM because Code.fromInline lands the source as
26
+ * `index.js`, which Lambda treats as CommonJS by default.
27
+ */
28
+ const {
29
+ ECSClient,
30
+ ListContainerInstancesCommand,
31
+ DescribeContainerInstancesCommand,
32
+ UpdateContainerInstancesStateCommand,
33
+ DeregisterContainerInstanceCommand
34
+ } = require("@aws-sdk/client-ecs");
35
+ const {
36
+ EC2Client,
37
+ DescribeAddressesCommand,
38
+ DisassociateAddressCommand,
39
+ DescribeNetworkInterfacesCommand,
40
+ DetachNetworkInterfaceCommand,
41
+ DescribeVolumesCommand,
42
+ DetachVolumeCommand
43
+ } = require("@aws-sdk/client-ec2");
44
+ const {
45
+ ElasticLoadBalancingV2Client,
46
+ DescribeTargetGroupsCommand,
47
+ DescribeTargetHealthCommand,
48
+ DeregisterTargetsCommand
49
+ } = require("@aws-sdk/client-elastic-load-balancing-v2");
50
+ const {
51
+ AutoScalingClient,
52
+ CompleteLifecycleActionCommand
53
+ } = require("@aws-sdk/client-auto-scaling");
54
+
55
+ const DRAIN_POLL_INTERVAL_MS = 10_000;
56
+ const DRAIN_DEADLINE_MS = 240_000;
57
+ const DETACH_POLL_INTERVAL_MS = 5_000;
58
+ const DETACH_DEADLINE_MS = 60_000;
59
+ const TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
60
+ const TAG_STACK_ID = "fjall:StackId";
61
+
62
+ function errMessage(err) {
63
+ return err && err.message ? err.message : String(err);
64
+ }
65
+
66
+ let _ecs;
67
+ let _ec2;
68
+ let _elbv2;
69
+ let _asg;
70
+ function getEcs() {
71
+ if (!_ecs) _ecs = new ECSClient({});
72
+ return _ecs;
73
+ }
74
+ function getEc2() {
75
+ if (!_ec2) _ec2 = new EC2Client({});
76
+ return _ec2;
77
+ }
78
+ function getElbv2() {
79
+ if (!_elbv2) _elbv2 = new ElasticLoadBalancingV2Client({});
80
+ return _elbv2;
81
+ }
82
+ function getAsg() {
83
+ if (!_asg) _asg = new AutoScalingClient({});
84
+ return _asg;
85
+ }
86
+
87
+ function parseLifecycleMessage(record) {
88
+ const envelope = JSON.parse(record.body);
89
+ const detail = envelope && envelope.detail;
90
+ const source = envelope && envelope.source;
91
+ const detailType = envelope && envelope["detail-type"];
92
+ const valid =
93
+ detail !== undefined &&
94
+ detail !== null &&
95
+ typeof detail === "object" &&
96
+ source === "aws.autoscaling" &&
97
+ typeof detailType === "string" &&
98
+ detailType.endsWith("Lifecycle Action");
99
+ if (!valid) {
100
+ return { valid: false };
101
+ }
102
+ return {
103
+ valid: true,
104
+ instanceId: detail.EC2InstanceId,
105
+ asgName: detail.AutoScalingGroupName,
106
+ actionToken: detail.LifecycleActionToken,
107
+ hookName: detail.LifecycleHookName,
108
+ isTest: false
109
+ };
110
+ }
111
+
112
+ async function findContainerInstanceArn(ecsClient, clusterArn, instanceId) {
113
+ const list = await ecsClient.send(
114
+ new ListContainerInstancesCommand({
115
+ cluster: clusterArn,
116
+ filter: `ec2InstanceId == ${instanceId}`
117
+ })
118
+ );
119
+ if (!list.containerInstanceArns || list.containerInstanceArns.length === 0) {
120
+ return undefined;
121
+ }
122
+ return list.containerInstanceArns[0];
123
+ }
124
+
125
+ async function drainContainerInstance(
126
+ ecsClient,
127
+ clusterArn,
128
+ containerInstanceArn,
129
+ sleepFn,
130
+ nowFn
131
+ ) {
132
+ await ecsClient.send(
133
+ new UpdateContainerInstancesStateCommand({
134
+ cluster: clusterArn,
135
+ containerInstances: [containerInstanceArn],
136
+ status: "DRAINING"
137
+ })
138
+ );
139
+
140
+ const deadline = nowFn() + DRAIN_DEADLINE_MS;
141
+ while (nowFn() < deadline) {
142
+ const desc = await ecsClient.send(
143
+ new DescribeContainerInstancesCommand({
144
+ cluster: clusterArn,
145
+ containerInstances: [containerInstanceArn]
146
+ })
147
+ );
148
+ const instance =
149
+ desc.containerInstances && desc.containerInstances[0]
150
+ ? desc.containerInstances[0]
151
+ : undefined;
152
+ if (!instance) return;
153
+ if ((instance.runningTasksCount || 0) === 0) return;
154
+ await sleepFn(DRAIN_POLL_INTERVAL_MS);
155
+ }
156
+ }
157
+
158
+ async function dissociateInstanceEips(ec2Client, instanceId) {
159
+ const result = await ec2Client.send(
160
+ new DescribeAddressesCommand({
161
+ Filters: [{ Name: "instance-id", Values: [instanceId] }]
162
+ })
163
+ );
164
+ const addresses = result.Addresses || [];
165
+ for (const addr of addresses) {
166
+ if (!addr.AssociationId) continue;
167
+ await ec2Client.send(
168
+ new DisassociateAddressCommand({ AssociationId: addr.AssociationId })
169
+ );
170
+ }
171
+ }
172
+
173
+ async function deregisterFromTargetGroups(elbv2Client, instanceId) {
174
+ const groups = await elbv2Client.send(new DescribeTargetGroupsCommand({}));
175
+ const targetGroups = groups.TargetGroups || [];
176
+ for (const tg of targetGroups) {
177
+ if (!tg.TargetGroupArn) continue;
178
+ let health;
179
+ try {
180
+ health = await elbv2Client.send(
181
+ new DescribeTargetHealthCommand({ TargetGroupArn: tg.TargetGroupArn })
182
+ );
183
+ } catch (err) {
184
+ console.debug(
185
+ JSON.stringify({
186
+ message: "DescribeTargetHealth skipped",
187
+ targetGroupArn: tg.TargetGroupArn,
188
+ error: errMessage(err)
189
+ })
190
+ );
191
+ continue;
192
+ }
193
+ const matches = (health.TargetHealthDescriptions || []).filter(
194
+ (d) => d.Target && d.Target.Id === instanceId
195
+ );
196
+ if (matches.length === 0) continue;
197
+ await elbv2Client.send(
198
+ new DeregisterTargetsCommand({
199
+ TargetGroupArn: tg.TargetGroupArn,
200
+ Targets: matches.map((m) => ({
201
+ Id: m.Target.Id,
202
+ Port: m.Target.Port
203
+ }))
204
+ })
205
+ );
206
+ }
207
+ }
208
+
209
+ async function detachSecondaryEnis(ec2Client, instanceId) {
210
+ const result = await ec2Client.send(
211
+ new DescribeNetworkInterfacesCommand({
212
+ Filters: [{ Name: "attachment.instance-id", Values: [instanceId] }]
213
+ })
214
+ );
215
+ const enis = result.NetworkInterfaces || [];
216
+ for (const eni of enis) {
217
+ const attachment = eni.Attachment;
218
+ if (!attachment || !attachment.AttachmentId) continue;
219
+ // Skip the primary interface (DeviceIndex 0); it is destroyed with the
220
+ // instance and cannot be detached independently.
221
+ if (attachment.DeviceIndex === 0) continue;
222
+ try {
223
+ await ec2Client.send(
224
+ new DetachNetworkInterfaceCommand({
225
+ AttachmentId: attachment.AttachmentId,
226
+ Force: true
227
+ })
228
+ );
229
+ } catch (err) {
230
+ console.debug(
231
+ JSON.stringify({
232
+ message: "DetachNetworkInterface skipped",
233
+ attachmentId: attachment.AttachmentId,
234
+ error: errMessage(err)
235
+ })
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ async function detachDataVolume(
242
+ ec2Client,
243
+ instanceId,
244
+ ownerLogicalId,
245
+ stackId,
246
+ sleepFn,
247
+ nowFn
248
+ ) {
249
+ const described = await ec2Client.send(
250
+ new DescribeVolumesCommand({
251
+ Filters: [
252
+ { Name: `tag:${TAG_OWNER_LOGICAL_ID}`, Values: [ownerLogicalId] },
253
+ { Name: `tag:${TAG_STACK_ID}`, Values: [stackId] },
254
+ { Name: "attachment.instance-id", Values: [instanceId] }
255
+ ]
256
+ })
257
+ );
258
+ const volumes = described.Volumes || [];
259
+ if (volumes.length === 0) return;
260
+ const volumeId = volumes[0].VolumeId;
261
+ if (!volumeId) return;
262
+ await ec2Client.send(
263
+ new DetachVolumeCommand({ VolumeId: volumeId, InstanceId: instanceId })
264
+ );
265
+ const deadline = nowFn() + DETACH_DEADLINE_MS;
266
+ while (nowFn() < deadline) {
267
+ const desc = await ec2Client.send(
268
+ new DescribeVolumesCommand({ VolumeIds: [volumeId] })
269
+ );
270
+ const volume =
271
+ desc.Volumes && desc.Volumes[0] ? desc.Volumes[0] : undefined;
272
+ if (volume && volume.State === "available") return;
273
+ await sleepFn(DETACH_POLL_INTERVAL_MS);
274
+ }
275
+ }
276
+
277
+ async function completeLifecycle(asgClient, asgName, hookName, actionToken) {
278
+ await asgClient.send(
279
+ new CompleteLifecycleActionCommand({
280
+ AutoScalingGroupName: asgName,
281
+ LifecycleHookName: hookName,
282
+ LifecycleActionToken: actionToken,
283
+ LifecycleActionResult: "CONTINUE"
284
+ })
285
+ );
286
+ }
287
+
288
+ function readEnv(env, name) {
289
+ const value = env[name];
290
+ if (typeof value !== "string" || value === "") return undefined;
291
+ return value;
292
+ }
293
+
294
+ async function processRecord(record, deps) {
295
+ const ecsClient = (deps && deps.ecsClient) || getEcs();
296
+ const ec2Client = (deps && deps.ec2Client) || getEc2();
297
+ const elbv2Client = (deps && deps.elbv2Client) || getElbv2();
298
+ const asgClient = (deps && deps.asgClient) || getAsg();
299
+ const sleepFn =
300
+ (deps && deps.sleep) ||
301
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
302
+ const nowFn = (deps && deps.now) || (() => Date.now());
303
+ const env = (deps && deps.env) || process.env;
304
+ const log =
305
+ (deps && deps.log) ||
306
+ ((msg, fields) => {
307
+ console.log(JSON.stringify({ message: msg, ...(fields || {}) }));
308
+ });
309
+
310
+ const parsed = parseLifecycleMessage(record);
311
+ if (!parsed.valid) {
312
+ log("Record is not an aws.autoscaling Lifecycle Action; skipping", {
313
+ bodyPreview: record.body ? String(record.body).slice(0, 200) : undefined
314
+ });
315
+ return;
316
+ }
317
+ const { instanceId, asgName, actionToken, hookName } = parsed;
318
+
319
+ const clusterArn = readEnv(env, "ECS_CLUSTER_ARN");
320
+ if (clusterArn !== undefined) {
321
+ try {
322
+ const containerInstanceArn = await findContainerInstanceArn(
323
+ ecsClient,
324
+ clusterArn,
325
+ instanceId
326
+ );
327
+ if (containerInstanceArn !== undefined) {
328
+ await drainContainerInstance(
329
+ ecsClient,
330
+ clusterArn,
331
+ containerInstanceArn,
332
+ sleepFn,
333
+ nowFn
334
+ );
335
+ await ecsClient.send(
336
+ new DeregisterContainerInstanceCommand({
337
+ cluster: clusterArn,
338
+ containerInstance: containerInstanceArn,
339
+ force: true
340
+ })
341
+ );
342
+ }
343
+ } catch (err) {
344
+ log("ECS drain step failed; continuing to generic cleanup", {
345
+ instanceId,
346
+ error: errMessage(err)
347
+ });
348
+ }
349
+ }
350
+
351
+ try {
352
+ await dissociateInstanceEips(ec2Client, instanceId);
353
+ } catch (err) {
354
+ log("EIP dissociation failed", {
355
+ instanceId,
356
+ error: errMessage(err)
357
+ });
358
+ }
359
+ try {
360
+ await deregisterFromTargetGroups(elbv2Client, instanceId);
361
+ } catch (err) {
362
+ log("Target-group deregistration failed", {
363
+ instanceId,
364
+ error: errMessage(err)
365
+ });
366
+ }
367
+ try {
368
+ await detachSecondaryEnis(ec2Client, instanceId);
369
+ } catch (err) {
370
+ log("ENI detachment failed", {
371
+ instanceId,
372
+ error: errMessage(err)
373
+ });
374
+ }
375
+
376
+ const dataVolumeOwnerLogicalId = readEnv(env, "DATA_VOLUME_OWNER_LOGICAL_ID");
377
+ const dataVolumeStackId = readEnv(env, "DATA_VOLUME_STACK_ID");
378
+ if (
379
+ dataVolumeOwnerLogicalId !== undefined &&
380
+ dataVolumeStackId !== undefined
381
+ ) {
382
+ try {
383
+ await detachDataVolume(
384
+ ec2Client,
385
+ instanceId,
386
+ dataVolumeOwnerLogicalId,
387
+ dataVolumeStackId,
388
+ sleepFn,
389
+ nowFn
390
+ );
391
+ } catch (err) {
392
+ log("Data volume detach failed; AWS will force-detach after heartbeat", {
393
+ instanceId,
394
+ ownerLogicalId: dataVolumeOwnerLogicalId,
395
+ error: errMessage(err)
396
+ });
397
+ }
398
+ }
399
+
400
+ await completeLifecycle(asgClient, asgName, hookName, actionToken);
401
+ }
402
+
403
+ exports.handler = async (event) => {
404
+ const records = (event && event.Records) || [];
405
+ for (const record of records) {
406
+ try {
407
+ await processRecord(record);
408
+ } catch (err) {
409
+ // Last-resort: try to release the lifecycle action so the stack does
410
+ // not hang. Failure here is logged and the message will retry via SQS.
411
+ console.error(
412
+ JSON.stringify({
413
+ message: "Graceful termination handler crashed; attempting CONTINUE",
414
+ error: errMessage(err)
415
+ })
416
+ );
417
+ try {
418
+ const parsed = parseLifecycleMessage(record);
419
+ if (!parsed.valid) {
420
+ continue;
421
+ }
422
+ await completeLifecycle(
423
+ getAsg(),
424
+ parsed.asgName,
425
+ parsed.hookName,
426
+ parsed.actionToken
427
+ );
428
+ } catch (innerErr) {
429
+ console.error(
430
+ JSON.stringify({
431
+ message: "CompleteLifecycleAction also failed",
432
+ error: errMessage(innerErr)
433
+ })
434
+ );
435
+ throw err;
436
+ }
437
+ }
438
+ }
439
+ };
440
+
441
+ exports._internals = {
442
+ parseLifecycleMessage,
443
+ findContainerInstanceArn,
444
+ drainContainerInstance,
445
+ dissociateInstanceEips,
446
+ deregisterFromTargetGroups,
447
+ detachSecondaryEnis,
448
+ detachDataVolume,
449
+ completeLifecycle,
450
+ readEnv,
451
+ processRecord,
452
+ DRAIN_POLL_INTERVAL_MS,
453
+ DRAIN_DEADLINE_MS,
454
+ DETACH_POLL_INTERVAL_MS,
455
+ DETACH_DEADLINE_MS,
456
+ TAG_OWNER_LOGICAL_ID,
457
+ TAG_STACK_ID
458
+ };