@fjall/components-infrastructure 0.96.0 → 0.99.3
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 +68 -1
- package/dist/lib/app.js +113 -4
- 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 +2 -4
- 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 +173 -0
- package/dist/lib/patterns/aws/clickhouseDatabase.js +601 -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 +93 -5
- package/dist/lib/patterns/aws/computeEcs.js +867 -37
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +528 -25
- package/dist/lib/patterns/aws/computeEcsTypes.js +10 -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 +6 -4
- package/dist/lib/patterns/aws/index.d.ts +1 -0
- package/dist/lib/patterns/aws/index.js +1 -0
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +7 -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 +2 -1
- package/dist/lib/patterns/aws/interfaces/index.js +3 -1
- 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/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 +22 -4
- 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.js +3 -1
- package/dist/lib/resources/aws/analytics/clickhouse.js +18 -9
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +24 -9
- package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +61 -10
- package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +3 -3
- package/dist/lib/resources/aws/analytics/clickhouseConstants.js +3 -3
- package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +7 -1
- package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +1 -1
- package/dist/lib/resources/aws/analytics/clickhouseUserData.js +53 -3
- 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/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 +102 -6
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +173 -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 +160 -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 +1 -1
- 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 -4
- package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +17 -13
- package/dist/lib/resources/aws/networking/dnsRecord/dnsRecordBase.js +7 -5
- package/dist/lib/resources/aws/networking/domainCertificate.d.ts +2 -2
- package/dist/lib/resources/aws/networking/domainCertificate.js +6 -4
- package/dist/lib/resources/aws/networking/hostedZone.js +6 -5
- 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 +4 -1
- package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +21 -3
- 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/cdkContext.d.ts +2 -0
- package/dist/lib/utils/cdkContext.js +4 -2
- package/dist/lib/utils/connections.js +6 -0
- package/dist/lib/utils/connector.d.ts +12 -0
- package/dist/lib/utils/costAllocationTags.d.ts +9 -0
- package/dist/lib/utils/costAllocationTags.js +11 -1
- 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 +1 -0
- package/dist/lib/utils/index.js +1 -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/package.json +22 -19
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cluster as CdkCluster, type FargateService, type Ec2Service } from "aws-cdk-lib/aws-ecs";
|
|
1
|
+
import { Cluster as CdkCluster, type FargateService, type Ec2Service, type TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Connections, type IConnectable } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import type { StackBuilder } from "../base/awsStack.js";
|
|
@@ -56,6 +56,7 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
56
56
|
private certificate?;
|
|
57
57
|
private asgState;
|
|
58
58
|
private services;
|
|
59
|
+
private scheduledTaskDefinitions;
|
|
59
60
|
private scope;
|
|
60
61
|
private props;
|
|
61
62
|
private outputName;
|
|
@@ -72,8 +73,33 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
72
73
|
getService(name: string): FargateService | Ec2Service | undefined;
|
|
73
74
|
/** Get all services in this cluster. */
|
|
74
75
|
getServices(): Map<string, FargateService | Ec2Service>;
|
|
76
|
+
/**
|
|
77
|
+
* Get the task definition for a service or a registered scheduled task.
|
|
78
|
+
* Used by Schedule / Subscription targets to construct the EcsTask adapter
|
|
79
|
+
* for EventBridge invocation. Returns undefined for unknown names — callers
|
|
80
|
+
* MUST validate before passing to `resolveTarget(...)`.
|
|
81
|
+
*/
|
|
82
|
+
getTaskDefinition(serviceName: string): TaskDefinition | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Register a `Ec2TaskDefinition` under a synthetic name so it can be
|
|
85
|
+
* resolved by `app.addSchedule(...)` against the existing
|
|
86
|
+
* `EcsScheduleTarget` shape (AC34). Throws if `name` collides with any
|
|
87
|
+
* steady-state service name OR a previously-registered scheduled task.
|
|
88
|
+
*/
|
|
89
|
+
registerScheduledTaskDefinition(name: string, taskDefinition: TaskDefinition): void;
|
|
75
90
|
/** Get the ECS cluster construct. */
|
|
76
91
|
getCluster(): CdkCluster;
|
|
92
|
+
/**
|
|
93
|
+
* Get the EC2 instance role for the cluster's ASG (D10). Returns undefined
|
|
94
|
+
* for Fargate-only clusters. Pulled from the shared ASG capacity state.
|
|
95
|
+
*/
|
|
96
|
+
getInstanceRole(): import("aws-cdk-lib/aws-iam").IRole | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Get the underlying ASG's `autoScalingGroupName` token (string-only,
|
|
99
|
+
* not the ASG construct itself — D10 forbids ASG-typed accessors).
|
|
100
|
+
* Used by alarm helpers that need CloudWatch dimension keys.
|
|
101
|
+
*/
|
|
102
|
+
getAutoScalingGroupName(): string | undefined;
|
|
77
103
|
/** Get the ALB URL (http:// or https://). */
|
|
78
104
|
getUrl(): string | undefined;
|
|
79
105
|
/**
|
|
@@ -69,8 +69,8 @@ export default class EcsCluster extends Construct {
|
|
|
69
69
|
autoScalingGroup: undefined,
|
|
70
70
|
asgSecurityGroup: undefined
|
|
71
71
|
};
|
|
72
|
-
// Per-service tracking
|
|
73
72
|
services = new Map();
|
|
73
|
+
scheduledTaskDefinitions = new Map();
|
|
74
74
|
// Configuration
|
|
75
75
|
scope;
|
|
76
76
|
props;
|
|
@@ -147,10 +147,50 @@ export default class EcsCluster extends Construct {
|
|
|
147
147
|
}
|
|
148
148
|
return result;
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the task definition for a service or a registered scheduled task.
|
|
152
|
+
* Used by Schedule / Subscription targets to construct the EcsTask adapter
|
|
153
|
+
* for EventBridge invocation. Returns undefined for unknown names — callers
|
|
154
|
+
* MUST validate before passing to `resolveTarget(...)`.
|
|
155
|
+
*/
|
|
156
|
+
getTaskDefinition(serviceName) {
|
|
157
|
+
return (this.services.get(serviceName)?.taskDefinition ??
|
|
158
|
+
this.scheduledTaskDefinitions.get(serviceName));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Register a `Ec2TaskDefinition` under a synthetic name so it can be
|
|
162
|
+
* resolved by `app.addSchedule(...)` against the existing
|
|
163
|
+
* `EcsScheduleTarget` shape (AC34). Throws if `name` collides with any
|
|
164
|
+
* steady-state service name OR a previously-registered scheduled task.
|
|
165
|
+
*/
|
|
166
|
+
registerScheduledTaskDefinition(name, taskDefinition) {
|
|
167
|
+
if (this.services.has(name)) {
|
|
168
|
+
throw new Error(`Scheduled task name '${name}' collides with a steady-state service in cluster '${this.props.clusterName}'.`);
|
|
169
|
+
}
|
|
170
|
+
if (this.scheduledTaskDefinitions.has(name)) {
|
|
171
|
+
throw new Error(`Scheduled task name '${name}' is already registered in cluster '${this.props.clusterName}'.`);
|
|
172
|
+
}
|
|
173
|
+
this.scheduledTaskDefinitions.set(name, taskDefinition);
|
|
174
|
+
}
|
|
150
175
|
/** Get the ECS cluster construct. */
|
|
151
176
|
getCluster() {
|
|
152
177
|
return this.cluster;
|
|
153
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the EC2 instance role for the cluster's ASG (D10). Returns undefined
|
|
181
|
+
* for Fargate-only clusters. Pulled from the shared ASG capacity state.
|
|
182
|
+
*/
|
|
183
|
+
getInstanceRole() {
|
|
184
|
+
return this.asgState.autoScalingGroup?.role;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get the underlying ASG's `autoScalingGroupName` token (string-only,
|
|
188
|
+
* not the ASG construct itself — D10 forbids ASG-typed accessors).
|
|
189
|
+
* Used by alarm helpers that need CloudWatch dimension keys.
|
|
190
|
+
*/
|
|
191
|
+
getAutoScalingGroupName() {
|
|
192
|
+
return this.asgState.autoScalingGroup?.autoScalingGroupName;
|
|
193
|
+
}
|
|
154
194
|
/** Get the ALB URL (http:// or https://). */
|
|
155
195
|
getUrl() {
|
|
156
196
|
if (!this.loadBalancer)
|
|
@@ -199,7 +239,7 @@ export default class EcsCluster extends Construct {
|
|
|
199
239
|
);
|
|
200
240
|
}
|
|
201
241
|
catch (error) {
|
|
202
|
-
throw new Error(`Failed to process connections for ECS service '${serviceName}': ${error instanceof Error ? error.message : String(error)}
|
|
242
|
+
throw new Error(`Failed to process connections for ECS service '${serviceName}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
203
243
|
}
|
|
204
244
|
}
|
|
205
245
|
// Per-service alarm wiring (shared topic on cluster, thresholds per service)
|
|
@@ -3,6 +3,15 @@ export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
|
|
|
3
3
|
export declare const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
4
4
|
export declare const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = true;
|
|
5
5
|
export declare const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
6
|
+
export declare const DEFAULT_FARGATE_CPU = 256;
|
|
7
|
+
export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
8
|
+
export declare const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
9
|
+
export declare const DEFAULT_ECS_FALLBACK_IMAGE = "amazon/amazon-ecs-sample";
|
|
10
|
+
export declare const DEFAULT_CUSTOM_RESOURCE_TIMEOUT_SECONDS = 300;
|
|
11
|
+
export declare const DEFAULT_HEALTH_CHECK_GRACE_SECONDS = 120;
|
|
12
|
+
export declare const DEFAULT_MIN_HEALTHY_PERCENT = 100;
|
|
13
|
+
export declare const DEFAULT_MAX_HEALTHY_PERCENT = 200;
|
|
14
|
+
export declare const DEFAULT_DESIRED_COUNT = 2;
|
|
6
15
|
/**
|
|
7
16
|
* Instance type prefixes that use ARM64 architecture (Graviton processors).
|
|
8
17
|
* All other prefixes are assumed to be x86-64 (STANDARD).
|
|
@@ -5,6 +5,22 @@ export const DEFAULT_WARM_POOL_MIN_SIZE = 1;
|
|
|
5
5
|
export const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = true;
|
|
6
6
|
// 14 days balances cost against retaining enough history for post-mortem debugging
|
|
7
7
|
export const DEFAULT_LOG_RETENTION_DAYS = 14;
|
|
8
|
+
// Smallest valid (cpu, memory) pair on the Fargate matrix — must move together.
|
|
9
|
+
export const DEFAULT_FARGATE_CPU = 256;
|
|
10
|
+
export const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
11
|
+
export const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
12
|
+
// AWS sample image used when no ECR repository is provided. Consumed by both
|
|
13
|
+
// the resources/ image resolver and the patterns/ defaults block — keep them
|
|
14
|
+
// in lockstep via this single export.
|
|
15
|
+
export const DEFAULT_ECS_FALLBACK_IMAGE = "amazon/amazon-ecs-sample";
|
|
16
|
+
// 5 minutes matches the Lambda service default; CDK uses it for sync providers.
|
|
17
|
+
export const DEFAULT_CUSTOM_RESOURCE_TIMEOUT_SECONDS = 300;
|
|
18
|
+
// 100/200 must move together — the rolling-deploy window relies on the gap.
|
|
19
|
+
export const DEFAULT_HEALTH_CHECK_GRACE_SECONDS = 120;
|
|
20
|
+
export const DEFAULT_MIN_HEALTHY_PERCENT = 100;
|
|
21
|
+
export const DEFAULT_MAX_HEALTHY_PERCENT = 200;
|
|
22
|
+
// Read at two sites (createService initial count + addServiceScaling derive-base) that must agree.
|
|
23
|
+
export const DEFAULT_DESIRED_COUNT = 2;
|
|
8
24
|
/**
|
|
9
25
|
* Instance type prefixes that use ARM64 architecture (Graviton processors).
|
|
10
26
|
* All other prefixes are assumed to be x86-64 (STANDARD).
|
|
@@ -1,35 +1,47 @@
|
|
|
1
|
+
import { CfnParameter, Stack } from "aws-cdk-lib";
|
|
1
2
|
import { ContainerImage } from "aws-cdk-lib/aws-ecs";
|
|
2
3
|
import { Repository } from "aws-cdk-lib/aws-ecr";
|
|
4
|
+
import { DEFAULT_ECS_FALLBACK_IMAGE } from "./ecsConstants.js";
|
|
5
|
+
import { toPascalCase } from "../../../utils/capitaliseString.js";
|
|
6
|
+
function buildImageTagDescription(serviceName) {
|
|
7
|
+
return `Image tag for ECS service ${serviceName}. Set by fjall deploy to the content-hash tag.`;
|
|
8
|
+
}
|
|
9
|
+
function getOrCreateImageTagParameter(ctx, serviceName) {
|
|
10
|
+
const stack = Stack.of(ctx.scope);
|
|
11
|
+
const paramLogicalId = `${toPascalCase(serviceName)}ImageTag`;
|
|
12
|
+
const description = buildImageTagDescription(serviceName);
|
|
13
|
+
const existing = stack.node.tryFindChild(paramLogicalId);
|
|
14
|
+
if (existing instanceof CfnParameter) {
|
|
15
|
+
if (existing.description !== description) {
|
|
16
|
+
throw new Error(`CfnParameter logical-ID collision: "${paramLogicalId}" is already registered for a different service. ` +
|
|
17
|
+
`Distinct service names cannot share a PascalCase identifier — rename one of the conflicting services.`);
|
|
18
|
+
}
|
|
19
|
+
return existing;
|
|
20
|
+
}
|
|
21
|
+
return new CfnParameter(stack, paramLogicalId, {
|
|
22
|
+
type: "String",
|
|
23
|
+
default: "latest",
|
|
24
|
+
description
|
|
25
|
+
});
|
|
26
|
+
}
|
|
3
27
|
export function getContainerImage(ctx, serviceName, containerConfig, serviceProps) {
|
|
4
28
|
const imageSource = containerConfig.image || serviceProps.image || ctx.props.ecrRepository;
|
|
5
29
|
if (!imageSource) {
|
|
6
|
-
return ContainerImage.fromRegistry(
|
|
30
|
+
return ContainerImage.fromRegistry(DEFAULT_ECS_FALLBACK_IMAGE);
|
|
7
31
|
}
|
|
8
|
-
// Build image tag with optional dockerTarget suffix
|
|
9
|
-
// Format: <service>-[<target>-]<version>
|
|
10
|
-
// imageVersion comes from CDK context (git SHA) to ensure CloudFormation
|
|
11
|
-
// detects template changes when new code is deployed. Falls back to 'latest'
|
|
12
|
-
// for apps without Dockerfiles (welcome image) or local dev.
|
|
13
|
-
const rawVersion = ctx.scope.node.tryGetContext("imageVersion");
|
|
14
|
-
const imageVersion = (typeof rawVersion === "string" ? rawVersion : undefined) || "latest";
|
|
15
|
-
const targetSuffix = serviceProps.dockerTarget
|
|
16
|
-
? `-${serviceProps.dockerTarget.toLowerCase()}`
|
|
17
|
-
: "";
|
|
18
|
-
const imageTag = `${serviceName.toLowerCase()}${targetSuffix}-${imageVersion}`;
|
|
19
32
|
if (typeof imageSource === "string") {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const isFullRegistryUrl = /^(docker\.io|registry\.hub\.docker\.com|ghcr\.io)\//i.test(imageSource) || // Docker Hub / GHCR URLs
|
|
24
|
-
imageSource.startsWith("public.ecr.aws/") || // Public ECR: public.ecr.aws/fjall/welcome
|
|
25
|
-
imageSource.includes(".dkr.ecr."); // Private ECR full URL: 123456789012.dkr.ecr.us-east-2.amazonaws.com/repo:tag
|
|
33
|
+
const isFullRegistryUrl = /^(docker\.io|registry\.hub\.docker\.com|ghcr\.io)\//i.test(imageSource) ||
|
|
34
|
+
imageSource.startsWith("public.ecr.aws/") ||
|
|
35
|
+
imageSource.includes(".dkr.ecr.");
|
|
26
36
|
if (isFullRegistryUrl) {
|
|
27
37
|
return ContainerImage.fromRegistry(imageSource);
|
|
28
38
|
}
|
|
29
|
-
|
|
39
|
+
const param = getOrCreateImageTagParameter(ctx, serviceName);
|
|
40
|
+
return ContainerImage.fromEcrRepository(Repository.fromRepositoryName(ctx.scope, `${serviceName}${containerConfig.name}EcrRepo`, imageSource), param.valueAsString);
|
|
30
41
|
}
|
|
31
42
|
if (imageSource instanceof Repository) {
|
|
32
|
-
|
|
43
|
+
const param = getOrCreateImageTagParameter(ctx, serviceName);
|
|
44
|
+
return ContainerImage.fromEcrRepository(imageSource, param.valueAsString);
|
|
33
45
|
}
|
|
34
46
|
return imageSource;
|
|
35
47
|
}
|