@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
2
|
+
import { type ICluster } from "aws-cdk-lib/aws-ecs";
|
|
3
|
+
/** Where the user data installs the gate on the instance. */
|
|
4
|
+
export declare const ECS_CAPACITY_PROVIDER_GATE_PATH = "/usr/local/bin/fjall-ecs-capacity-provider-gate";
|
|
5
|
+
/** Every line the gate prints starts with this, so a console read can find it. */
|
|
6
|
+
export declare const ECS_CAPACITY_PROVIDER_GATE_LOG_PREFIX = "fjall-ecs-capacity-provider-gate:";
|
|
7
|
+
/**
|
|
8
|
+
* Holds an instance's first ECS registration until a capacity provider naming
|
|
9
|
+
* its Auto Scaling group is ACTIVE and associated with the cluster.
|
|
10
|
+
*
|
|
11
|
+
* ECS binds a container instance to a capacity provider only if the provider
|
|
12
|
+
* already exists when the instance calls RegisterContainerInstance. An
|
|
13
|
+
* instance that registers first stays unbound, and a service whose strategy
|
|
14
|
+
* names only capacity providers never places a task on it. CloudFormation
|
|
15
|
+
* creates the provider after the group is complete, so the group's first
|
|
16
|
+
* instances race it (fjall-deploy-tests #94: the provider was created in the
|
|
17
|
+
* same second the instance registered, and no task was ever created). The
|
|
18
|
+
* ECS agent starts after cloud-final, so user data that waits here delays the
|
|
19
|
+
* registration.
|
|
20
|
+
*
|
|
21
|
+
* Arguments: cluster name, then optional overall budget, poll interval and
|
|
22
|
+
* error budget in seconds. It prints only the gate's own lines, AWS error
|
|
23
|
+
* codes, and the group, provider and cluster names. It exits 0 on every path:
|
|
24
|
+
* a gate that cannot decide lets the agent register rather than hold the
|
|
25
|
+
* boot. It never contains `${`, so a renderer that substitutes `${…}` (as
|
|
26
|
+
* `Fn::Sub` does) passes it through unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ECS_CAPACITY_PROVIDER_GATE_SCRIPT: string;
|
|
29
|
+
/**
|
|
30
|
+
* Makes the group's instances wait for their capacity provider before the ECS
|
|
31
|
+
* agent registers them (see {@link ECS_CAPACITY_PROVIDER_GATE_SCRIPT}), and
|
|
32
|
+
* grants the two reads the wait needs.
|
|
33
|
+
*
|
|
34
|
+
* It applies wherever `cluster.addAsgCapacityProvider` writes the
|
|
35
|
+
* `ECS_CLUSTER` line as shell, which is every group that is not Windows.
|
|
36
|
+
* Called before that, the block lands ahead of the line, so it is guarded by
|
|
37
|
+
* `|| true`: user data may run under `set -e` (ClickHouse's does), and a
|
|
38
|
+
* failure here would stop it before `ECS_CLUSTER` is written, sending the
|
|
39
|
+
* agent to the default cluster.
|
|
40
|
+
*/
|
|
41
|
+
export declare function addEcsCapacityProviderGate(autoScalingGroup: AutoScalingGroup, cluster: ICluster): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { OperatingSystemType } from "aws-cdk-lib/aws-ec2";
|
|
3
|
+
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
4
|
+
/** Where the user data installs the gate on the instance. */
|
|
5
|
+
export const ECS_CAPACITY_PROVIDER_GATE_PATH = "/usr/local/bin/fjall-ecs-capacity-provider-gate";
|
|
6
|
+
/** Every line the gate prints starts with this, so a console read can find it. */
|
|
7
|
+
export const ECS_CAPACITY_PROVIDER_GATE_LOG_PREFIX = "fjall-ecs-capacity-provider-gate:";
|
|
8
|
+
const HEREDOC_DELIMITER = "FJALL_ECS_CAPACITY_PROVIDER_GATE";
|
|
9
|
+
/**
|
|
10
|
+
* Holds an instance's first ECS registration until a capacity provider naming
|
|
11
|
+
* its Auto Scaling group is ACTIVE and associated with the cluster.
|
|
12
|
+
*
|
|
13
|
+
* ECS binds a container instance to a capacity provider only if the provider
|
|
14
|
+
* already exists when the instance calls RegisterContainerInstance. An
|
|
15
|
+
* instance that registers first stays unbound, and a service whose strategy
|
|
16
|
+
* names only capacity providers never places a task on it. CloudFormation
|
|
17
|
+
* creates the provider after the group is complete, so the group's first
|
|
18
|
+
* instances race it (fjall-deploy-tests #94: the provider was created in the
|
|
19
|
+
* same second the instance registered, and no task was ever created). The
|
|
20
|
+
* ECS agent starts after cloud-final, so user data that waits here delays the
|
|
21
|
+
* registration.
|
|
22
|
+
*
|
|
23
|
+
* Arguments: cluster name, then optional overall budget, poll interval and
|
|
24
|
+
* error budget in seconds. It prints only the gate's own lines, AWS error
|
|
25
|
+
* codes, and the group, provider and cluster names. It exits 0 on every path:
|
|
26
|
+
* a gate that cannot decide lets the agent register rather than hold the
|
|
27
|
+
* boot. It never contains `${`, so a renderer that substitutes `${…}` (as
|
|
28
|
+
* `Fn::Sub` does) passes it through unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export const ECS_CAPACITY_PROVIDER_GATE_SCRIPT = String.raw `#!/bin/bash
|
|
31
|
+
cluster=$1 budget=$2 interval=$3 error_budget=$4
|
|
32
|
+
[ -n "$budget" ] || budget=600
|
|
33
|
+
[ -n "$interval" ] || interval=5
|
|
34
|
+
[ -n "$error_budget" ] || error_budget=120
|
|
35
|
+
say() { echo "${ECS_CAPACITY_PROVIDER_GATE_LOG_PREFIX} $*"; }
|
|
36
|
+
[ -n "$cluster" ] || { say "no cluster named, not waiting"; exit 0; }
|
|
37
|
+
command -v aws >/dev/null 2>&1 || { say "no AWS CLI, not waiting"; exit 0; }
|
|
38
|
+
err=$(mktemp) || { say "no temporary file, not waiting"; exit 0; }
|
|
39
|
+
trap 'rm -f "$err"' EXIT
|
|
40
|
+
imds() { curl -sf --max-time 5 -H "X-aws-ec2-metadata-token: $token" "http://169.254.169.254/latest/meta-data/$1"; }
|
|
41
|
+
ecs() { aws ecs "$@" --region "$region" --output text --cli-connect-timeout 5 --cli-read-timeout 10 2>"$err"; }
|
|
42
|
+
failed() {
|
|
43
|
+
code=$(sed -n 's/.*An error occurred (\([A-Za-z0-9.]*\)).*/\1/p' "$err" | head -n 1)
|
|
44
|
+
[ -n "$code" ] || code=CallFailed
|
|
45
|
+
}
|
|
46
|
+
asg= region= since= last= waiting=
|
|
47
|
+
while :; do
|
|
48
|
+
code=
|
|
49
|
+
if [ -z "$asg" ] || [ -z "$region" ]; then
|
|
50
|
+
token=$(curl -sf --max-time 5 -X PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
|
|
51
|
+
asg=$(imds tags/instance/aws:autoscaling:groupName) region=$(imds placement/region)
|
|
52
|
+
[ -n "$asg" ] && [ -n "$region" ] || code=InstanceMetadataUnavailable
|
|
53
|
+
fi
|
|
54
|
+
if [ -n "$code" ]; then :
|
|
55
|
+
elif ! listed=$(ecs describe-clusters --clusters "$cluster" --query 'clusters[0].capacityProviders'); then failed
|
|
56
|
+
else
|
|
57
|
+
names=
|
|
58
|
+
for name in $listed; do case $name in None|FARGATE|FARGATE_SPOT) ;; *) names="$names $name" ;; esac; done
|
|
59
|
+
if [ -z "$names" ]; then :
|
|
60
|
+
elif ! rows=$(ecs describe-capacity-providers --capacity-providers $names --query "capacityProviders[?status=='ACTIVE'].[name,autoScalingGroupProvider.autoScalingGroupArn]"); then failed
|
|
61
|
+
else
|
|
62
|
+
while read -r name group; do
|
|
63
|
+
case $group in "$asg"|*":autoScalingGroupName/$asg")
|
|
64
|
+
say "capacity provider $name names $asg on cluster $cluster after $SECONDS s, registering"; exit 0 ;;
|
|
65
|
+
esac
|
|
66
|
+
done <<EOF
|
|
67
|
+
$rows
|
|
68
|
+
EOF
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
if [ -n "$code" ]; then
|
|
72
|
+
[ -n "$since" ] || since=$SECONDS
|
|
73
|
+
[ "$code" = "$last" ] || { say "cannot check yet: $code"; last=$code; }
|
|
74
|
+
[ $((SECONDS - since)) -lt "$error_budget" ] || { say "gave up after $SECONDS s on $code, registering unbound"; exit 0; }
|
|
75
|
+
else
|
|
76
|
+
since= last=
|
|
77
|
+
[ -n "$waiting" ] || { say "waiting for a capacity provider naming $asg on cluster $cluster"; waiting=1; }
|
|
78
|
+
fi
|
|
79
|
+
[ "$SECONDS" -lt "$budget" ] || { say "gave up after $SECONDS s, no capacity provider names $asg on cluster $cluster, registering unbound"; exit 0; }
|
|
80
|
+
sleep "$interval"
|
|
81
|
+
done
|
|
82
|
+
`;
|
|
83
|
+
/**
|
|
84
|
+
* Makes the group's instances wait for their capacity provider before the ECS
|
|
85
|
+
* agent registers them (see {@link ECS_CAPACITY_PROVIDER_GATE_SCRIPT}), and
|
|
86
|
+
* grants the two reads the wait needs.
|
|
87
|
+
*
|
|
88
|
+
* It applies wherever `cluster.addAsgCapacityProvider` writes the
|
|
89
|
+
* `ECS_CLUSTER` line as shell, which is every group that is not Windows.
|
|
90
|
+
* Called before that, the block lands ahead of the line, so it is guarded by
|
|
91
|
+
* `|| true`: user data may run under `set -e` (ClickHouse's does), and a
|
|
92
|
+
* failure here would stop it before `ECS_CLUSTER` is written, sending the
|
|
93
|
+
* agent to the default cluster.
|
|
94
|
+
*/
|
|
95
|
+
export function addEcsCapacityProviderGate(autoScalingGroup, cluster) {
|
|
96
|
+
if (autoScalingGroup.osType === OperatingSystemType.WINDOWS)
|
|
97
|
+
return;
|
|
98
|
+
autoScalingGroup.addUserData("{", `cat > ${ECS_CAPACITY_PROVIDER_GATE_PATH} <<'${HEREDOC_DELIMITER}'`, ECS_CAPACITY_PROVIDER_GATE_SCRIPT.trimEnd(), HEREDOC_DELIMITER, `chmod 755 ${ECS_CAPACITY_PROVIDER_GATE_PATH}`, `${ECS_CAPACITY_PROVIDER_GATE_PATH} '${cluster.clusterName}'`, "} || true");
|
|
99
|
+
autoScalingGroup.addToRolePolicy(new PolicyStatement({
|
|
100
|
+
actions: ["ecs:DescribeClusters"],
|
|
101
|
+
resources: [cluster.clusterArn]
|
|
102
|
+
}));
|
|
103
|
+
// A provider's name is not known until CloudFormation creates it, and
|
|
104
|
+
// naming it here would make the role's policy wait for the provider the
|
|
105
|
+
// instances are waiting for.
|
|
106
|
+
autoScalingGroup.addToRolePolicy(new PolicyStatement({
|
|
107
|
+
actions: ["ecs:DescribeCapacityProviders"],
|
|
108
|
+
resources: [
|
|
109
|
+
Stack.of(autoScalingGroup).formatArn({
|
|
110
|
+
service: "ecs",
|
|
111
|
+
resource: "capacity-provider",
|
|
112
|
+
resourceName: "*"
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
@@ -8,6 +8,7 @@ import { Alarm, ComparisonOperator, MathExpression } from "aws-cdk-lib/aws-cloud
|
|
|
8
8
|
import { ApplicationScalingAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
9
9
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
10
10
|
import { Ec2Instance } from "./ec2.js";
|
|
11
|
+
import { addEcsCapacityProviderGate } from "./ecsCapacityProviderGate.js";
|
|
11
12
|
import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
|
|
12
13
|
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
13
14
|
import { stackScopedExportName } from "../../../utils/exportNaming.js";
|
|
@@ -176,6 +177,9 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
176
177
|
instanceMonitoring: ec2Config.instanceMonitoring ?? DEFAULT_EC2_INSTANCE_MONITORING,
|
|
177
178
|
capacityRebalance: true,
|
|
178
179
|
ecsClusterArn: ctx.cluster.clusterArn,
|
|
180
|
+
...(ctx.props.alertsTopic !== undefined && {
|
|
181
|
+
alertsTopic: ctx.props.alertsTopic
|
|
182
|
+
}),
|
|
179
183
|
...(ec2Config.desiredCapacity !== undefined && {
|
|
180
184
|
desiredCapacity: ec2Config.desiredCapacity
|
|
181
185
|
}),
|
|
@@ -207,6 +211,7 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
207
211
|
...(ec2Config.tags !== undefined && { tags: ec2Config.tags })
|
|
208
212
|
});
|
|
209
213
|
const asg = ec2Instance.getAutoScalingGroup();
|
|
214
|
+
addEcsCapacityProviderGate(asg, ctx.cluster);
|
|
210
215
|
const provider = new AsgCapacityProvider(ctx.scope, `${anchor}AsgCapacityProvider`, {
|
|
211
216
|
autoScalingGroup: asg,
|
|
212
217
|
enableManagedDraining: true,
|
|
@@ -170,11 +170,12 @@ export interface Ec2CapacityConfig {
|
|
|
170
170
|
availabilityZones?: string[];
|
|
171
171
|
/**
|
|
172
172
|
* Pairs the EC2 capacity ASG with a standalone EBS data volume that
|
|
173
|
-
* re-attaches across instance refreshes. Forwarded to `Ec2Instance
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* same slot is rejected at synth
|
|
177
|
-
* it its own slot instead.
|
|
173
|
+
* re-attaches across instance refreshes. Forwarded to `Ec2Instance`, whose
|
|
174
|
+
* LAUNCHING lifecycle hook attaches the volume to a new instance once the
|
|
175
|
+
* instance that held it has terminated. Implies a singleton service — a
|
|
176
|
+
* second service declaring the same slot is rejected at synth
|
|
177
|
+
* (`assertSharedAsgConfigMatches`); give it its own slot instead. Cannot be
|
|
178
|
+
* combined with `warmPool`.
|
|
178
179
|
*/
|
|
179
180
|
persistentDataVolume?: Ec2InstancePersistentDataVolumeConfig;
|
|
180
181
|
/**
|
|
@@ -3,6 +3,7 @@ import { type Bucket } from "aws-cdk-lib/aws-s3";
|
|
|
3
3
|
import { PolicyStatement, type IRole } from "aws-cdk-lib/aws-iam";
|
|
4
4
|
import { type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
5
5
|
import type { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
6
|
+
import { LogGroup } from "../logging/logGroup.js";
|
|
6
7
|
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
7
8
|
import { type IQueue } from "aws-cdk-lib/aws-sqs";
|
|
8
9
|
import { type ITable } from "aws-cdk-lib/aws-dynamodb";
|
|
@@ -127,6 +128,8 @@ export declare class SingletonFunction extends singletonFunction {
|
|
|
127
128
|
get executionRole(): IRole | undefined;
|
|
128
129
|
}
|
|
129
130
|
export declare class LambdaFunction extends Function {
|
|
131
|
+
/** The explicit log group this function writes to. */
|
|
132
|
+
readonly functionLogGroup: LogGroup;
|
|
130
133
|
private functionUrlValue?;
|
|
131
134
|
private readonly asyncFailureQueueProp?;
|
|
132
135
|
private asyncFailureQueueWired?;
|
|
@@ -172,6 +172,8 @@ export class SingletonFunction extends singletonFunction {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
export class LambdaFunction extends Function {
|
|
175
|
+
/** The explicit log group this function writes to. */
|
|
176
|
+
functionLogGroup;
|
|
175
177
|
functionUrlValue;
|
|
176
178
|
asyncFailureQueueProp;
|
|
177
179
|
asyncFailureQueueWired;
|
|
@@ -187,6 +189,12 @@ export class LambdaFunction extends Function {
|
|
|
187
189
|
// Grant-based: Lambda encrypts the environment through a KMS grant the
|
|
188
190
|
// deploying role creates, so the execution role needs no key policy.
|
|
189
191
|
const encryptionKey = StackEncryptionKey.forTier(scope, appBackupTier);
|
|
192
|
+
const functionLogGroup = new LogGroup(scope, `${id}LogGroup`, {
|
|
193
|
+
retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION,
|
|
194
|
+
...(encryptionKey !== undefined && {
|
|
195
|
+
encryptionKey: encryptionKey.forLogGroups()
|
|
196
|
+
})
|
|
197
|
+
});
|
|
190
198
|
super(scope, id, {
|
|
191
199
|
...cdkProps,
|
|
192
200
|
vpcSubnets,
|
|
@@ -201,13 +209,9 @@ export class LambdaFunction extends Function {
|
|
|
201
209
|
: undefined,
|
|
202
210
|
description: props.lambdaDescription ?? `${id} Lambda`,
|
|
203
211
|
environment: props.environment,
|
|
204
|
-
logGroup:
|
|
205
|
-
retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION,
|
|
206
|
-
...(encryptionKey !== undefined && {
|
|
207
|
-
encryptionKey: encryptionKey.forLogGroups()
|
|
208
|
-
})
|
|
209
|
-
})
|
|
212
|
+
logGroup: functionLogGroup
|
|
210
213
|
});
|
|
214
|
+
this.functionLogGroup = functionLogGroup;
|
|
211
215
|
this.asyncFailureQueueProp = props.asyncFailureQueue;
|
|
212
216
|
addPoliciesToRole(this, props.inlinePolicy);
|
|
213
217
|
applyRoleDescription(this, props.roleDescription);
|
|
@@ -13,6 +13,13 @@ import { SQSQueue } from "../messaging/sqs.js";
|
|
|
13
13
|
* not silently break the test query.
|
|
14
14
|
*/
|
|
15
15
|
export declare const PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION = "PersistentDataVolume launching handler";
|
|
16
|
+
/**
|
|
17
|
+
* How long a launching instance waits for its data volume. The Lambda's full
|
|
18
|
+
* attempt must fit inside it, and the instance's user data waits this long
|
|
19
|
+
* for the device to appear: the hook ABANDONs the instance when it expires,
|
|
20
|
+
* so waiting any longer on the instance buys nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS = 900;
|
|
16
23
|
export declare const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE = "fjall:Lifecycle";
|
|
17
24
|
export declare const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE = "data-volume";
|
|
18
25
|
export declare const PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
|
|
@@ -66,11 +73,11 @@ export interface PersistentDataVolumeProps {
|
|
|
66
73
|
* Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
|
|
67
74
|
* re-attaches the same physical volume across instance refreshes.
|
|
68
75
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* Nothing detaches the volume. The instance holding it releases it when it
|
|
77
|
+
* terminates, after `Ec2GracefulTerminationHandler` has proven its tasks
|
|
78
|
+
* stopped; the LAUNCHING Lambda waits for that release and then attaches the
|
|
79
|
+
* volume to the new instance. A forced detach would pull the filesystem out
|
|
80
|
+
* from under a database that is still running.
|
|
74
81
|
*
|
|
75
82
|
* Volume tagging: three tags carry the re-attach identity —
|
|
76
83
|
* `fjall:Lifecycle = data-volume` (constant; gates IAM scoping)
|
|
@@ -82,9 +89,7 @@ export interface PersistentDataVolumeProps {
|
|
|
82
89
|
* StackId and cannot collide with the new deploy's volume)
|
|
83
90
|
*
|
|
84
91
|
* The LAUNCHING Lambda filters DescribeVolumes by all three tags AND ABANDONs
|
|
85
|
-
* on multi-match.
|
|
86
|
-
* `ownerLogicalId` (and is in the same stack, so shares the StackId
|
|
87
|
-
* automatically) to locate the volume for detach.
|
|
92
|
+
* on multi-match.
|
|
88
93
|
*
|
|
89
94
|
* Why three tags. The first two are stable per logical construct; without
|
|
90
95
|
* the StackId discriminator, every prior-iteration orphan (CREATE_ROLLBACK,
|
|
@@ -105,12 +110,10 @@ export declare class PersistentDataVolume extends Construct {
|
|
|
105
110
|
readonly queue: SQSQueue;
|
|
106
111
|
/**
|
|
107
112
|
* Identifier used as the `fjall:OwnerLogicalId` tag on the volume and the
|
|
108
|
-
* `OWNER_LOGICAL_ID` env on the Lambda.
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* matches, new instance fails its launch hook) — recovery is a manual
|
|
113
|
-
* snapshot-restore step.
|
|
113
|
+
* `OWNER_LOGICAL_ID` env on the Lambda. Carries `props.stableOwnerId`
|
|
114
|
+
* verbatim; changing it across an existing deployment breaks the re-attach
|
|
115
|
+
* chain (Lambda filter no longer matches, new instance fails its launch
|
|
116
|
+
* hook) — recovery is a manual snapshot-restore step.
|
|
114
117
|
*/
|
|
115
118
|
readonly ownerLogicalId: string;
|
|
116
119
|
readonly attachFailureAlarm: Alarm;
|
|
@@ -120,7 +123,6 @@ declare function validatePersistentDataVolumeProps(props: PersistentDataVolumePr
|
|
|
120
123
|
export declare const _internals: {
|
|
121
124
|
LAUNCHING_LAMBDA_SOURCE_FILE: string;
|
|
122
125
|
LAMBDA_TIMEOUT_SECONDS: number;
|
|
123
|
-
LAUNCHING_HOOK_HEARTBEAT_SECONDS: number;
|
|
124
126
|
QUEUE_VISIBILITY_TIMEOUT_SECONDS: number;
|
|
125
127
|
ALARM_EVALUATION_PERIODS: number;
|
|
126
128
|
ALARM_DATAPOINTS_TO_ALARM: number;
|
|
@@ -24,10 +24,19 @@ const LAUNCHING_LAMBDA_SOURCE_FILE = "persistentDataVolumeLambda.source.cjs";
|
|
|
24
24
|
* not silently break the test query.
|
|
25
25
|
*/
|
|
26
26
|
export const PERSISTENT_DATA_VOLUME_LAUNCHING_DESCRIPTION = "PersistentDataVolume launching handler";
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const
|
|
27
|
+
// Covers the Lambda's release wait (480 s) and attach wait (90 s), each of
|
|
28
|
+
// which may end on a read that started at its deadline, plus the calls around
|
|
29
|
+
// them. Every call is bounded; a test pins the sum.
|
|
30
|
+
const LAMBDA_TIMEOUT_SECONDS = 700;
|
|
31
|
+
// SQS requires a visibility timeout of at least the function timeout.
|
|
32
|
+
const QUEUE_VISIBILITY_TIMEOUT_SECONDS = 720;
|
|
33
|
+
/**
|
|
34
|
+
* How long a launching instance waits for its data volume. The Lambda's full
|
|
35
|
+
* attempt must fit inside it, and the instance's user data waits this long
|
|
36
|
+
* for the device to appear: the hook ABANDONs the instance when it expires,
|
|
37
|
+
* so waiting any longer on the instance buys nothing.
|
|
38
|
+
*/
|
|
39
|
+
export const PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS = 900;
|
|
31
40
|
export const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE = "fjall:Lifecycle";
|
|
32
41
|
export const PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE = "data-volume";
|
|
33
42
|
export const PERSISTENT_DATA_VOLUME_TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
|
|
@@ -39,11 +48,11 @@ const ALARM_THRESHOLD = 1;
|
|
|
39
48
|
* Standalone EBS data volume + ASG `EC2_INSTANCE_LAUNCHING` hook that
|
|
40
49
|
* re-attaches the same physical volume across instance refreshes.
|
|
41
50
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
51
|
+
* Nothing detaches the volume. The instance holding it releases it when it
|
|
52
|
+
* terminates, after `Ec2GracefulTerminationHandler` has proven its tasks
|
|
53
|
+
* stopped; the LAUNCHING Lambda waits for that release and then attaches the
|
|
54
|
+
* volume to the new instance. A forced detach would pull the filesystem out
|
|
55
|
+
* from under a database that is still running.
|
|
47
56
|
*
|
|
48
57
|
* Volume tagging: three tags carry the re-attach identity —
|
|
49
58
|
* `fjall:Lifecycle = data-volume` (constant; gates IAM scoping)
|
|
@@ -55,9 +64,7 @@ const ALARM_THRESHOLD = 1;
|
|
|
55
64
|
* StackId and cannot collide with the new deploy's volume)
|
|
56
65
|
*
|
|
57
66
|
* The LAUNCHING Lambda filters DescribeVolumes by all three tags AND ABANDONs
|
|
58
|
-
* on multi-match.
|
|
59
|
-
* `ownerLogicalId` (and is in the same stack, so shares the StackId
|
|
60
|
-
* automatically) to locate the volume for detach.
|
|
67
|
+
* on multi-match.
|
|
61
68
|
*
|
|
62
69
|
* Why three tags. The first two are stable per logical construct; without
|
|
63
70
|
* the StackId discriminator, every prior-iteration orphan (CREATE_ROLLBACK,
|
|
@@ -78,12 +85,10 @@ export class PersistentDataVolume extends Construct {
|
|
|
78
85
|
queue;
|
|
79
86
|
/**
|
|
80
87
|
* Identifier used as the `fjall:OwnerLogicalId` tag on the volume and the
|
|
81
|
-
* `OWNER_LOGICAL_ID` env on the Lambda.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* matches, new instance fails its launch hook) — recovery is a manual
|
|
86
|
-
* snapshot-restore step.
|
|
88
|
+
* `OWNER_LOGICAL_ID` env on the Lambda. Carries `props.stableOwnerId`
|
|
89
|
+
* verbatim; changing it across an existing deployment breaks the re-attach
|
|
90
|
+
* chain (Lambda filter no longer matches, new instance fails its launch
|
|
91
|
+
* hook) — recovery is a manual snapshot-restore step.
|
|
87
92
|
*/
|
|
88
93
|
ownerLogicalId;
|
|
89
94
|
attachFailureAlarm;
|
|
@@ -179,7 +184,9 @@ export class PersistentDataVolume extends Construct {
|
|
|
179
184
|
completeLifecyclePolicy
|
|
180
185
|
]
|
|
181
186
|
});
|
|
182
|
-
|
|
187
|
+
// One lifecycle action per invocation: a batch is processed in turn, so
|
|
188
|
+
// launches arriving together would share one timeout.
|
|
189
|
+
this.lambda.addSqsEventSource(this.queue.getQueue(), { batchSize: 1 });
|
|
183
190
|
// Hook name embeds Aws.STACK_NAME (a pseudo-parameter Ref, NOT a resource
|
|
184
191
|
// Ref — does not create a CFN dep) so that LifecycleHookName alone is
|
|
185
192
|
// sufficient to discriminate across stacks in the EB pattern below. The
|
|
@@ -191,7 +198,7 @@ export class PersistentDataVolume extends Construct {
|
|
|
191
198
|
hookName: launchingHookName,
|
|
192
199
|
lifecycleTransition: LifecycleTransition.INSTANCE_LAUNCHING,
|
|
193
200
|
defaultResult: DefaultResult.ABANDON,
|
|
194
|
-
heartbeatTimeoutSeconds:
|
|
201
|
+
heartbeatTimeoutSeconds: PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS
|
|
195
202
|
});
|
|
196
203
|
// Pattern discriminates by LifecycleHookName only — the hook name carries
|
|
197
204
|
// Aws.STACK_NAME so it is unique per stack. AutoScalingGroupName would
|
|
@@ -252,7 +259,6 @@ function validatePersistentDataVolumeProps(props) {
|
|
|
252
259
|
export const _internals = {
|
|
253
260
|
LAUNCHING_LAMBDA_SOURCE_FILE,
|
|
254
261
|
LAMBDA_TIMEOUT_SECONDS,
|
|
255
|
-
LAUNCHING_HOOK_HEARTBEAT_SECONDS,
|
|
256
262
|
QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
257
263
|
ALARM_EVALUATION_PERIODS,
|
|
258
264
|
ALARM_DATAPOINTS_TO_ALARM,
|