@fjall/components-infrastructure 0.96.0 → 0.99.3

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 +173 -0
  32. package/dist/lib/patterns/aws/clickhouseDatabase.js +601 -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 +160 -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
@@ -1,21 +1,37 @@
1
- import { SQSQueue } from "../../resources/aws/messaging/sqs.js";
1
+ import { EventBus as CdkEventBus } from "aws-cdk-lib/aws-events";
2
+ import { SQSQueue, SQS_LIMITS } from "../../resources/aws/messaging/sqs.js";
2
3
  import { SNSTopic } from "../../resources/aws/messaging/sns.js";
3
4
  import { EventBridgeBus } from "../../resources/aws/messaging/eventbridge.js";
5
+ import { Subscription } from "../../resources/aws/messaging/subscription.js";
4
6
  import { FjallLogger } from "../../utils/validationLogger.js";
5
7
  /**
6
8
  * Validates SQS props and logs warnings for misconfigured options.
7
9
  */
8
10
  function validateSQSProps(props) {
9
- // Validate visibility timeout range (0-43200 seconds = 12 hours)
10
11
  if (props.visibilityTimeout !== undefined &&
11
- (props.visibilityTimeout < 0 || props.visibilityTimeout > 43200)) {
12
- throw new Error("visibilityTimeout must be between 0 and 43200 seconds (12 hours)");
12
+ (props.visibilityTimeout < SQS_LIMITS.VISIBILITY_TIMEOUT.MIN_SECONDS ||
13
+ props.visibilityTimeout > SQS_LIMITS.VISIBILITY_TIMEOUT.MAX_SECONDS)) {
14
+ throw new Error(`visibilityTimeout must be between ${SQS_LIMITS.VISIBILITY_TIMEOUT.MIN_SECONDS} and ${SQS_LIMITS.VISIBILITY_TIMEOUT.MAX_SECONDS} seconds (12 hours)`);
13
15
  }
14
- // Validate message retention period (60-1209600 seconds = 1 minute to 14 days)
15
16
  if (props.messageRetentionPeriod !== undefined &&
16
- (props.messageRetentionPeriod < 60 ||
17
- props.messageRetentionPeriod > 1209600)) {
18
- throw new Error("messageRetentionPeriod must be between 60 and 1209600 seconds (1 minute to 14 days)");
17
+ (props.messageRetentionPeriod < SQS_LIMITS.MESSAGE_RETENTION.MIN_SECONDS ||
18
+ props.messageRetentionPeriod > SQS_LIMITS.MESSAGE_RETENTION.MAX_SECONDS)) {
19
+ throw new Error(`messageRetentionPeriod must be between ${SQS_LIMITS.MESSAGE_RETENTION.MIN_SECONDS} and ${SQS_LIMITS.MESSAGE_RETENTION.MAX_SECONDS} seconds (1 minute to 14 days)`);
20
+ }
21
+ if (props.deliveryDelay !== undefined &&
22
+ (props.deliveryDelay < SQS_LIMITS.DELAY.MIN_SECONDS ||
23
+ props.deliveryDelay > SQS_LIMITS.DELAY.MAX_SECONDS)) {
24
+ throw new Error(`deliveryDelay must be between ${SQS_LIMITS.DELAY.MIN_SECONDS} and ${SQS_LIMITS.DELAY.MAX_SECONDS} seconds (15 minutes)`);
25
+ }
26
+ if (props.receiveMessageWaitTime !== undefined &&
27
+ (props.receiveMessageWaitTime < SQS_LIMITS.RECEIVE_WAIT_TIME.MIN_SECONDS ||
28
+ props.receiveMessageWaitTime > SQS_LIMITS.RECEIVE_WAIT_TIME.MAX_SECONDS)) {
29
+ throw new Error(`receiveMessageWaitTime must be between ${SQS_LIMITS.RECEIVE_WAIT_TIME.MIN_SECONDS} and ${SQS_LIMITS.RECEIVE_WAIT_TIME.MAX_SECONDS} seconds (long-poll cap)`);
30
+ }
31
+ if (props.maxMessageSize !== undefined &&
32
+ (props.maxMessageSize < SQS_LIMITS.MESSAGE_SIZE.MIN_BYTES ||
33
+ props.maxMessageSize > SQS_LIMITS.MESSAGE_SIZE.MAX_BYTES)) {
34
+ throw new Error(`maxMessageSize must be between ${SQS_LIMITS.MESSAGE_SIZE.MIN_BYTES} and ${SQS_LIMITS.MESSAGE_SIZE.MAX_BYTES} bytes (256 KB)`);
19
35
  }
20
36
  // Warn about FIFO-specific options on standard queues
21
37
  if (props.queueType !== "fifo") {
@@ -58,6 +74,7 @@ export class QueueMessaging extends SQSQueue {
58
74
  /** The connector type for unified connection processing. */
59
75
  connectorType = "queue";
60
76
  constructor(scope, id, props) {
77
+ validateSQSProps(props);
61
78
  const queueProps = {
62
79
  queueType: props.queueType,
63
80
  visibilityTimeout: props.visibilityTimeout,
@@ -120,14 +137,8 @@ export class TopicMessaging extends SNSTopic {
120
137
  /** Type discriminator for runtime type narrowing. */
121
138
  messagingType = "topic";
122
139
  constructor(scope, id, props) {
123
- const topicProps = {
124
- topicName: props.topicName,
125
- displayName: props.displayName,
126
- fifo: props.fifo,
127
- contentBasedDeduplication: props.contentBasedDeduplication,
128
- removalPolicy: props.removalPolicy
129
- };
130
- super(scope, id, topicProps);
140
+ validateTopicProps(props);
141
+ super(scope, id, props);
131
142
  }
132
143
  /**
133
144
  * Grant publish permissions to the grantee.
@@ -151,12 +162,33 @@ export class TopicMessaging extends SNSTopic {
151
162
  export class EventBusMessaging extends EventBridgeBus {
152
163
  /** Type discriminator for runtime type narrowing. */
153
164
  messagingType = "eventBus";
165
+ /**
166
+ * Application name threaded into Subscription default descriptions when the
167
+ * caller does not supply one. Captured at construction so `subscribe(...)`
168
+ * has the same naming context as `App.addSchedule(...)`.
169
+ */
170
+ #appName;
171
+ /**
172
+ * Brand distinguishing app buses from the AWS-managed account+region default
173
+ * bus. Used by `subscribe(...)` to refuse `aws.*` source patterns on app
174
+ * buses (silent-no-op trap — AWS service events only fire on the default
175
+ * bus). Set by `fromAwsServiceBus(...)`; defaults to `false` for buses
176
+ * created via the regular constructor path or `fromEventBusArn(...)`.
177
+ */
178
+ #isAwsServiceBus;
154
179
  constructor(scope, id, props) {
155
180
  const eventBusProps = {
156
181
  eventBusName: props.eventBusName,
157
- removalPolicy: props.removalPolicy
182
+ appName: props.appName,
183
+ description: props.description,
184
+ removalPolicy: props.removalPolicy,
185
+ importedBus: props.importedBus
158
186
  };
159
187
  super(scope, id, eventBusProps);
188
+ if (props.appName !== undefined) {
189
+ this.#appName = props.appName;
190
+ }
191
+ this.#isAwsServiceBus = props.awsServiceBus === true;
160
192
  }
161
193
  /**
162
194
  * Grant put events permissions to the grantee.
@@ -165,6 +197,70 @@ export class EventBusMessaging extends EventBridgeBus {
165
197
  grantPutEventsTo(grantee) {
166
198
  return this.getEventBus().grantPutEventsTo(grantee);
167
199
  }
200
+ /**
201
+ * Add a pub/sub subscription scoped to this bus. Threads the bus's
202
+ * `IEventBus` and the captured `appName` into the Subscription so the
203
+ * synthesised rule attaches to this bus (not the default AWS bus) and the
204
+ * default description picks up the bus's app context.
205
+ *
206
+ * Refuses `aws.*` source patterns when the wrapper is an app bus rather
207
+ * than the AWS-managed default bus. AWS service events fire only on the
208
+ * account+region default bus; subscribing them on a custom app bus
209
+ * synthesises and deploys cleanly but never invokes the target. Use
210
+ * `EventBusMessaging.fromAwsServiceBus(...)` to import the default bus
211
+ * for AWS service event traffic.
212
+ */
213
+ subscribe(id, props) {
214
+ if (!this.#isAwsServiceBus) {
215
+ const sources = props.pattern.source ?? [];
216
+ const awsSources = sources.filter((s) => typeof s === "string" && s.startsWith("aws."));
217
+ if (awsSources.length > 0) {
218
+ throw new Error(`Subscription '${id}' uses AWS service source(s) [${awsSources.join(", ")}] on a custom app event bus. AWS service events fire only on the account+region default bus; subscribing them on a custom bus deploys cleanly but never invokes the target. Use EventBusMessaging.fromAwsServiceBus(scope, id, region, account) instead of app.getEventBus().`);
219
+ }
220
+ }
221
+ return new Subscription(this, id, {
222
+ ...props,
223
+ eventBus: this.getEventBus(),
224
+ appName: props.appName ?? this.#appName
225
+ });
226
+ }
227
+ /**
228
+ * Wrap a bus imported from another account/region by ARN per D19. The
229
+ * returned handle supports `subscribe(...)` identically to created buses;
230
+ * rules are owned by the importing stack and attach to the imported bus's
231
+ * ARN. The base wrapper short-circuits resource creation when `importedBus`
232
+ * is set, so no `AWS::Events::EventBus` is synthesised and bus-level
233
+ * mutations (description, removalPolicy, CfnOutputs) are skipped.
234
+ */
235
+ static fromEventBusArn(scope, id, arn) {
236
+ const importedBus = CdkEventBus.fromEventBusArn(scope, `${id}Bus`, arn);
237
+ return new EventBusMessaging(scope, id, {
238
+ type: "eventBus",
239
+ importedBus
240
+ });
241
+ }
242
+ /**
243
+ * Wrap the AWS-managed account+region `default` event bus. This is the
244
+ * bus that receives `aws.*` service events emitted via CloudTrail
245
+ * (`aws.ecr` `CreateRepository`, `aws.ec2` state-change, etc.); custom
246
+ * Fjall app buses do not receive this traffic. The returned wrapper has
247
+ * the AWS-service brand set, so `subscribe(...)` accepts `aws.*` source
248
+ * patterns; subscribing app-source patterns is still allowed but
249
+ * unusual (the canonical app-source path is `app.getEventBus()`).
250
+ *
251
+ * @param region AWS region holding the default bus (typically the stack
252
+ * region — the default bus is per-account-per-region).
253
+ * @param account AWS account id holding the default bus.
254
+ */
255
+ static fromAwsServiceBus(scope, id, region, account) {
256
+ const arn = `arn:aws:events:${region}:${account}:event-bus/default`;
257
+ const importedBus = CdkEventBus.fromEventBusArn(scope, `${id}Bus`, arn);
258
+ return new EventBusMessaging(scope, id, {
259
+ type: "eventBus",
260
+ importedBus,
261
+ awsServiceBus: true
262
+ });
263
+ }
168
264
  }
169
265
  /**
170
266
  * Factory for creating messaging resources.
@@ -209,10 +305,8 @@ export class MessagingFactory {
209
305
  return (_app, scope) => {
210
306
  switch (props.type) {
211
307
  case "queue":
212
- validateSQSProps(props);
213
308
  return new QueueMessaging(scope, id, props);
214
309
  case "topic":
215
- validateTopicProps(props);
216
310
  return new TopicMessaging(scope, id, props);
217
311
  case "eventBus":
218
312
  return new EventBusMessaging(scope, id, props);
@@ -224,5 +318,6 @@ export class MessagingFactory {
224
318
  };
225
319
  }
226
320
  }
227
- // Re-export type guards
321
+ // Canonical D18(b) re-export pinned by subscription.test.ts integrity suite.
322
+ export { EventField, RuleTargetInput } from "aws-cdk-lib/aws-events";
228
323
  export { isQueueMessaging, isTopicMessaging, isEventBusMessaging } from "./interfaces/messaging.js";
@@ -1,5 +1,5 @@
1
1
  import { Construct } from "constructs";
2
- import { Vpc } from "../../resources/aws/networking/vpc.js";
2
+ import { DEFAULT_MAX_AZS, Vpc } from "../../resources/aws/networking/vpc.js";
3
3
  import { FjallLogger } from "../../utils/validationLogger.js";
4
4
  function validateNetworkProps(props) {
5
5
  if (props.maxAzs !== undefined && (props.maxAzs < 1 || props.maxAzs > 3)) {
@@ -8,7 +8,7 @@ function validateNetworkProps(props) {
8
8
  if (typeof props.natGateways === "object" &&
9
9
  props.natGateways !== null &&
10
10
  props.natGateways.count !== undefined) {
11
- const maxAzs = props.maxAzs ?? 3;
11
+ const maxAzs = props.maxAzs ?? DEFAULT_MAX_AZS;
12
12
  if (props.natGateways.count > maxAzs) {
13
13
  FjallLogger.warn(`'natGateways.count' (${props.natGateways.count}) exceeds maxAzs (${maxAzs}).`);
14
14
  }
@@ -20,7 +20,6 @@ function validateNetworkProps(props) {
20
20
  export class NetworkFactory {
21
21
  static build(id, props) {
22
22
  return (app, scope) => {
23
- validateNetworkProps(props);
24
23
  return new Network(scope, id, props, app);
25
24
  };
26
25
  }
@@ -29,15 +28,25 @@ export class Network extends Construct {
29
28
  vpc;
30
29
  constructor(scope, id, props, app) {
31
30
  super(scope, id);
31
+ validateNetworkProps(props);
32
32
  this.vpc = this.createVpc(id, props, app);
33
33
  }
34
34
  createVpc(id, props, app) {
35
35
  const rawAccountId = app.node.tryGetContext("accountId");
36
- const accountId = (typeof rawAccountId === "string" ? rawAccountId : undefined) ||
37
- process.env.CDK_DEFAULT_ACCOUNT;
38
- const region = process.env.CDK_DEFAULT_REGION;
36
+ const guardedCtxAccountId = typeof rawAccountId === "string" && rawAccountId !== ""
37
+ ? rawAccountId
38
+ : undefined;
39
+ const envAccountId = process.env.CDK_DEFAULT_ACCOUNT;
40
+ const accountId = guardedCtxAccountId ??
41
+ (envAccountId !== undefined && envAccountId !== ""
42
+ ? envAccountId
43
+ : undefined);
44
+ const envRegion = process.env.CDK_DEFAULT_REGION;
45
+ const region = envRegion !== undefined && envRegion !== "" ? envRegion : undefined;
39
46
  const rawIpamPoolId = app.node.tryGetContext("ipamPoolId");
40
- const ipv4IpamPoolId = typeof rawIpamPoolId === "string" ? rawIpamPoolId : undefined;
47
+ const ipv4IpamPoolId = typeof rawIpamPoolId === "string" && rawIpamPoolId !== ""
48
+ ? rawIpamPoolId
49
+ : undefined;
41
50
  const vpcProps = {
42
51
  vpcName: props.vpcName,
43
52
  accountId,
@@ -1,4 +1,5 @@
1
1
  import { type Environment, Stack, type StackProps } from "aws-cdk-lib";
2
+ import { type CustomPermissionSets } from "../../config/aws/identityCenter.js";
2
3
  import { ScpPreset } from "../../config/aws/scpPreset.js";
3
4
  import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
4
5
  import { OrganisationResource } from "../../resources/aws/organisation/index.js";
@@ -32,11 +33,14 @@ export declare class Organisation extends Stack {
32
33
  private accountRefs;
33
34
  private accountMap;
34
35
  private accountsConfig;
36
+ private identityCenter?;
35
37
  constructor(id: string, props: OrganisationProps);
36
38
  private createAccountReferences;
37
39
  private setupOrganisationFeatures;
38
40
  private setupCostAllocationTagActivator;
39
41
  private setupIdentityCenter;
42
+ declarePermissionSets(customs: CustomPermissionSets): void;
43
+ assignGroupMembers(members: Record<string, string[]>): void;
40
44
  getOrganisation(): OrganisationResource;
41
45
  getAccounts(): Record<string, string>;
42
46
  enableScps(props: ScpPresetProps): ScpPreset;
@@ -23,6 +23,7 @@ export class Organisation extends Stack {
23
23
  accountRefs = [];
24
24
  accountMap;
25
25
  accountsConfig;
26
+ identityCenter;
26
27
  constructor(id, props) {
27
28
  const config = getConfig();
28
29
  const env = props.env ?? {
@@ -42,8 +43,9 @@ export class Organisation extends Stack {
42
43
  }
43
44
  this.accountsConfig = props.accounts;
44
45
  const orgId = this.node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
45
- const rootId = this.node.tryGetContext("rootId");
46
- const managementAccountId = this.node.tryGetContext("managementAccountId") ?? this.account;
46
+ const rootId = this.node.tryGetContext(CDK_CONTEXT_KEYS.ROOT_ID);
47
+ const managementAccountId = this.node.tryGetContext(CDK_CONTEXT_KEYS.MANAGEMENT_ACCOUNT_ID) ??
48
+ this.account;
47
49
  if (!orgId || !rootId) {
48
50
  throw new Error("orgId and rootId must be provided via CDK context. Run deployment through the Fjall CLI.");
49
51
  }
@@ -76,7 +78,11 @@ export class Organisation extends Stack {
76
78
  this.setupCostAllocationTagActivator();
77
79
  }
78
80
  setupCostAllocationTagActivator() {
79
- new CostAllocationTagActivator(this, "CostAllocationTagActivator");
81
+ const activator = new CostAllocationTagActivator(this, "CostAllocationTagActivator");
82
+ App.getInstance().addSchedule("CostAllocationTagActivatorSchedule", {
83
+ schedule: "rate(24 hours)",
84
+ target: activator
85
+ });
80
86
  }
81
87
  setupIdentityCenter(identityCenter, managementAccountId) {
82
88
  if (identityCenter) {
@@ -88,11 +94,23 @@ export class Organisation extends Stack {
88
94
  ssoAccounts[name] = id;
89
95
  }
90
96
  }
91
- new IdentityCenter(this, "IdentityCenter", {
97
+ this.identityCenter = new IdentityCenter(this, "IdentityCenter", {
92
98
  accounts: ssoAccounts
93
99
  });
94
100
  }
95
101
  }
102
+ declarePermissionSets(customs) {
103
+ if (this.identityCenter === undefined) {
104
+ throw new Error("Identity Center is not enabled. Pass identityCenter: true to OrganisationFactory.");
105
+ }
106
+ this.identityCenter.declarePermissionSets(customs);
107
+ }
108
+ assignGroupMembers(members) {
109
+ if (this.identityCenter === undefined) {
110
+ throw new Error("Identity Center is not enabled. Pass identityCenter: true to OrganisationFactory.");
111
+ }
112
+ this.identityCenter.assignGroupMembers(members);
113
+ }
96
114
  getOrganisation() {
97
115
  return this.org;
98
116
  }
@@ -1,9 +1,8 @@
1
1
  import { Construct } from "constructs";
2
2
  import { type IBucket, type EventType, type IBucketNotificationDestination, type NotificationKeyFilter } from "aws-cdk-lib/aws-s3";
3
- import { BucketDeployment } from "aws-cdk-lib/aws-s3-deployment";
4
3
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
5
4
  import type App from "../../app.js";
6
- import { type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
5
+ import { BucketDeployment, type WebsiteHostingConfig } from "../../resources/aws/storage/index.js";
7
6
  import { type BackupTier } from "../../utils/backupTierMapping.js";
8
7
  import { type IStorage } from "./interfaces/storage.js";
9
8
  import { type IStorageConnector } from "./interfaces/connector.js";
@@ -1,9 +1,9 @@
1
1
  import { Construct } from "constructs";
2
2
  import { BucketEncryption, HttpMethods } from "aws-cdk-lib/aws-s3";
3
- import { BucketDeployment, Source, CacheControl } from "aws-cdk-lib/aws-s3-deployment";
3
+ import { Source, CacheControl } from "aws-cdk-lib/aws-s3-deployment";
4
4
  import { Key } from "aws-cdk-lib/aws-kms";
5
5
  import { CfnOutput, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
6
- import { S3Bucket } from "../../resources/aws/storage/index.js";
6
+ import { BucketDeployment, S3Bucket } from "../../resources/aws/storage/index.js";
7
7
  import { FjallLogger } from "../../utils/validationLogger.js";
8
8
  export { isStorage } from "./interfaces/storage.js";
9
9
  function toBucketEncryption(encryption) {
@@ -50,6 +50,7 @@ export class Storage extends Construct {
50
50
  bucketDeployment;
51
51
  constructor(scope, id, props) {
52
52
  super(scope, id);
53
+ validateStorageProps(props);
53
54
  const encryptionKey = props.kmsKeyArn
54
55
  ? Key.fromKeyArn(this, `${id}KmsKey`, props.kmsKeyArn)
55
56
  : undefined;
@@ -1,3 +1,4 @@
1
+ import { Stack } from "aws-cdk-lib";
1
2
  import { StringListParameter, StringParameter } from "aws-cdk-lib/aws-ssm";
2
3
  import { buildSsmPrefix, DEFAULT_ORG_ID, resolveOrgId } from "../../utils/cdkContext.js";
3
4
  import { VpcPeeringConnection } from "../../resources/aws/networking/vpcPeeringConnection.js";
@@ -16,13 +17,14 @@ export class VpcPeerFactory {
16
17
  const peerRouteTableIds = props.peerRouteTableIds ??
17
18
  StringListParameter.valueForTypedListParameter(scope, `${ssmPrefix}/route-table-ids`);
18
19
  const resolvedPeerOrgId = orgId !== DEFAULT_ORG_ID ? orgId : undefined;
20
+ const peerRegion = props.peerRegion ?? Stack.of(scope).region;
19
21
  return new VpcPeeringConnection(scope, id, {
20
22
  localVpc,
21
23
  localVpcCidr: localVpc.vpcCidrBlock,
22
24
  peerVpcId,
23
25
  peerVpcCidr,
24
26
  peerAccountId: props.peerAccountId,
25
- peerRegion: props.peerRegion,
27
+ peerRegion,
26
28
  peerRoleArn,
27
29
  peerRouteTableIds,
28
30
  enableDnsResolution: props.enableDnsResolution,
@@ -5,7 +5,7 @@ import { InstanceType, SubnetType, Connections, Port, UserData } from "aws-cdk-l
5
5
  import { AutoScalingGroup, Monitoring, BlockDeviceVolume, EbsDeviceVolumeType } from "aws-cdk-lib/aws-autoscaling";
6
6
  import { Duration, Stack } from "aws-cdk-lib";
7
7
  import { Construct } from "constructs";
8
- import { RetentionDays } from "aws-cdk-lib/aws-logs";
8
+ import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
9
9
  import { S3Bucket } from "../storage/s3.js";
10
10
  import { Secret } from "../secrets/secret.js";
11
11
  import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
@@ -238,7 +238,10 @@ export default class ClickHouse extends Construct {
238
238
  });
239
239
  // 11. Scheduled weekly backup to S3
240
240
  const backupDestUrl = `https://${backupBucket.bucketName}.s3.${Stack.of(this).region}.amazonaws.com/`;
241
- const backupTask = new ScheduledEc2Task(this, "ClickHouseBackupTask", {
241
+ const backupTaskLogGroup = new LogGroup(this, "ClickHouseBackupTaskLogGroup", {
242
+ retention: RetentionDays.TWO_WEEKS
243
+ });
244
+ new ScheduledEc2Task(this, "ClickHouseBackupTask", {
242
245
  cluster,
243
246
  schedule: Schedule.expression(BACKUP_SCHEDULE),
244
247
  scheduledEc2TaskImageOptions: {
@@ -255,7 +258,7 @@ export default class ClickHouse extends Construct {
255
258
  },
256
259
  logDriver: LogDriver.awsLogs({
257
260
  streamPrefix: "clickhouse-backup",
258
- logRetention: RetentionDays.TWO_WEEKS
261
+ logGroup: backupTaskLogGroup
259
262
  })
260
263
  },
261
264
  securityGroups: [securityGroup],
@@ -263,9 +266,11 @@ export default class ClickHouse extends Construct {
263
266
  subnetType
264
267
  }
265
268
  });
266
- // 12. Grant S3 write access to the backup task role
267
- backupBucket.grantReadWrite(backupTask.taskDefinition.taskRole);
268
- // 13. Grant secret read to execution role
269
+ // BACKUP DATABASE TO S3 runs inside the ClickHouse server process on the
270
+ // ASG instance, not the ephemeral backup task; the grant must therefore
271
+ // attach to the ASG instance role, not the task role.
272
+ backupBucket.grantReadWrite(asg.role);
273
+ // 12. Grant secret read to execution role
269
274
  const executionRole = taskDefinition.executionRole;
270
275
  if (!executionRole) {
271
276
  throw new Error("ClickHouse task definition has no execution role — cannot grant secret access");
@@ -274,15 +279,19 @@ export default class ClickHouse extends Construct {
274
279
  auditPasswordSecret.secret.grantRead(executionRole);
275
280
  backupPasswordSecret.secret.grantRead(executionRole);
276
281
  schemaPasswordSecret.secret.grantRead(executionRole);
277
- // 14. CloudWatch alarms (CPU, memory, disk) — wired only when an SNS topic is supplied
278
282
  if (props.alarmTopic) {
283
+ if (!props.webappLogGroup) {
284
+ throw new Error("ClickHouse: alarmTopic requires webappLogGroup so the stuck-merge metric filter can be wired.");
285
+ }
279
286
  createClickHouseAlarms({
280
287
  scope: this,
281
288
  asg,
282
- alarmTopic: props.alarmTopic
289
+ alarmTopic: props.alarmTopic,
290
+ webappLogGroup: props.webappLogGroup,
291
+ backupTaskLogGroup
283
292
  });
284
293
  }
285
- // 15. Connections and outputs
294
+ // 13. Connections and outputs
286
295
  this.connections = new Connections({
287
296
  securityGroups: [securityGroup],
288
297
  defaultPort: Port.tcp(CLICKHOUSE_HTTP_PORT)
@@ -1,6 +1,7 @@
1
1
  import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
2
2
  import type { AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
3
3
  import type { ITopic } from "aws-cdk-lib/aws-sns";
4
+ import type { ILogGroup } from "aws-cdk-lib/aws-logs";
4
5
  import type { Construct } from "constructs";
5
6
  export interface ClickHouseAlarmThresholds {
6
7
  /** EC2 host CPU % over 5 min. Default 90. */
@@ -16,19 +17,33 @@ export interface ClickHouseAlarmsProps {
16
17
  scope: Construct;
17
18
  asg: AutoScalingGroup;
18
19
  alarmTopic: ITopic;
20
+ /**
21
+ * Webapp log group. Required to wire the stuck-merge alarm — `client.ts`
22
+ * emits `serverLogger.warn("ClickHouse", "Stuck merge detected")` when
23
+ * `system.merges` shows a merge elapsed > 30 min.
24
+ */
25
+ webappLogGroup: ILogGroup;
26
+ /**
27
+ * Backup-task log group. Required to wire the backup-failure alarm —
28
+ * `BACKUP DATABASE … TO S3(…)` emits `AccessDenied` / `S3Exception` lines
29
+ * when the IAM grant or bucket policy is misconfigured (silent before the
30
+ * alarm landed; the daily backup task exited non-zero with no signal).
31
+ */
32
+ backupTaskLogGroup: ILogGroup;
19
33
  config?: ClickHouseAlarmThresholds;
20
34
  }
21
35
  /**
22
36
  * Single-node ClickHouse posture alarms. Covers host-level CPU + (optional)
23
- * memory and disk via the CloudWatch Agent metric namespace `CWAgent`.
37
+ * memory and disk via the CloudWatch Agent metric namespace `CWAgent`, plus
38
+ * two log-driven alarms:
24
39
  *
25
- * Engine-level alarms (parts count, failed inserts) require the Prometheus
26
- * exporter to be scraped into CW custom metrics. The Prometheus endpoint is
27
- * enabled in the user-data XML (port 9363); the scraper is a follow-up.
28
- *
29
- * Stuck merges are detected app-side: `client.ts` queries `system.merges`
30
- * every 5 min and logs `serverLogger.warn("ClickHouse", "Stuck merge detected")`
31
- * when elapsed > 30 min. A CW Logs metric filter on that pattern + alarm
32
- * completes the circuit once the webapp log group ARN is available here.
40
+ * - **Stuck merges** `client.ts` polls `system.merges` every 5 min and logs
41
+ * `serverLogger.warn("ClickHouse", "Stuck merge detected")` when elapsed
42
+ * exceeds 30 min. The metric filter on the webapp log group emits a count
43
+ * metric per match; the alarm fires on Sum >= 1 over 5 min × 2 evaluations.
44
+ * - **Backup failures** `AccessDenied` or `S3Exception` from the backup
45
+ * task's BACKUP DATABASE TO S3 statement. Closes the silent-failure mode
46
+ * that masked the original IAM-grant misconfiguration (see
47
+ * `designs/2026-04-27-clickhouse-backup-iam-role.md`).
33
48
  */
34
49
  export declare function createClickHouseAlarms(props: ClickHouseAlarmsProps): Alarm[];
@@ -2,22 +2,25 @@ import { Duration } from "aws-cdk-lib";
2
2
  import { Alarm, ComparisonOperator, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
3
3
  import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
4
4
  import { Metric } from "aws-cdk-lib/aws-cloudwatch";
5
+ import { FilterPattern, MetricFilter } from "aws-cdk-lib/aws-logs";
5
6
  import { ALARM_DEFAULTS, registerAlarm, buildAlarmDescription } from "../monitoring/alarmDefaults.js";
7
+ const CLICKHOUSE_METRIC_NAMESPACE = "Fjall/ClickHouse";
6
8
  /**
7
9
  * Single-node ClickHouse posture alarms. Covers host-level CPU + (optional)
8
- * memory and disk via the CloudWatch Agent metric namespace `CWAgent`.
10
+ * memory and disk via the CloudWatch Agent metric namespace `CWAgent`, plus
11
+ * two log-driven alarms:
9
12
  *
10
- * Engine-level alarms (parts count, failed inserts) require the Prometheus
11
- * exporter to be scraped into CW custom metrics. The Prometheus endpoint is
12
- * enabled in the user-data XML (port 9363); the scraper is a follow-up.
13
- *
14
- * Stuck merges are detected app-side: `client.ts` queries `system.merges`
15
- * every 5 min and logs `serverLogger.warn("ClickHouse", "Stuck merge detected")`
16
- * when elapsed > 30 min. A CW Logs metric filter on that pattern + alarm
17
- * completes the circuit once the webapp log group ARN is available here.
13
+ * - **Stuck merges** `client.ts` polls `system.merges` every 5 min and logs
14
+ * `serverLogger.warn("ClickHouse", "Stuck merge detected")` when elapsed
15
+ * exceeds 30 min. The metric filter on the webapp log group emits a count
16
+ * metric per match; the alarm fires on Sum >= 1 over 5 min × 2 evaluations.
17
+ * - **Backup failures** `AccessDenied` or `S3Exception` from the backup
18
+ * task's BACKUP DATABASE TO S3 statement. Closes the silent-failure mode
19
+ * that masked the original IAM-grant misconfiguration (see
20
+ * `designs/2026-04-27-clickhouse-backup-iam-role.md`).
18
21
  */
19
22
  export function createClickHouseAlarms(props) {
20
- const { scope, asg, alarmTopic, config = {} } = props;
23
+ const { scope, asg, alarmTopic, webappLogGroup, backupTaskLogGroup, config = {} } = props;
21
24
  const alarms = [];
22
25
  const snsAction = new SnsAction(alarmTopic);
23
26
  const asgName = asg.autoScalingGroupName;
@@ -85,5 +88,53 @@ export function createClickHouseAlarms(props) {
85
88
  treatMissingData: TreatMissingData.NOT_BREACHING
86
89
  });
87
90
  registerAlarm(diskCriticalAlarm, snsAction, alarms);
91
+ const stuckMergeMetricName = "ClickHouseStuckMergeCount";
92
+ new MetricFilter(scope, "ClickHouseStuckMergeMetricFilter", {
93
+ logGroup: webappLogGroup,
94
+ metricNamespace: CLICKHOUSE_METRIC_NAMESPACE,
95
+ metricName: stuckMergeMetricName,
96
+ filterPattern: FilterPattern.literal('"Stuck merge detected"'),
97
+ metricValue: "1",
98
+ defaultValue: 0
99
+ });
100
+ const stuckMergeAlarm = new Alarm(scope, "ClickHouseStuckMergeAlarm", {
101
+ alarmDescription: buildAlarmDescription("ClickHouse merge stuck > 30 min — investigate parts pressure or replica health", undefined),
102
+ metric: new Metric({
103
+ namespace: CLICKHOUSE_METRIC_NAMESPACE,
104
+ metricName: stuckMergeMetricName,
105
+ period: Duration.minutes(5),
106
+ statistic: "Sum"
107
+ }),
108
+ threshold: 1,
109
+ evaluationPeriods: 2,
110
+ datapointsToAlarm: 2,
111
+ comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
112
+ treatMissingData: TreatMissingData.NOT_BREACHING
113
+ });
114
+ registerAlarm(stuckMergeAlarm, snsAction, alarms);
115
+ const backupFailureMetricName = "ClickHouseBackupFailureCount";
116
+ new MetricFilter(scope, "ClickHouseBackupFailureMetricFilter", {
117
+ logGroup: backupTaskLogGroup,
118
+ metricNamespace: CLICKHOUSE_METRIC_NAMESPACE,
119
+ metricName: backupFailureMetricName,
120
+ filterPattern: FilterPattern.anyTerm("AccessDenied", "S3Exception"),
121
+ metricValue: "1",
122
+ defaultValue: 0
123
+ });
124
+ const backupFailureAlarm = new Alarm(scope, "ClickHouseBackupFailureAlarm", {
125
+ alarmDescription: buildAlarmDescription("ClickHouse BACKUP TO S3 emitted AccessDenied/S3Exception — verify ASG instance role grant on backup bucket", undefined),
126
+ metric: new Metric({
127
+ namespace: CLICKHOUSE_METRIC_NAMESPACE,
128
+ metricName: backupFailureMetricName,
129
+ period: Duration.hours(1),
130
+ statistic: "Sum"
131
+ }),
132
+ threshold: 1,
133
+ evaluationPeriods: 1,
134
+ datapointsToAlarm: 1,
135
+ comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
136
+ treatMissingData: TreatMissingData.NOT_BREACHING
137
+ });
138
+ registerAlarm(backupFailureAlarm, snsAction, alarms);
88
139
  return alarms;
89
140
  }
@@ -64,10 +64,10 @@ export declare const OPTIMISE_MV_TABLES: readonly ["metrics_hourly_mv", "metrics
64
64
  /** Resource allocation for the lightweight optimise task. */
65
65
  export declare const OPTIMISE_TASK_MEMORY_MIB = 256;
66
66
  export declare const OPTIMISE_TASK_CPU_UNITS = 256;
67
- /** Automated backup schedule (weekly, Sunday 03:00 UTC — low-traffic window). */
68
- export declare const BACKUP_SCHEDULE = "cron(0 3 ? * SUN *)";
67
+ /** Automated backup schedule (daily 03:00 UTC — low-traffic window). */
68
+ export declare const BACKUP_SCHEDULE = "cron(0 3 * * ? *)";
69
69
  /** Resource allocation for the backup task (lightweight — clickhouse-client only). */
70
70
  export declare const BACKUP_TASK_MEMORY_MIB = 256;
71
71
  export declare const BACKUP_TASK_CPU_UNITS = 256;
72
- /** Backup object expiration: 14 days (retains 2 weekly snapshots). */
72
+ /** Backup object expiration: 14 days (retains 14 daily snapshots). */
73
73
  export declare const BACKUP_RETENTION_DAYS = 14;
@@ -80,10 +80,10 @@ export const OPTIMISE_MV_TABLES = [
80
80
  /** Resource allocation for the lightweight optimise task. */
81
81
  export const OPTIMISE_TASK_MEMORY_MIB = 256;
82
82
  export const OPTIMISE_TASK_CPU_UNITS = 256;
83
- /** Automated backup schedule (weekly, Sunday 03:00 UTC — low-traffic window). */
84
- export const BACKUP_SCHEDULE = "cron(0 3 ? * SUN *)";
83
+ /** Automated backup schedule (daily 03:00 UTC — low-traffic window). */
84
+ export const BACKUP_SCHEDULE = "cron(0 3 * * ? *)";
85
85
  /** Resource allocation for the backup task (lightweight — clickhouse-client only). */
86
86
  export const BACKUP_TASK_MEMORY_MIB = 256;
87
87
  export const BACKUP_TASK_CPU_UNITS = 256;
88
- /** Backup object expiration: 14 days (retains 2 weekly snapshots). */
88
+ /** Backup object expiration: 14 days (retains 14 daily snapshots). */
89
89
  export const BACKUP_RETENTION_DAYS = 14;
@@ -1,4 +1,5 @@
1
1
  import type { IVpc, ISecurityGroup } from "aws-cdk-lib/aws-ec2";
2
+ import type { ILogGroup } from "aws-cdk-lib/aws-logs";
2
3
  import type { IBucket } from "aws-cdk-lib/aws-s3";
3
4
  import type { ISecret } from "aws-cdk-lib/aws-secretsmanager";
4
5
  import type { ITopic } from "aws-cdk-lib/aws-sns";
@@ -23,10 +24,15 @@ export interface ClickHouseProps {
23
24
  */
24
25
  r2Config?: ClickHouseR2Config;
25
26
  /**
26
- * SNS topic for CloudWatch alarms (CPU, memory, disk).
27
+ * SNS topic for CloudWatch alarms (CPU, memory, disk, stuck merges).
27
28
  * If omitted, posture alarms are not created.
28
29
  */
29
30
  alarmTopic?: ITopic;
31
+ /**
32
+ * Webapp log group, required when `alarmTopic` is set so the stuck-merge
33
+ * metric filter can read the structured warning emitted by `client.ts`.
34
+ */
35
+ webappLogGroup?: ILogGroup;
30
36
  }
31
37
  /** Cloudflare R2 configuration for tiered storage and backups. */
32
38
  export interface ClickHouseR2Config {