@fjall/components-infrastructure 0.79.3 → 0.80.4

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.
Files changed (94) hide show
  1. package/dist/lib/app.d.ts +39 -2
  2. package/dist/lib/app.js +85 -4
  3. package/dist/lib/aspects/resourceInventory.d.ts +41 -0
  4. package/dist/lib/aspects/resourceInventory.js +56 -0
  5. package/dist/lib/config/audit.d.ts +18 -0
  6. package/dist/lib/config/audit.js +22 -0
  7. package/dist/lib/config/aws/accountId.js +3 -3
  8. package/dist/lib/config/aws/ecrDefaultImage.js +7 -7
  9. package/dist/lib/config/aws/identityCenter.js +7 -8
  10. package/dist/lib/config/aws/identityCenterGroupMembership.js +6 -6
  11. package/dist/lib/config/aws/ipam.js +3 -3
  12. package/dist/lib/config/aws/ipamPoolId.js +4 -4
  13. package/dist/lib/config/aws/organisation.js +2 -3
  14. package/dist/lib/config/aws/organisationId.js +4 -4
  15. package/dist/lib/patterns/aws/auditRole.d.ts +44 -0
  16. package/dist/lib/patterns/aws/auditRole.js +58 -0
  17. package/dist/lib/patterns/aws/buildkite.js +12 -12
  18. package/dist/lib/patterns/aws/cdn.d.ts +133 -0
  19. package/dist/lib/patterns/aws/cdn.js +216 -0
  20. package/dist/lib/patterns/aws/compute.d.ts +32 -13
  21. package/dist/lib/patterns/aws/compute.js +24 -14
  22. package/dist/lib/patterns/aws/database.d.ts +2 -1
  23. package/dist/lib/patterns/aws/database.js +7 -2
  24. package/dist/lib/patterns/aws/dynamodb.d.ts +66 -0
  25. package/dist/lib/patterns/aws/dynamodb.js +106 -0
  26. package/dist/lib/patterns/aws/hostedZone.js +1 -1
  27. package/dist/lib/patterns/aws/index.d.ts +1 -0
  28. package/dist/lib/patterns/aws/index.js +2 -1
  29. package/dist/lib/patterns/aws/loadBalancer.d.ts +163 -0
  30. package/dist/lib/patterns/aws/loadBalancer.js +278 -0
  31. package/dist/lib/patterns/aws/managedAccount.js +3 -4
  32. package/dist/lib/patterns/aws/queue.d.ts +61 -0
  33. package/dist/lib/patterns/aws/queue.js +103 -0
  34. package/dist/lib/patterns/aws/storage.js +3 -2
  35. package/dist/lib/patterns/aws/subdomainHostedZone.js +2 -2
  36. package/dist/lib/resources/aws/audit/auditRole.d.ts +32 -0
  37. package/dist/lib/resources/aws/audit/auditRole.js +44 -0
  38. package/dist/lib/resources/aws/backup/backupPlan.js +3 -4
  39. package/dist/lib/resources/aws/backup/backupVault.js +3 -4
  40. package/dist/lib/resources/aws/base/awsStack.js +1 -2
  41. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +65 -0
  42. package/dist/lib/resources/aws/cdn/cloudFront.js +135 -0
  43. package/dist/lib/resources/aws/cdn/index.d.ts +1 -0
  44. package/dist/lib/resources/aws/cdn/index.js +18 -0
  45. package/dist/lib/resources/aws/compute/capacityProviderDrainWaiter.d.ts +20 -0
  46. package/dist/lib/resources/aws/compute/capacityProviderDrainWaiter.js +180 -0
  47. package/dist/lib/resources/aws/compute/ecs.d.ts +65 -11
  48. package/dist/lib/resources/aws/compute/ecs.js +293 -124
  49. package/dist/lib/resources/aws/compute/ecsFreeTier.d.ts +1 -2
  50. package/dist/lib/resources/aws/compute/ecsFreeTier.js +1 -4
  51. package/dist/lib/resources/aws/compute/ecsSpot.d.ts +1 -2
  52. package/dist/lib/resources/aws/compute/ecsSpot.js +1 -4
  53. package/dist/lib/resources/aws/compute/lambda.d.ts +10 -1
  54. package/dist/lib/resources/aws/compute/lambda.js +71 -4
  55. package/dist/lib/resources/aws/database/database.js +1 -1
  56. package/dist/lib/resources/aws/database/dynamodb.d.ts +70 -0
  57. package/dist/lib/resources/aws/database/dynamodb.js +170 -0
  58. package/dist/lib/resources/aws/database/rdsAurora.js +5 -5
  59. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +7 -7
  60. package/dist/lib/resources/aws/database/rdsDeletionWaiter.d.ts +33 -0
  61. package/dist/lib/resources/aws/database/rdsDeletionWaiter.js +74 -0
  62. package/dist/lib/resources/aws/database/rdsInstance.js +3 -3
  63. package/dist/lib/resources/aws/iam/identityCenter/assignment.js +2 -2
  64. package/dist/lib/resources/aws/iam/identityCenter/attachManagedPolicy.js +1 -1
  65. package/dist/lib/resources/aws/iam/identityCenter/group.js +1 -1
  66. package/dist/lib/resources/aws/iam/identityCenter/permissionSet.js +1 -1
  67. package/dist/lib/resources/aws/logging/cloudTrail.js +1 -1
  68. package/dist/lib/resources/aws/messaging/index.d.ts +1 -0
  69. package/dist/lib/resources/aws/messaging/index.js +18 -0
  70. package/dist/lib/resources/aws/messaging/sqs.d.ts +65 -0
  71. package/dist/lib/resources/aws/messaging/sqs.js +195 -0
  72. package/dist/lib/resources/aws/monitoring/monitoringRole.js +2 -3
  73. package/dist/lib/resources/aws/networking/ipamPool.js +1 -1
  74. package/dist/lib/resources/aws/networking/vpc.d.ts +1 -0
  75. package/dist/lib/resources/aws/networking/vpc.js +7 -3
  76. package/dist/lib/resources/aws/networking/vpcEndpoint.d.ts +20 -0
  77. package/dist/lib/resources/aws/networking/vpcEndpoint.js +59 -0
  78. package/dist/lib/resources/aws/networking/vpcEndpoints.d.ts +71 -0
  79. package/dist/lib/resources/aws/networking/vpcEndpoints.js +125 -0
  80. package/dist/lib/resources/aws/secrets/secret.js +1 -1
  81. package/dist/lib/resources/aws/storage/ecr.d.ts +1 -2
  82. package/dist/lib/resources/aws/storage/ecr.js +2 -2
  83. package/dist/lib/resources/aws/utilities/cfnOutput.test.d.ts +1 -0
  84. package/dist/lib/resources/aws/utilities/cfnOutput.test.js +102 -0
  85. package/dist/lib/resources/aws/utilities/codeBuild.d.ts +1 -2
  86. package/dist/lib/resources/aws/utilities/codeBuild.js +1 -1
  87. package/dist/lib/resources/aws/utilities/customResource.d.ts +2 -1
  88. package/dist/lib/resources/aws/utilities/customResource.js +1 -1
  89. package/dist/lib/utils/getStackOutput.js +2 -2
  90. package/dist/lib/utils/sanitizeCfnKey.d.ts +5 -0
  91. package/dist/lib/utils/sanitizeCfnKey.js +11 -0
  92. package/dist/lib/utils/tagResource.d.ts +24 -0
  93. package/dist/lib/utils/tagResource.js +30 -0
  94. package/package.json +6 -5
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CapacityProviderDrainWaiter = void 0;
4
+ const aws_cdk_lib_1 = require("aws-cdk-lib");
5
+ const aws_iam_1 = require("aws-cdk-lib/aws-iam");
6
+ const aws_lambda_1 = require("aws-cdk-lib/aws-lambda");
7
+ const constructs_1 = require("constructs");
8
+ const customResource_1 = require("../utilities/customResource");
9
+ /**
10
+ * Custom resource that waits for ECS services to drain before allowing
11
+ * capacity provider disassociation.
12
+ *
13
+ * @see https://github.com/aws/aws-cdk/issues/14732
14
+ */
15
+ class CapacityProviderDrainWaiter extends constructs_1.Construct {
16
+ constructor(scope, id, props) {
17
+ super(scope, id);
18
+ const { clusterName, capacityProviderName } = props;
19
+ // Lambda inline code for the custom resource handler
20
+ const handlerCode = `
21
+ const { ECSClient, ListServicesCommand, DescribeServicesCommand, PutClusterCapacityProvidersCommand, DescribeClustersCommand } = require("@aws-sdk/client-ecs");
22
+
23
+ const ecsClient = new ECSClient({});
24
+ const POLL_INTERVAL_MS = 5000;
25
+ const MAX_WAIT_MS = 300000; // 5 minutes
26
+
27
+ async function sleep(ms) {
28
+ return new Promise(resolve => setTimeout(resolve, ms));
29
+ }
30
+
31
+ async function getClusterCapacityProviders(clusterName) {
32
+ const response = await ecsClient.send(new DescribeClustersCommand({
33
+ clusters: [clusterName]
34
+ }));
35
+ return response.clusters?.[0]?.capacityProviders || [];
36
+ }
37
+
38
+ async function waitForServicesDrained(clusterName, capacityProviderName) {
39
+ const startTime = Date.now();
40
+
41
+ while (Date.now() - startTime < MAX_WAIT_MS) {
42
+ // List all services in the cluster
43
+ const listResponse = await ecsClient.send(new ListServicesCommand({
44
+ cluster: clusterName,
45
+ maxResults: 100
46
+ }));
47
+
48
+ const serviceArns = listResponse.serviceArns || [];
49
+
50
+ if (serviceArns.length === 0) {
51
+ console.log("No services found in cluster - drain complete");
52
+ return true;
53
+ }
54
+
55
+ // Describe services to check running counts
56
+ const describeResponse = await ecsClient.send(new DescribeServicesCommand({
57
+ cluster: clusterName,
58
+ services: serviceArns
59
+ }));
60
+
61
+ const activeServices = (describeResponse.services || []).filter(svc => {
62
+ // Check if service uses this capacity provider
63
+ const usesCapacityProvider = (svc.capacityProviderStrategy || [])
64
+ .some(strategy => strategy.capacityProvider === capacityProviderName);
65
+
66
+ // Only care about services using this capacity provider that still have running tasks
67
+ return usesCapacityProvider && svc.runningCount > 0;
68
+ });
69
+
70
+ if (activeServices.length === 0) {
71
+ console.log("All services using capacity provider have drained");
72
+ return true;
73
+ }
74
+
75
+ console.log(\`Waiting for \${activeServices.length} services to drain: \${activeServices.map(s => s.serviceName).join(", ")}\`);
76
+ await sleep(POLL_INTERVAL_MS);
77
+ }
78
+
79
+ console.log("Timeout waiting for services to drain, proceeding anyway");
80
+ return false;
81
+ }
82
+
83
+ async function disassociateCapacityProvider(clusterName, capacityProviderName) {
84
+ try {
85
+ // Get current capacity providers
86
+ const currentProviders = await getClusterCapacityProviders(clusterName);
87
+
88
+ // Filter out the one we want to remove
89
+ const remainingProviders = currentProviders.filter(cp => cp !== capacityProviderName);
90
+
91
+ if (remainingProviders.length === currentProviders.length) {
92
+ console.log(\`Capacity provider \${capacityProviderName} not associated with cluster\`);
93
+ return true;
94
+ }
95
+
96
+ // Ensure we keep at least FARGATE as a provider (required by ECS)
97
+ if (remainingProviders.length === 0) {
98
+ remainingProviders.push("FARGATE");
99
+ }
100
+
101
+ console.log(\`Disassociating \${capacityProviderName} from cluster. Remaining: \${remainingProviders.join(", ")}\`);
102
+
103
+ await ecsClient.send(new PutClusterCapacityProvidersCommand({
104
+ cluster: clusterName,
105
+ capacityProviders: remainingProviders,
106
+ defaultCapacityProviderStrategy: []
107
+ }));
108
+
109
+ console.log("Successfully disassociated capacity provider");
110
+ return true;
111
+ } catch (error) {
112
+ // If cluster or capacity provider doesn't exist, that's fine (already deleted)
113
+ if (error.name === "ClusterNotFoundException" ||
114
+ error.name === "ResourceNotFoundException" ||
115
+ error.message?.includes("does not exist")) {
116
+ console.log("Cluster or capacity provider not found - already cleaned up");
117
+ return true;
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ exports.handler = async (event) => {
124
+ console.log("Event:", JSON.stringify(event, null, 2));
125
+
126
+ const clusterName = event.ResourceProperties.ClusterName;
127
+ const capacityProviderName = event.ResourceProperties.CapacityProviderName;
128
+ const requestType = event.RequestType;
129
+
130
+ try {
131
+ if (requestType === "Delete") {
132
+ console.log(\`Handling DELETE for capacity provider \${capacityProviderName} in cluster \${clusterName}\`);
133
+
134
+ // Wait for services to drain
135
+ await waitForServicesDrained(clusterName, capacityProviderName);
136
+
137
+ // Disassociate capacity provider from cluster
138
+ await disassociateCapacityProvider(clusterName, capacityProviderName);
139
+ }
140
+
141
+ // Return success for all request types
142
+ return {
143
+ PhysicalResourceId: \`\${clusterName}-\${capacityProviderName}-drain-waiter\`,
144
+ Data: {
145
+ Message: \`\${requestType} completed successfully\`
146
+ }
147
+ };
148
+ } catch (error) {
149
+ console.error("Error:", error);
150
+ throw error;
151
+ }
152
+ };
153
+ `;
154
+ new customResource_1.CustomResource(this, "DrainWaiter", {
155
+ runtime: aws_lambda_1.Runtime.NODEJS_22_X,
156
+ inlineCode: handlerCode,
157
+ timeout: aws_cdk_lib_1.Duration.minutes(6),
158
+ lambdaDescription: `Waits for ECS services to drain before capacity provider disassociation`,
159
+ roleDescription: `Role for ${clusterName} capacity provider drain waiter`,
160
+ inlinePolicy: [
161
+ new aws_iam_1.PolicyStatement({
162
+ effect: aws_iam_1.Effect.ALLOW,
163
+ actions: [
164
+ "ecs:ListServices",
165
+ "ecs:DescribeServices",
166
+ "ecs:DescribeClusters",
167
+ "ecs:PutClusterCapacityProviders"
168
+ ],
169
+ resources: ["*"]
170
+ })
171
+ ],
172
+ properties: {
173
+ ClusterName: clusterName,
174
+ CapacityProviderName: capacityProviderName
175
+ }
176
+ });
177
+ }
178
+ }
179
+ exports.CapacityProviderDrainWaiter = CapacityProviderDrainWaiter;
180
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FwYWNpdHlQcm92aWRlckRyYWluV2FpdGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vbGliL3Jlc291cmNlcy9hd3MvY29tcHV0ZS9jYXBhY2l0eVByb3ZpZGVyRHJhaW5XYWl0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsNkNBQXVDO0FBQ3ZDLGlEQUE4RDtBQUM5RCx1REFBaUQ7QUFDakQsMkNBQXVDO0FBQ3ZDLGdFQUE2RDtBQVk3RDs7Ozs7R0FLRztBQUNILE1BQWEsMkJBQTRCLFNBQVEsc0JBQVM7SUFDeEQsWUFDRSxLQUFnQixFQUNoQixFQUFVLEVBQ1YsS0FBdUM7UUFFdkMsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVqQixNQUFNLEVBQUUsV0FBVyxFQUFFLG9CQUFvQixFQUFFLEdBQUcsS0FBSyxDQUFDO1FBRXBELHFEQUFxRDtRQUNyRCxNQUFNLFdBQVcsR0FBRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztDQXFJdkIsQ0FBQztRQUVFLElBQUksK0JBQWMsQ0FBQyxJQUFJLEVBQUUsYUFBYSxFQUFFO1lBQ3RDLE9BQU8sRUFBRSxvQkFBTyxDQUFDLFdBQVc7WUFDNUIsVUFBVSxFQUFFLFdBQVc7WUFDdkIsT0FBTyxFQUFFLHNCQUFRLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztZQUM1QixpQkFBaUIsRUFBRSx5RUFBeUU7WUFDNUYsZUFBZSxFQUFFLFlBQVksV0FBVyxpQ0FBaUM7WUFDekUsWUFBWSxFQUFFO2dCQUNaLElBQUkseUJBQWUsQ0FBQztvQkFDbEIsTUFBTSxFQUFFLGdCQUFNLENBQUMsS0FBSztvQkFDcEIsT0FBTyxFQUFFO3dCQUNQLGtCQUFrQjt3QkFDbEIsc0JBQXNCO3dCQUN0QixzQkFBc0I7d0JBQ3RCLGlDQUFpQztxQkFDbEM7b0JBQ0QsU0FBUyxFQUFFLENBQUMsR0FBRyxDQUFDO2lCQUNqQixDQUFDO2FBQ0g7WUFDRCxVQUFVLEVBQUU7Z0JBQ1YsV0FBVyxFQUFFLFdBQVc7Z0JBQ3hCLG9CQUFvQixFQUFFLG9CQUFvQjthQUMzQztTQUNGLENBQUMsQ0FBQztJQUNMLENBQUM7Q0FDRjtBQTFLRCxrRUEwS0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBEdXJhdGlvbiB9IGZyb20gXCJhd3MtY2RrLWxpYlwiO1xuaW1wb3J0IHsgUG9saWN5U3RhdGVtZW50LCBFZmZlY3QgfSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWlhbVwiO1xuaW1wb3J0IHsgUnVudGltZSB9IGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtbGFtYmRhXCI7XG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tIFwiY29uc3RydWN0c1wiO1xuaW1wb3J0IHsgQ3VzdG9tUmVzb3VyY2UgfSBmcm9tIFwiLi4vdXRpbGl0aWVzL2N1c3RvbVJlc291cmNlXCI7XG5cbi8qKlxuICogUHJvcHMgZm9yIENhcGFjaXR5UHJvdmlkZXJEcmFpbldhaXRlclxuICovXG5pbnRlcmZhY2UgQ2FwYWNpdHlQcm92aWRlckRyYWluV2FpdGVyUHJvcHMge1xuICAvKiogVGhlIEVDUyBjbHVzdGVyIG5hbWUgKi9cbiAgY2x1c3Rlck5hbWU6IHN0cmluZztcbiAgLyoqIFRoZSBjYXBhY2l0eSBwcm92aWRlciBuYW1lIHRvIHdhaXQgZm9yICovXG4gIGNhcGFjaXR5UHJvdmlkZXJOYW1lOiBzdHJpbmc7XG59XG5cbi8qKlxuICogQ3VzdG9tIHJlc291cmNlIHRoYXQgd2FpdHMgZm9yIEVDUyBzZXJ2aWNlcyB0byBkcmFpbiBiZWZvcmUgYWxsb3dpbmdcbiAqIGNhcGFjaXR5IHByb3ZpZGVyIGRpc2Fzc29jaWF0aW9uLlxuICpcbiAqIEBzZWUgaHR0cHM6Ly9naXRodWIuY29tL2F3cy9hd3MtY2RrL2lzc3Vlcy8xNDczMlxuICovXG5leHBvcnQgY2xhc3MgQ2FwYWNpdHlQcm92aWRlckRyYWluV2FpdGVyIGV4dGVuZHMgQ29uc3RydWN0IHtcbiAgY29uc3RydWN0b3IoXG4gICAgc2NvcGU6IENvbnN0cnVjdCxcbiAgICBpZDogc3RyaW5nLFxuICAgIHByb3BzOiBDYXBhY2l0eVByb3ZpZGVyRHJhaW5XYWl0ZXJQcm9wc1xuICApIHtcbiAgICBzdXBlcihzY29wZSwgaWQpO1xuXG4gICAgY29uc3QgeyBjbHVzdGVyTmFtZSwgY2FwYWNpdHlQcm92aWRlck5hbWUgfSA9IHByb3BzO1xuXG4gICAgLy8gTGFtYmRhIGlubGluZSBjb2RlIGZvciB0aGUgY3VzdG9tIHJlc291cmNlIGhhbmRsZXJcbiAgICBjb25zdCBoYW5kbGVyQ29kZSA9IGBcbmNvbnN0IHsgRUNTQ2xpZW50LCBMaXN0U2VydmljZXNDb21tYW5kLCBEZXNjcmliZVNlcnZpY2VzQ29tbWFuZCwgUHV0Q2x1c3RlckNhcGFjaXR5UHJvdmlkZXJzQ29tbWFuZCwgRGVzY3JpYmVDbHVzdGVyc0NvbW1hbmQgfSA9IHJlcXVpcmUoXCJAYXdzLXNkay9jbGllbnQtZWNzXCIpO1xuXG5jb25zdCBlY3NDbGllbnQgPSBuZXcgRUNTQ2xpZW50KHt9KTtcbmNvbnN0IFBPTExfSU5URVJWQUxfTVMgPSA1MDAwO1xuY29uc3QgTUFYX1dBSVRfTVMgPSAzMDAwMDA7IC8vIDUgbWludXRlc1xuXG5hc3luYyBmdW5jdGlvbiBzbGVlcChtcykge1xuICByZXR1cm4gbmV3IFByb21pc2UocmVzb2x2ZSA9PiBzZXRUaW1lb3V0KHJlc29sdmUsIG1zKSk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGdldENsdXN0ZXJDYXBhY2l0eVByb3ZpZGVycyhjbHVzdGVyTmFtZSkge1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGVjc0NsaWVudC5zZW5kKG5ldyBEZXNjcmliZUNsdXN0ZXJzQ29tbWFuZCh7XG4gICAgY2x1c3RlcnM6IFtjbHVzdGVyTmFtZV1cbiAgfSkpO1xuICByZXR1cm4gcmVzcG9uc2UuY2x1c3RlcnM/LlswXT8uY2FwYWNpdHlQcm92aWRlcnMgfHwgW107XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHdhaXRGb3JTZXJ2aWNlc0RyYWluZWQoY2x1c3Rlck5hbWUsIGNhcGFjaXR5UHJvdmlkZXJOYW1lKSB7XG4gIGNvbnN0IHN0YXJ0VGltZSA9IERhdGUubm93KCk7XG5cbiAgd2hpbGUgKERhdGUubm93KCkgLSBzdGFydFRpbWUgPCBNQVhfV0FJVF9NUykge1xuICAgIC8vIExpc3QgYWxsIHNlcnZpY2VzIGluIHRoZSBjbHVzdGVyXG4gICAgY29uc3QgbGlzdFJlc3BvbnNlID0gYXdhaXQgZWNzQ2xpZW50LnNlbmQobmV3IExpc3RTZXJ2aWNlc0NvbW1hbmQoe1xuICAgICAgY2x1c3RlcjogY2x1c3Rlck5hbWUsXG4gICAgICBtYXhSZXN1bHRzOiAxMDBcbiAgICB9KSk7XG5cbiAgICBjb25zdCBzZXJ2aWNlQXJucyA9IGxpc3RSZXNwb25zZS5zZXJ2aWNlQXJucyB8fCBbXTtcblxuICAgIGlmIChzZXJ2aWNlQXJucy5sZW5ndGggPT09IDApIHtcbiAgICAgIGNvbnNvbGUubG9nKFwiTm8gc2VydmljZXMgZm91bmQgaW4gY2x1c3RlciAtIGRyYWluIGNvbXBsZXRlXCIpO1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuXG4gICAgLy8gRGVzY3JpYmUgc2VydmljZXMgdG8gY2hlY2sgcnVubmluZyBjb3VudHNcbiAgICBjb25zdCBkZXNjcmliZVJlc3BvbnNlID0gYXdhaXQgZWNzQ2xpZW50LnNlbmQobmV3IERlc2NyaWJlU2VydmljZXNDb21tYW5kKHtcbiAgICAgIGNsdXN0ZXI6IGNsdXN0ZXJOYW1lLFxuICAgICAgc2VydmljZXM6IHNlcnZpY2VBcm5zXG4gICAgfSkpO1xuXG4gICAgY29uc3QgYWN0aXZlU2VydmljZXMgPSAoZGVzY3JpYmVSZXNwb25zZS5zZXJ2aWNlcyB8fCBbXSkuZmlsdGVyKHN2YyA9PiB7XG4gICAgICAvLyBDaGVjayBpZiBzZXJ2aWNlIHVzZXMgdGhpcyBjYXBhY2l0eSBwcm92aWRlclxuICAgICAgY29uc3QgdXNlc0NhcGFjaXR5UHJvdmlkZXIgPSAoc3ZjLmNhcGFjaXR5UHJvdmlkZXJTdHJhdGVneSB8fCBbXSlcbiAgICAgICAgLnNvbWUoc3RyYXRlZ3kgPT4gc3RyYXRlZ3kuY2FwYWNpdHlQcm92aWRlciA9PT0gY2FwYWNpdHlQcm92aWRlck5hbWUpO1xuXG4gICAgICAvLyBPbmx5IGNhcmUgYWJvdXQgc2VydmljZXMgdXNpbmcgdGhpcyBjYXBhY2l0eSBwcm92aWRlciB0aGF0IHN0aWxsIGhhdmUgcnVubmluZyB0YXNrc1xuICAgICAgcmV0dXJuIHVzZXNDYXBhY2l0eVByb3ZpZGVyICYmIHN2Yy5ydW5uaW5nQ291bnQgPiAwO1xuICAgIH0pO1xuXG4gICAgaWYgKGFjdGl2ZVNlcnZpY2VzLmxlbmd0aCA9PT0gMCkge1xuICAgICAgY29uc29sZS5sb2coXCJBbGwgc2VydmljZXMgdXNpbmcgY2FwYWNpdHkgcHJvdmlkZXIgaGF2ZSBkcmFpbmVkXCIpO1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuXG4gICAgY29uc29sZS5sb2coXFxgV2FpdGluZyBmb3IgXFwke2FjdGl2ZVNlcnZpY2VzLmxlbmd0aH0gc2VydmljZXMgdG8gZHJhaW46IFxcJHthY3RpdmVTZXJ2aWNlcy5tYXAocyA9PiBzLnNlcnZpY2VOYW1lKS5qb2luKFwiLCBcIil9XFxgKTtcbiAgICBhd2FpdCBzbGVlcChQT0xMX0lOVEVSVkFMX01TKTtcbiAgfVxuXG4gIGNvbnNvbGUubG9nKFwiVGltZW91dCB3YWl0aW5nIGZvciBzZXJ2aWNlcyB0byBkcmFpbiwgcHJvY2VlZGluZyBhbnl3YXlcIik7XG4gIHJldHVybiBmYWxzZTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gZGlzYXNzb2NpYXRlQ2FwYWNpdHlQcm92aWRlcihjbHVzdGVyTmFtZSwgY2FwYWNpdHlQcm92aWRlck5hbWUpIHtcbiAgdHJ5IHtcbiAgICAvLyBHZXQgY3VycmVudCBjYXBhY2l0eSBwcm92aWRlcnNcbiAgICBjb25zdCBjdXJyZW50UHJvdmlkZXJzID0gYXdhaXQgZ2V0Q2x1c3RlckNhcGFjaXR5UHJvdmlkZXJzKGNsdXN0ZXJOYW1lKTtcblxuICAgIC8vIEZpbHRlciBvdXQgdGhlIG9uZSB3ZSB3YW50IHRvIHJlbW92ZVxuICAgIGNvbnN0IHJlbWFpbmluZ1Byb3ZpZGVycyA9IGN1cnJlbnRQcm92aWRlcnMuZmlsdGVyKGNwID0+IGNwICE9PSBjYXBhY2l0eVByb3ZpZGVyTmFtZSk7XG5cbiAgICBpZiAocmVtYWluaW5nUHJvdmlkZXJzLmxlbmd0aCA9PT0gY3VycmVudFByb3ZpZGVycy5sZW5ndGgpIHtcbiAgICAgIGNvbnNvbGUubG9nKFxcYENhcGFjaXR5IHByb3ZpZGVyIFxcJHtjYXBhY2l0eVByb3ZpZGVyTmFtZX0gbm90IGFzc29jaWF0ZWQgd2l0aCBjbHVzdGVyXFxgKTtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cblxuICAgIC8vIEVuc3VyZSB3ZSBrZWVwIGF0IGxlYXN0IEZBUkdBVEUgYXMgYSBwcm92aWRlciAocmVxdWlyZWQgYnkgRUNTKVxuICAgIGlmIChyZW1haW5pbmdQcm92aWRlcnMubGVuZ3RoID09PSAwKSB7XG4gICAgICByZW1haW5pbmdQcm92aWRlcnMucHVzaChcIkZBUkdBVEVcIik7XG4gICAgfVxuXG4gICAgY29uc29sZS5sb2coXFxgRGlzYXNzb2NpYXRpbmcgXFwke2NhcGFjaXR5UHJvdmlkZXJOYW1lfSBmcm9tIGNsdXN0ZXIuIFJlbWFpbmluZzogXFwke3JlbWFpbmluZ1Byb3ZpZGVycy5qb2luKFwiLCBcIil9XFxgKTtcblxuICAgIGF3YWl0IGVjc0NsaWVudC5zZW5kKG5ldyBQdXRDbHVzdGVyQ2FwYWNpdHlQcm92aWRlcnNDb21tYW5kKHtcbiAgICAgIGNsdXN0ZXI6IGNsdXN0ZXJOYW1lLFxuICAgICAgY2FwYWNpdHlQcm92aWRlcnM6IHJlbWFpbmluZ1Byb3ZpZGVycyxcbiAgICAgIGRlZmF1bHRDYXBhY2l0eVByb3ZpZGVyU3RyYXRlZ3k6IFtdXG4gICAgfSkpO1xuXG4gICAgY29uc29sZS5sb2coXCJTdWNjZXNzZnVsbHkgZGlzYXNzb2NpYXRlZCBjYXBhY2l0eSBwcm92aWRlclwiKTtcbiAgICByZXR1cm4gdHJ1ZTtcbiAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICAvLyBJZiBjbHVzdGVyIG9yIGNhcGFjaXR5IHByb3ZpZGVyIGRvZXNuJ3QgZXhpc3QsIHRoYXQncyBmaW5lIChhbHJlYWR5IGRlbGV0ZWQpXG4gICAgaWYgKGVycm9yLm5hbWUgPT09IFwiQ2x1c3Rlck5vdEZvdW5kRXhjZXB0aW9uXCIgfHxcbiAgICAgICAgZXJyb3IubmFtZSA9PT0gXCJSZXNvdXJjZU5vdEZvdW5kRXhjZXB0aW9uXCIgfHxcbiAgICAgICAgZXJyb3IubWVzc2FnZT8uaW5jbHVkZXMoXCJkb2VzIG5vdCBleGlzdFwiKSkge1xuICAgICAgY29uc29sZS5sb2coXCJDbHVzdGVyIG9yIGNhcGFjaXR5IHByb3ZpZGVyIG5vdCBmb3VuZCAtIGFscmVhZHkgY2xlYW5lZCB1cFwiKTtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cbiAgICB0aHJvdyBlcnJvcjtcbiAgfVxufVxuXG5leHBvcnRzLmhhbmRsZXIgPSBhc3luYyAoZXZlbnQpID0+IHtcbiAgY29uc29sZS5sb2coXCJFdmVudDpcIiwgSlNPTi5zdHJpbmdpZnkoZXZlbnQsIG51bGwsIDIpKTtcblxuICBjb25zdCBjbHVzdGVyTmFtZSA9IGV2ZW50LlJlc291cmNlUHJvcGVydGllcy5DbHVzdGVyTmFtZTtcbiAgY29uc3QgY2FwYWNpdHlQcm92aWRlck5hbWUgPSBldmVudC5SZXNvdXJjZVByb3BlcnRpZXMuQ2FwYWNpdHlQcm92aWRlck5hbWU7XG4gIGNvbnN0IHJlcXVlc3RUeXBlID0gZXZlbnQuUmVxdWVzdFR5cGU7XG5cbiAgdHJ5IHtcbiAgICBpZiAocmVxdWVzdFR5cGUgPT09IFwiRGVsZXRlXCIpIHtcbiAgICAgIGNvbnNvbGUubG9nKFxcYEhhbmRsaW5nIERFTEVURSBmb3IgY2FwYWNpdHkgcHJvdmlkZXIgXFwke2NhcGFjaXR5UHJvdmlkZXJOYW1lfSBpbiBjbHVzdGVyIFxcJHtjbHVzdGVyTmFtZX1cXGApO1xuXG4gICAgICAvLyBXYWl0IGZvciBzZXJ2aWNlcyB0byBkcmFpblxuICAgICAgYXdhaXQgd2FpdEZvclNlcnZpY2VzRHJhaW5lZChjbHVzdGVyTmFtZSwgY2FwYWNpdHlQcm92aWRlck5hbWUpO1xuXG4gICAgICAvLyBEaXNhc3NvY2lhdGUgY2FwYWNpdHkgcHJvdmlkZXIgZnJvbSBjbHVzdGVyXG4gICAgICBhd2FpdCBkaXNhc3NvY2lhdGVDYXBhY2l0eVByb3ZpZGVyKGNsdXN0ZXJOYW1lLCBjYXBhY2l0eVByb3ZpZGVyTmFtZSk7XG4gICAgfVxuXG4gICAgLy8gUmV0dXJuIHN1Y2Nlc3MgZm9yIGFsbCByZXF1ZXN0IHR5cGVzXG4gICAgcmV0dXJuIHtcbiAgICAgIFBoeXNpY2FsUmVzb3VyY2VJZDogXFxgXFwke2NsdXN0ZXJOYW1lfS1cXCR7Y2FwYWNpdHlQcm92aWRlck5hbWV9LWRyYWluLXdhaXRlclxcYCxcbiAgICAgIERhdGE6IHtcbiAgICAgICAgTWVzc2FnZTogXFxgXFwke3JlcXVlc3RUeXBlfSBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5XFxgXG4gICAgICB9XG4gICAgfTtcbiAgfSBjYXRjaCAoZXJyb3IpIHtcbiAgICBjb25zb2xlLmVycm9yKFwiRXJyb3I6XCIsIGVycm9yKTtcbiAgICB0aHJvdyBlcnJvcjtcbiAgfVxufTtcbmA7XG5cbiAgICBuZXcgQ3VzdG9tUmVzb3VyY2UodGhpcywgXCJEcmFpbldhaXRlclwiLCB7XG4gICAgICBydW50aW1lOiBSdW50aW1lLk5PREVKU18yMl9YLFxuICAgICAgaW5saW5lQ29kZTogaGFuZGxlckNvZGUsXG4gICAgICB0aW1lb3V0OiBEdXJhdGlvbi5taW51dGVzKDYpLFxuICAgICAgbGFtYmRhRGVzY3JpcHRpb246IGBXYWl0cyBmb3IgRUNTIHNlcnZpY2VzIHRvIGRyYWluIGJlZm9yZSBjYXBhY2l0eSBwcm92aWRlciBkaXNhc3NvY2lhdGlvbmAsXG4gICAgICByb2xlRGVzY3JpcHRpb246IGBSb2xlIGZvciAke2NsdXN0ZXJOYW1lfSBjYXBhY2l0eSBwcm92aWRlciBkcmFpbiB3YWl0ZXJgLFxuICAgICAgaW5saW5lUG9saWN5OiBbXG4gICAgICAgIG5ldyBQb2xpY3lTdGF0ZW1lbnQoe1xuICAgICAgICAgIGVmZmVjdDogRWZmZWN0LkFMTE9XLFxuICAgICAgICAgIGFjdGlvbnM6IFtcbiAgICAgICAgICAgIFwiZWNzOkxpc3RTZXJ2aWNlc1wiLFxuICAgICAgICAgICAgXCJlY3M6RGVzY3JpYmVTZXJ2aWNlc1wiLFxuICAgICAgICAgICAgXCJlY3M6RGVzY3JpYmVDbHVzdGVyc1wiLFxuICAgICAgICAgICAgXCJlY3M6UHV0Q2x1c3RlckNhcGFjaXR5UHJvdmlkZXJzXCJcbiAgICAgICAgICBdLFxuICAgICAgICAgIHJlc291cmNlczogW1wiKlwiXVxuICAgICAgICB9KVxuICAgICAgXSxcbiAgICAgIHByb3BlcnRpZXM6IHtcbiAgICAgICAgQ2x1c3Rlck5hbWU6IGNsdXN0ZXJOYW1lLFxuICAgICAgICBDYXBhY2l0eVByb3ZpZGVyTmFtZTogY2FwYWNpdHlQcm92aWRlck5hbWVcbiAgICAgIH1cbiAgICB9KTtcbiAgfVxufVxuIl19
@@ -3,7 +3,7 @@ import { Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
3
3
  import { Construct } from "constructs";
4
4
  import { type StackBuilder } from "../base/awsStack";
5
5
  import { type ApplicationListener, ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
6
- import { type IManagedPolicy, PolicyDocument } from "aws-cdk-lib/aws-iam";
6
+ import { type IManagedPolicy, type PolicyDocument } from "aws-cdk-lib/aws-iam";
7
7
  import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
8
8
  import { type GeoLocation } from "aws-cdk-lib/aws-route53";
9
9
  import { Repository } from "aws-cdk-lib/aws-ecr";
@@ -210,6 +210,17 @@ export interface EcsServiceProps {
210
210
  * Creates security group rules to allow traffic from this specific service only.
211
211
  */
212
212
  connections?: IConnectable[];
213
+ /**
214
+ * Capacity provider for this service. REQUIRED.
215
+ * Each service specifies its own capacity provider.
216
+ */
217
+ capacityProvider: EcsCapacityProvider;
218
+ /**
219
+ * EC2 capacity configuration for this service.
220
+ * Only used when service capacityProvider is "EC2".
221
+ * Services with matching ec2Config share an ASG for efficiency.
222
+ */
223
+ ec2Config?: Ec2CapacityConfig;
213
224
  }
214
225
  /**
215
226
  * Props for creating an ECS cluster with multiple services.
@@ -221,10 +232,6 @@ export type EcsClusterProps = {
221
232
  vpc?: IVpc;
222
233
  /** Default ECR repository or container image */
223
234
  ecrRepository: Repository | RepositoryImage | string;
224
- /** Capacity provider determines Fargate vs EC2 infrastructure */
225
- capacityProvider?: EcsCapacityProvider;
226
- /** EC2-specific configuration. Only used when capacityProvider is "EC2" */
227
- ec2Config?: Ec2CapacityConfig;
228
235
  /**
229
236
  * Cluster configuration.
230
237
  * Controls the shared ALB for all services.
@@ -233,6 +240,7 @@ export type EcsClusterProps = {
233
240
  /**
234
241
  * Services in this cluster.
235
242
  * Each service gets its own task definition, scaling, and target group.
243
+ * Each service MUST specify its own capacityProvider.
236
244
  * All services share the cluster's ALB (unless disabled).
237
245
  * Task role policies are configured per-service for least-privilege.
238
246
  */
@@ -286,12 +294,10 @@ export default class EcsCluster extends Construct implements IConnectable {
286
294
  private asgSecurityGroup?;
287
295
  private asgCapacityProvider?;
288
296
  private loadBalancerSecurityGroup?;
289
- private drainWaiter?;
290
- private fargateCapacityProviderAssociations?;
291
297
  private services;
298
+ private asgCapacityProviders;
292
299
  private scope;
293
300
  private props;
294
- private capacityProvider;
295
301
  private loadBalancerDisabled;
296
302
  private directAccessEnabled;
297
303
  private nextPriority;
@@ -334,10 +340,58 @@ export default class EcsCluster extends Construct implements IConnectable {
334
340
  private registerServiceWithALB;
335
341
  private buildRoutingConditions;
336
342
  private addServiceScaling;
337
- private isEc2;
338
- private isFargate;
343
+ /**
344
+ * Checks if any service in the cluster uses EC2 capacity provider.
345
+ */
346
+ private hasAnyEc2Service;
347
+ /**
348
+ * Checks if any service in the cluster uses Fargate capacity provider.
349
+ */
350
+ private hasAnyFargateService;
351
+ /**
352
+ * Check if the VPC has NAT gateways.
353
+ * - For Fjall Vpc: uses hasNatGateways property
354
+ * - For other VPCs: checks if private subnets exist (assumes NAT if present)
355
+ */
356
+ private vpcHasNatGateways;
357
+ /**
358
+ * Create DeployableService outputs for deployment automation.
359
+ * Each service gets a DeployableService output so the deployment service
360
+ * can find and deploy all services in the cluster.
361
+ */
362
+ addDeployableServiceOutputs(props: EcsClusterProps): void;
363
+ /**
364
+ * Gets the capacity provider for a service.
365
+ * Each service MUST specify its own capacityProvider.
366
+ */
367
+ private getServiceCapacityProvider;
368
+ /**
369
+ * Checks if a service uses a Fargate capacity provider.
370
+ */
371
+ private isServiceFargate;
372
+ /**
373
+ * Checks if a service uses an EC2 capacity provider.
374
+ */
375
+ private isServiceEc2;
376
+ /**
377
+ * Generates a unique key for EC2 config (for ASG deduplication).
378
+ * Services with matching keys share an ASG.
379
+ */
380
+ private getEc2ConfigKey;
381
+ /**
382
+ * Gets or creates an ASG capacity provider for a service.
383
+ * Services with matching EC2 configs share the same ASG.
384
+ */
385
+ private getOrCreateAsgCapacityProvider;
386
+ /**
387
+ * Checks if any service in the cluster uses a Fargate capacity provider.
388
+ */
389
+ private anyServiceUsesFargate;
390
+ /**
391
+ * Checks if any service in the cluster uses an EC2 capacity provider.
392
+ */
393
+ private anyServiceUsesEc2;
339
394
  addCluster(props: EcsClusterProps): void;
340
- addAutoScalingGroup(props: EcsClusterProps): void;
341
395
  addLoadBalancer(props: EcsClusterProps): void;
342
396
  private addDirectAccessOutputs;
343
397
  addLoadBalancerListener(props: EcsClusterProps): void;