@fjall/components-infrastructure 3.7.0 → 3.8.0

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.
@@ -8,8 +8,24 @@ import { Construct } from "constructs";
8
8
  * producer.
9
9
  */
10
10
  export declare const SHARED_ALARM_TOPIC_EXPORT_NAME = "SharedAlarmTopicArn";
11
+ export interface SharedAlarmTopicProps {
12
+ /**
13
+ * Delivery endpoints for alarm notifications. Without at least one
14
+ * subscription the topic receives every ALARM/OK transition and drops it —
15
+ * alarms exist but page nobody.
16
+ *
17
+ * `emails` require the recipient to click SNS's confirmation email before
18
+ * delivery starts. `httpsEndpoints` must speak the SNS webhook protocol
19
+ * (SubscriptionConfirmation handshake); the Fjall webapp ingest at
20
+ * `/api/internal/alarm-webhook` does.
21
+ */
22
+ subscriptions?: {
23
+ emails?: string[];
24
+ httpsEndpoints?: string[];
25
+ };
26
+ }
11
27
  export declare class SharedAlarmTopic extends Construct {
12
28
  readonly topic: ITopic;
13
29
  readonly topicArn: CfnOutput;
14
- constructor(scope: Construct, id: string);
30
+ constructor(scope: Construct, id: string, props?: SharedAlarmTopicProps);
15
31
  }
@@ -1,4 +1,5 @@
1
1
  import { CfnOutput } from "aws-cdk-lib";
2
+ import { EmailSubscription, UrlSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
2
3
  import { Construct } from "constructs";
3
4
  import { SNSTopic } from "../../resources/aws/messaging/sns.js";
4
5
  /**
@@ -11,12 +12,18 @@ export const SHARED_ALARM_TOPIC_EXPORT_NAME = "SharedAlarmTopicArn";
11
12
  export class SharedAlarmTopic extends Construct {
12
13
  topic;
13
14
  topicArn;
14
- constructor(scope, id) {
15
+ constructor(scope, id, props) {
15
16
  super(scope, id);
16
17
  const wrapped = new SNSTopic(this, "AlarmNotifications", {
17
18
  displayName: "Fjall CloudWatch Alarm Notifications"
18
19
  });
19
20
  this.topic = wrapped.getTopic();
21
+ for (const email of props?.subscriptions?.emails ?? []) {
22
+ this.topic.addSubscription(new EmailSubscription(email));
23
+ }
24
+ for (const endpoint of props?.subscriptions?.httpsEndpoints ?? []) {
25
+ this.topic.addSubscription(new UrlSubscription(endpoint));
26
+ }
20
27
  // SharedAlarmTopicArn is a well-known cross-stack export consumed via
21
28
  // Fn.importValue (see resolveAlertsTopic + its tests). Do not delete
22
29
  // thinking it duplicates the wrapper's AlarmNotificationsTopicArn —
@@ -1,4 +1,5 @@
1
1
  import { Stack, type StackProps } from "aws-cdk-lib";
2
+ import { type SharedAlarmTopicProps } from "../../config/aws/index.js";
2
3
  import { type Construct } from "constructs";
3
4
  import { ConfigRulePreset } from "../../config/aws/configRulePreset.js";
4
5
  import { GuardDutyDetector } from "../../config/aws/guardDutyDetector.js";
@@ -25,6 +26,13 @@ export interface AccountProps extends StackProps {
25
26
  * persisted anywhere the deploy flow could read it).
26
27
  */
27
28
  securityTier?: GovernancePreset;
29
+ /**
30
+ * Delivery endpoints for the account's shared alarm topic. Undefined ⇒
31
+ * the topic synthesises with no subscribers and every alarm transition is
32
+ * dropped. Sourced from the generated `account/infrastructure.ts`, same as
33
+ * `securityTier`.
34
+ */
35
+ alarmSubscriptions?: SharedAlarmTopicProps["subscriptions"];
28
36
  }
29
37
  export declare class Account extends Stack {
30
38
  readonly organisationType: OrganisationType;
@@ -48,7 +48,11 @@ export class Account extends Stack {
48
48
  exportName: "AccountId",
49
49
  description: "AWS Account ID for this account"
50
50
  });
51
- new SharedAlarmTopic(this, "AlarmTopic");
51
+ new SharedAlarmTopic(this, "AlarmTopic", {
52
+ ...(props.alarmSubscriptions !== undefined && {
53
+ subscriptions: props.alarmSubscriptions
54
+ })
55
+ });
52
56
  const isStandaloneAccount = this.constructor === Account;
53
57
  const ipamPoolId = this.node.tryGetContext("ipamPoolId");
54
58
  if (isStandaloneAccount && ipamPoolId && this.resolvedRegion) {
@@ -427,7 +427,10 @@ export class RelationalDatabase extends Construct {
427
427
  databaseInsights: props.databaseInsights,
428
428
  deletionProtection: props.deletionProtection,
429
429
  snapshotIdentifier: props.snapshotIdentifier,
430
- snapshotUsername: props.snapshotUsername
430
+ snapshotUsername: props.snapshotUsername,
431
+ alertsTopic: this.resolveAlertsTopic(props.alertsTopic),
432
+ alarms: props.alarms,
433
+ applicationId: props.applicationId
431
434
  });
432
435
  this.connections = this.database.connections;
433
436
  this.addDatabaseOutputs(props);
@@ -1,7 +1,7 @@
1
1
  import { CfnOutput, Duration, Stack, Tags } from "aws-cdk-lib";
2
2
  import { Port } from "aws-cdk-lib/aws-ec2";
3
3
  import { Cluster } from "aws-cdk-lib/aws-ecs";
4
- import { Repository } from "aws-cdk-lib/aws-ecr";
4
+ import { Repository, TagMutability } from "aws-cdk-lib/aws-ecr";
5
5
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
6
6
  import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
7
7
  import { PublicHostedZone } from "aws-cdk-lib/aws-route53";
@@ -238,10 +238,17 @@ export class DevSubstrate extends Construct {
238
238
  // getDefaultContainerRegistry's `${stackPrefix}Ecr`.
239
239
  const slotEcrId = `${toPascalCase(this.app.getName())}${DEV_SUBSTRATE_SLOT_ECR_ID}`;
240
240
  const slotEcrRepositoryName = `${devFenceName(props.appKebab)}-slots`;
241
- // Adopt-mode is an import only — no CFN resource, so it carries no fence tags.
241
+ // Adopt-mode is an import only — no CFN resource, so it carries no fence tags
242
+ // (nor mutability convergence — a pre-BUG-22 adopted repo keeps IMMUTABLE
243
+ // until flipped out-of-band).
242
244
  const slotRepository = props.adoptSlotEcr === true
243
245
  ? Repository.fromRepositoryName(this, slotEcrId, slotEcrRepositoryName)
244
- : new Ecr(this, slotEcrId, { repositoryName: slotEcrRepositoryName });
246
+ : new Ecr(this, slotEcrId, {
247
+ repositoryName: slotEcrRepositoryName,
248
+ // Every `fjall dev up` repushes the branch tag; IMMUTABLE breaks the
249
+ // first same-branch recreate (BUG-22).
250
+ tagMutability: TagMutability.MUTABLE
251
+ });
245
252
  return {
246
253
  vpc,
247
254
  slotSecurityGroup,
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
 
3
3
  // Dev-substrate slot waker, invoked as an ALB Lambda target. Milestone
4
- // placeholder the real wake-token check + ECS/Aurora resume land in §5.4.
4
+ // placeholder - the real wake-token check + ECS/Aurora resume land in 5.4.
5
5
  // ALB Lambda-target responses MUST carry all five fields; a missing
6
6
  // statusDescription or isBase64Encoded 502s at the ALB.
7
+ // ASCII-ONLY FILE: this source ships inline as the template ZipFile, and
8
+ // CloudFormation GetTemplate lossily transcodes non-ASCII to "?" - a single
9
+ // non-ASCII byte here wedges recreate/forget surgery on the Compute stack
10
+ // (the flip proof reports Code as directly modified). Pinned by test.
7
11
  exports.handler = async () => ({
8
12
  statusCode: 200,
9
13
  statusDescription: "200 OK",
@@ -1,12 +1,14 @@
1
1
  import { type Construct } from "constructs";
2
2
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
3
3
  import { type IEventBus } from "aws-cdk-lib/aws-events";
4
+ import type { ITopic } from "aws-cdk-lib/aws-sns";
4
5
  import type App from "../../app.js";
5
6
  import { SQSQueue } from "../../resources/aws/messaging/sqs.js";
6
7
  import { SNSTopic, type SNSTopicProps } from "../../resources/aws/messaging/sns.js";
7
8
  import { EventBridgeBus } from "../../resources/aws/messaging/eventbridge.js";
8
9
  import { type RemovalPolicyString } from "../../resources/aws/messaging/utils.js";
9
10
  import { Subscription, type SubscriptionProps } from "../../resources/aws/messaging/subscription.js";
11
+ import { type SqsAlarmThresholds } from "../../resources/aws/monitoring/index.js";
10
12
  import { type IQueueMessaging, type ITopicMessaging, type IEventBusMessaging, type AnyMessaging } from "./interfaces/messaging.js";
11
13
  import { type IQueueConnector } from "./interfaces/connector.js";
12
14
  /**
@@ -31,6 +33,16 @@ export interface IQueueProps {
31
33
  fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
32
34
  deduplicationScope?: "queue" | "messageGroup";
33
35
  removalPolicy?: RemovalPolicyString;
36
+ /**
37
+ * SNS topic for the DLQ-depth alarm. Accepts ITopic, ARN string, or
38
+ * "import:ExportName" (production apps use "import:SharedAlarmTopicArn").
39
+ * Only queues with `deadLetterQueue.enabled` gain the alarm.
40
+ */
41
+ alertsTopic?: ITopic | string;
42
+ /** Alarm thresholds. false to disable, undefined for defaults, object to override. */
43
+ alarms?: SqsAlarmThresholds | false;
44
+ /** Application ID for alarm tagging. */
45
+ applicationId?: string;
34
46
  }
35
47
  /**
36
48
  * Topic messaging props with type discriminator. Extends SNSTopicProps so the
@@ -4,6 +4,7 @@ import { SNSTopic } from "../../resources/aws/messaging/sns.js";
4
4
  import { EventBridgeBus } from "../../resources/aws/messaging/eventbridge.js";
5
5
  import { Subscription } from "../../resources/aws/messaging/subscription.js";
6
6
  import { FjallLogger } from "../../utils/validationLogger.js";
7
+ import { resolveAlertsTopic } from "../../utils/resolveAlertsTopic.js";
7
8
  /**
8
9
  * Validates SQS props and logs warnings for misconfigured options.
9
10
  */
@@ -88,7 +89,11 @@ export class QueueMessaging extends SQSQueue {
88
89
  contentBasedDeduplication: props.contentBasedDeduplication,
89
90
  fifoThroughputLimit: props.fifoThroughputLimit,
90
91
  deduplicationScope: props.deduplicationScope,
91
- removalPolicy: props.removalPolicy
92
+ removalPolicy: props.removalPolicy,
93
+ // Resolved in the parent scope — `this` does not exist before super().
94
+ alertsTopic: resolveAlertsTopic(scope, `${id}AlertsTopic`, props.alertsTopic),
95
+ alarms: props.alarms,
96
+ applicationId: props.applicationId
92
97
  };
93
98
  super(scope, id, queueProps);
94
99
  }
@@ -39,7 +39,8 @@ export interface PersistentDataVolumeProps {
39
39
  /**
40
40
  * SNS topic `attachFailureAlarm` notifies. Omitted, the alarm still exists
41
41
  * but fires no action — pass the stack's alerts topic or the failure is
42
- * only visible to someone watching the CloudWatch console.
42
+ * only visible to someone watching the CloudWatch console. Also wires the
43
+ * re-attach queue's DLQ-depth alarm (created only when the topic is set).
43
44
  */
44
45
  alarmTopic?: ITopic;
45
46
  }
@@ -110,7 +110,8 @@ export class PersistentDataVolume extends Construct {
110
110
  // Transient volume-attach signals — no durable state (the EBS volume
111
111
  // itself is SNAPSHOT above). Pinned DESTROY (now also the SQSQueue wrapper
112
112
  // default) so a replacing deploy reclaims this queue + DLQ, not orphans.
113
- removalPolicy: "DESTROY"
113
+ removalPolicy: "DESTROY",
114
+ alertsTopic: props.alarmTopic
114
115
  });
115
116
  const sourcePath = path.resolve(__dirname, LAUNCHING_LAMBDA_SOURCE_FILE);
116
117
  const source = readFileSync(sourcePath, "utf-8");
@@ -5,6 +5,7 @@ import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
5
5
  import { SQSQueue } from "./sqs.js";
6
6
  import { resolveTarget } from "./eventTargets.js";
7
7
  import { createScheduleAlarms } from "../monitoring/scheduleAlarms.js";
8
+ import { createSqsDlqAlarms } from "../monitoring/sqsAlarms.js";
8
9
  /**
9
10
  * Build the default description for a Schedule.
10
11
  */
@@ -31,7 +32,8 @@ export class EventBridgeRule extends Construct {
31
32
  constructor(scope, id, props) {
32
33
  super(scope, id);
33
34
  this.id = id;
34
- const dlq = this.#resolveOrProvisionDlq(id, props.deadLetterQueue);
35
+ const dlqResolution = this.#resolveOrProvisionDlq(id, props.deadLetterQueue);
36
+ const dlq = dlqResolution?.queue;
35
37
  if (dlq !== undefined)
36
38
  this.deadLetterQueue = dlq;
37
39
  grantKmsDecryptToEventBridge(dlq);
@@ -53,20 +55,24 @@ export class EventBridgeRule extends Construct {
53
55
  crossStackScope: props.crossStackScope,
54
56
  targets: [targetAdapter]
55
57
  });
56
- provisionAlarms(this, id, props, this.#rule, dlq);
58
+ provisionAlarms(this, id, props, this.#rule, dlqResolution);
57
59
  }
58
60
  #resolveOrProvisionDlq(id, prop) {
59
61
  if (prop === undefined)
60
62
  return undefined;
61
63
  if (prop instanceof SQSQueue)
62
- return prop;
64
+ return { queue: prop, provisioned: false };
63
65
  if (prop.enabled === false)
64
66
  return undefined;
65
- if ("queue" in prop && prop.queue !== undefined)
66
- return prop.queue;
67
- return new SQSQueue(this, `${id}Dlq`, {
68
- queueType: "standard"
69
- });
67
+ if ("queue" in prop && prop.queue !== undefined) {
68
+ return { queue: prop.queue, provisioned: false };
69
+ }
70
+ return {
71
+ queue: new SQSQueue(this, `${id}Dlq`, {
72
+ queueType: "standard"
73
+ }),
74
+ provisioned: true
75
+ };
70
76
  }
71
77
  getRule() {
72
78
  return this.#rule;
@@ -107,4 +113,18 @@ function provisionAlarms(scope, id, props, rule, dlq) {
107
113
  applicationId: props.applicationId,
108
114
  dlqEnabled: dlq !== undefined
109
115
  });
116
+ // Depth alarm only on the rule-provisioned DLQ — a BYO/shared queue would
117
+ // collect one duplicate alarm per referencing rule; its owner alarms it.
118
+ if (dlq !== undefined && dlq.provisioned) {
119
+ createSqsDlqAlarms({
120
+ scope,
121
+ queueName: id,
122
+ deadLetterQueue: dlq.queue.getQueue(),
123
+ config: props.alarmConfig?.dlqDepthThreshold !== undefined
124
+ ? { dlqDepthThreshold: props.alarmConfig.dlqDepthThreshold }
125
+ : {},
126
+ alarmTopic: props.alertsTopic,
127
+ applicationId: props.applicationId
128
+ });
129
+ }
110
130
  }
@@ -1,7 +1,9 @@
1
1
  import { Construct } from "constructs";
2
2
  import { type IQueue } from "aws-cdk-lib/aws-sqs";
3
3
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
4
+ import type { ITopic } from "aws-cdk-lib/aws-sns";
4
5
  import { type RemovalPolicyString } from "./utils.js";
6
+ import { type SqsAlarmThresholds } from "../monitoring/index.js";
5
7
  /**
6
8
  * AWS SQS service limits and defaults.
7
9
  * @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-quotas.html
@@ -67,6 +69,20 @@ export interface SQSQueueProps {
67
69
  * irreplaceable and must survive stack deletion.
68
70
  */
69
71
  removalPolicy?: RemovalPolicyString;
72
+ /**
73
+ * SNS topic for the DLQ-depth alarm. When set (and `alarms` is not false),
74
+ * the auto-created dead-letter queue gains an alarm that fires on any
75
+ * visible message — a dead-lettered message means a consumer exhausted its
76
+ * receive budget, and an unwatched DLQ ages messages out silently after
77
+ * 14 days. BYO DLQs (`deadLetterQueue.queue`) are not alarmed here: the
78
+ * queue's owner wires its alarms, and a shared DLQ would otherwise collect
79
+ * one duplicate alarm per referencing queue.
80
+ */
81
+ alertsTopic?: ITopic;
82
+ /** Alarm thresholds. false to disable, undefined for defaults, object to override. */
83
+ alarms?: SqsAlarmThresholds | false;
84
+ /** Application ID for alarm tagging. */
85
+ applicationId?: string;
70
86
  }
71
87
  export declare class SQSQueue extends Construct {
72
88
  readonly id: string;
@@ -3,6 +3,7 @@ import { CfnOutput, Duration } from "aws-cdk-lib";
3
3
  import { toPascalCase } from "../../../utils/capitaliseString.js";
4
4
  import { Queue, QueueEncryption, FifoThroughputLimit, DeduplicationScope } from "aws-cdk-lib/aws-sqs";
5
5
  import { toRemovalPolicy } from "./utils.js";
6
+ import { createSqsDlqAlarms } from "../monitoring/index.js";
6
7
  /**
7
8
  * AWS SQS service limits and defaults.
8
9
  * @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-quotas.html
@@ -181,6 +182,16 @@ export class SQSQueue extends Construct {
181
182
  value: this.queue.queueName,
182
183
  description: `SQS queue name for ${id}`
183
184
  });
185
+ if (props.alertsTopic && props.alarms !== false && this.dlq !== undefined) {
186
+ createSqsDlqAlarms({
187
+ scope: this,
188
+ queueName: id,
189
+ deadLetterQueue: this.dlq,
190
+ config: typeof props.alarms === "object" ? props.alarms : {},
191
+ alarmTopic: props.alertsTopic,
192
+ applicationId: props.applicationId
193
+ });
194
+ }
184
195
  }
185
196
  /**
186
197
  * Get the SQS queue URL.
@@ -27,6 +27,9 @@ export declare const ALARM_DEFAULTS: {
27
27
  readonly ERROR_RATE: 5;
28
28
  readonly DURATION_PERCENT: 80;
29
29
  };
30
+ readonly SQS: {
31
+ readonly DLQ_VISIBLE_MESSAGES: 0;
32
+ };
30
33
  };
31
34
  /** Build alarm description, appending the application ID tag for webhook extraction. */
32
35
  export declare function buildAlarmDescription(baseDescription: string, applicationId: string | undefined): string;
@@ -10,7 +10,8 @@ export const ALARM_DEFAULTS = {
10
10
  ECS: { CPU: 80, MEMORY: 80, RUNNING_TASKS_MIN: 1 },
11
11
  ALB: { HTTP_5XX_PERCENT: 5, P99_RESPONSE_TIME_MS: 3000 },
12
12
  RDS: { CPU: 80, FREE_STORAGE_GIB: 5, CONNECTIONS: 50 },
13
- LAMBDA: { ERROR_RATE: 5, DURATION_PERCENT: 80 }
13
+ LAMBDA: { ERROR_RATE: 5, DURATION_PERCENT: 80 },
14
+ SQS: { DLQ_VISIBLE_MESSAGES: 0 }
14
15
  };
15
16
  /** Build alarm description, appending the application ID tag for webhook extraction. */
16
17
  export function buildAlarmDescription(baseDescription, applicationId) {
@@ -3,6 +3,7 @@ export { createEcsServiceAlarms, type EcsServiceAlarmThresholds, type EcsService
3
3
  export { createRdsAlarms, type RdsAlarmThresholds, type RdsAlarmsProps } from "./rdsAlarms.js";
4
4
  export { createLambdaAlarms, type LambdaAlarmThresholds, type LambdaAlarmsProps } from "./lambdaAlarms.js";
5
5
  export { createScheduleAlarms, type ScheduleAlarmThresholds, type CreateScheduleAlarmsProps } from "./scheduleAlarms.js";
6
+ export { createSqsDlqAlarms, type SqsAlarmThresholds, type SqsDlqAlarmsProps } from "./sqsAlarms.js";
6
7
  export { createClickHouseAlarms, type ClickHouseAlarmThresholds, type ClickHouseAlarmsProps } from "./clickhouseAlarms.js";
7
8
  export { createBuildkiteAlarms, type BuildkiteAlarmsProps } from "./buildkiteAlarms.js";
8
9
  export { createLogPatternAlarms, type LogPatternAlarmSpec, type LogPatternAlarmsProps } from "./logPatternAlarms.js";
@@ -3,6 +3,7 @@ export { createEcsServiceAlarms } from "./ecsAlarms.js";
3
3
  export { createRdsAlarms } from "./rdsAlarms.js";
4
4
  export { createLambdaAlarms } from "./lambdaAlarms.js";
5
5
  export { createScheduleAlarms } from "./scheduleAlarms.js";
6
+ export { createSqsDlqAlarms } from "./sqsAlarms.js";
6
7
  export { createClickHouseAlarms } from "./clickhouseAlarms.js";
7
8
  export { createBuildkiteAlarms } from "./buildkiteAlarms.js";
8
9
  export { createLogPatternAlarms } from "./logPatternAlarms.js";
@@ -17,6 +17,12 @@ export interface ScheduleAlarmThresholds {
17
17
  * expected cadence (silent disablement detector).
18
18
  */
19
19
  matchedEventsLowFloorRatio?: number;
20
+ /**
21
+ * Visible-message count above which the rule's DLQ-depth alarm fires
22
+ * (`createSqsDlqAlarms`, wired by `eventBridgeRule.ts` when a DLQ exists).
23
+ * Default 0 — any dead-lettered event pages.
24
+ */
25
+ dlqDepthThreshold?: number;
20
26
  }
21
27
  export interface CreateScheduleAlarmsProps {
22
28
  scope: Construct;
@@ -0,0 +1,23 @@
1
+ import { Alarm } from "aws-cdk-lib/aws-cloudwatch";
2
+ import type { IQueue } from "aws-cdk-lib/aws-sqs";
3
+ import type { ITopic } from "aws-cdk-lib/aws-sns";
4
+ import type { Construct } from "constructs";
5
+ export interface SqsAlarmThresholds {
6
+ /** Visible-message count above which the DLQ-depth alarm fires. Default 0 — any dead-lettered message pages. */
7
+ dlqDepthThreshold?: number;
8
+ }
9
+ export interface SqsDlqAlarmsProps {
10
+ scope: Construct;
11
+ queueName: string;
12
+ deadLetterQueue: IQueue;
13
+ config: SqsAlarmThresholds;
14
+ alarmTopic: ITopic;
15
+ applicationId?: string;
16
+ }
17
+ /**
18
+ * DLQ-depth alarm: fires when the dead-letter queue holds any visible
19
+ * message. A dead-lettered message is a single-event failure (a poison
20
+ * message has exhausted its receive budget), so this uses the 1/1
21
+ * single-event evaluation shape rather than the utilisation 3/2 shape.
22
+ */
23
+ export declare function createSqsDlqAlarms(props: SqsDlqAlarmsProps): Alarm[];
@@ -0,0 +1,29 @@
1
+ import { Alarm, ComparisonOperator, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
2
+ import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
3
+ import { ALARM_DEFAULTS, registerAlarm, tagAlarmsWithApplicationId, buildAlarmDescription } from "./alarmDefaults.js";
4
+ /**
5
+ * DLQ-depth alarm: fires when the dead-letter queue holds any visible
6
+ * message. A dead-lettered message is a single-event failure (a poison
7
+ * message has exhausted its receive budget), so this uses the 1/1
8
+ * single-event evaluation shape rather than the utilisation 3/2 shape.
9
+ */
10
+ export function createSqsDlqAlarms(props) {
11
+ const { scope, queueName, deadLetterQueue, config, alarmTopic, applicationId } = props;
12
+ const alarms = [];
13
+ const snsAction = new SnsAction(alarmTopic);
14
+ const dlqDepthAlarm = new Alarm(scope, `${queueName}DlqDepthAlarm`, {
15
+ alarmDescription: buildAlarmDescription(`Dead-letter queue for ${queueName} contains messages`, applicationId),
16
+ metric: deadLetterQueue.metricApproximateNumberOfMessagesVisible({
17
+ period: ALARM_DEFAULTS.EVALUATION_PERIOD,
18
+ statistic: "Maximum"
19
+ }),
20
+ threshold: config.dlqDepthThreshold ?? ALARM_DEFAULTS.SQS.DLQ_VISIBLE_MESSAGES,
21
+ evaluationPeriods: 1,
22
+ datapointsToAlarm: 1,
23
+ comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
24
+ treatMissingData: TreatMissingData.NOT_BREACHING
25
+ });
26
+ registerAlarm(dlqDepthAlarm, snsAction, alarms);
27
+ tagAlarmsWithApplicationId(alarms, applicationId);
28
+ return alarms;
29
+ }
@@ -1,9 +1,12 @@
1
1
  import { type Construct } from "constructs";
2
- import { Repository, type RepositoryProps } from "aws-cdk-lib/aws-ecr";
2
+ import { Repository, type RepositoryProps, TagMutability } from "aws-cdk-lib/aws-ecr";
3
3
  import type App from "../../../app.js";
4
4
  import { type StackBuilder } from "../base/awsStack.js";
5
5
  interface EcrProps {
6
6
  repositoryName?: string;
7
+ /** Default IMMUTABLE (supply-chain guard). Dev-slot repos MUST pass MUTABLE —
8
+ * every `fjall dev up` repushes the branch tag (BUG-22). */
9
+ tagMutability?: TagMutability;
7
10
  }
8
11
  export declare class EcrFactory {
9
12
  static build(id: string, props?: EcrProps): (app: App, scope: Construct) => Ecr;
@@ -29,7 +29,7 @@ export class Ecr extends Repository {
29
29
  return {
30
30
  ...(props?.repositoryName && { repositoryName: props.repositoryName }),
31
31
  imageScanOnPush: true,
32
- imageTagMutability: TagMutability.IMMUTABLE,
32
+ imageTagMutability: props?.tagMutability ?? TagMutability.IMMUTABLE,
33
33
  emptyOnDelete: false,
34
34
  removalPolicy: RemovalPolicy.RETAIN
35
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^3.7.0",
71
- "@fjall/util": "^3.7.0",
70
+ "@fjall/generator": "^3.8.0",
71
+ "@fjall/util": "^3.8.0",
72
72
  "constructs": "^10.6.0"
73
73
  },
74
74
  "overrides": {
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
84
  },
85
- "gitHead": "2ee5e51bc6aa45e34ec70160b36ddf976371ccd5"
85
+ "gitHead": "9a137b6bd380f432f6c6e8e43b7932dec0b64a13"
86
86
  }