@fjall/components-infrastructure 2.16.0 → 2.17.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.
@@ -43,8 +43,13 @@ export class Ec2GracefulTerminationHandler extends Construct {
43
43
  const dataVolumeOwnerLogicalId = resolveOptionalString(props.dataVolumeOwnerLogicalId);
44
44
  this.queue = new SQSQueue(this, `${id}Queue`, {
45
45
  visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
46
- deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
46
+ deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
47
+ // Transient instance-drain signals — no durable state. Pinned DESTROY
48
+ // (now also the SQSQueue wrapper default) so a replacing deploy of the
49
+ // parent Ec2Instance reclaims this queue + DLQ instead of orphaning them.
50
+ removalPolicy: "DESTROY"
47
51
  });
52
+ const stack = Stack.of(this);
48
53
  const ecsPolicies = ecsClusterArn !== undefined
49
54
  ? [
50
55
  new PolicyStatement({
@@ -66,6 +71,9 @@ export class Ec2GracefulTerminationHandler extends Construct {
66
71
  })
67
72
  ]
68
73
  : [];
74
+ // EIP/ENI mutations cannot be ARN-scoped (the resources are not known at
75
+ // synth), but the Lambda only ever drains instances in its own region — a
76
+ // region clamp removes the cross-region blast radius a bare "*" would allow.
69
77
  const ec2Policies = new PolicyStatement({
70
78
  effect: Effect.ALLOW,
71
79
  actions: [
@@ -74,7 +82,10 @@ export class Ec2GracefulTerminationHandler extends Construct {
74
82
  "ec2:DescribeNetworkInterfaces",
75
83
  "ec2:DetachNetworkInterface"
76
84
  ],
77
- resources: ["*"]
85
+ resources: ["*"],
86
+ conditions: {
87
+ StringEquals: { "aws:RequestedRegion": stack.region }
88
+ }
78
89
  });
79
90
  const elbReadPolicy = new PolicyStatement({
80
91
  effect: Effect.ALLOW,
@@ -94,7 +105,6 @@ export class Ec2GracefulTerminationHandler extends Construct {
94
105
  }
95
106
  }
96
107
  });
97
- const stack = Stack.of(this);
98
108
  // Account/region-scoped wildcard rather than the specific ASG ARN —
99
109
  // see PersistentDataVolume for the full deadlock writeup. Same gotcha:
100
110
  // a specific ARN creates a CFN Ref to the ASG, the Lambda's IAM Policy
@@ -105,7 +105,11 @@ export class PersistentDataVolume extends Construct {
105
105
  Tags.of(this.volume).add(PERSISTENT_DATA_VOLUME_TAG_STACK_ID, Aws.STACK_ID);
106
106
  this.queue = new SQSQueue(this, `${id}Queue`, {
107
107
  visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
108
- deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
108
+ deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
109
+ // Transient volume-attach signals — no durable state (the EBS volume
110
+ // itself is SNAPSHOT above). Pinned DESTROY (now also the SQSQueue wrapper
111
+ // default) so a replacing deploy reclaims this queue + DLQ, not orphans.
112
+ removalPolicy: "DESTROY"
109
113
  });
110
114
  const sourcePath = path.resolve(__dirname, LAUNCHING_LAMBDA_SOURCE_FILE);
111
115
  const source = readFileSync(sourcePath, "utf-8");
@@ -7,6 +7,11 @@ export interface SNSTopicProps {
7
7
  displayName?: string;
8
8
  fifo?: boolean;
9
9
  contentBasedDeduplication?: boolean;
10
+ /**
11
+ * Removal policy for the topic. Defaults to DESTROY — a topic is a transient
12
+ * fan-out medium with no durable state (see the constructor). Pass "RETAIN"
13
+ * only for a topic that must survive stack deletion.
14
+ */
10
15
  removalPolicy?: RemovalPolicyString;
11
16
  }
12
17
  export declare class SNSTopic extends Construct {
@@ -22,7 +22,13 @@ export class SNSTopic extends Construct {
22
22
  ? (props.contentBasedDeduplication ?? true)
23
23
  : undefined
24
24
  });
25
- this.topic.applyRemovalPolicy(toRemovalPolicy(props.removalPolicy));
25
+ // An SNS topic is a transient fan-out medium: it holds no durable state
26
+ // (subscriptions are re-created by the next deploy), so the wrapper defaults
27
+ // to DESTROY — deliberately NOT the env-aware production->RETAIN of
28
+ // data-bearing wrappers (S3, EventBus). RETAIN here buys no protection and
29
+ // orphans the topic on a parent construct's logical-ID churn (the same leak
30
+ // class fixed for SQS). Durable topics opt into RETAIN via props.removalPolicy.
31
+ this.topic.applyRemovalPolicy(toRemovalPolicy(props.removalPolicy ?? "DESTROY"));
26
32
  new CfnOutput(this, `${id}TopicArn`, {
27
33
  key: `${id}TopicArn`,
28
34
  value: this.topic.topicArn,
@@ -60,6 +60,12 @@ export interface SQSQueueProps {
60
60
  contentBasedDeduplication?: boolean;
61
61
  fifoThroughputLimit?: "perQueue" | "perMessageGroupId";
62
62
  deduplicationScope?: "queue" | "messageGroup";
63
+ /**
64
+ * Removal policy for the queue (and its auto-created DLQ, which tracks it).
65
+ * Defaults to DESTROY — a queue is a transient work medium (see the
66
+ * constructor). Pass "RETAIN" only for a queue whose contents are
67
+ * irreplaceable and must survive stack deletion.
68
+ */
63
69
  removalPolicy?: RemovalPolicyString;
64
70
  }
65
71
  export declare class SQSQueue extends Construct {
@@ -83,6 +83,14 @@ export class SQSQueue extends Construct {
83
83
  this.id = id;
84
84
  // Sanitise id for CloudFormation output keys (must be alphanumeric)
85
85
  const outputName = toPascalCase(id);
86
+ // SQS queues are transient work mediums: their contents (job messages,
87
+ // failed-delivery copies) regenerate from a source of truth held elsewhere
88
+ // (Postgres, the producing schedule/rule), so the wrapper defaults to
89
+ // DESTROY — deliberately NOT the env-aware production->RETAIN of
90
+ // data-bearing wrappers (S3, EventBus). RETAIN here buys no protection and
91
+ // orphans the queue on a parent construct's logical-ID churn (the prod
92
+ // orphan leak). Durable queues opt into RETAIN via props.removalPolicy.
93
+ const resolvedRemovalPolicy = toRemovalPolicy(props.removalPolicy ?? "DESTROY");
86
94
  const isFifo = props.queueType === "fifo";
87
95
  const queueName = props.queueName
88
96
  ? isFifo
@@ -110,7 +118,7 @@ export class SQSQueue extends Construct {
110
118
  fifo: isFifo,
111
119
  encryption: toEncryption(props.encryption),
112
120
  retentionPeriod: Duration.days(SQS_LIMITS.DEAD_LETTER_QUEUE.DEFAULT_RETENTION_DAYS),
113
- removalPolicy: toRemovalPolicy(props.removalPolicy)
121
+ removalPolicy: resolvedRemovalPolicy
114
122
  });
115
123
  deadLetterQueue = {
116
124
  queue: this.dlq,
@@ -156,7 +164,7 @@ export class SQSQueue extends Construct {
156
164
  deduplicationScope: isFifo
157
165
  ? toDeduplicationScope(props.deduplicationScope)
158
166
  : undefined,
159
- removalPolicy: toRemovalPolicy(props.removalPolicy)
167
+ removalPolicy: resolvedRemovalPolicy
160
168
  });
161
169
  new CfnOutput(this, `${outputName}QueueUrl`, {
162
170
  key: `${outputName}QueueUrl`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@aws-sdk/client-organizations": "^3.1038.0",
66
- "@fjall/generator": "^2.16.0",
67
- "@fjall/util": "^2.16.0",
66
+ "@fjall/generator": "^2.17.0",
67
+ "@fjall/util": "^2.17.0",
68
68
  "constructs": "^10.0.0",
69
69
  "uuid": "^14.0.0"
70
70
  },
@@ -79,5 +79,5 @@
79
79
  "engines": {
80
80
  "node": ">=18.0.0"
81
81
  },
82
- "gitHead": "2383b19f1e7db980ae603a6c75dca2c61b7a1d42"
82
+ "gitHead": "21cfe1aae339e12183af2813ec81f581b9b77d49"
83
83
  }