@fjall/components-infrastructure 39.0.1 → 39.0.2
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.
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +20 -8
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.d.ts +11 -2
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.js +12 -2
- package/dist/lib/resources/aws/compute/ec2.d.ts +10 -3
- package/dist/lib/resources/aws/compute/ec2.js +8 -4
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.d.ts +17 -5
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +55 -46
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs +422 -147
- package/dist/lib/resources/aws/compute/ecsCapacityProviderGate.d.ts +41 -0
- package/dist/lib/resources/aws/compute/ecsCapacityProviderGate.js +116 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +5 -0
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +6 -5
- package/dist/lib/resources/aws/compute/lambda.d.ts +3 -0
- package/dist/lib/resources/aws/compute/lambda.js +10 -6
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +17 -15
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +27 -21
- package/dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs +235 -104
- package/dist/lib/resources/aws/database/clickhouseUserData.js +49 -6
- package/dist/lib/resources/aws/monitoring/metricNamespaces.d.ts +4 -1
- package/dist/lib/resources/aws/monitoring/metricNamespaces.js +4 -1
- package/package.json +3 -3
|
@@ -41,10 +41,10 @@ export interface ClickHouseDatabaseProps {
|
|
|
41
41
|
* It is still not free: the type lives in the launch template and in
|
|
42
42
|
* user-data-baked server tuning, so it takes an instance refresh, and on
|
|
43
43
|
* this single-node cluster that refresh is a restart. The ASG terminates
|
|
44
|
-
* the instance
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* the instance and launches a replacement, and the data volume moves
|
|
45
|
+
* across by the path `storageGb` describes — ClickHouse is unavailable
|
|
46
|
+
* across that window. Size the change into a quiet period; do not plan a
|
|
47
|
+
* snapshot-restore migration for it. */
|
|
48
48
|
instanceType?: string;
|
|
49
49
|
/**
|
|
50
50
|
* EBS data-volume size in GiB. Default `CLICKHOUSE_EBS_VOLUME_SIZE_GB`
|
|
@@ -64,10 +64,22 @@ export interface ClickHouseDatabaseProps {
|
|
|
64
64
|
* is embedded in that user data, so changing this prop versions the launch
|
|
65
65
|
* template and triggers the ASG instance refresh whose boot grows the
|
|
66
66
|
* filesystem — the increase applies in the same deploy, by the same
|
|
67
|
-
* mechanism an `instanceType` change rides.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* mechanism an `instanceType` change rides.
|
|
68
|
+
*
|
|
69
|
+
* On this single-node cluster that relaunch is a restart, and the data
|
|
70
|
+
* survives it because nothing detaches the volume. The TERMINATING hook
|
|
71
|
+
* drains the old instance and waits until every task on it is proven
|
|
72
|
+
* STOPPED, stopping any that outlast the drain and alarming when it cannot
|
|
73
|
+
* prove it; the instance's own shutdown then unmounts the filesystem and
|
|
74
|
+
* releases the volume. The LAUNCHING hook waits for that release, attaches
|
|
75
|
+
* the volume to the replacement, and ABANDONs the replacement if the
|
|
76
|
+
* volume is not released or attached in time, so the ASG launches another
|
|
77
|
+
* and the wait starts over. At boot the replacement formats the device
|
|
78
|
+
* only when it carries no filesystem signature, reads back, and holds the
|
|
79
|
+
* ext4 magic in neither its primary superblock nor its first backup; an
|
|
80
|
+
* ext4 filesystem is mounted as found, and anything else is refused rather
|
|
81
|
+
* than overwritten. The cluster is unavailable across the
|
|
82
|
+
* window, so size increases into a quiet period, exactly as with
|
|
71
83
|
* `instanceType`.
|
|
72
84
|
*/
|
|
73
85
|
storageGb?: number;
|
|
@@ -40,13 +40,22 @@ export interface InlineAsgLifecycleHookProps {
|
|
|
40
40
|
* events on the account+region default bus for every lifecycle hook regardless
|
|
41
41
|
* of whether a notification target is configured. Each consumer attaches a
|
|
42
42
|
* `Subscription` (from `lib/resources/aws/messaging/subscription.ts`) whose
|
|
43
|
-
* event pattern discriminates by `
|
|
43
|
+
* event pattern discriminates by `LifecycleHookName` alone — the name embeds
|
|
44
|
+
* the stack name, and matching on `AutoScalingGroupName` would Ref the ASG —
|
|
44
45
|
* targeting the consumer's own SQS queue. The Lambda sees the EventBridge event
|
|
45
46
|
* envelope and reads `detail.LifecycleActionToken` etc. from the unwrapped
|
|
46
47
|
* detail.
|
|
47
48
|
*
|
|
49
|
+
* The ASG depends on `owner`, the construct that consumes the hook's events.
|
|
50
|
+
* A hook whose rule, queue or Lambda does not exist yet drops its event and
|
|
51
|
+
* runs out its heartbeat, so the consumer must be created before the ASG
|
|
52
|
+
* launches, updated before an instance refresh replaces an instance, and
|
|
53
|
+
* deleted only after the ASG has terminated its instances. The consumer
|
|
54
|
+
* therefore must never Ref the ASG; the hook name and a wildcard ASG ARN are
|
|
55
|
+
* how the consumers avoid one.
|
|
56
|
+
*
|
|
48
57
|
* Multiple consumers may call this helper against the same ASG — the existing
|
|
49
58
|
* spec list is read, the new entry appended, and the merged array assigned
|
|
50
59
|
* back. Synth is sequential per scope, so the merge is race-free.
|
|
51
60
|
*/
|
|
52
|
-
export declare function attachInlineAsgLifecycleHook(
|
|
61
|
+
export declare function attachInlineAsgLifecycleHook(owner: Construct, _id: string, props: InlineAsgLifecycleHookProps): void;
|
|
@@ -26,16 +26,26 @@
|
|
|
26
26
|
* events on the account+region default bus for every lifecycle hook regardless
|
|
27
27
|
* of whether a notification target is configured. Each consumer attaches a
|
|
28
28
|
* `Subscription` (from `lib/resources/aws/messaging/subscription.ts`) whose
|
|
29
|
-
* event pattern discriminates by `
|
|
29
|
+
* event pattern discriminates by `LifecycleHookName` alone — the name embeds
|
|
30
|
+
* the stack name, and matching on `AutoScalingGroupName` would Ref the ASG —
|
|
30
31
|
* targeting the consumer's own SQS queue. The Lambda sees the EventBridge event
|
|
31
32
|
* envelope and reads `detail.LifecycleActionToken` etc. from the unwrapped
|
|
32
33
|
* detail.
|
|
33
34
|
*
|
|
35
|
+
* The ASG depends on `owner`, the construct that consumes the hook's events.
|
|
36
|
+
* A hook whose rule, queue or Lambda does not exist yet drops its event and
|
|
37
|
+
* runs out its heartbeat, so the consumer must be created before the ASG
|
|
38
|
+
* launches, updated before an instance refresh replaces an instance, and
|
|
39
|
+
* deleted only after the ASG has terminated its instances. The consumer
|
|
40
|
+
* therefore must never Ref the ASG; the hook name and a wildcard ASG ARN are
|
|
41
|
+
* how the consumers avoid one.
|
|
42
|
+
*
|
|
34
43
|
* Multiple consumers may call this helper against the same ASG — the existing
|
|
35
44
|
* spec list is read, the new entry appended, and the merged array assigned
|
|
36
45
|
* back. Synth is sequential per scope, so the merge is race-free.
|
|
37
46
|
*/
|
|
38
|
-
export function attachInlineAsgLifecycleHook(
|
|
47
|
+
export function attachInlineAsgLifecycleHook(owner, _id, props) {
|
|
48
|
+
props.autoScalingGroup.node.addDependency(owner);
|
|
39
49
|
const cfnAsg = props.autoScalingGroup.node
|
|
40
50
|
.defaultChild;
|
|
41
51
|
const existing = readLifecycleHookSpecList(cfnAsg);
|
|
@@ -3,6 +3,7 @@ import { Construct } from "constructs";
|
|
|
3
3
|
import { type Ec2SshAccessProps } from "./ec2SshAccess.js";
|
|
4
4
|
import { Duration, Stack, type StackProps } from "aws-cdk-lib";
|
|
5
5
|
import { type Role } from "aws-cdk-lib/aws-iam";
|
|
6
|
+
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
6
7
|
import { AutoScalingGroup, Monitoring, type WarmPoolOptions } from "aws-cdk-lib/aws-autoscaling";
|
|
7
8
|
import { type PersistentDataVolumeProps } from "./persistentDataVolume.js";
|
|
8
9
|
export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup" | "stableOwnerId">;
|
|
@@ -114,13 +115,19 @@ export interface Ec2InstanceProps extends StackProps {
|
|
|
114
115
|
* Bare-EC2 consumers (bastion, Fivetran) leave this unset.
|
|
115
116
|
*/
|
|
116
117
|
ecsClusterArn?: string;
|
|
118
|
+
/**
|
|
119
|
+
* SNS topic for the graceful-termination handler's alarms: its queue's
|
|
120
|
+
* dead letters and, with `ecsClusterArn`, a terminating instance whose
|
|
121
|
+
* tasks were not proven STOPPED.
|
|
122
|
+
*/
|
|
123
|
+
alertsTopic?: ITopic;
|
|
117
124
|
/**
|
|
118
125
|
* Pairs the ASG with a standalone EBS data volume that re-attaches across
|
|
119
126
|
* instance refreshes. When set, requires `vpcSubnets.availabilityZones` to
|
|
120
127
|
* be exactly one entry (matching `persistentDataVolume.availabilityZone`);
|
|
121
|
-
* the wrapper's volume is AZ-local and cannot follow a multi-AZ ASG.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
128
|
+
* the wrapper's volume is AZ-local and cannot follow a multi-AZ ASG. It
|
|
129
|
+
* cannot be combined with `warmPool`: a single-attach volume cannot follow
|
|
130
|
+
* a pre-warmed instance.
|
|
124
131
|
*/
|
|
125
132
|
persistentDataVolume?: Ec2InstancePersistentDataVolumeConfig;
|
|
126
133
|
/**
|
|
@@ -222,7 +222,7 @@ export class Ec2Instance extends Construct {
|
|
|
222
222
|
updatePolicy: resolvedUpdatePolicy
|
|
223
223
|
}),
|
|
224
224
|
// ECS task drain on terminate is handled by the EC2_INSTANCE_TERMINATING
|
|
225
|
-
// lifecycle hook + Ec2GracefulTerminationHandler
|
|
225
|
+
// lifecycle hook + Ec2GracefulTerminationHandler, not by
|
|
226
226
|
// managed termination protection. Leaving instances scale-in-protected
|
|
227
227
|
// wedges CFN rollback because ASG cannot terminate the instance.
|
|
228
228
|
newInstancesProtectedFromScaleIn: false,
|
|
@@ -289,15 +289,14 @@ export class Ec2Instance extends Construct {
|
|
|
289
289
|
* ASGs (ClickHouse) without duplicate plumbing in the ECS layer.
|
|
290
290
|
*/
|
|
291
291
|
addGracefulTerminationHandler(props) {
|
|
292
|
-
const dataVolumeOwnerLogicalId = this.persistentDataVolume?.ownerLogicalId;
|
|
293
292
|
new Ec2GracefulTerminationHandler(this, `${props.serviceName}GracefulTermination`, {
|
|
294
293
|
autoScalingGroup: this.autoScalingGroup,
|
|
295
294
|
...(props.ecsClusterArn !== undefined &&
|
|
296
295
|
props.ecsClusterArn !== "" && {
|
|
297
296
|
ecsClusterArn: props.ecsClusterArn
|
|
298
297
|
}),
|
|
299
|
-
...(
|
|
300
|
-
|
|
298
|
+
...(props.alertsTopic !== undefined && {
|
|
299
|
+
alertsTopic: props.alertsTopic
|
|
301
300
|
})
|
|
302
301
|
});
|
|
303
302
|
}
|
|
@@ -362,6 +361,11 @@ function validateEc2InstanceProps(props) {
|
|
|
362
361
|
if (azs[0] !== props.persistentDataVolume.availabilityZone) {
|
|
363
362
|
throw new Error(`Ec2Instance.persistentDataVolume.availabilityZone must match vpcSubnets.availabilityZones[0]; got '${props.persistentDataVolume.availabilityZone}' vs '${azs[0]}'`);
|
|
364
363
|
}
|
|
364
|
+
if (props.warmPool !== undefined) {
|
|
365
|
+
throw new Error("Ec2Instance.persistentDataVolume cannot be combined with warmPool — " +
|
|
366
|
+
"the volume attaches to one instance at a time, so a pre-warmed " +
|
|
367
|
+
"instance can never receive it and its launch is abandoned on every attempt");
|
|
368
|
+
}
|
|
365
369
|
}
|
|
366
370
|
if (props.tags !== undefined) {
|
|
367
371
|
for (const [key, value] of Object.entries(props.tags)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
1
2
|
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
2
3
|
import { Construct } from "constructs";
|
|
3
4
|
import { LambdaFunction } from "./lambda.js";
|
|
@@ -9,6 +10,12 @@ import { SQSQueue } from "../messaging/sqs.js";
|
|
|
9
10
|
* source of truth so a rename does not silently break the test query.
|
|
10
11
|
*/
|
|
11
12
|
export declare const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
|
|
13
|
+
/**
|
|
14
|
+
* The Lambda logs this term when it cannot prove every task on a terminating
|
|
15
|
+
* instance STOPPED (`DRAIN_EVENT.UNPROVEN` in the Lambda source; a test pins
|
|
16
|
+
* the two together).
|
|
17
|
+
*/
|
|
18
|
+
export declare const DRAIN_UNPROVEN_EVENT = "drain_unproven";
|
|
12
19
|
export interface Ec2GracefulTerminationHandlerProps {
|
|
13
20
|
/** ASG to attach the EC2_INSTANCE_TERMINATING hook to. */
|
|
14
21
|
autoScalingGroup: AutoScalingGroup;
|
|
@@ -20,11 +27,16 @@ export interface Ec2GracefulTerminationHandlerProps {
|
|
|
20
27
|
*/
|
|
21
28
|
ecsClusterArn?: string;
|
|
22
29
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
* SNS topic for the queue's dead-letter alarm and, when `ecsClusterArn` is
|
|
31
|
+
* set, the alarm that fires when a terminating instance's tasks are not
|
|
32
|
+
* proven STOPPED.
|
|
33
|
+
*/
|
|
34
|
+
alertsTopic?: ITopic;
|
|
35
|
+
/**
|
|
36
|
+
* Ignored. The handler no longer detaches a paired `PersistentDataVolume`:
|
|
37
|
+
* the terminating instance's shutdown releases it.
|
|
38
|
+
*
|
|
39
|
+
* @deprecated Removed in 40.0.0; stop passing it.
|
|
28
40
|
*/
|
|
29
41
|
dataVolumeOwnerLogicalId?: string;
|
|
30
42
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { Aws, Stack } from "aws-cdk-lib";
|
|
4
|
+
import { Aws, Duration, Stack } from "aws-cdk-lib";
|
|
5
5
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
6
|
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
7
7
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
@@ -9,9 +9,10 @@ import { LifecycleTransition, DefaultResult } from "aws-cdk-lib/aws-autoscaling"
|
|
|
9
9
|
import { Construct } from "constructs";
|
|
10
10
|
import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
|
|
11
11
|
import { LambdaFunction } from "./lambda.js";
|
|
12
|
-
import { PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE, PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE, PERSISTENT_DATA_VOLUME_TAG_STACK_ID } from "./persistentDataVolume.js";
|
|
13
12
|
import { SQSQueue } from "../messaging/sqs.js";
|
|
14
13
|
import { Subscription } from "../messaging/subscription.js";
|
|
14
|
+
import { createLogPatternAlarms } from "../monitoring/logPatternAlarms.js";
|
|
15
|
+
import { METRIC_NAMESPACE, stackScopedMetricNamespace } from "../monitoring/metricNamespaces.js";
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const LAMBDA_SOURCE_FILE = "ec2GracefulTerminationLambda.source.cjs";
|
|
17
18
|
/**
|
|
@@ -21,11 +22,21 @@ const LAMBDA_SOURCE_FILE = "ec2GracefulTerminationLambda.source.cjs";
|
|
|
21
22
|
* source of truth so a rename does not silently break the test query.
|
|
22
23
|
*/
|
|
23
24
|
export const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The Lambda logs this term when it cannot prove every task on a terminating
|
|
27
|
+
* instance STOPPED (`DRAIN_EVENT.UNPROVEN` in the Lambda source; a test pins
|
|
28
|
+
* the two together).
|
|
29
|
+
*/
|
|
30
|
+
export const DRAIN_UNPROVEN_EVENT = "drain_unproven";
|
|
31
|
+
// Covers the Lambda's drain (240 s) and StopTask (150 s) budgets plus the
|
|
32
|
+
// 90 s it keeps back for deregistration, cleanup and completion.
|
|
33
|
+
const LAMBDA_TIMEOUT_SECONDS = 540;
|
|
34
|
+
// SQS requires a visibility timeout of at least the function timeout.
|
|
35
|
+
const QUEUE_VISIBILITY_TIMEOUT_SECONDS = 600;
|
|
36
|
+
// Outlasts one full attempt and the redelivery of a timed-out one. The Lambda
|
|
37
|
+
// reads it too: a proof read after the heartbeat could have released the
|
|
38
|
+
// instance does not count.
|
|
39
|
+
const HOOK_HEARTBEAT_SECONDS = 900;
|
|
29
40
|
/**
|
|
30
41
|
* ASG `EC2_INSTANCE_TERMINATING` hook + Lambda that drains the instance
|
|
31
42
|
* cleanly. Drain ownership lives at the EC2 layer; ECS-specific drain plugs
|
|
@@ -40,29 +51,36 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
40
51
|
const sourcePath = path.resolve(__dirname, LAMBDA_SOURCE_FILE);
|
|
41
52
|
const source = readFileSync(sourcePath, "utf-8");
|
|
42
53
|
const ecsClusterArn = resolveOptionalString(props.ecsClusterArn);
|
|
43
|
-
const dataVolumeOwnerLogicalId = resolveOptionalString(props.dataVolumeOwnerLogicalId);
|
|
44
54
|
this.queue = new SQSQueue(this, `${id}Queue`, {
|
|
45
55
|
visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
46
56
|
deadLetterQueue: { enabled: true, maxReceiveCount: 5 },
|
|
47
57
|
// Transient instance-drain signals — no durable state. Pinned DESTROY
|
|
48
58
|
// (now also the SQSQueue wrapper default) so a replacing deploy of the
|
|
49
59
|
// parent Ec2Instance reclaims this queue + DLQ instead of orphaning them.
|
|
50
|
-
removalPolicy: "DESTROY"
|
|
60
|
+
removalPolicy: "DESTROY",
|
|
61
|
+
...(props.alertsTopic !== undefined && { alertsTopic: props.alertsTopic })
|
|
51
62
|
});
|
|
52
63
|
const stack = Stack.of(this);
|
|
53
64
|
const ecsPolicies = ecsClusterArn !== undefined
|
|
54
65
|
? [
|
|
66
|
+
// Both act on the cluster resource and carry no `ecs:cluster`
|
|
67
|
+
// condition key (AWS service authorization reference), so a
|
|
68
|
+
// condition on that key would deny them.
|
|
55
69
|
new PolicyStatement({
|
|
56
70
|
effect: Effect.ALLOW,
|
|
57
|
-
actions: [
|
|
71
|
+
actions: [
|
|
72
|
+
"ecs:ListContainerInstances",
|
|
73
|
+
"ecs:DeregisterContainerInstance"
|
|
74
|
+
],
|
|
58
75
|
resources: [ecsClusterArn]
|
|
59
76
|
}),
|
|
60
77
|
new PolicyStatement({
|
|
61
78
|
effect: Effect.ALLOW,
|
|
62
79
|
actions: [
|
|
63
|
-
"ecs:DescribeContainerInstances",
|
|
64
80
|
"ecs:UpdateContainerInstancesState",
|
|
65
|
-
"ecs:
|
|
81
|
+
"ecs:ListTasks",
|
|
82
|
+
"ecs:DescribeTasks",
|
|
83
|
+
"ecs:StopTask"
|
|
66
84
|
],
|
|
67
85
|
resources: ["*"],
|
|
68
86
|
conditions: {
|
|
@@ -116,33 +134,6 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
116
134
|
actions: ["autoscaling:CompleteLifecycleAction"],
|
|
117
135
|
resources: [asgArnWildcard]
|
|
118
136
|
});
|
|
119
|
-
const volumeArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/*`;
|
|
120
|
-
const instanceArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance/*`;
|
|
121
|
-
const dataVolumePolicies = dataVolumeOwnerLogicalId !== undefined
|
|
122
|
-
? [
|
|
123
|
-
new PolicyStatement({
|
|
124
|
-
effect: Effect.ALLOW,
|
|
125
|
-
actions: ["ec2:DescribeVolumes"],
|
|
126
|
-
resources: ["*"]
|
|
127
|
-
}),
|
|
128
|
-
new PolicyStatement({
|
|
129
|
-
effect: Effect.ALLOW,
|
|
130
|
-
actions: ["ec2:DetachVolume"],
|
|
131
|
-
resources: [volumeArnPattern],
|
|
132
|
-
conditions: {
|
|
133
|
-
StringEquals: {
|
|
134
|
-
[`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE}`]: PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE,
|
|
135
|
-
[`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_STACK_ID}`]: Aws.STACK_ID
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}),
|
|
139
|
-
new PolicyStatement({
|
|
140
|
-
effect: Effect.ALLOW,
|
|
141
|
-
actions: ["ec2:DetachVolume"],
|
|
142
|
-
resources: [instanceArnPattern]
|
|
143
|
-
})
|
|
144
|
-
]
|
|
145
|
-
: [];
|
|
146
137
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
147
138
|
runtime: Runtime.NODEJS_24_X,
|
|
148
139
|
handler: "index.handler",
|
|
@@ -153,10 +144,9 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
153
144
|
memorySize: 256,
|
|
154
145
|
logGroupRetention: RetentionDays.ONE_MONTH,
|
|
155
146
|
environment: {
|
|
156
|
-
...(ecsClusterArn !== undefined && {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
DATA_VOLUME_STACK_ID: Aws.STACK_ID
|
|
147
|
+
...(ecsClusterArn !== undefined && {
|
|
148
|
+
ECS_CLUSTER_ARN: ecsClusterArn,
|
|
149
|
+
LIFECYCLE_HEARTBEAT_SECONDS: String(HOOK_HEARTBEAT_SECONDS)
|
|
160
150
|
})
|
|
161
151
|
},
|
|
162
152
|
inlinePolicy: [
|
|
@@ -164,11 +154,30 @@ export class Ec2GracefulTerminationHandler extends Construct {
|
|
|
164
154
|
ec2Policies,
|
|
165
155
|
elbReadPolicy,
|
|
166
156
|
elbDeregisterPolicy,
|
|
167
|
-
asgPolicy
|
|
168
|
-
...dataVolumePolicies
|
|
157
|
+
asgPolicy
|
|
169
158
|
]
|
|
170
159
|
});
|
|
171
|
-
|
|
160
|
+
if (ecsClusterArn !== undefined && props.alertsTopic !== undefined) {
|
|
161
|
+
createLogPatternAlarms({
|
|
162
|
+
scope: this,
|
|
163
|
+
logGroup: this.lambda.functionLogGroup,
|
|
164
|
+
alarmTopic: props.alertsTopic,
|
|
165
|
+
metricNamespace: stackScopedMetricNamespace(METRIC_NAMESPACE.EC2),
|
|
166
|
+
specs: [
|
|
167
|
+
{
|
|
168
|
+
idStem: `${id}DrainUnproven`,
|
|
169
|
+
metricName: `${id}DrainUnprovenCount`,
|
|
170
|
+
description: `${id}: a terminating instance's ECS tasks were not proven STOPPED ` +
|
|
171
|
+
"before it was released — check the handler's log for the unstopped task ARNs",
|
|
172
|
+
anyTerms: [DRAIN_UNPROVEN_EVENT],
|
|
173
|
+
period: Duration.minutes(5)
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// One lifecycle action per invocation: a batch is processed in turn, so
|
|
179
|
+
// several instances terminating together would share one timeout.
|
|
180
|
+
this.lambda.addSqsEventSource(this.queue.getQueue(), { batchSize: 1 });
|
|
172
181
|
// Hook name embeds Aws.STACK_NAME (pseudo-parameter Ref, NOT a resource
|
|
173
182
|
// Ref — no CFN dep) so LifecycleHookName alone discriminates across
|
|
174
183
|
// stacks in the EB pattern. Sibling LAUNCHING hook uses "-launching".
|