@fjall/components-infrastructure 0.95.0 → 0.99.1
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/app.d.ts +90 -107
- package/dist/lib/app.js +149 -139
- package/dist/lib/config/aws/__t17fixture.d.ts +1 -0
- package/dist/lib/config/aws/__t17fixture.js +3 -0
- package/dist/lib/config/aws/__t17fixtureType.d.ts +2 -0
- package/dist/lib/config/aws/__t17fixtureType.js +1 -0
- package/dist/lib/config/aws/alarmTopic.js +8 -4
- package/dist/lib/config/aws/cloudTrail.js +1 -1
- package/dist/lib/config/aws/disasterRecovery.js +11 -16
- package/dist/lib/config/aws/ecrDefaultImage.d.ts +0 -1
- package/dist/lib/config/aws/ecrDefaultImage.js +13 -23
- package/dist/lib/config/aws/identityCenter.d.ts +10 -3
- package/dist/lib/config/aws/identityCenter.js +101 -37
- package/dist/lib/config/aws/identityCenterGroupMembership.js +8 -2
- package/dist/lib/config/aws/identityCenterMembership.d.ts +11 -0
- package/dist/lib/config/aws/identityCenterMembership.js +61 -0
- package/dist/lib/config/aws/index.d.ts +1 -1
- package/dist/lib/config/aws/index.js +1 -1
- package/dist/lib/config/aws/ipam.js +6 -11
- package/dist/lib/config/aws/oidcConnector.js +5 -1
- package/dist/lib/config/aws/scpPreset.js +4 -1
- package/dist/lib/patterns/aws/_eslint_test_tmp/leak.d.ts +1 -0
- package/dist/lib/patterns/aws/_eslint_test_tmp/leak.js +4 -0
- package/dist/lib/patterns/aws/account.js +7 -8
- package/dist/lib/patterns/aws/apexDomainPattern.js +10 -10
- package/dist/lib/patterns/aws/bastionFactory.d.ts +10 -0
- package/dist/lib/patterns/aws/bastionFactory.js +29 -0
- package/dist/lib/patterns/aws/buildkite.d.ts +2 -2
- package/dist/lib/patterns/aws/buildkite.js +51 -97
- package/dist/lib/patterns/aws/cdn.js +1 -1
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +172 -0
- package/dist/lib/patterns/aws/clickhouseDatabase.js +600 -0
- package/dist/lib/patterns/aws/compute.d.ts +4 -6
- package/dist/lib/patterns/aws/compute.js +7 -13
- package/dist/lib/patterns/aws/computeEcs.d.ts +95 -396
- package/dist/lib/patterns/aws/computeEcs.js +880 -46
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +889 -0
- package/dist/lib/patterns/aws/computeEcsTypes.js +12 -0
- package/dist/lib/patterns/aws/computeLambda.d.ts +0 -5
- package/dist/lib/patterns/aws/computeLambda.js +1 -2
- package/dist/lib/patterns/aws/database.d.ts +50 -8
- package/dist/lib/patterns/aws/database.js +183 -27
- package/dist/lib/patterns/aws/domain.js +8 -7
- package/dist/lib/patterns/aws/index.d.ts +3 -0
- package/dist/lib/patterns/aws/index.js +3 -0
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +13 -1
- package/dist/lib/patterns/aws/interfaces/connector.d.ts +1 -1
- package/dist/lib/patterns/aws/interfaces/connector.js +1 -1
- package/dist/lib/patterns/aws/interfaces/database.d.ts +187 -8
- package/dist/lib/patterns/aws/interfaces/database.js +17 -3
- package/dist/lib/patterns/aws/interfaces/index.d.ts +4 -2
- package/dist/lib/patterns/aws/interfaces/index.js +4 -2
- package/dist/lib/patterns/aws/interfaces/messaging.d.ts +7 -0
- package/dist/lib/patterns/aws/interfaces/migrationContributor.d.ts +47 -0
- package/dist/lib/patterns/aws/interfaces/migrationContributor.js +9 -0
- package/dist/lib/patterns/aws/interfaces/vpcPeer.d.ts +7 -0
- package/dist/lib/patterns/aws/interfaces/vpcPeer.js +1 -0
- package/dist/lib/patterns/aws/messaging.d.ts +66 -10
- package/dist/lib/patterns/aws/messaging.js +115 -20
- package/dist/lib/patterns/aws/network.js +16 -7
- package/dist/lib/patterns/aws/organisation.d.ts +4 -0
- package/dist/lib/patterns/aws/organisation.js +24 -5
- package/dist/lib/patterns/aws/storage.d.ts +1 -2
- package/dist/lib/patterns/aws/storage.js +3 -2
- package/dist/lib/patterns/aws/vpcPeer.d.ts +34 -0
- package/dist/lib/patterns/aws/vpcPeer.js +38 -0
- package/dist/lib/patterns/aws/vpcPeerAccepter.d.ts +29 -0
- package/dist/lib/patterns/aws/vpcPeerAccepter.js +196 -0
- package/dist/lib/resources/aws/analytics/clickhouse.js +25 -7
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +49 -0
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +140 -0
- package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +4 -4
- package/dist/lib/resources/aws/analytics/clickhouseConstants.js +6 -4
- package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +12 -0
- package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -0
- package/dist/lib/resources/aws/analytics/clickhouseUserData.js +56 -5
- package/dist/lib/resources/aws/analytics/index.d.ts +2 -0
- package/dist/lib/resources/aws/analytics/index.js +1 -0
- package/dist/lib/resources/aws/base/awsStack.js +4 -2
- package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +2 -0
- package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +11 -0
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.d.ts +52 -0
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.js +60 -0
- package/dist/lib/resources/aws/compute/blockDeviceVolume.d.ts +8 -0
- package/dist/lib/resources/aws/compute/blockDeviceVolume.js +10 -0
- package/dist/lib/resources/aws/compute/ec2.d.ts +132 -12
- package/dist/lib/resources/aws/compute/ec2.js +163 -23
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.d.ts +41 -0
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +194 -0
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs +458 -0
- package/dist/lib/resources/aws/compute/ecs.d.ts +27 -1
- package/dist/lib/resources/aws/compute/ecs.js +42 -2
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +9 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +16 -0
- package/dist/lib/resources/aws/compute/ecsImages.js +32 -20
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.d.ts +96 -0
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +113 -0
- package/dist/lib/resources/aws/compute/ecsNetworking.d.ts +2 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.js +18 -6
- package/dist/lib/resources/aws/compute/ecsRemoteConnections.d.ts +38 -0
- package/dist/lib/resources/aws/compute/ecsRemoteConnections.js +80 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +13 -4
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +155 -33
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +31 -1
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +110 -6
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +180 -13
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +9 -0
- package/dist/lib/resources/aws/compute/ecsValidation.js +63 -0
- package/dist/lib/resources/aws/compute/index.d.ts +2 -0
- package/dist/lib/resources/aws/compute/index.js +2 -0
- package/dist/lib/resources/aws/compute/lambda.d.ts +7 -13
- package/dist/lib/resources/aws/compute/lambda.js +30 -38
- package/dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs +192 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +104 -0
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +245 -0
- package/dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs +398 -0
- package/dist/lib/resources/aws/compute/samApplication.d.ts +15 -0
- package/dist/lib/resources/aws/compute/samApplication.js +27 -0
- package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +159 -0
- package/dist/lib/resources/aws/database/clickhouseConstants.js +181 -0
- package/dist/lib/resources/aws/database/clickhouseSchemas.d.ts +71 -0
- package/dist/lib/resources/aws/database/clickhouseSchemas.js +157 -0
- package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +14 -0
- package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +23 -0
- package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +69 -0
- package/dist/lib/resources/aws/database/clickhouseUserData.js +371 -0
- package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +56 -0
- package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +112 -0
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +8 -1
- package/dist/lib/resources/aws/database/rdsAurora.js +42 -32
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +15 -2
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +39 -43
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +6 -0
- package/dist/lib/resources/aws/database/rdsDefaults.js +7 -1
- package/dist/lib/resources/aws/database/rdsHelpers.d.ts +3 -3
- package/dist/lib/resources/aws/database/rdsHelpers.js +1 -0
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +8 -1
- package/dist/lib/resources/aws/database/rdsInstance.js +51 -34
- package/dist/lib/resources/aws/database/rdsProxyOutput.d.ts +1 -1
- package/dist/lib/resources/aws/database/rdsProxyOutput.js +1 -1
- package/dist/lib/resources/aws/iam/delegationRole.js +12 -5
- package/dist/lib/resources/aws/iam/identityCenter/groupMembership.d.ts +9 -0
- package/dist/lib/resources/aws/iam/identityCenter/groupMembership.js +12 -0
- package/dist/lib/resources/aws/iam/identityCenter/index.d.ts +1 -0
- package/dist/lib/resources/aws/iam/identityCenter/index.js +1 -0
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.d.ts +1 -0
- package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +1 -0
- package/dist/lib/resources/aws/logging/logGroup.d.ts +0 -8
- package/dist/lib/resources/aws/logging/logGroup.js +0 -11
- package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +7 -0
- package/dist/lib/resources/aws/messaging/defaultEventBus.js +21 -0
- package/dist/lib/resources/aws/messaging/eventBridgeRule.d.ts +96 -0
- package/dist/lib/resources/aws/messaging/eventBridgeRule.js +110 -0
- package/dist/lib/resources/aws/messaging/eventTargets.d.ts +84 -0
- package/dist/lib/resources/aws/messaging/eventTargets.js +152 -0
- package/dist/lib/resources/aws/messaging/eventbridge.d.ts +25 -2
- package/dist/lib/resources/aws/messaging/eventbridge.js +22 -10
- package/dist/lib/resources/aws/messaging/index.d.ts +5 -0
- package/dist/lib/resources/aws/messaging/index.js +2 -0
- package/dist/lib/resources/aws/messaging/schedule.d.ts +118 -0
- package/dist/lib/resources/aws/messaging/schedule.js +64 -0
- package/dist/lib/resources/aws/messaging/sns.d.ts +2 -1
- package/dist/lib/resources/aws/messaging/sqs.d.ts +2 -1
- package/dist/lib/resources/aws/messaging/subscription.d.ts +112 -0
- package/dist/lib/resources/aws/messaging/subscription.js +67 -0
- package/dist/lib/resources/aws/messaging/utils.d.ts +6 -0
- package/dist/lib/resources/aws/messaging/utils.js +10 -0
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +60 -0
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +139 -0
- package/dist/lib/resources/aws/monitoring/index.d.ts +2 -0
- package/dist/lib/resources/aws/monitoring/index.js +2 -0
- package/dist/lib/resources/aws/monitoring/scheduleAlarms.d.ts +47 -0
- package/dist/lib/resources/aws/monitoring/scheduleAlarms.js +106 -0
- package/dist/lib/resources/aws/networking/crossAccountDelegationRecord.js +6 -3
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.d.ts +40 -0
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +158 -0
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +7 -4
- package/dist/lib/resources/aws/networking/domainCertificate.d.ts +2 -2
- package/dist/lib/resources/aws/networking/domainCertificate.js +6 -3
- package/dist/lib/resources/aws/networking/hostedZone.js +6 -4
- package/dist/lib/resources/aws/networking/index.d.ts +3 -0
- package/dist/lib/resources/aws/networking/index.js +3 -0
- package/dist/lib/resources/aws/networking/serviceDiscovery.d.ts +96 -0
- package/dist/lib/resources/aws/networking/serviceDiscovery.js +96 -0
- package/dist/lib/resources/aws/networking/vpc.d.ts +4 -1
- package/dist/lib/resources/aws/networking/vpc.js +10 -3
- package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.d.ts +18 -0
- package/dist/lib/resources/aws/networking/vpcPeeringAccepterRole.js +61 -0
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.d.ts +49 -0
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +106 -0
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.d.ts +16 -5
- package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +17 -3
- package/dist/lib/resources/aws/organisation/index.d.ts +1 -1
- package/dist/lib/resources/aws/organisation/organisationPolicy.d.ts +2 -0
- package/dist/lib/resources/aws/organisation/organisationPolicy.js +3 -2
- package/dist/lib/resources/aws/secrets/secret.d.ts +7 -0
- package/dist/lib/resources/aws/secrets/secret.js +4 -3
- package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +16 -0
- package/dist/lib/resources/aws/storage/bucketDeployment.js +17 -0
- package/dist/lib/resources/aws/storage/ecr.js +5 -5
- package/dist/lib/resources/aws/storage/index.d.ts +1 -0
- package/dist/lib/resources/aws/storage/index.js +1 -0
- package/dist/lib/resources/aws/storage/s3.js +10 -3
- package/dist/lib/resources/aws/utilities/customResource.js +18 -9
- package/dist/lib/synth_dump.d.ts +1 -0
- package/dist/lib/synth_dump.js +42 -0
- package/dist/lib/utils/bastionFactory.d.ts +10 -0
- package/dist/lib/utils/bastionFactory.js +29 -0
- package/dist/lib/utils/capitaliseString.d.ts +1 -1
- package/dist/lib/utils/capitaliseString.js +1 -1
- package/dist/lib/utils/cdkContext.d.ts +10 -0
- package/dist/lib/utils/cdkContext.js +13 -0
- package/dist/lib/utils/connections.d.ts +7 -1
- package/dist/lib/utils/connections.js +21 -0
- package/dist/lib/utils/connector.d.ts +30 -2
- package/dist/lib/utils/connector.js +6 -1
- package/dist/lib/utils/costAllocationTags.d.ts +15 -0
- package/dist/lib/utils/costAllocationTags.js +16 -0
- package/dist/lib/utils/databaseTypes.d.ts +14 -0
- package/dist/lib/utils/getConfig.d.ts +2 -0
- package/dist/lib/utils/getConfig.js +2 -0
- package/dist/lib/utils/index.d.ts +4 -0
- package/dist/lib/utils/index.js +4 -0
- package/dist/lib/utils/manifestWriter.d.ts +6 -89
- package/dist/lib/utils/manifestWriter.js +36 -23
- package/dist/lib/utils/migrationVersionResolvers.d.ts +2 -0
- package/dist/lib/utils/migrationVersionResolvers.js +2 -0
- package/dist/lib/utils/orgConfigParser.js +2 -1
- package/dist/lib/utils/resolveAlertsTopic.d.ts +14 -0
- package/dist/lib/utils/resolveAlertsTopic.js +30 -0
- package/dist/lib/utils/validationLogger.js +6 -3
- package/dist/lib/utils/vpcPeerInterface.d.ts +22 -0
- package/dist/lib/utils/vpcPeerInterface.js +1 -0
- package/package.json +22 -18
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
|
|
2
|
+
import { Construct } from "constructs";
|
|
3
|
+
import { LambdaFunction } from "./lambda.js";
|
|
4
|
+
import { SQSQueue } from "../messaging/sqs.js";
|
|
5
|
+
/**
|
|
6
|
+
* Description prefix baked into the Lambda + execution-role descriptions.
|
|
7
|
+
* Tests grep CFN output for this needle to locate the function across
|
|
8
|
+
* stacks that may contain unrelated Lambdas — keep it here as the single
|
|
9
|
+
* source of truth so a rename does not silently break the test query.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
|
|
12
|
+
export interface Ec2GracefulTerminationHandlerProps {
|
|
13
|
+
/** ASG to attach the EC2_INSTANCE_TERMINATING hook to. */
|
|
14
|
+
autoScalingGroup: AutoScalingGroup;
|
|
15
|
+
/**
|
|
16
|
+
* ECS cluster ARN — when set, the Lambda drains and deregisters the
|
|
17
|
+
* container instance before generic cleanup. Empty string is normalised
|
|
18
|
+
* to `undefined`; consumers should pass `undefined` for bare-EC2
|
|
19
|
+
* deployments (bastion, Fivetran).
|
|
20
|
+
*/
|
|
21
|
+
ecsClusterArn?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `fjall:OwnerLogicalId` of a paired `PersistentDataVolume`. When set, the
|
|
24
|
+
* Lambda detaches the tagged volume from the terminating instance before
|
|
25
|
+
* `CompleteLifecycleAction`. Empty string is normalised to `undefined`.
|
|
26
|
+
* Pass the `PersistentDataVolume.ownerLogicalId` so the TERMINATING and
|
|
27
|
+
* LAUNCHING handlers locate the same volume.
|
|
28
|
+
*/
|
|
29
|
+
dataVolumeOwnerLogicalId?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* ASG `EC2_INSTANCE_TERMINATING` hook + Lambda that drains the instance
|
|
33
|
+
* cleanly. Drain ownership lives at the EC2 layer; ECS-specific drain plugs
|
|
34
|
+
* in via `ecsClusterArn` so a single hook handles both bare-EC2 (bastion,
|
|
35
|
+
* Fivetran) and ECS-wired ASGs (ClickHouse).
|
|
36
|
+
*/
|
|
37
|
+
export declare class Ec2GracefulTerminationHandler extends Construct {
|
|
38
|
+
readonly lambda: LambdaFunction;
|
|
39
|
+
readonly queue: SQSQueue;
|
|
40
|
+
constructor(scope: Construct, id: string, props: Ec2GracefulTerminationHandlerProps);
|
|
41
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Aws, Stack } from "aws-cdk-lib";
|
|
5
|
+
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
|
+
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
7
|
+
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
8
|
+
import { LifecycleTransition, DefaultResult } from "aws-cdk-lib/aws-autoscaling";
|
|
9
|
+
import { Construct } from "constructs";
|
|
10
|
+
import { attachInlineAsgLifecycleHook } from "./asgInlineLifecycleHook.js";
|
|
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
|
+
import { SQSQueue } from "../messaging/sqs.js";
|
|
14
|
+
import { Subscription } from "../messaging/subscription.js";
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const LAMBDA_SOURCE_FILE = "ec2GracefulTerminationLambda.source.cjs";
|
|
17
|
+
/**
|
|
18
|
+
* Description prefix baked into the Lambda + execution-role descriptions.
|
|
19
|
+
* Tests grep CFN output for this needle to locate the function across
|
|
20
|
+
* stacks that may contain unrelated Lambdas — keep it here as the single
|
|
21
|
+
* source of truth so a rename does not silently break the test query.
|
|
22
|
+
*/
|
|
23
|
+
export const EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION = "EC2 graceful termination handler";
|
|
24
|
+
const LAMBDA_TIMEOUT_SECONDS = 300;
|
|
25
|
+
// Heartbeat must comfortably exceed the Lambda's drain (240s) + detach (60s)
|
|
26
|
+
// budget so the hook does not time out mid-cleanup when PDV is wired.
|
|
27
|
+
const HOOK_HEARTBEAT_SECONDS = 600;
|
|
28
|
+
const QUEUE_VISIBILITY_TIMEOUT_SECONDS = 360;
|
|
29
|
+
/**
|
|
30
|
+
* ASG `EC2_INSTANCE_TERMINATING` hook + Lambda that drains the instance
|
|
31
|
+
* cleanly. Drain ownership lives at the EC2 layer; ECS-specific drain plugs
|
|
32
|
+
* in via `ecsClusterArn` so a single hook handles both bare-EC2 (bastion,
|
|
33
|
+
* Fivetran) and ECS-wired ASGs (ClickHouse).
|
|
34
|
+
*/
|
|
35
|
+
export class Ec2GracefulTerminationHandler extends Construct {
|
|
36
|
+
lambda;
|
|
37
|
+
queue;
|
|
38
|
+
constructor(scope, id, props) {
|
|
39
|
+
super(scope, id);
|
|
40
|
+
const sourcePath = path.resolve(__dirname, LAMBDA_SOURCE_FILE);
|
|
41
|
+
const source = readFileSync(sourcePath, "utf-8");
|
|
42
|
+
const ecsClusterArn = resolveOptionalString(props.ecsClusterArn);
|
|
43
|
+
const dataVolumeOwnerLogicalId = resolveOptionalString(props.dataVolumeOwnerLogicalId);
|
|
44
|
+
this.queue = new SQSQueue(this, `${id}Queue`, {
|
|
45
|
+
visibilityTimeout: QUEUE_VISIBILITY_TIMEOUT_SECONDS,
|
|
46
|
+
deadLetterQueue: { enabled: true, maxReceiveCount: 5 }
|
|
47
|
+
});
|
|
48
|
+
const ecsPolicies = ecsClusterArn !== undefined
|
|
49
|
+
? [
|
|
50
|
+
new PolicyStatement({
|
|
51
|
+
effect: Effect.ALLOW,
|
|
52
|
+
actions: ["ecs:ListContainerInstances"],
|
|
53
|
+
resources: [ecsClusterArn]
|
|
54
|
+
}),
|
|
55
|
+
new PolicyStatement({
|
|
56
|
+
effect: Effect.ALLOW,
|
|
57
|
+
actions: [
|
|
58
|
+
"ecs:DescribeContainerInstances",
|
|
59
|
+
"ecs:UpdateContainerInstancesState",
|
|
60
|
+
"ecs:DeregisterContainerInstance"
|
|
61
|
+
],
|
|
62
|
+
resources: ["*"],
|
|
63
|
+
conditions: {
|
|
64
|
+
ArnEquals: { "ecs:cluster": ecsClusterArn }
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
: [];
|
|
69
|
+
const ec2Policies = new PolicyStatement({
|
|
70
|
+
effect: Effect.ALLOW,
|
|
71
|
+
actions: [
|
|
72
|
+
"ec2:DescribeAddresses",
|
|
73
|
+
"ec2:DisassociateAddress",
|
|
74
|
+
"ec2:DescribeNetworkInterfaces",
|
|
75
|
+
"ec2:DetachNetworkInterface"
|
|
76
|
+
],
|
|
77
|
+
resources: ["*"]
|
|
78
|
+
});
|
|
79
|
+
const elbReadPolicy = new PolicyStatement({
|
|
80
|
+
effect: Effect.ALLOW,
|
|
81
|
+
actions: [
|
|
82
|
+
"elasticloadbalancing:DescribeTargetGroups",
|
|
83
|
+
"elasticloadbalancing:DescribeTargetHealth"
|
|
84
|
+
],
|
|
85
|
+
resources: ["*"]
|
|
86
|
+
});
|
|
87
|
+
const elbDeregisterPolicy = new PolicyStatement({
|
|
88
|
+
effect: Effect.ALLOW,
|
|
89
|
+
actions: ["elasticloadbalancing:DeregisterTargets"],
|
|
90
|
+
resources: ["*"],
|
|
91
|
+
conditions: {
|
|
92
|
+
StringEquals: {
|
|
93
|
+
"aws:ResourceTag/aws:cloudformation:stack-id": Aws.STACK_ID
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const stack = Stack.of(this);
|
|
98
|
+
// Account/region-scoped wildcard rather than the specific ASG ARN —
|
|
99
|
+
// see PersistentDataVolume for the full deadlock writeup. Same gotcha:
|
|
100
|
+
// a specific ARN creates a CFN Ref to the ASG, the Lambda's IAM Policy
|
|
101
|
+
// waits for ASG CREATE_COMPLETE, the ASG can't complete without this
|
|
102
|
+
// Lambda's CompleteLifecycleAction call on its first terminating instance.
|
|
103
|
+
const asgArnWildcard = `arn:${stack.partition}:autoscaling:${stack.region}:${stack.account}:autoScalingGroup:*:autoScalingGroupName/*`;
|
|
104
|
+
const asgPolicy = new PolicyStatement({
|
|
105
|
+
effect: Effect.ALLOW,
|
|
106
|
+
actions: ["autoscaling:CompleteLifecycleAction"],
|
|
107
|
+
resources: [asgArnWildcard]
|
|
108
|
+
});
|
|
109
|
+
const volumeArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/*`;
|
|
110
|
+
const instanceArnPattern = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance/*`;
|
|
111
|
+
const dataVolumePolicies = dataVolumeOwnerLogicalId !== undefined
|
|
112
|
+
? [
|
|
113
|
+
new PolicyStatement({
|
|
114
|
+
effect: Effect.ALLOW,
|
|
115
|
+
actions: ["ec2:DescribeVolumes"],
|
|
116
|
+
resources: ["*"]
|
|
117
|
+
}),
|
|
118
|
+
new PolicyStatement({
|
|
119
|
+
effect: Effect.ALLOW,
|
|
120
|
+
actions: ["ec2:DetachVolume"],
|
|
121
|
+
resources: [volumeArnPattern],
|
|
122
|
+
conditions: {
|
|
123
|
+
StringEquals: {
|
|
124
|
+
[`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE}`]: PERSISTENT_DATA_VOLUME_TAG_LIFECYCLE_VALUE,
|
|
125
|
+
[`aws:ResourceTag/${PERSISTENT_DATA_VOLUME_TAG_STACK_ID}`]: Aws.STACK_ID
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
new PolicyStatement({
|
|
130
|
+
effect: Effect.ALLOW,
|
|
131
|
+
actions: ["ec2:DetachVolume"],
|
|
132
|
+
resources: [instanceArnPattern]
|
|
133
|
+
})
|
|
134
|
+
]
|
|
135
|
+
: [];
|
|
136
|
+
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
137
|
+
runtime: Runtime.NODEJS_22_X,
|
|
138
|
+
handler: "index.handler",
|
|
139
|
+
code: Code.fromInline(source),
|
|
140
|
+
lambdaDescription: `${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
|
|
141
|
+
roleDescription: `Execution role for ${id} ${EC2_GRACEFUL_TERMINATION_HANDLER_DESCRIPTION}`,
|
|
142
|
+
timeout: LAMBDA_TIMEOUT_SECONDS,
|
|
143
|
+
memorySize: 256,
|
|
144
|
+
logGroupRetention: RetentionDays.ONE_MONTH,
|
|
145
|
+
environment: {
|
|
146
|
+
...(ecsClusterArn !== undefined && { ECS_CLUSTER_ARN: ecsClusterArn }),
|
|
147
|
+
...(dataVolumeOwnerLogicalId !== undefined && {
|
|
148
|
+
DATA_VOLUME_OWNER_LOGICAL_ID: dataVolumeOwnerLogicalId,
|
|
149
|
+
DATA_VOLUME_STACK_ID: Aws.STACK_ID
|
|
150
|
+
})
|
|
151
|
+
},
|
|
152
|
+
inlinePolicy: [
|
|
153
|
+
...ecsPolicies,
|
|
154
|
+
ec2Policies,
|
|
155
|
+
elbReadPolicy,
|
|
156
|
+
elbDeregisterPolicy,
|
|
157
|
+
asgPolicy,
|
|
158
|
+
...dataVolumePolicies
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
this.lambda.addSqsEventSource(this.queue.getQueue());
|
|
162
|
+
// Hook name embeds Aws.STACK_NAME (pseudo-parameter Ref, NOT a resource
|
|
163
|
+
// Ref — no CFN dep) so LifecycleHookName alone discriminates across
|
|
164
|
+
// stacks in the EB pattern. Sibling LAUNCHING hook uses "-launching".
|
|
165
|
+
const terminatingHookName = `${Aws.STACK_NAME}-${id}-terminating`;
|
|
166
|
+
attachInlineAsgLifecycleHook(this, `${id}TerminatingHook`, {
|
|
167
|
+
autoScalingGroup: props.autoScalingGroup,
|
|
168
|
+
hookName: terminatingHookName,
|
|
169
|
+
lifecycleTransition: LifecycleTransition.INSTANCE_TERMINATING,
|
|
170
|
+
defaultResult: DefaultResult.CONTINUE,
|
|
171
|
+
heartbeatTimeoutSeconds: HOOK_HEARTBEAT_SECONDS
|
|
172
|
+
});
|
|
173
|
+
// No AutoScalingGroupName in the pattern — that would create a CFN Ref to
|
|
174
|
+
// the ASG and block the EB rule on ASG CREATE_COMPLETE (deadlock — see
|
|
175
|
+
// wildcard ARN comment above and the PersistentDataVolume sibling).
|
|
176
|
+
new Subscription(this, `${id}TerminatingSub`, {
|
|
177
|
+
pattern: {
|
|
178
|
+
source: ["aws.autoscaling"],
|
|
179
|
+
detailType: ["EC2 Instance-terminate Lifecycle Action"],
|
|
180
|
+
detail: {
|
|
181
|
+
LifecycleHookName: [terminatingHookName]
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
target: this.queue
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function resolveOptionalString(value) {
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (value === "")
|
|
192
|
+
return undefined;
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC2 graceful termination handler. Inlined into a Lambda at synth time by
|
|
3
|
+
* ec2GracefulTerminationHandler.ts. Triggered by an SQS queue fed by an
|
|
4
|
+
* EventBridge rule subscribed to the ASG EC2_INSTANCE_TERMINATING lifecycle
|
|
5
|
+
* event on the default bus.
|
|
6
|
+
*
|
|
7
|
+
* Wire format: the SQS message body is the EventBridge event envelope
|
|
8
|
+
* `{ version, "detail-type", source, account, time, region, resources,
|
|
9
|
+
* detail: { LifecycleActionToken, AutoScalingGroupName,
|
|
10
|
+
* LifecycleHookName, EC2InstanceId, LifecycleTransition,
|
|
11
|
+
* NotificationMetadata, ... } }`.
|
|
12
|
+
* The lifecycle payload lives at `body.detail`. Records whose envelope does
|
|
13
|
+
* not match (wrong source, missing detail, non-Lifecycle Action detail-type)
|
|
14
|
+
* are logged and skipped.
|
|
15
|
+
*
|
|
16
|
+
* Behaviour, in order:
|
|
17
|
+
* 1. (Conditional) ECS_CLUSTER_ARN set + non-empty → drain container
|
|
18
|
+
* instance, wait for runningTasksCount=0, deregister.
|
|
19
|
+
* 2. (Always) Dissociate EIPs, deregister from any target groups,
|
|
20
|
+
* detach non-primary ENIs.
|
|
21
|
+
* 3. CompleteLifecycleAction CONTINUE — even on partial failure, so the
|
|
22
|
+
* ASG never stalls indefinitely. Better to terminate dirty than hang
|
|
23
|
+
* the whole stack delete.
|
|
24
|
+
*
|
|
25
|
+
* CommonJS rather than ESM because Code.fromInline lands the source as
|
|
26
|
+
* `index.js`, which Lambda treats as CommonJS by default.
|
|
27
|
+
*/
|
|
28
|
+
const {
|
|
29
|
+
ECSClient,
|
|
30
|
+
ListContainerInstancesCommand,
|
|
31
|
+
DescribeContainerInstancesCommand,
|
|
32
|
+
UpdateContainerInstancesStateCommand,
|
|
33
|
+
DeregisterContainerInstanceCommand
|
|
34
|
+
} = require("@aws-sdk/client-ecs");
|
|
35
|
+
const {
|
|
36
|
+
EC2Client,
|
|
37
|
+
DescribeAddressesCommand,
|
|
38
|
+
DisassociateAddressCommand,
|
|
39
|
+
DescribeNetworkInterfacesCommand,
|
|
40
|
+
DetachNetworkInterfaceCommand,
|
|
41
|
+
DescribeVolumesCommand,
|
|
42
|
+
DetachVolumeCommand
|
|
43
|
+
} = require("@aws-sdk/client-ec2");
|
|
44
|
+
const {
|
|
45
|
+
ElasticLoadBalancingV2Client,
|
|
46
|
+
DescribeTargetGroupsCommand,
|
|
47
|
+
DescribeTargetHealthCommand,
|
|
48
|
+
DeregisterTargetsCommand
|
|
49
|
+
} = require("@aws-sdk/client-elastic-load-balancing-v2");
|
|
50
|
+
const {
|
|
51
|
+
AutoScalingClient,
|
|
52
|
+
CompleteLifecycleActionCommand
|
|
53
|
+
} = require("@aws-sdk/client-auto-scaling");
|
|
54
|
+
|
|
55
|
+
const DRAIN_POLL_INTERVAL_MS = 10_000;
|
|
56
|
+
const DRAIN_DEADLINE_MS = 240_000;
|
|
57
|
+
const DETACH_POLL_INTERVAL_MS = 5_000;
|
|
58
|
+
const DETACH_DEADLINE_MS = 60_000;
|
|
59
|
+
const TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
|
|
60
|
+
const TAG_STACK_ID = "fjall:StackId";
|
|
61
|
+
|
|
62
|
+
function errMessage(err) {
|
|
63
|
+
return err && err.message ? err.message : String(err);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let _ecs;
|
|
67
|
+
let _ec2;
|
|
68
|
+
let _elbv2;
|
|
69
|
+
let _asg;
|
|
70
|
+
function getEcs() {
|
|
71
|
+
if (!_ecs) _ecs = new ECSClient({});
|
|
72
|
+
return _ecs;
|
|
73
|
+
}
|
|
74
|
+
function getEc2() {
|
|
75
|
+
if (!_ec2) _ec2 = new EC2Client({});
|
|
76
|
+
return _ec2;
|
|
77
|
+
}
|
|
78
|
+
function getElbv2() {
|
|
79
|
+
if (!_elbv2) _elbv2 = new ElasticLoadBalancingV2Client({});
|
|
80
|
+
return _elbv2;
|
|
81
|
+
}
|
|
82
|
+
function getAsg() {
|
|
83
|
+
if (!_asg) _asg = new AutoScalingClient({});
|
|
84
|
+
return _asg;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseLifecycleMessage(record) {
|
|
88
|
+
const envelope = JSON.parse(record.body);
|
|
89
|
+
const detail = envelope && envelope.detail;
|
|
90
|
+
const source = envelope && envelope.source;
|
|
91
|
+
const detailType = envelope && envelope["detail-type"];
|
|
92
|
+
const valid =
|
|
93
|
+
detail !== undefined &&
|
|
94
|
+
detail !== null &&
|
|
95
|
+
typeof detail === "object" &&
|
|
96
|
+
source === "aws.autoscaling" &&
|
|
97
|
+
typeof detailType === "string" &&
|
|
98
|
+
detailType.endsWith("Lifecycle Action");
|
|
99
|
+
if (!valid) {
|
|
100
|
+
return { valid: false };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
valid: true,
|
|
104
|
+
instanceId: detail.EC2InstanceId,
|
|
105
|
+
asgName: detail.AutoScalingGroupName,
|
|
106
|
+
actionToken: detail.LifecycleActionToken,
|
|
107
|
+
hookName: detail.LifecycleHookName,
|
|
108
|
+
isTest: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function findContainerInstanceArn(ecsClient, clusterArn, instanceId) {
|
|
113
|
+
const list = await ecsClient.send(
|
|
114
|
+
new ListContainerInstancesCommand({
|
|
115
|
+
cluster: clusterArn,
|
|
116
|
+
filter: `ec2InstanceId == ${instanceId}`
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
if (!list.containerInstanceArns || list.containerInstanceArns.length === 0) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return list.containerInstanceArns[0];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function drainContainerInstance(
|
|
126
|
+
ecsClient,
|
|
127
|
+
clusterArn,
|
|
128
|
+
containerInstanceArn,
|
|
129
|
+
sleepFn,
|
|
130
|
+
nowFn
|
|
131
|
+
) {
|
|
132
|
+
await ecsClient.send(
|
|
133
|
+
new UpdateContainerInstancesStateCommand({
|
|
134
|
+
cluster: clusterArn,
|
|
135
|
+
containerInstances: [containerInstanceArn],
|
|
136
|
+
status: "DRAINING"
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const deadline = nowFn() + DRAIN_DEADLINE_MS;
|
|
141
|
+
while (nowFn() < deadline) {
|
|
142
|
+
const desc = await ecsClient.send(
|
|
143
|
+
new DescribeContainerInstancesCommand({
|
|
144
|
+
cluster: clusterArn,
|
|
145
|
+
containerInstances: [containerInstanceArn]
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
const instance =
|
|
149
|
+
desc.containerInstances && desc.containerInstances[0]
|
|
150
|
+
? desc.containerInstances[0]
|
|
151
|
+
: undefined;
|
|
152
|
+
if (!instance) return;
|
|
153
|
+
if ((instance.runningTasksCount || 0) === 0) return;
|
|
154
|
+
await sleepFn(DRAIN_POLL_INTERVAL_MS);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function dissociateInstanceEips(ec2Client, instanceId) {
|
|
159
|
+
const result = await ec2Client.send(
|
|
160
|
+
new DescribeAddressesCommand({
|
|
161
|
+
Filters: [{ Name: "instance-id", Values: [instanceId] }]
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
const addresses = result.Addresses || [];
|
|
165
|
+
for (const addr of addresses) {
|
|
166
|
+
if (!addr.AssociationId) continue;
|
|
167
|
+
await ec2Client.send(
|
|
168
|
+
new DisassociateAddressCommand({ AssociationId: addr.AssociationId })
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function deregisterFromTargetGroups(elbv2Client, instanceId) {
|
|
174
|
+
const groups = await elbv2Client.send(new DescribeTargetGroupsCommand({}));
|
|
175
|
+
const targetGroups = groups.TargetGroups || [];
|
|
176
|
+
for (const tg of targetGroups) {
|
|
177
|
+
if (!tg.TargetGroupArn) continue;
|
|
178
|
+
let health;
|
|
179
|
+
try {
|
|
180
|
+
health = await elbv2Client.send(
|
|
181
|
+
new DescribeTargetHealthCommand({ TargetGroupArn: tg.TargetGroupArn })
|
|
182
|
+
);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.debug(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
message: "DescribeTargetHealth skipped",
|
|
187
|
+
targetGroupArn: tg.TargetGroupArn,
|
|
188
|
+
error: errMessage(err)
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const matches = (health.TargetHealthDescriptions || []).filter(
|
|
194
|
+
(d) => d.Target && d.Target.Id === instanceId
|
|
195
|
+
);
|
|
196
|
+
if (matches.length === 0) continue;
|
|
197
|
+
await elbv2Client.send(
|
|
198
|
+
new DeregisterTargetsCommand({
|
|
199
|
+
TargetGroupArn: tg.TargetGroupArn,
|
|
200
|
+
Targets: matches.map((m) => ({
|
|
201
|
+
Id: m.Target.Id,
|
|
202
|
+
Port: m.Target.Port
|
|
203
|
+
}))
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function detachSecondaryEnis(ec2Client, instanceId) {
|
|
210
|
+
const result = await ec2Client.send(
|
|
211
|
+
new DescribeNetworkInterfacesCommand({
|
|
212
|
+
Filters: [{ Name: "attachment.instance-id", Values: [instanceId] }]
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
const enis = result.NetworkInterfaces || [];
|
|
216
|
+
for (const eni of enis) {
|
|
217
|
+
const attachment = eni.Attachment;
|
|
218
|
+
if (!attachment || !attachment.AttachmentId) continue;
|
|
219
|
+
// Skip the primary interface (DeviceIndex 0); it is destroyed with the
|
|
220
|
+
// instance and cannot be detached independently.
|
|
221
|
+
if (attachment.DeviceIndex === 0) continue;
|
|
222
|
+
try {
|
|
223
|
+
await ec2Client.send(
|
|
224
|
+
new DetachNetworkInterfaceCommand({
|
|
225
|
+
AttachmentId: attachment.AttachmentId,
|
|
226
|
+
Force: true
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.debug(
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
message: "DetachNetworkInterface skipped",
|
|
233
|
+
attachmentId: attachment.AttachmentId,
|
|
234
|
+
error: errMessage(err)
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function detachDataVolume(
|
|
242
|
+
ec2Client,
|
|
243
|
+
instanceId,
|
|
244
|
+
ownerLogicalId,
|
|
245
|
+
stackId,
|
|
246
|
+
sleepFn,
|
|
247
|
+
nowFn
|
|
248
|
+
) {
|
|
249
|
+
const described = await ec2Client.send(
|
|
250
|
+
new DescribeVolumesCommand({
|
|
251
|
+
Filters: [
|
|
252
|
+
{ Name: `tag:${TAG_OWNER_LOGICAL_ID}`, Values: [ownerLogicalId] },
|
|
253
|
+
{ Name: `tag:${TAG_STACK_ID}`, Values: [stackId] },
|
|
254
|
+
{ Name: "attachment.instance-id", Values: [instanceId] }
|
|
255
|
+
]
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
const volumes = described.Volumes || [];
|
|
259
|
+
if (volumes.length === 0) return;
|
|
260
|
+
const volumeId = volumes[0].VolumeId;
|
|
261
|
+
if (!volumeId) return;
|
|
262
|
+
await ec2Client.send(
|
|
263
|
+
new DetachVolumeCommand({ VolumeId: volumeId, InstanceId: instanceId })
|
|
264
|
+
);
|
|
265
|
+
const deadline = nowFn() + DETACH_DEADLINE_MS;
|
|
266
|
+
while (nowFn() < deadline) {
|
|
267
|
+
const desc = await ec2Client.send(
|
|
268
|
+
new DescribeVolumesCommand({ VolumeIds: [volumeId] })
|
|
269
|
+
);
|
|
270
|
+
const volume =
|
|
271
|
+
desc.Volumes && desc.Volumes[0] ? desc.Volumes[0] : undefined;
|
|
272
|
+
if (volume && volume.State === "available") return;
|
|
273
|
+
await sleepFn(DETACH_POLL_INTERVAL_MS);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function completeLifecycle(asgClient, asgName, hookName, actionToken) {
|
|
278
|
+
await asgClient.send(
|
|
279
|
+
new CompleteLifecycleActionCommand({
|
|
280
|
+
AutoScalingGroupName: asgName,
|
|
281
|
+
LifecycleHookName: hookName,
|
|
282
|
+
LifecycleActionToken: actionToken,
|
|
283
|
+
LifecycleActionResult: "CONTINUE"
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readEnv(env, name) {
|
|
289
|
+
const value = env[name];
|
|
290
|
+
if (typeof value !== "string" || value === "") return undefined;
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function processRecord(record, deps) {
|
|
295
|
+
const ecsClient = (deps && deps.ecsClient) || getEcs();
|
|
296
|
+
const ec2Client = (deps && deps.ec2Client) || getEc2();
|
|
297
|
+
const elbv2Client = (deps && deps.elbv2Client) || getElbv2();
|
|
298
|
+
const asgClient = (deps && deps.asgClient) || getAsg();
|
|
299
|
+
const sleepFn =
|
|
300
|
+
(deps && deps.sleep) ||
|
|
301
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
302
|
+
const nowFn = (deps && deps.now) || (() => Date.now());
|
|
303
|
+
const env = (deps && deps.env) || process.env;
|
|
304
|
+
const log =
|
|
305
|
+
(deps && deps.log) ||
|
|
306
|
+
((msg, fields) => {
|
|
307
|
+
console.log(JSON.stringify({ message: msg, ...(fields || {}) }));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const parsed = parseLifecycleMessage(record);
|
|
311
|
+
if (!parsed.valid) {
|
|
312
|
+
log("Record is not an aws.autoscaling Lifecycle Action; skipping", {
|
|
313
|
+
bodyPreview: record.body ? String(record.body).slice(0, 200) : undefined
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const { instanceId, asgName, actionToken, hookName } = parsed;
|
|
318
|
+
|
|
319
|
+
const clusterArn = readEnv(env, "ECS_CLUSTER_ARN");
|
|
320
|
+
if (clusterArn !== undefined) {
|
|
321
|
+
try {
|
|
322
|
+
const containerInstanceArn = await findContainerInstanceArn(
|
|
323
|
+
ecsClient,
|
|
324
|
+
clusterArn,
|
|
325
|
+
instanceId
|
|
326
|
+
);
|
|
327
|
+
if (containerInstanceArn !== undefined) {
|
|
328
|
+
await drainContainerInstance(
|
|
329
|
+
ecsClient,
|
|
330
|
+
clusterArn,
|
|
331
|
+
containerInstanceArn,
|
|
332
|
+
sleepFn,
|
|
333
|
+
nowFn
|
|
334
|
+
);
|
|
335
|
+
await ecsClient.send(
|
|
336
|
+
new DeregisterContainerInstanceCommand({
|
|
337
|
+
cluster: clusterArn,
|
|
338
|
+
containerInstance: containerInstanceArn,
|
|
339
|
+
force: true
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
log("ECS drain step failed; continuing to generic cleanup", {
|
|
345
|
+
instanceId,
|
|
346
|
+
error: errMessage(err)
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await dissociateInstanceEips(ec2Client, instanceId);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
log("EIP dissociation failed", {
|
|
355
|
+
instanceId,
|
|
356
|
+
error: errMessage(err)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await deregisterFromTargetGroups(elbv2Client, instanceId);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
log("Target-group deregistration failed", {
|
|
363
|
+
instanceId,
|
|
364
|
+
error: errMessage(err)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await detachSecondaryEnis(ec2Client, instanceId);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
log("ENI detachment failed", {
|
|
371
|
+
instanceId,
|
|
372
|
+
error: errMessage(err)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const dataVolumeOwnerLogicalId = readEnv(env, "DATA_VOLUME_OWNER_LOGICAL_ID");
|
|
377
|
+
const dataVolumeStackId = readEnv(env, "DATA_VOLUME_STACK_ID");
|
|
378
|
+
if (
|
|
379
|
+
dataVolumeOwnerLogicalId !== undefined &&
|
|
380
|
+
dataVolumeStackId !== undefined
|
|
381
|
+
) {
|
|
382
|
+
try {
|
|
383
|
+
await detachDataVolume(
|
|
384
|
+
ec2Client,
|
|
385
|
+
instanceId,
|
|
386
|
+
dataVolumeOwnerLogicalId,
|
|
387
|
+
dataVolumeStackId,
|
|
388
|
+
sleepFn,
|
|
389
|
+
nowFn
|
|
390
|
+
);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
log("Data volume detach failed; AWS will force-detach after heartbeat", {
|
|
393
|
+
instanceId,
|
|
394
|
+
ownerLogicalId: dataVolumeOwnerLogicalId,
|
|
395
|
+
error: errMessage(err)
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await completeLifecycle(asgClient, asgName, hookName, actionToken);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
exports.handler = async (event) => {
|
|
404
|
+
const records = (event && event.Records) || [];
|
|
405
|
+
for (const record of records) {
|
|
406
|
+
try {
|
|
407
|
+
await processRecord(record);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
// Last-resort: try to release the lifecycle action so the stack does
|
|
410
|
+
// not hang. Failure here is logged and the message will retry via SQS.
|
|
411
|
+
console.error(
|
|
412
|
+
JSON.stringify({
|
|
413
|
+
message: "Graceful termination handler crashed; attempting CONTINUE",
|
|
414
|
+
error: errMessage(err)
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
try {
|
|
418
|
+
const parsed = parseLifecycleMessage(record);
|
|
419
|
+
if (!parsed.valid) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
await completeLifecycle(
|
|
423
|
+
getAsg(),
|
|
424
|
+
parsed.asgName,
|
|
425
|
+
parsed.hookName,
|
|
426
|
+
parsed.actionToken
|
|
427
|
+
);
|
|
428
|
+
} catch (innerErr) {
|
|
429
|
+
console.error(
|
|
430
|
+
JSON.stringify({
|
|
431
|
+
message: "CompleteLifecycleAction also failed",
|
|
432
|
+
error: errMessage(innerErr)
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
exports._internals = {
|
|
442
|
+
parseLifecycleMessage,
|
|
443
|
+
findContainerInstanceArn,
|
|
444
|
+
drainContainerInstance,
|
|
445
|
+
dissociateInstanceEips,
|
|
446
|
+
deregisterFromTargetGroups,
|
|
447
|
+
detachSecondaryEnis,
|
|
448
|
+
detachDataVolume,
|
|
449
|
+
completeLifecycle,
|
|
450
|
+
readEnv,
|
|
451
|
+
processRecord,
|
|
452
|
+
DRAIN_POLL_INTERVAL_MS,
|
|
453
|
+
DRAIN_DEADLINE_MS,
|
|
454
|
+
DETACH_POLL_INTERVAL_MS,
|
|
455
|
+
DETACH_DEADLINE_MS,
|
|
456
|
+
TAG_OWNER_LOGICAL_ID,
|
|
457
|
+
TAG_STACK_ID
|
|
458
|
+
};
|