@fjall/components-infrastructure 0.96.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 (209) hide show
  1. package/dist/lib/app.d.ts +68 -1
  2. package/dist/lib/app.js +113 -4
  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 +2 -4
  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 +93 -5
  36. package/dist/lib/patterns/aws/computeEcs.js +867 -37
  37. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +528 -25
  38. package/dist/lib/patterns/aws/computeEcsTypes.js +10 -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 +6 -4
  44. package/dist/lib/patterns/aws/index.d.ts +1 -0
  45. package/dist/lib/patterns/aws/index.js +1 -0
  46. package/dist/lib/patterns/aws/interfaces/compute.d.ts +7 -1
  47. package/dist/lib/patterns/aws/interfaces/database.d.ts +187 -8
  48. package/dist/lib/patterns/aws/interfaces/database.js +17 -3
  49. package/dist/lib/patterns/aws/interfaces/index.d.ts +2 -1
  50. package/dist/lib/patterns/aws/interfaces/index.js +3 -1
  51. package/dist/lib/patterns/aws/interfaces/messaging.d.ts +7 -0
  52. package/dist/lib/patterns/aws/interfaces/migrationContributor.d.ts +47 -0
  53. package/dist/lib/patterns/aws/interfaces/migrationContributor.js +9 -0
  54. package/dist/lib/patterns/aws/messaging.d.ts +66 -10
  55. package/dist/lib/patterns/aws/messaging.js +115 -20
  56. package/dist/lib/patterns/aws/network.js +16 -7
  57. package/dist/lib/patterns/aws/organisation.d.ts +4 -0
  58. package/dist/lib/patterns/aws/organisation.js +22 -4
  59. package/dist/lib/patterns/aws/storage.d.ts +1 -2
  60. package/dist/lib/patterns/aws/storage.js +3 -2
  61. package/dist/lib/patterns/aws/vpcPeer.js +3 -1
  62. package/dist/lib/resources/aws/analytics/clickhouse.js +18 -9
  63. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +24 -9
  64. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +61 -10
  65. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +3 -3
  66. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +3 -3
  67. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +7 -1
  68. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -1
  69. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +53 -3
  70. package/dist/lib/resources/aws/base/awsStack.js +4 -2
  71. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +2 -0
  72. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +11 -0
  73. package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.d.ts +52 -0
  74. package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.js +60 -0
  75. package/dist/lib/resources/aws/compute/blockDeviceVolume.d.ts +8 -0
  76. package/dist/lib/resources/aws/compute/blockDeviceVolume.js +10 -0
  77. package/dist/lib/resources/aws/compute/ec2.d.ts +132 -12
  78. package/dist/lib/resources/aws/compute/ec2.js +163 -23
  79. package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.d.ts +41 -0
  80. package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +194 -0
  81. package/dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs +458 -0
  82. package/dist/lib/resources/aws/compute/ecs.d.ts +27 -1
  83. package/dist/lib/resources/aws/compute/ecs.js +42 -2
  84. package/dist/lib/resources/aws/compute/ecsConstants.d.ts +9 -0
  85. package/dist/lib/resources/aws/compute/ecsConstants.js +16 -0
  86. package/dist/lib/resources/aws/compute/ecsImages.js +32 -20
  87. package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.d.ts +96 -0
  88. package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +113 -0
  89. package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +2 -1
  90. package/dist/lib/resources/aws/compute/ecsNetworking.js +18 -6
  91. package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +13 -4
  92. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +155 -33
  93. package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +31 -1
  94. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +102 -6
  95. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +173 -13
  96. package/dist/lib/resources/aws/compute/ecsValidation.d.ts +9 -0
  97. package/dist/lib/resources/aws/compute/ecsValidation.js +63 -0
  98. package/dist/lib/resources/aws/compute/index.d.ts +2 -0
  99. package/dist/lib/resources/aws/compute/index.js +2 -0
  100. package/dist/lib/resources/aws/compute/lambda.d.ts +7 -13
  101. package/dist/lib/resources/aws/compute/lambda.js +30 -38
  102. package/dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs +192 -0
  103. package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +104 -0
  104. package/dist/lib/resources/aws/compute/persistentDataVolume.js +245 -0
  105. package/dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs +398 -0
  106. package/dist/lib/resources/aws/compute/samApplication.d.ts +15 -0
  107. package/dist/lib/resources/aws/compute/samApplication.js +27 -0
  108. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +159 -0
  109. package/dist/lib/resources/aws/database/clickhouseConstants.js +181 -0
  110. package/dist/lib/resources/aws/database/clickhouseSchemas.d.ts +71 -0
  111. package/dist/lib/resources/aws/database/clickhouseSchemas.js +157 -0
  112. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +14 -0
  113. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +23 -0
  114. package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +69 -0
  115. package/dist/lib/resources/aws/database/clickhouseUserData.js +371 -0
  116. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +56 -0
  117. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +112 -0
  118. package/dist/lib/resources/aws/database/rdsAurora.d.ts +8 -1
  119. package/dist/lib/resources/aws/database/rdsAurora.js +42 -32
  120. package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +15 -2
  121. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +39 -43
  122. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +6 -0
  123. package/dist/lib/resources/aws/database/rdsDefaults.js +7 -1
  124. package/dist/lib/resources/aws/database/rdsHelpers.d.ts +3 -3
  125. package/dist/lib/resources/aws/database/rdsHelpers.js +1 -0
  126. package/dist/lib/resources/aws/database/rdsInstance.d.ts +8 -1
  127. package/dist/lib/resources/aws/database/rdsInstance.js +51 -34
  128. package/dist/lib/resources/aws/database/rdsProxyOutput.d.ts +1 -1
  129. package/dist/lib/resources/aws/database/rdsProxyOutput.js +1 -1
  130. package/dist/lib/resources/aws/iam/delegationRole.js +1 -1
  131. package/dist/lib/resources/aws/iam/identityCenter/groupMembership.d.ts +9 -0
  132. package/dist/lib/resources/aws/iam/identityCenter/groupMembership.js +12 -0
  133. package/dist/lib/resources/aws/iam/identityCenter/index.d.ts +1 -0
  134. package/dist/lib/resources/aws/iam/identityCenter/index.js +1 -0
  135. package/dist/lib/resources/aws/iam/identityCenter/permissionSet.d.ts +1 -0
  136. package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +1 -0
  137. package/dist/lib/resources/aws/logging/logGroup.d.ts +0 -8
  138. package/dist/lib/resources/aws/logging/logGroup.js +0 -11
  139. package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +7 -0
  140. package/dist/lib/resources/aws/messaging/defaultEventBus.js +21 -0
  141. package/dist/lib/resources/aws/messaging/eventBridgeRule.d.ts +96 -0
  142. package/dist/lib/resources/aws/messaging/eventBridgeRule.js +110 -0
  143. package/dist/lib/resources/aws/messaging/eventTargets.d.ts +84 -0
  144. package/dist/lib/resources/aws/messaging/eventTargets.js +152 -0
  145. package/dist/lib/resources/aws/messaging/eventbridge.d.ts +25 -2
  146. package/dist/lib/resources/aws/messaging/eventbridge.js +22 -10
  147. package/dist/lib/resources/aws/messaging/index.d.ts +5 -0
  148. package/dist/lib/resources/aws/messaging/index.js +2 -0
  149. package/dist/lib/resources/aws/messaging/schedule.d.ts +118 -0
  150. package/dist/lib/resources/aws/messaging/schedule.js +64 -0
  151. package/dist/lib/resources/aws/messaging/sns.d.ts +2 -1
  152. package/dist/lib/resources/aws/messaging/sqs.d.ts +2 -1
  153. package/dist/lib/resources/aws/messaging/subscription.d.ts +112 -0
  154. package/dist/lib/resources/aws/messaging/subscription.js +67 -0
  155. package/dist/lib/resources/aws/messaging/utils.d.ts +6 -0
  156. package/dist/lib/resources/aws/messaging/utils.js +10 -0
  157. package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +60 -0
  158. package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +139 -0
  159. package/dist/lib/resources/aws/monitoring/index.d.ts +2 -0
  160. package/dist/lib/resources/aws/monitoring/index.js +2 -0
  161. package/dist/lib/resources/aws/monitoring/scheduleAlarms.d.ts +47 -0
  162. package/dist/lib/resources/aws/monitoring/scheduleAlarms.js +106 -0
  163. package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +6 -4
  164. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +17 -13
  165. package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +7 -5
  166. package/dist/lib/resources/aws/networking/domainCertificate.d.ts +2 -2
  167. package/dist/lib/resources/aws/networking/domainCertificate.js +6 -4
  168. package/dist/lib/resources/aws/networking/hostedZone.js +6 -5
  169. package/dist/lib/resources/aws/networking/serviceDiscovery.d.ts +96 -0
  170. package/dist/lib/resources/aws/networking/serviceDiscovery.js +96 -0
  171. package/dist/lib/resources/aws/networking/vpc.d.ts +4 -1
  172. package/dist/lib/resources/aws/networking/vpc.js +4 -1
  173. package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +21 -3
  174. package/dist/lib/resources/aws/organisation/costAllocationTagActivator.d.ts +16 -5
  175. package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +17 -3
  176. package/dist/lib/resources/aws/organisation/index.d.ts +1 -1
  177. package/dist/lib/resources/aws/organisation/organisationPolicy.d.ts +2 -0
  178. package/dist/lib/resources/aws/organisation/organisationPolicy.js +3 -2
  179. package/dist/lib/resources/aws/secrets/secret.d.ts +7 -0
  180. package/dist/lib/resources/aws/secrets/secret.js +4 -3
  181. package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +16 -0
  182. package/dist/lib/resources/aws/storage/bucketDeployment.js +17 -0
  183. package/dist/lib/resources/aws/storage/ecr.js +5 -5
  184. package/dist/lib/resources/aws/storage/index.d.ts +1 -0
  185. package/dist/lib/resources/aws/storage/index.js +1 -0
  186. package/dist/lib/resources/aws/storage/s3.js +10 -3
  187. package/dist/lib/resources/aws/utilities/customResource.js +18 -9
  188. package/dist/lib/synth_dump.d.ts +1 -0
  189. package/dist/lib/synth_dump.js +42 -0
  190. package/dist/lib/utils/cdkContext.d.ts +2 -0
  191. package/dist/lib/utils/cdkContext.js +4 -2
  192. package/dist/lib/utils/connections.js +6 -0
  193. package/dist/lib/utils/connector.d.ts +12 -0
  194. package/dist/lib/utils/costAllocationTags.d.ts +9 -0
  195. package/dist/lib/utils/costAllocationTags.js +11 -1
  196. package/dist/lib/utils/databaseTypes.d.ts +14 -0
  197. package/dist/lib/utils/getConfig.d.ts +2 -0
  198. package/dist/lib/utils/getConfig.js +2 -0
  199. package/dist/lib/utils/index.d.ts +1 -0
  200. package/dist/lib/utils/index.js +1 -0
  201. package/dist/lib/utils/manifestWriter.d.ts +6 -89
  202. package/dist/lib/utils/manifestWriter.js +36 -23
  203. package/dist/lib/utils/migrationVersionResolvers.d.ts +2 -0
  204. package/dist/lib/utils/migrationVersionResolvers.js +2 -0
  205. package/dist/lib/utils/orgConfigParser.js +2 -1
  206. package/dist/lib/utils/resolveAlertsTopic.d.ts +14 -0
  207. package/dist/lib/utils/resolveAlertsTopic.js +30 -0
  208. package/dist/lib/utils/validationLogger.js +6 -3
  209. package/package.json +22 -19
@@ -6,9 +6,8 @@ import { fileURLToPath } from "node:url";
6
6
  import { SqsEventSource, DynamoEventSource, S3EventSource } from "aws-cdk-lib/aws-lambda-event-sources";
7
7
  import { EventType } from "aws-cdk-lib/aws-s3";
8
8
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
9
- import { Rule, Schedule } from "aws-cdk-lib/aws-events";
10
- import { LambdaFunction as LambdaTarget } from "aws-cdk-lib/aws-events-targets";
11
- import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
9
+ import { RetentionDays } from "aws-cdk-lib/aws-logs";
10
+ import { LogGroup } from "../logging/logGroup.js";
12
11
  import { Secret } from "aws-cdk-lib/aws-secretsmanager";
13
12
  import { v4 as uuid } from "uuid";
14
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -20,6 +19,18 @@ function addPoliciesToRole(target, statements) {
20
19
  target.addToRolePolicy(statement);
21
20
  }
22
21
  }
22
+ /**
23
+ * CDK's auto-generated Lambda execution role does not accept a description
24
+ * through FunctionProps; reach for the L1 CfnRole and set it directly so
25
+ * SOC2 audits see a meaningful purpose on every role.
26
+ */
27
+ function applyRoleDescription(fn, description) {
28
+ if (description === undefined)
29
+ return;
30
+ const cfnRole = fn.role?.node.defaultChild;
31
+ if (cfnRole !== undefined)
32
+ cfnRole.description = description;
33
+ }
23
34
  /**
24
35
  * AWS Parameters and Secrets Lambda Extension configuration.
25
36
  * @see https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
@@ -30,19 +41,26 @@ const SECRETS_EXTENSION = {
30
41
  /** Cache TTL in seconds - 60s supports secret rotation while reducing API calls */
31
42
  CACHE_TTL_SECONDS: "60"
32
43
  };
44
+ /**
45
+ * Default Lambda timeout in seconds. Coupled across the singleton constructor,
46
+ * the standard constructor, and the alarm wiring — drift would silently
47
+ * mis-tune alarms relative to runtime behaviour.
48
+ */
49
+ const LAMBDA_DEFAULT_TIMEOUT_SECONDS = 300;
33
50
  export class SingletonFunction extends singletonFunction {
34
51
  constructor(scope, id, props) {
35
52
  super(scope, id, {
36
53
  ...props,
37
54
  uuid: props.uuid ?? uuid(),
38
- timeout: Duration.seconds(300),
39
- description: props.lambdaDescription || `${id} singleton lambda`,
55
+ timeout: Duration.seconds(props.timeout ?? LAMBDA_DEFAULT_TIMEOUT_SECONDS),
56
+ description: props.lambdaDescription ?? `${id} singleton lambda`,
40
57
  runtime: props.runtime,
41
58
  ephemeralStorageSize: props.ephemeralStorageSize
42
59
  ? Size.mebibytes(props.ephemeralStorageSize)
43
60
  : undefined
44
61
  });
45
62
  addPoliciesToRole(this, props.inlinePolicy);
63
+ applyRoleDescription(this, props.roleDescription);
46
64
  }
47
65
  /**
48
66
  * The Lambda's execution role (auto-generated by CDK)
@@ -60,26 +78,25 @@ export class LambdaFunction extends Function {
60
78
  super(scope, id, {
61
79
  ...props,
62
80
  vpcSubnets,
63
- timeout: props.timeout
64
- ? Duration.seconds(props.timeout)
65
- : Duration.seconds(300),
66
- memorySize: props.memorySize || 128,
81
+ timeout: Duration.seconds(props.timeout ?? LAMBDA_DEFAULT_TIMEOUT_SECONDS),
82
+ memorySize: props.memorySize ?? 128,
67
83
  ephemeralStorageSize: props.ephemeralStorageSize
68
84
  ? Size.mebibytes(props.ephemeralStorageSize)
69
85
  : undefined,
70
- description: props.lambdaDescription || `${id} Lambda`,
86
+ description: props.lambdaDescription ?? `${id} Lambda`,
71
87
  environment: props.environment,
72
88
  logGroup: new LogGroup(scope, `${id}LogGroup`, {
73
- retention: RetentionDays.ONE_WEEK
89
+ retention: props.logGroupRetention ?? RetentionDays.ONE_WEEK
74
90
  })
75
91
  });
76
92
  addPoliciesToRole(this, props.inlinePolicy);
93
+ applyRoleDescription(this, props.roleDescription);
77
94
  this.addSecretsSupport(props.secrets, props.ssmSecretsPath, props.secretsImport, props.appName, props.functionName, props.architecture);
78
95
  // Sanitise id for CloudFormation output keys (must be alphanumeric)
79
96
  const outputName = toPascalCase(id);
80
97
  if (props.enableFunctionUrl) {
81
98
  const functionUrl = this.addFunctionUrl({
82
- authType: props.functionUrlAuthType || FunctionUrlAuthType.AWS_IAM,
99
+ authType: props.functionUrlAuthType ?? FunctionUrlAuthType.AWS_IAM,
83
100
  cors: props.functionUrlCors,
84
101
  invokeMode: props.functionUrlInvokeMode
85
102
  });
@@ -95,14 +112,8 @@ export class LambdaFunction extends Function {
95
112
  value: this.functionArn,
96
113
  description: `${id} Function ARN`
97
114
  });
98
- if (props.scheduleExpression) {
99
- const rule = new Rule(this, `${id}ScheduleRule`, {
100
- schedule: Schedule.expression(props.scheduleExpression)
101
- });
102
- rule.addTarget(new LambdaTarget(this));
103
- }
104
115
  if (props.alertsTopic && props.alarms !== false) {
105
- const timeoutSeconds = props.timeout ?? 300;
116
+ const timeoutSeconds = props.timeout ?? LAMBDA_DEFAULT_TIMEOUT_SECONDS;
106
117
  createLambdaAlarms({
107
118
  scope: this,
108
119
  functionName: id,
@@ -194,25 +205,6 @@ export class LambdaFunction extends Function {
194
205
  const eventSource = new S3EventSource(bucket, s3EventSourceProps);
195
206
  this.addEventSource(eventSource);
196
207
  }
197
- /**
198
- * Add an EventBridge rule as an event source for this Lambda function.
199
- * This will trigger the Lambda when events matching the pattern are published.
200
- * Useful for scheduled jobs, cross-service event handling, and custom event patterns.
201
- */
202
- addEventBridgeEventSource(ruleId, options) {
203
- if (!options.schedule && !options.eventPattern) {
204
- throw new Error("EventBridge rule requires either schedule or eventPattern");
205
- }
206
- const rule = new Rule(this, ruleId, {
207
- schedule: options.schedule
208
- ? Schedule.expression(options.schedule)
209
- : undefined,
210
- eventPattern: options.eventPattern,
211
- description: options.description
212
- });
213
- rule.addTarget(new LambdaTarget(this));
214
- return rule;
215
- }
216
208
  /**
217
209
  * Add secrets support using AWS Parameters and Secrets Lambda Extension.
218
210
  *
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ECS deployment lifecycle hook (PRE_SCALE_UP) runner. Inlined into a Lambda
3
+ * at synth time by ecsLifecycleHookMigration.ts.
4
+ *
5
+ * Re-invocation safety: ECS hook responses do NOT carry state between
6
+ * IN_PROGRESS invocations. On every invocation we reconstruct the
7
+ * deterministic startedBy tag from the event's targetServiceRevisionArn and
8
+ * use ListTasks to find an existing running migration. RunTask fires only on
9
+ * the first invocation.
10
+ *
11
+ * CommonJS rather than ESM because Code.fromInline lands the source as
12
+ * `index.js`, which Lambda treats as CommonJS by default.
13
+ */
14
+ const {
15
+ ECSClient,
16
+ RunTaskCommand,
17
+ DescribeTasksCommand,
18
+ ListTasksCommand
19
+ } = require("@aws-sdk/client-ecs");
20
+
21
+ const POLL_INTERVAL_MS = 5_000;
22
+ const MAX_POLLS_PER_INVOCATION = 20;
23
+ const HOOK_CALLBACK_DELAY_SECONDS = 30;
24
+
25
+ let _defaultClient;
26
+ function getDefaultClient() {
27
+ if (!_defaultClient) _defaultClient = new ECSClient({});
28
+ return _defaultClient;
29
+ }
30
+
31
+ function startedByTag(targetServiceRevisionArn) {
32
+ // ECS startedBy is bounded to 36 chars; revision id alone is shorter.
33
+ const suffix =
34
+ String(targetServiceRevisionArn || "")
35
+ .split("/")
36
+ .pop() || "unknown";
37
+ return `fjall-migrate-${suffix}`.slice(0, 36);
38
+ }
39
+
40
+ function buildContainerOverrides(
41
+ name,
42
+ command,
43
+ image,
44
+ entryPoint,
45
+ environment
46
+ ) {
47
+ const override = { name, command };
48
+ if (image) override.image = image;
49
+ if (entryPoint && entryPoint.length > 0) override.entryPoint = entryPoint;
50
+ if (environment && Object.keys(environment).length > 0) {
51
+ override.environment = Object.entries(environment).map(([key, value]) => ({
52
+ name: key,
53
+ value
54
+ }));
55
+ }
56
+ return [override];
57
+ }
58
+
59
+ async function findExistingTaskArn(client, clusterArn, startedBy) {
60
+ const result = await client.send(
61
+ new ListTasksCommand({ cluster: clusterArn, startedBy })
62
+ );
63
+ if (!result.taskArns || result.taskArns.length === 0) return undefined;
64
+ return result.taskArns[0];
65
+ }
66
+
67
+ async function pollTaskUntilStopped(client, clusterArn, taskArn, sleep) {
68
+ const sleepFn =
69
+ sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
70
+ for (let i = 0; i < MAX_POLLS_PER_INVOCATION; i++) {
71
+ const desc = await client.send(
72
+ new DescribeTasksCommand({ cluster: clusterArn, tasks: [taskArn] })
73
+ );
74
+ const task = desc.tasks && desc.tasks[0];
75
+ if (!task) {
76
+ return { hookStatus: "FAILED", reason: "DescribeTasks returned no task" };
77
+ }
78
+ if (task.lastStatus === "STOPPED") {
79
+ const exitCode =
80
+ task.containers && task.containers[0]
81
+ ? task.containers[0].exitCode
82
+ : undefined;
83
+ if (exitCode === 0) return { hookStatus: "SUCCEEDED" };
84
+ return {
85
+ hookStatus: "FAILED",
86
+ reason: `Task exited ${exitCode}: ${task.stoppedReason || "no reason"}`
87
+ };
88
+ }
89
+ if (i < MAX_POLLS_PER_INVOCATION - 1) {
90
+ await sleepFn(POLL_INTERVAL_MS);
91
+ }
92
+ }
93
+ return {
94
+ hookStatus: "IN_PROGRESS",
95
+ callBackDelay: HOOK_CALLBACK_DELAY_SECONDS
96
+ };
97
+ }
98
+
99
+ async function runHandler(event, deps) {
100
+ const client = (deps && deps.client) || getDefaultClient();
101
+ const sleep = deps && deps.sleep;
102
+ const config = JSON.parse(
103
+ (deps && deps.migrateConfig) || process.env.MIGRATE_CONFIG
104
+ );
105
+ const startedBy = startedByTag(event && event.targetServiceRevisionArn);
106
+
107
+ const existingTaskArn = await findExistingTaskArn(
108
+ client,
109
+ config.clusterArn,
110
+ startedBy
111
+ );
112
+ if (existingTaskArn) {
113
+ return pollTaskUntilStopped(
114
+ client,
115
+ config.clusterArn,
116
+ existingTaskArn,
117
+ sleep
118
+ );
119
+ }
120
+
121
+ const result = await client.send(
122
+ new RunTaskCommand({
123
+ cluster: config.clusterArn,
124
+ taskDefinition: config.taskDefinitionArn,
125
+ launchType: "FARGATE",
126
+ networkConfiguration: {
127
+ awsvpcConfiguration: config.networkConfiguration
128
+ },
129
+ overrides: {
130
+ containerOverrides: buildContainerOverrides(
131
+ config.name,
132
+ config.command,
133
+ config.hasSeparateMigrationTaskDef ? undefined : config.image,
134
+ config.hasSeparateMigrationTaskDef ? undefined : config.entryPoint,
135
+ config.environment
136
+ )
137
+ },
138
+ startedBy
139
+ })
140
+ );
141
+ const taskArn =
142
+ result.tasks && result.tasks[0] ? result.tasks[0].taskArn : undefined;
143
+ if (!taskArn) {
144
+ return {
145
+ hookStatus: "FAILED",
146
+ reason: "RunTask did not return a task ARN"
147
+ };
148
+ }
149
+
150
+ return pollTaskUntilStopped(client, config.clusterArn, taskArn, sleep);
151
+ }
152
+
153
+ exports.handler = async (event) => {
154
+ // Log invocation context + outcome — ECS hooks only surface "hook execution
155
+ // failure" to the deployment; without these the Lambda log is silent and
156
+ // the failure cause is unrecoverable.
157
+ console.log(
158
+ JSON.stringify({
159
+ msg: "lifecycle-hook-invoked",
160
+ targetServiceRevisionArn:
161
+ (event && event.targetServiceRevisionArn) || "(missing)",
162
+ executionId:
163
+ (event &&
164
+ event.executionDetails &&
165
+ event.executionDetails.executionId) ||
166
+ "(missing)"
167
+ })
168
+ );
169
+ try {
170
+ const result = await runHandler(event);
171
+ console.log(JSON.stringify({ msg: "lifecycle-hook-result", ...result }));
172
+ return result;
173
+ } catch (err) {
174
+ const reason = err && err.message ? err.message : String(err);
175
+ const stack = err && err.stack ? err.stack : undefined;
176
+ console.error(
177
+ JSON.stringify({ msg: "lifecycle-hook-crashed", reason, stack })
178
+ );
179
+ return { hookStatus: "FAILED", reason: `Runner crashed: ${reason}` };
180
+ }
181
+ };
182
+
183
+ exports._internals = {
184
+ startedByTag,
185
+ buildContainerOverrides,
186
+ findExistingTaskArn,
187
+ pollTaskUntilStopped,
188
+ runHandler,
189
+ POLL_INTERVAL_MS,
190
+ MAX_POLLS_PER_INVOCATION,
191
+ HOOK_CALLBACK_DELAY_SECONDS
192
+ };
@@ -0,0 +1,104 @@
1
+ import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
2
+ import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
3
+ import { EbsDeviceVolumeType, Volume } from "aws-cdk-lib/aws-ec2";
4
+ import { Construct } from "constructs";
5
+ import { LambdaFunction } from "./lambda.js";
6
+ import { SQSQueue } from "../messaging/sqs.js";
7
+ /**
8
+ * Description prefix baked into the LAUNCHING Lambda + execution-role
9
+ * descriptions. Tests grep CFN output for this needle to locate the function
10
+ * across stacks; keep it here as the single source of truth so a rename does
11
+ * not silently break the test query.
12
+ */
13
+ export declare const PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION = "PersistentDataVolume launching handler";
14
+ export declare const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE = "fjall:Lifecycle";
15
+ export declare const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE = "data-volume";
16
+ export declare const PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
17
+ export declare const PERSISTENT_DATA_VOLUME_TAG_STACK_ID = "fjall:StackId";
18
+ export interface PersistentDataVolumeProps {
19
+ /** ASG whose EC2_INSTANCE_LAUNCHING transitions trigger the re-attach. */
20
+ autoScalingGroup: AutoScalingGroup;
21
+ /** Size in GiB. */
22
+ sizeGb: number;
23
+ /** Device path the bootstrap script expects (e.g. /dev/xvdf). */
24
+ deviceName: string;
25
+ /**
26
+ * Availability zone the volume lives in. MUST match the ASG's single AZ —
27
+ * standalone EBS volumes are AZ-local and cannot follow a multi-AZ ASG.
28
+ * Pass `Stack.of(this).availabilityZones[0]` from a single-AZ-constrained
29
+ * consumer.
30
+ */
31
+ availabilityZone: string;
32
+ /** EBS volume type. Defaults to gp3. */
33
+ volumeType?: EbsDeviceVolumeType;
34
+ /** Provisioned IOPS (gp3/io1/io2 only). */
35
+ iops?: number;
36
+ /** Throughput in MiB/s (gp3 only). */
37
+ throughputMbps?: number;
38
+ }
39
+ /**
40
+ * Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
41
+ * re-attaches the same physical volume across instance refreshes.
42
+ *
43
+ * Pairs with the TERMINATING-side DetachVolume branch in
44
+ * `ec2GracefulTerminationLambda.source.cjs` (see `DetachVolumeCommand` import
45
+ * + invocation) wired by `ec2GracefulTerminationHandler.ts` when
46
+ * `dataVolumeOwnerLogicalId` is forwarded from `Ec2Instance`. The two
47
+ * Lambdas form a closed re-attach contract.
48
+ *
49
+ * Volume tagging: three tags carry the re-attach identity —
50
+ * `fjall:Lifecycle = data-volume` (constant; gates IAM scoping)
51
+ * `fjall:OwnerLogicalId = <this.node.path>` (per-construct, stable across
52
+ * instance refreshes in one stack so the Lambda re-finds the same volume)
53
+ * `fjall:StackId = <Aws.STACK_ID>` (per-stack-creation discriminator;
54
+ * CloudFormation issues a fresh StackId for every CREATE, so orphans
55
+ * from prior failed deploys or aborted `cdk destroy` runs carry a stale
56
+ * StackId and cannot collide with the new deploy's volume)
57
+ *
58
+ * The LAUNCHING Lambda filters DescribeVolumes by all three tags AND ABANDONs
59
+ * on multi-match. The TERMINATING handler MUST be configured with the same
60
+ * `ownerLogicalId` (and is in the same stack, so shares the StackId
61
+ * automatically) to locate the volume for detach.
62
+ *
63
+ * Why three tags. The first two are stable per logical construct; without
64
+ * the StackId discriminator, every prior-iteration orphan (CREATE_ROLLBACK,
65
+ * UPDATE_ROLLBACK_FAILED, DELETE_FAILED, manual interruption) that CFN
66
+ * cannot guarantee to delete on rollback would multi-match the LAUNCHING
67
+ * filter and force ABANDON on the next deploy. The StackId tag makes the
68
+ * system orphan-tolerant by construction — orphans simply don't match.
69
+ *
70
+ * Removal policy `SNAPSHOT` is independent: it covers data preservation
71
+ * across clean `cdk destroy`, not orphan defence. Recovery from a snapshot
72
+ * after destroy is still a manual restore-with-new-tags step (intentionally
73
+ * undocumented as a runbook — `cdk destroy` of a stateful stack is rare
74
+ * enough that ad-hoc recovery is acceptable).
75
+ */
76
+ export declare class PersistentDataVolume extends Construct {
77
+ readonly volume: Volume;
78
+ readonly lambda: LambdaFunction;
79
+ readonly queue: SQSQueue;
80
+ /**
81
+ * Identifier used as the `fjall:OwnerLogicalId` tag on the volume and the
82
+ * `OWNER_LOGICAL_ID` env on the Lambda. Consumers MUST pass this exact
83
+ * value to the TERMINATING handler so it locates the same volume for
84
+ * detach. Derived from `this.node.path`; renaming or reparenting any
85
+ * ancestor changes the value, which breaks the re-attach chain (Lambda
86
+ * filter no longer matches, new instance fails its launch hook). Rename
87
+ * across an existing deployment is a manual snapshot-restore step.
88
+ */
89
+ readonly ownerLogicalId: string;
90
+ readonly attachFailureAlarm: Alarm;
91
+ constructor(scope: Construct, id: string, props: PersistentDataVolumeProps);
92
+ }
93
+ declare function validatePersistentDataVolumeProps(props: PersistentDataVolumeProps): void;
94
+ export declare const _internals: {
95
+ LAUNCHING_LAMBDA_SOURCE_FILE: string;
96
+ LAMBDA_TIMEOUT_SECONDS: number;
97
+ LAUNCHING_HOOK_HEARTBEAT_SECONDS: number;
98
+ QUEUE_VISIBILITY_TIMEOUT_SECONDS: number;
99
+ ALARM_EVALUATION_PERIODS: number;
100
+ ALARM_DATAPOINTS_TO_ALARM: number;
101
+ ALARM_THRESHOLD: number;
102
+ validatePersistentDataVolumeProps: typeof validatePersistentDataVolumeProps;
103
+ };
104
+ export {};
@@ -0,0 +1,245 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Aws, Duration, RemovalPolicy, Size, Stack, Tags } from "aws-cdk-lib";
5
+ import { DefaultResult, LifecycleTransition } from "aws-cdk-lib/aws-autoscaling";
6
+ import { Alarm, ComparisonOperator, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
7
+ import { EbsDeviceVolumeType, Volume } from "aws-cdk-lib/aws-ec2";
8
+ import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
9
+ import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
10
+ import { RetentionDays } from "aws-cdk-lib/aws-logs";
11
+ import { Construct } from "constructs";
12
+ import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
13
+ import { LambdaFunction } from "./lambda.js";
14
+ import { SQSQueue } from "../messaging/sqs.js";
15
+ import { Subscription } from "../messaging/subscription.js";
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const LAUNCHING_LAMBDA_SOURCE_FILE = "persistentDataVolumeLambda.source.cjs";
18
+ /**
19
+ * Description prefix baked into the LAUNCHING Lambda + execution-role
20
+ * descriptions. Tests grep CFN output for this needle to locate the function
21
+ * across stacks; keep it here as the single source of truth so a rename does
22
+ * not silently break the test query.
23
+ */
24
+ export const PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION = "PersistentDataVolume launching handler";
25
+ const LAMBDA_TIMEOUT_SECONDS = 180;
26
+ // Heartbeat must comfortably exceed the Lambda's 90s attach deadline.
27
+ const LAUNCHING_HOOK_HEARTBEAT_SECONDS = 300;
28
+ const QUEUE_VISIBILITY_TIMEOUT_SECONDS = 240;
29
+ export const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE = "fjall:Lifecycle";
30
+ export const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE = "data-volume";
31
+ export const PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
32
+ export const PERSISTENT_DATA_VOLUME_TAG_STACK_ID = "fjall:StackId";
33
+ const ALARM_EVALUATION_PERIODS = 3;
34
+ const ALARM_DATAPOINTS_TO_ALARM = 3;
35
+ const ALARM_THRESHOLD = 1;
36
+ /**
37
+ * Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
38
+ * re-attaches the same physical volume across instance refreshes.
39
+ *
40
+ * Pairs with the TERMINATING-side DetachVolume branch in
41
+ * `ec2GracefulTerminationLambda.source.cjs` (see `DetachVolumeCommand` import
42
+ * + invocation) wired by `ec2GracefulTerminationHandler.ts` when
43
+ * `dataVolumeOwnerLogicalId` is forwarded from `Ec2Instance`. The two
44
+ * Lambdas form a closed re-attach contract.
45
+ *
46
+ * Volume tagging: three tags carry the re-attach identity —
47
+ * `fjall:Lifecycle = data-volume` (constant; gates IAM scoping)
48
+ * `fjall:OwnerLogicalId = <this.node.path>` (per-construct, stable across
49
+ * instance refreshes in one stack so the Lambda re-finds the same volume)
50
+ * `fjall:StackId = <Aws.STACK_ID>` (per-stack-creation discriminator;
51
+ * CloudFormation issues a fresh StackId for every CREATE, so orphans
52
+ * from prior failed deploys or aborted `cdk destroy` runs carry a stale
53
+ * StackId and cannot collide with the new deploy's volume)
54
+ *
55
+ * The LAUNCHING Lambda filters DescribeVolumes by all three tags AND ABANDONs
56
+ * on multi-match. The TERMINATING handler MUST be configured with the same
57
+ * `ownerLogicalId` (and is in the same stack, so shares the StackId
58
+ * automatically) to locate the volume for detach.
59
+ *
60
+ * Why three tags. The first two are stable per logical construct; without
61
+ * the StackId discriminator, every prior-iteration orphan (CREATE_ROLLBACK,
62
+ * UPDATE_ROLLBACK_FAILED, DELETE_FAILED, manual interruption) that CFN
63
+ * cannot guarantee to delete on rollback would multi-match the LAUNCHING
64
+ * filter and force ABANDON on the next deploy. The StackId tag makes the
65
+ * system orphan-tolerant by construction — orphans simply don't match.
66
+ *
67
+ * Removal policy `SNAPSHOT` is independent: it covers data preservation
68
+ * across clean `cdk destroy`, not orphan defence. Recovery from a snapshot
69
+ * after destroy is still a manual restore-with-new-tags step (intentionally
70
+ * undocumented as a runbook — `cdk destroy` of a stateful stack is rare
71
+ * enough that ad-hoc recovery is acceptable).
72
+ */
73
+ export class PersistentDataVolume extends Construct {
74
+ volume;
75
+ lambda;
76
+ queue;
77
+ /**
78
+ * Identifier used as the `fjall:OwnerLogicalId` tag on the volume and the
79
+ * `OWNER_LOGICAL_ID` env on the Lambda. Consumers MUST pass this exact
80
+ * value to the TERMINATING handler so it locates the same volume for
81
+ * detach. Derived from `this.node.path`; renaming or reparenting any
82
+ * ancestor changes the value, which breaks the re-attach chain (Lambda
83
+ * filter no longer matches, new instance fails its launch hook). Rename
84
+ * across an existing deployment is a manual snapshot-restore step.
85
+ */
86
+ ownerLogicalId;
87
+ attachFailureAlarm;
88
+ constructor(scope, id, props) {
89
+ super(scope, id);
90
+ validatePersistentDataVolumeProps(props);
91
+ this.ownerLogicalId = this.node.path;
92
+ this.volume = new Volume(this, "Volume", {
93
+ availabilityZone: props.availabilityZone,
94
+ size: Size.gibibytes(props.sizeGb),
95
+ volumeType: props.volumeType ?? EbsDeviceVolumeType.GP3,
96
+ encrypted: true,
97
+ ...(props.iops !== undefined && { iops: props.iops }),
98
+ ...(props.throughputMbps !== undefined && {
99
+ throughput: props.throughputMbps
100
+ })
101
+ });
102
+ this.volume.applyRemovalPolicy(RemovalPolicy.SNAPSHOT);
103
+ Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE, PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE);
104
+ Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID, this.ownerLogicalId);
105
+ Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_STACK_ID, Aws.STACK_ID);
106
+ this.queue = new SQSQueue(this, `${id}Queue`, {
107
+ visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
108
+ deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
109
+ });
110
+ const sourcePath = path.resolve(__dirname, LAUNCHING_LAMBDA_SOURCE_FILE);
111
+ const source = readFileSync(sourcePath, "utf-8");
112
+ const stack = Stack.of(this);
113
+ const volumeArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/*`;
114
+ const instanceArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance/*`;
115
+ // Account/region-scoped wildcard rather than the specific ASG ARN. The
116
+ // specific ARN would create an implicit CFN Ref to the ASG, forcing the
117
+ // Lambda's IAM Policy to wait for ASG CREATE_COMPLETE. The ASG cannot
118
+ // complete until the Lambda calls CompleteLifecycleAction on its first
119
+ // instance — the wildcard breaks the deadlock without widening blast
120
+ // radius: the Lambda is only invoked by its own SQS queue, which is fed
121
+ // by an EventBridge rule discriminating by the stack-scoped
122
+ // LifecycleHookName below.
123
+ const asgArnWildcard = `arn:${stack.partition}:autoscaling:${stack.region}:${stack.account}:autoScalingGroup:*:autoScalingGroupName/*`;
124
+ const describeVolumesPolicy = new PolicyStatement({
125
+ effect: Effect.ALLOW,
126
+ actions: ["ec2:DescribeVolumes"],
127
+ resources: ["*"]
128
+ });
129
+ // Split volume + instance: StringEqualsIfExists granted AttachVolume on
130
+ // any untagged resource (incl. arbitrary instances). Use StringEquals on
131
+ // the volume side; instances launched by the ASG carry no lifecycle tag.
132
+ const attachVolumeOnTaggedVolumePolicy = new PolicyStatement({
133
+ effect: Effect.ALLOW,
134
+ actions: ["ec2:AttachVolume"],
135
+ resources: [volumeArnPattern],
136
+ conditions: {
137
+ StringEquals: {
138
+ [`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE}`]: PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE,
139
+ [`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_STACK_ID}`]: Aws.STACK_ID
140
+ }
141
+ }
142
+ });
143
+ const attachVolumeOnInstancePolicy = new PolicyStatement({
144
+ effect: Effect.ALLOW,
145
+ actions: ["ec2:AttachVolume"],
146
+ resources: [instanceArnPattern]
147
+ });
148
+ const completeLifecyclePolicy = new PolicyStatement({
149
+ effect: Effect.ALLOW,
150
+ actions: ["autoscaling:CompleteLifecycleAction"],
151
+ resources: [asgArnWildcard]
152
+ });
153
+ this.lambda = new LambdaFunction(this, `${id}Fn`, {
154
+ runtime: Runtime.NODEJS_22_X,
155
+ handler: "index.handler",
156
+ code: Code.fromInline(source),
157
+ lambdaDescription: `${id} ${PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION}`,
158
+ roleDescription: `Execution role for ${id} ${PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION}`,
159
+ timeout: LAMBDA_TIMEOUT_SECONDS,
160
+ memorySize: 256,
161
+ logGroupRetention: RetentionDays.ONE_MONTH,
162
+ environment: {
163
+ OWNER_LOGICAL_ID: this.ownerLogicalId,
164
+ STACK_ID: Aws.STACK_ID,
165
+ DEVICE_NAME: props.deviceName
166
+ },
167
+ inlinePolicy: [
168
+ describeVolumesPolicy,
169
+ attachVolumeOnTaggedVolumePolicy,
170
+ attachVolumeOnInstancePolicy,
171
+ completeLifecyclePolicy
172
+ ]
173
+ });
174
+ this.lambda.addSqsEventSource(this.queue.getQueue());
175
+ // Hook name embeds Aws.STACK_NAME (a pseudo-parameter Ref, NOT a resource
176
+ // Ref — does not create a CFN dep) so that LifecycleHookName alone is
177
+ // sufficient to discriminate across stacks in the EB pattern below. The
178
+ // sibling TERMINATING hook uses "-terminating" so the two never collide
179
+ // within a single stack either.
180
+ const launchingHookName = `${Aws.STACK_NAME}-${id}-launching`;
181
+ attachInlineAsgLifecycleHook(this, `${id}LaunchingHook`, {
182
+ autoScalingGroup: props.autoScalingGroup,
183
+ hookName: launchingHookName,
184
+ lifecycleTransition: LifecycleTransition.INSTANCE_LAUNCHING,
185
+ defaultResult: DefaultResult.ABANDON,
186
+ heartbeatTimeoutSeconds: LAUNCHING_HOOK_HEARTBEAT_SECONDS
187
+ });
188
+ // Pattern discriminates by LifecycleHookName only — the hook name carries
189
+ // Aws.STACK_NAME so it is unique per stack. AutoScalingGroupName would
190
+ // also discriminate but creates an implicit Ref to the ASG, blocking the
191
+ // EB rule on ASG CREATE_COMPLETE (which can't happen without this Lambda
192
+ // running — the deadlock that motivated the wildcard above).
193
+ new Subscription(this, `${id}LaunchingSub`, {
194
+ pattern: {
195
+ source: ["aws.autoscaling"],
196
+ detailType: ["EC2 Instance-launch Lifecycle Action"],
197
+ detail: {
198
+ LifecycleHookName: [launchingHookName]
199
+ }
200
+ },
201
+ target: this.queue
202
+ });
203
+ this.attachFailureAlarm = new Alarm(this, "AttachFailureAlarm", {
204
+ alarmDescription: `${id} PersistentDataVolume LAUNCHING Lambda errors — ` +
205
+ `volume re-attach has failed; new instances will not see existing data.`,
206
+ metric: new Metric({
207
+ namespace: "AWS/Lambda",
208
+ metricName: "Errors",
209
+ dimensionsMap: { FunctionName: this.lambda.functionName },
210
+ statistic: "Sum",
211
+ period: Duration.minutes(1)
212
+ }),
213
+ threshold: ALARM_THRESHOLD,
214
+ evaluationPeriods: ALARM_EVALUATION_PERIODS,
215
+ datapointsToAlarm: ALARM_DATAPOINTS_TO_ALARM,
216
+ comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
217
+ treatMissingData: TreatMissingData.NOT_BREACHING
218
+ });
219
+ }
220
+ }
221
+ function validatePersistentDataVolumeProps(props) {
222
+ if (props.sizeGb <= 0 || !Number.isFinite(props.sizeGb)) {
223
+ throw new Error(`PersistentDataVolume.sizeGb must be a positive number; got ${props.sizeGb}`);
224
+ }
225
+ if (props.deviceName === "") {
226
+ throw new Error("PersistentDataVolume.deviceName must be non-empty");
227
+ }
228
+ if (props.availabilityZone === "") {
229
+ throw new Error("PersistentDataVolume.availabilityZone must be non-empty");
230
+ }
231
+ if (props.throughputMbps !== undefined &&
232
+ (props.volumeType ?? EbsDeviceVolumeType.GP3) !== EbsDeviceVolumeType.GP3) {
233
+ throw new Error("PersistentDataVolume.throughputMbps is only valid for gp3 volumes");
234
+ }
235
+ }
236
+ export const _internals = {
237
+ LAUNCHING_LAMBDA_SOURCE_FILE,
238
+ LAMBDA_TIMEOUT_SECONDS,
239
+ LAUNCHING_HOOK_HEARTBEAT_SECONDS,
240
+ QUEUE_VISIBILITY_TIMEOUT_SECONDS,
241
+ ALARM_EVALUATION_PERIODS,
242
+ ALARM_DATAPOINTS_TO_ALARM,
243
+ ALARM_THRESHOLD,
244
+ validatePersistentDataVolumeProps
245
+ };