cdk-factory 0.16.15__py3-none-any.whl → 0.18.9__py3-none-any.whl
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.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +1 -1
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/resources/acm.py +9 -2
- cdk_factory/configurations/resources/auto_scaling.py +2 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +12 -5
- cdk_factory/configurations/resources/ecs_service.py +7 -2
- cdk_factory/configurations/resources/load_balancer.py +8 -9
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +7 -8
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +1 -1
- cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
- cdk_factory/configurations/resources/vpc.py +19 -0
- cdk_factory/configurations/workload.py +32 -2
- cdk_factory/constructs/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +4 -4
- cdk_factory/interfaces/networked_stack_mixin.py +6 -6
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +3 -3
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/acm_stack.py +2 -2
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +344 -535
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
- cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
- cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +1 -3
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +157 -73
- cdk_factory/stack_library/ecs/ecs_service_stack.py +10 -26
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +96 -119
- cdk_factory/stack_library/rds/rds_stack.py +73 -73
- cdk_factory/stack_library/route53/route53_stack.py +2 -2
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
- cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
- cdk_factory/stack_library/websites/static_website_stack.py +1 -1
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +5 -5
- cdk_factory/utilities/json_loading_utility.py +1 -1
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +57 -57
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Auto Scaling Group Stack Pattern for CDK-Factory
|
|
2
|
+
Auto Scaling Group Stack Pattern for CDK-Factory (Standardized SSM Version)
|
|
3
3
|
Maintainers: Eric Wilson
|
|
4
4
|
MIT License. See Project Root for the license information.
|
|
5
5
|
"""
|
|
@@ -22,29 +22,38 @@ from cdk_factory.configurations.stack import StackConfig
|
|
|
22
22
|
from cdk_factory.configurations.resources.auto_scaling import AutoScalingConfig
|
|
23
23
|
from cdk_factory.interfaces.istack import IStack
|
|
24
24
|
from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
|
|
25
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
25
26
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
26
27
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
27
28
|
|
|
28
|
-
logger = Logger(service="
|
|
29
|
+
logger = Logger(service="AutoScalingStackStandardized")
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@register_stack("auto_scaling_library_module")
|
|
32
33
|
@register_stack("auto_scaling_stack")
|
|
33
|
-
class AutoScalingStack(IStack, VPCProviderMixin):
|
|
34
|
+
class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
34
35
|
"""
|
|
35
|
-
Reusable stack for AWS Auto Scaling Groups.
|
|
36
|
-
Supports creating EC2 Auto Scaling Groups with customizable configurations.
|
|
36
|
+
Reusable stack for AWS Auto Scaling Groups with standardized SSM integration.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
This version uses the StandardizedSsmMixin to provide consistent SSM parameter
|
|
39
|
+
handling across all CDK Factory modules.
|
|
40
|
+
|
|
41
|
+
Key Features:
|
|
42
|
+
- Standardized SSM import/export patterns
|
|
43
|
+
- Template variable resolution
|
|
44
|
+
- Comprehensive validation
|
|
45
|
+
- Clear error handling
|
|
46
|
+
- Backward compatibility
|
|
39
47
|
"""
|
|
40
48
|
|
|
41
49
|
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
42
|
-
# Initialize parent
|
|
50
|
+
# Initialize parent classes properly
|
|
43
51
|
super().__init__(scope, id, **kwargs)
|
|
44
52
|
|
|
45
53
|
# Initialize VPC cache from mixin
|
|
46
54
|
self._initialize_vpc_cache()
|
|
47
55
|
|
|
56
|
+
# Initialize module attributes
|
|
48
57
|
self.asg_config = None
|
|
49
58
|
self.stack_config = None
|
|
50
59
|
self.deployment = None
|
|
@@ -56,9 +65,6 @@ class AutoScalingStack(IStack, VPCProviderMixin):
|
|
|
56
65
|
self.user_data = None
|
|
57
66
|
self.user_data_commands = [] # Store raw commands for ECS cluster detection
|
|
58
67
|
self.ecs_cluster = None
|
|
59
|
-
|
|
60
|
-
# SSM imports storage is now handled by the enhanced SsmParameterMixin via IStack
|
|
61
|
-
# VPC caching is now handled by VPCProviderMixin
|
|
62
68
|
|
|
63
69
|
def build(
|
|
64
70
|
self,
|
|
@@ -85,21 +91,33 @@ class AutoScalingStack(IStack, VPCProviderMixin):
|
|
|
85
91
|
)
|
|
86
92
|
asg_name = deployment.build_resource_name(self.asg_config.name)
|
|
87
93
|
|
|
88
|
-
#
|
|
89
|
-
self.
|
|
94
|
+
# Setup standardized SSM integration
|
|
95
|
+
self.setup_ssm_integration(
|
|
96
|
+
scope=self,
|
|
97
|
+
config=self.asg_config,
|
|
98
|
+
resource_type="auto_scaling",
|
|
99
|
+
resource_name=asg_name,
|
|
100
|
+
deployment=deployment,
|
|
101
|
+
workload=workload
|
|
102
|
+
)
|
|
90
103
|
|
|
91
|
-
#
|
|
104
|
+
# Process SSM imports using standardized method
|
|
105
|
+
self.process_ssm_imports()
|
|
106
|
+
|
|
107
|
+
# Get security groups using standardized approach
|
|
92
108
|
self.security_groups = self._get_security_groups()
|
|
93
109
|
|
|
94
110
|
# Create IAM role for instances
|
|
95
111
|
self.instance_role = self._create_instance_role(asg_name)
|
|
96
112
|
|
|
97
|
-
# Create
|
|
98
|
-
self.
|
|
113
|
+
# Create VPC once to be reused by both ECS cluster and ASG
|
|
114
|
+
self._vpc = None # Store VPC for reuse
|
|
99
115
|
|
|
100
116
|
# Create ECS cluster if ECS configuration is detected
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
self.ecs_cluster = self._create_ecs_cluster_if_needed()
|
|
118
|
+
|
|
119
|
+
# Create user data (after ECS cluster so it can reference it)
|
|
120
|
+
self.user_data = self._create_user_data()
|
|
103
121
|
|
|
104
122
|
# Create launch template
|
|
105
123
|
self.launch_template = self._create_launch_template(asg_name)
|
|
@@ -109,83 +127,97 @@ class AutoScalingStack(IStack, VPCProviderMixin):
|
|
|
109
127
|
|
|
110
128
|
# Add scaling policies
|
|
111
129
|
self._add_scaling_policies()
|
|
130
|
+
|
|
131
|
+
# Add update policy
|
|
132
|
+
self._add_update_policy()
|
|
112
133
|
|
|
113
|
-
#
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
# Export resources
|
|
117
|
-
self._export_resources(asg_name)
|
|
118
|
-
|
|
119
|
-
@property
|
|
120
|
-
def vpc(self) -> ec2.IVpc:
|
|
121
|
-
"""Get the VPC for the Auto Scaling Group using VPCProviderMixin"""
|
|
122
|
-
if not self.asg_config:
|
|
123
|
-
raise AttributeError("AutoScalingStack not properly initialized. Call build() first.")
|
|
124
|
-
|
|
125
|
-
# Use VPCProviderMixin to resolve VPC with proper subnet handling
|
|
126
|
-
return self.resolve_vpc(
|
|
127
|
-
config=self.asg_config,
|
|
128
|
-
deployment=self.deployment,
|
|
129
|
-
workload=self.workload
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
def _get_target_group_arns(self) -> List[str]:
|
|
133
|
-
"""Get target group ARNs from SSM imports using enhanced SsmParameterMixin"""
|
|
134
|
-
target_group_arns = []
|
|
135
|
-
|
|
136
|
-
# Check if we have SSM imports for target groups using enhanced mixin
|
|
137
|
-
if self.has_ssm_import("target_group_arns"):
|
|
138
|
-
imported_tg_arns = self.get_ssm_imported_value("target_group_arns", [])
|
|
139
|
-
if isinstance(imported_tg_arns, list):
|
|
140
|
-
target_group_arns.extend(imported_tg_arns)
|
|
141
|
-
else:
|
|
142
|
-
target_group_arns.append(imported_tg_arns)
|
|
134
|
+
# Export SSM parameters
|
|
135
|
+
self._export_ssm_parameters()
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
if self.asg_config.target_group_arns:
|
|
146
|
-
for arn in self.asg_config.target_group_arns:
|
|
147
|
-
logger.info(f"Adding target group ARN: {arn}")
|
|
148
|
-
target_group_arns.append(arn)
|
|
137
|
+
logger.info(f"Auto Scaling Group {asg_name} built successfully")
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"""Attach the Auto Scaling Group to target groups"""
|
|
154
|
-
target_group_arns = self._get_target_group_arns()
|
|
155
|
-
|
|
156
|
-
if not target_group_arns:
|
|
157
|
-
logger.warning("No target group ARNs found for Auto Scaling Group")
|
|
158
|
-
print(
|
|
159
|
-
"⚠️ No target group ARNs found for Auto Scaling Group. Nothing will be attached."
|
|
160
|
-
)
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
# Get the underlying CloudFormation resource to add target group ARNs
|
|
164
|
-
cfn_asg = asg.node.default_child
|
|
165
|
-
cfn_asg.add_property_override("TargetGroupARNs", target_group_arns)
|
|
139
|
+
def _get_ssm_imports(self) -> Dict[str, Any]:
|
|
140
|
+
"""Get SSM imports from standardized mixin processing"""
|
|
141
|
+
return self.get_all_ssm_imports()
|
|
166
142
|
|
|
167
143
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
168
|
-
"""
|
|
144
|
+
"""
|
|
145
|
+
Get security groups for the Auto Scaling Group using standardized SSM imports.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of security group references
|
|
149
|
+
"""
|
|
169
150
|
security_groups = []
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
151
|
+
|
|
152
|
+
# Primary method: Use standardized SSM imports
|
|
153
|
+
ssm_imports = self._get_ssm_imports()
|
|
154
|
+
if "security_group_ids" in ssm_imports:
|
|
155
|
+
imported_sg_ids = ssm_imports["security_group_ids"]
|
|
156
|
+
if isinstance(imported_sg_ids, list):
|
|
157
|
+
for idx, sg_id in enumerate(imported_sg_ids):
|
|
175
158
|
security_groups.append(
|
|
176
159
|
ec2.SecurityGroup.from_security_group_id(
|
|
177
|
-
self, f"SecurityGroup-{
|
|
160
|
+
self, f"SecurityGroup-SSM-{idx}", sg_id
|
|
178
161
|
)
|
|
179
162
|
)
|
|
163
|
+
logger.info(f"Added {len(imported_sg_ids)} security groups from SSM imports")
|
|
180
164
|
else:
|
|
181
|
-
# TODO: add some additional checks to make it more robust
|
|
182
165
|
security_groups.append(
|
|
183
166
|
ec2.SecurityGroup.from_security_group_id(
|
|
184
|
-
self, f"SecurityGroup-
|
|
167
|
+
self, f"SecurityGroup-SSM-0", imported_sg_ids
|
|
185
168
|
)
|
|
186
169
|
)
|
|
170
|
+
logger.info(f"Added security group from SSM imports")
|
|
171
|
+
|
|
172
|
+
# Fallback: Check for direct configuration (backward compatibility)
|
|
173
|
+
elif self.asg_config.security_group_ids:
|
|
174
|
+
logger.warning("Using direct security group configuration - consider migrating to SSM imports")
|
|
175
|
+
for idx, sg_id in enumerate(self.asg_config.security_group_ids):
|
|
176
|
+
logger.info(f"Adding security group from direct config: {sg_id}")
|
|
177
|
+
# Handle comma-separated security group IDs
|
|
178
|
+
if "," in sg_id:
|
|
179
|
+
blocks = sg_id.split(",")
|
|
180
|
+
for block_idx, block in enumerate(blocks):
|
|
181
|
+
security_groups.append(
|
|
182
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
183
|
+
self, f"SecurityGroup-Direct-{idx}-{block_idx}", block.strip()
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
security_groups.append(
|
|
188
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
189
|
+
self, f"SecurityGroup-Direct-{idx}", sg_id
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
logger.warning("No security groups found from SSM imports or direct configuration")
|
|
194
|
+
|
|
187
195
|
return security_groups
|
|
188
196
|
|
|
197
|
+
def _get_vpc_id(self) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Get VPC ID using the centralized VPC provider mixin.
|
|
200
|
+
"""
|
|
201
|
+
# Use the centralized VPC resolution from VPCProviderMixin
|
|
202
|
+
vpc = self.resolve_vpc(
|
|
203
|
+
config=self.asg_config,
|
|
204
|
+
deployment=self.deployment,
|
|
205
|
+
workload=self.workload
|
|
206
|
+
)
|
|
207
|
+
return vpc.vpc_id
|
|
208
|
+
|
|
209
|
+
def _get_subnet_ids(self) -> List[str]:
|
|
210
|
+
"""
|
|
211
|
+
Get subnet IDs using standardized SSM approach.
|
|
212
|
+
"""
|
|
213
|
+
# Primary method: Use standardized SSM imports
|
|
214
|
+
# ssm_imports = self._get_ssm_imports()
|
|
215
|
+
|
|
216
|
+
subnet_ids = self.get_subnet_ids(self.asg_config)
|
|
217
|
+
|
|
218
|
+
return subnet_ids
|
|
219
|
+
|
|
220
|
+
|
|
189
221
|
def _create_instance_role(self, asg_name: str) -> iam.Role:
|
|
190
222
|
"""Create IAM role for EC2 instances"""
|
|
191
223
|
role = iam.Role(
|
|
@@ -201,521 +233,298 @@ class AutoScalingStack(IStack, VPCProviderMixin):
|
|
|
201
233
|
iam.ManagedPolicy.from_aws_managed_policy_name(policy_name)
|
|
202
234
|
)
|
|
203
235
|
|
|
204
|
-
|
|
205
|
-
for policy_config in self.asg_config.iam_inline_policies:
|
|
206
|
-
policy_name = policy_config.get("name", "CustomPolicy")
|
|
207
|
-
statements = policy_config.get("statements", [])
|
|
208
|
-
|
|
209
|
-
if not statements:
|
|
210
|
-
logger.warning(f"No statements found for inline policy {policy_name}, skipping")
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
# Build policy statements
|
|
214
|
-
policy_statements = []
|
|
215
|
-
for stmt in statements:
|
|
216
|
-
effect = iam.Effect.ALLOW if stmt.get("effect", "Allow") == "Allow" else iam.Effect.DENY
|
|
217
|
-
actions = stmt.get("actions", [])
|
|
218
|
-
resources = stmt.get("resources", [])
|
|
219
|
-
|
|
220
|
-
if not actions or not resources:
|
|
221
|
-
logger.warning(f"Incomplete statement in policy {policy_name}, skipping")
|
|
222
|
-
continue
|
|
223
|
-
|
|
224
|
-
policy_statements.append(
|
|
225
|
-
iam.PolicyStatement(
|
|
226
|
-
effect=effect,
|
|
227
|
-
actions=actions,
|
|
228
|
-
resources=resources
|
|
229
|
-
)
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if policy_statements:
|
|
233
|
-
role.add_to_principal_policy(policy_statements[0])
|
|
234
|
-
for stmt in policy_statements[1:]:
|
|
235
|
-
role.add_to_principal_policy(stmt)
|
|
236
|
-
|
|
237
|
-
logger.info(f"Added inline policy {policy_name} with {len(policy_statements)} statements")
|
|
238
|
-
|
|
236
|
+
logger.info(f"Created instance role: {role.role_name}")
|
|
239
237
|
return role
|
|
240
238
|
|
|
241
239
|
def _create_user_data(self) -> ec2.UserData:
|
|
242
240
|
"""Create user data for EC2 instances"""
|
|
243
241
|
user_data = ec2.UserData.for_linux()
|
|
242
|
+
|
|
243
|
+
# Add basic setup commands
|
|
244
|
+
user_data.add_commands(
|
|
245
|
+
"#!/bin/bash",
|
|
246
|
+
"yum update -y",
|
|
247
|
+
"yum install -y aws-cfn-bootstrap",
|
|
248
|
+
)
|
|
244
249
|
|
|
245
|
-
#
|
|
246
|
-
self.user_data_commands
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
250
|
+
# Add user data commands from configuration
|
|
251
|
+
if self.asg_config.user_data_commands:
|
|
252
|
+
# Process template variables in user data commands
|
|
253
|
+
processed_commands = []
|
|
254
|
+
ssm_imports = self._get_ssm_imports()
|
|
255
|
+
for command in self.asg_config.user_data_commands:
|
|
256
|
+
processed_command = command
|
|
257
|
+
# Substitute SSM-imported values
|
|
258
|
+
if "cluster_name" in ssm_imports and "{{cluster_name}}" in command:
|
|
259
|
+
cluster_name = ssm_imports["cluster_name"]
|
|
260
|
+
processed_command = command.replace("{{cluster_name}}", cluster_name)
|
|
261
|
+
processed_commands.append(processed_command)
|
|
262
|
+
|
|
263
|
+
user_data.add_commands(*processed_commands)
|
|
264
|
+
self.user_data_commands = processed_commands
|
|
265
|
+
|
|
266
|
+
# Add ECS cluster configuration if needed
|
|
267
|
+
if self.ecs_cluster:
|
|
268
|
+
# Use the SSM-imported cluster name if available, otherwise fallback to default format
|
|
269
|
+
ssm_imports = self._get_ssm_imports()
|
|
270
|
+
if "cluster_name" in ssm_imports:
|
|
271
|
+
cluster_name = ssm_imports["cluster_name"]
|
|
272
|
+
ecs_commands = [
|
|
273
|
+
f"echo 'ECS_CLUSTER={cluster_name}' >> /etc/ecs/ecs.config",
|
|
274
|
+
"systemctl restart ecs"
|
|
275
|
+
]
|
|
276
|
+
else:
|
|
277
|
+
# Fallback to default naming pattern
|
|
278
|
+
ecs_commands = [
|
|
279
|
+
"echo 'ECS_CLUSTER={}{}' >> /etc/ecs/ecs.config".format(
|
|
280
|
+
self.deployment.workload_name, self.deployment.environment
|
|
281
|
+
),
|
|
282
|
+
"systemctl restart ecs"
|
|
283
|
+
]
|
|
284
|
+
user_data.add_commands(*ecs_commands)
|
|
266
285
|
|
|
286
|
+
logger.info(f"Created user data with {len(self.user_data_commands)} custom commands")
|
|
267
287
|
return user_data
|
|
268
288
|
|
|
269
|
-
def
|
|
270
|
-
"""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"""
|
|
275
|
-
from pathlib import Path
|
|
276
|
-
|
|
277
|
-
for script_config in self.asg_config.user_data_scripts:
|
|
278
|
-
script_type = script_config.get("type", "file")
|
|
279
|
-
|
|
280
|
-
if script_type == "file":
|
|
281
|
-
# Load script from file
|
|
282
|
-
script_path = script_config.get("path")
|
|
283
|
-
if not script_path:
|
|
284
|
-
logger.warning("Script path not specified, skipping")
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
# Resolve path (relative to project root or absolute)
|
|
288
|
-
path = Path(script_path)
|
|
289
|
-
if not path.is_absolute():
|
|
290
|
-
# Try relative to current working directory
|
|
291
|
-
path = Path.cwd() / script_path
|
|
292
|
-
|
|
293
|
-
if not path.exists():
|
|
294
|
-
logger.warning(f"Script file not found: {path}, skipping")
|
|
295
|
-
continue
|
|
296
|
-
|
|
297
|
-
# Read script content
|
|
298
|
-
try:
|
|
299
|
-
with open(path, 'r') as f:
|
|
300
|
-
script_content = f.read()
|
|
301
|
-
except Exception as e:
|
|
302
|
-
logger.error(f"Failed to read script file {path}: {e}")
|
|
303
|
-
continue
|
|
304
|
-
|
|
305
|
-
elif script_type == "inline":
|
|
306
|
-
# Use inline script content
|
|
307
|
-
script_content = script_config.get("content", "")
|
|
308
|
-
if not script_content:
|
|
309
|
-
logger.warning("Inline script content is empty, skipping")
|
|
310
|
-
continue
|
|
311
|
-
else:
|
|
312
|
-
logger.warning(f"Unknown script type: {script_type}, skipping")
|
|
313
|
-
continue
|
|
314
|
-
|
|
315
|
-
# Perform variable substitution
|
|
316
|
-
variables = script_config.get("variables", {})
|
|
317
|
-
for var_name, var_value in variables.items():
|
|
318
|
-
placeholder = f"{{{{{var_name}}}}}" # {{VAR_NAME}}
|
|
319
|
-
script_content = script_content.replace(placeholder, str(var_value))
|
|
289
|
+
def _get_or_create_vpc(self) -> ec2.Vpc:
|
|
290
|
+
"""Get or create VPC for reuse across the stack"""
|
|
291
|
+
if self._vpc is None:
|
|
292
|
+
vpc_id = self._get_vpc_id()
|
|
293
|
+
subnet_ids = self._get_subnet_ids()
|
|
320
294
|
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
logger.info(f"Added user data script from {script_type}: {script_config.get('path', 'inline')}")
|
|
328
|
-
|
|
329
|
-
def _substitute_variables(self, command: str) -> str:
|
|
330
|
-
"""
|
|
331
|
-
Perform variable substitution on a user data command.
|
|
332
|
-
Uses workload and deployment configuration for substitution.
|
|
333
|
-
"""
|
|
334
|
-
if not command:
|
|
335
|
-
return command
|
|
295
|
+
# Create VPC and subnets from imported values
|
|
296
|
+
self._vpc = ec2.Vpc.from_vpc_attributes(
|
|
297
|
+
self, "ImportedVPC",
|
|
298
|
+
vpc_id=vpc_id,
|
|
299
|
+
availability_zones=["us-east-1a", "us-east-1b"] # Add required availability zones
|
|
300
|
+
)
|
|
336
301
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
"WORKLOAD": getattr(self.workload, 'name', ''),
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
# Add deployment variables
|
|
352
|
-
if self.deployment:
|
|
353
|
-
variables.update({
|
|
354
|
-
"DEPLOYMENT_NAME": getattr(self.deployment, 'name', ''),
|
|
355
|
-
"REGION": getattr(self.deployment, 'region', ''),
|
|
356
|
-
"ACCOUNT": getattr(self.deployment, 'account', ''),
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
# Add stack-level variables
|
|
360
|
-
variables.update({
|
|
361
|
-
"STACK_NAME": self.stack_name,
|
|
362
|
-
})
|
|
302
|
+
# Create and store subnets if we have subnet IDs
|
|
303
|
+
self._subnets = []
|
|
304
|
+
if subnet_ids:
|
|
305
|
+
for i, subnet_id in enumerate(subnet_ids):
|
|
306
|
+
subnet = ec2.Subnet.from_subnet_id(
|
|
307
|
+
self, f"ImportedSubnet-{i}", subnet_id
|
|
308
|
+
)
|
|
309
|
+
self._subnets.append(subnet)
|
|
310
|
+
else:
|
|
311
|
+
# Use default subnets from VPC
|
|
312
|
+
self._subnets = self._vpc.public_subnets
|
|
363
313
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
314
|
+
return self._vpc
|
|
315
|
+
|
|
316
|
+
def _get_subnets(self) -> List[ec2.Subnet]:
|
|
317
|
+
"""Get the subnets from the shared VPC"""
|
|
318
|
+
return getattr(self, '_subnets', [])
|
|
319
|
+
|
|
320
|
+
def _create_ecs_cluster_if_needed(self) -> Optional[ecs.Cluster]:
|
|
321
|
+
"""Create ECS cluster if ECS configuration is detected"""
|
|
322
|
+
# Check if user data contains ECS configuration (use raw config since user_data_commands might not be set yet)
|
|
323
|
+
ecs_detected = False
|
|
324
|
+
if self.asg_config.user_data_commands:
|
|
325
|
+
ecs_detected = any("ECS_CLUSTER" in cmd for cmd in self.asg_config.user_data_commands)
|
|
369
326
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
ecr_config = container_config["ecr"]
|
|
384
|
-
user_data.add_commands(
|
|
385
|
-
f"ACCOUNT_ID={ecr_config.get('account_id', self.account)}",
|
|
386
|
-
f"REGION={ecr_config.get('region', self.region)}",
|
|
387
|
-
f"REPO={ecr_config.get('repo', 'app')}",
|
|
388
|
-
f"TAG={ecr_config.get('tag', 'latest')}",
|
|
389
|
-
"aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com",
|
|
390
|
-
"docker pull ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:${TAG}",
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
# Database configuration
|
|
394
|
-
if "database" in container_config:
|
|
395
|
-
db_config = container_config["database"]
|
|
396
|
-
secret_arn = db_config.get("secret_arn", "")
|
|
397
|
-
if secret_arn:
|
|
398
|
-
user_data.add_commands(
|
|
399
|
-
f"DB_SECRET_ARN={secret_arn}",
|
|
400
|
-
'if [ -n "$DB_SECRET_ARN" ]; then DB_JSON=$(aws secretsmanager get-secret-value --secret-id $DB_SECRET_ARN --query SecretString --output text --region $REGION); fi',
|
|
401
|
-
'if [ -n "$DB_SECRET_ARN" ]; then DB_HOST=$(echo $DB_JSON | jq -r .host); DB_USER=$(echo $DB_JSON | jq -r .username); DB_PASS=$(echo $DB_JSON | jq -r .password); DB_NAME=$(echo $DB_JSON | jq -r .dbname); fi',
|
|
327
|
+
if ecs_detected:
|
|
328
|
+
ssm_imports = self._get_ssm_imports()
|
|
329
|
+
if "cluster_name" in ssm_imports:
|
|
330
|
+
cluster_name = ssm_imports["cluster_name"]
|
|
331
|
+
|
|
332
|
+
# Use the shared VPC
|
|
333
|
+
vpc = self._get_or_create_vpc()
|
|
334
|
+
|
|
335
|
+
self.ecs_cluster = ecs.Cluster.from_cluster_attributes(
|
|
336
|
+
self,
|
|
337
|
+
"ImportedECSCluster",
|
|
338
|
+
cluster_name=cluster_name,
|
|
339
|
+
vpc=vpc
|
|
402
340
|
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
user_data.add_commands(container_config["run_command"])
|
|
407
|
-
elif "ecr" in container_config:
|
|
408
|
-
port = container_config.get("port", 8080)
|
|
409
|
-
user_data.add_commands(
|
|
410
|
-
f"docker run -d --name app -p {port}:{port} "
|
|
411
|
-
'-e DB_HOST="$DB_HOST" -e DB_USER="$DB_USER" -e DB_PASS="$DB_PASS" -e DB_NAME="$DB_NAME" '
|
|
412
|
-
"--restart=always ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:${TAG}"
|
|
413
|
-
)
|
|
341
|
+
logger.info(f"Connected to existing ECS cluster: {cluster_name}")
|
|
342
|
+
|
|
343
|
+
return self.ecs_cluster
|
|
414
344
|
|
|
415
345
|
def _create_launch_template(self, asg_name: str) -> ec2.LaunchTemplate:
|
|
416
|
-
"""Create launch template for
|
|
417
|
-
|
|
418
|
-
|
|
346
|
+
"""Create launch template for Auto Scaling Group"""
|
|
347
|
+
|
|
348
|
+
# Use the configured AMI ID or fall back to appropriate lookup
|
|
419
349
|
if self.asg_config.ami_id:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if "." in instance_type_str:
|
|
434
|
-
parts = instance_type_str.split(".")
|
|
435
|
-
if len(parts) == 2:
|
|
436
|
-
try:
|
|
437
|
-
instance_class = ec2.InstanceClass[parts[0].upper()]
|
|
438
|
-
instance_size = ec2.InstanceSize[parts[1].upper()]
|
|
439
|
-
instance_type = ec2.InstanceType.of(instance_class, instance_size)
|
|
440
|
-
except (KeyError, ValueError):
|
|
441
|
-
instance_type = ec2.InstanceType(instance_type_str)
|
|
350
|
+
# Use explicit AMI ID provided by user
|
|
351
|
+
machine_image = ec2.MachineImage.lookup(name=self.asg_config.ami_id)
|
|
352
|
+
elif self.asg_config.ami_type:
|
|
353
|
+
# Use AMI type for dynamic lookup
|
|
354
|
+
if self.asg_config.ami_type.upper() == "AMAZON-LINUX-2023":
|
|
355
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
356
|
+
elif self.asg_config.ami_type.upper() == "AMAZON-LINUX-2022":
|
|
357
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2022()
|
|
358
|
+
elif self.asg_config.ami_type.upper() == "AMAZON-LINUX-2":
|
|
359
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2()
|
|
360
|
+
elif self.asg_config.ami_type.upper() == "ECS_OPTIMIZED":
|
|
361
|
+
# Use actual ECS-optimized AMI (Amazon Linux 2 based)
|
|
362
|
+
machine_image = ec2.MachineImage.lookup(name="amzn2-ami-ecs-hvm-*-x86_64-ebs")
|
|
442
363
|
else:
|
|
443
|
-
|
|
364
|
+
# Default to latest Amazon Linux
|
|
365
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
444
366
|
else:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
block_devices = []
|
|
449
|
-
for device in self.asg_config.block_devices:
|
|
450
|
-
block_devices.append(
|
|
451
|
-
ec2.BlockDevice(
|
|
452
|
-
device_name=device.get("device_name", "/dev/xvda"),
|
|
453
|
-
volume=ec2.BlockDeviceVolume.ebs(
|
|
454
|
-
volume_size=device.get("volume_size", 8),
|
|
455
|
-
volume_type=ec2.EbsDeviceVolumeType(
|
|
456
|
-
str(device.get("volume_type", "gp3")).upper()
|
|
457
|
-
),
|
|
458
|
-
delete_on_termination=device.get("delete_on_termination", True),
|
|
459
|
-
encrypted=device.get("encrypted", True),
|
|
460
|
-
),
|
|
461
|
-
)
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Create launch template
|
|
367
|
+
# Default fallback
|
|
368
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
369
|
+
|
|
465
370
|
launch_template = ec2.LaunchTemplate(
|
|
466
371
|
self,
|
|
467
372
|
f"{asg_name}-LaunchTemplate",
|
|
468
|
-
|
|
469
|
-
|
|
373
|
+
instance_type=ec2.InstanceType(self.asg_config.instance_type),
|
|
374
|
+
machine_image=machine_image,
|
|
470
375
|
role=self.instance_role,
|
|
471
|
-
security_group=self.security_groups[0] if self.security_groups else None,
|
|
472
376
|
user_data=self.user_data,
|
|
377
|
+
security_group=self.security_groups[0] if self.security_groups else None,
|
|
378
|
+
key_name=self.asg_config.key_name,
|
|
473
379
|
detailed_monitoring=self.asg_config.detailed_monitoring,
|
|
474
|
-
block_devices=
|
|
380
|
+
block_devices=[
|
|
381
|
+
ec2.BlockDevice(
|
|
382
|
+
device_name=block_device.get("device_name", "/dev/xvda"),
|
|
383
|
+
volume=ec2.BlockDeviceVolume.ebs(
|
|
384
|
+
volume_size=block_device.get("volume_size", 8),
|
|
385
|
+
volume_type=getattr(ec2.EbsDeviceVolumeType, block_device.get("volume_type", "GP3").upper()),
|
|
386
|
+
delete_on_termination=block_device.get("delete_on_termination", True),
|
|
387
|
+
encrypted=block_device.get("encrypted", False),
|
|
388
|
+
)
|
|
389
|
+
) for block_device in self.asg_config.block_devices
|
|
390
|
+
] if self.asg_config.block_devices else None,
|
|
475
391
|
)
|
|
476
392
|
|
|
393
|
+
logger.info(f"Created launch template: {launch_template.launch_template_name}")
|
|
477
394
|
return launch_template
|
|
478
395
|
|
|
479
396
|
def _create_auto_scaling_group(self, asg_name: str) -> autoscaling.AutoScalingGroup:
|
|
480
|
-
"""Create
|
|
481
|
-
#
|
|
482
|
-
|
|
483
|
-
subnets =
|
|
484
|
-
|
|
485
|
-
# Configure health check
|
|
486
|
-
health_check_type = autoscaling.HealthCheck.ec2()
|
|
487
|
-
if self.asg_config.health_check_type.upper() == "ELB":
|
|
488
|
-
health_check_type = autoscaling.HealthCheck.elb(
|
|
489
|
-
grace=Duration.seconds(self.asg_config.health_check_grace_period)
|
|
490
|
-
)
|
|
397
|
+
"""Create Auto Scaling Group"""
|
|
398
|
+
# Use the shared VPC and subnets
|
|
399
|
+
vpc = self._get_or_create_vpc()
|
|
400
|
+
subnets = self._get_subnets()
|
|
491
401
|
|
|
492
|
-
|
|
493
|
-
asg = autoscaling.AutoScalingGroup(
|
|
402
|
+
auto_scaling_group = autoscaling.AutoScalingGroup(
|
|
494
403
|
self,
|
|
495
|
-
asg_name,
|
|
496
|
-
vpc=
|
|
497
|
-
vpc_subnets=subnets,
|
|
404
|
+
f"{asg_name}-ASG",
|
|
405
|
+
vpc=vpc,
|
|
406
|
+
vpc_subnets=ec2.SubnetSelection(subnets=subnets),
|
|
407
|
+
launch_template=self.launch_template,
|
|
498
408
|
min_capacity=self.asg_config.min_capacity,
|
|
499
409
|
max_capacity=self.asg_config.max_capacity,
|
|
500
410
|
desired_capacity=self.asg_config.desired_capacity,
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
411
|
+
health_check=autoscaling.HealthCheck.elb(
|
|
412
|
+
grace=cdk.Duration.seconds(self.asg_config.health_check_grace_period)
|
|
413
|
+
) if self.asg_config.health_check_type.upper() == "ELB" else autoscaling.HealthCheck.ec2(
|
|
414
|
+
grace=cdk.Duration.seconds(self.asg_config.health_check_grace_period)
|
|
415
|
+
),
|
|
416
|
+
cooldown=cdk.Duration.seconds(self.asg_config.cooldown),
|
|
504
417
|
termination_policies=[
|
|
505
|
-
autoscaling.TerminationPolicy(
|
|
418
|
+
getattr(autoscaling.TerminationPolicy, policy.upper())
|
|
506
419
|
for policy in self.asg_config.termination_policies
|
|
507
420
|
],
|
|
508
421
|
)
|
|
509
422
|
|
|
510
|
-
# Attach
|
|
511
|
-
self._attach_target_groups(
|
|
512
|
-
|
|
513
|
-
# Configure update policy
|
|
514
|
-
# Only apply update policy if it was explicitly configured
|
|
515
|
-
if "update_policy" in self.stack_config.dictionary.get("auto_scaling", {}):
|
|
516
|
-
update_policy = self.asg_config.update_policy
|
|
517
|
-
# Apply the update policy to the ASG's CloudFormation resource
|
|
518
|
-
cfn_asg = asg.node.default_child
|
|
519
|
-
cfn_asg.add_override(
|
|
520
|
-
"UpdatePolicy",
|
|
521
|
-
{
|
|
522
|
-
"AutoScalingRollingUpdate": {
|
|
523
|
-
"MinInstancesInService": update_policy.get(
|
|
524
|
-
"min_instances_in_service", 1
|
|
525
|
-
),
|
|
526
|
-
"MaxBatchSize": update_policy.get("max_batch_size", 1),
|
|
527
|
-
"PauseTime": f"PT{update_policy.get('pause_time', 300) // 60}M",
|
|
528
|
-
}
|
|
529
|
-
},
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
# Add tags
|
|
533
|
-
for key, value in self.asg_config.tags.items():
|
|
534
|
-
cdk.Tags.of(asg).add(key, value)
|
|
535
|
-
|
|
536
|
-
return asg
|
|
537
|
-
|
|
538
|
-
def _configure_scaling_policies(self) -> None:
|
|
539
|
-
"""Configure scaling policies for the Auto Scaling Group"""
|
|
540
|
-
for policy in self.asg_config.scaling_policies:
|
|
541
|
-
policy_type = policy.get("type", "target_tracking")
|
|
542
|
-
|
|
543
|
-
if policy_type == "target_tracking":
|
|
544
|
-
self.auto_scaling_group.scale_on_metric(
|
|
545
|
-
f"{self.asg_config.name}-{policy.get('name', 'scaling-policy')}",
|
|
546
|
-
metric=self._get_metric(policy),
|
|
547
|
-
scaling_steps=self._get_scaling_steps(policy),
|
|
548
|
-
adjustment_type=autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
|
|
549
|
-
)
|
|
550
|
-
elif policy_type == "step":
|
|
551
|
-
self.auto_scaling_group.scale_on_metric(
|
|
552
|
-
f"{self.asg_config.name}-{policy.get('name', 'scaling-policy')}",
|
|
553
|
-
metric=self._get_metric(policy),
|
|
554
|
-
scaling_steps=self._get_scaling_steps(policy),
|
|
555
|
-
adjustment_type=autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
def _get_metric(self, policy: Dict[str, Any]) -> cloudwatch.Metric:
|
|
559
|
-
"""Get metric for scaling policy"""
|
|
560
|
-
# This is a simplified implementation
|
|
561
|
-
# In a real-world scenario, you would use CloudWatch metrics
|
|
562
|
-
return cloudwatch.Metric(
|
|
563
|
-
namespace="AWS/EC2",
|
|
564
|
-
metric_name=policy.get("metric_name", "CPUUtilization"),
|
|
565
|
-
dimensions_map={
|
|
566
|
-
"AutoScalingGroupName": self.auto_scaling_group.auto_scaling_group_name
|
|
567
|
-
},
|
|
568
|
-
statistic=policy.get("statistic", "Average"),
|
|
569
|
-
period=Duration.seconds(policy.get("period", 60)),
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
def _get_scaling_steps(
|
|
573
|
-
self, policy: Dict[str, Any]
|
|
574
|
-
) -> List[autoscaling.ScalingInterval]:
|
|
575
|
-
"""Get scaling steps for scaling policy"""
|
|
576
|
-
steps = policy.get("steps", [])
|
|
577
|
-
scaling_intervals = []
|
|
578
|
-
|
|
579
|
-
for step in steps:
|
|
580
|
-
# Handle upper bound - if not specified, don't set it (let CDK handle it)
|
|
581
|
-
interval_kwargs = {
|
|
582
|
-
"lower": step.get("lower", 0),
|
|
583
|
-
"change": step.get("change", 1),
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
# Only set upper if it's explicitly provided
|
|
587
|
-
if "upper" in step:
|
|
588
|
-
interval_kwargs["upper"] = step["upper"]
|
|
423
|
+
# Attach target groups if configured
|
|
424
|
+
self._attach_target_groups(auto_scaling_group)
|
|
589
425
|
|
|
590
|
-
|
|
426
|
+
logger.info(f"Created Auto Scaling Group: {asg_name}")
|
|
427
|
+
return auto_scaling_group
|
|
591
428
|
|
|
592
|
-
|
|
429
|
+
def _attach_target_groups(self, asg: autoscaling.AutoScalingGroup) -> None:
|
|
430
|
+
"""Attach the Auto Scaling Group to target groups"""
|
|
431
|
+
target_group_arns = self._get_target_group_arns()
|
|
593
432
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
# Auto Scaling Group Name
|
|
598
|
-
cdk.CfnOutput(
|
|
599
|
-
self,
|
|
600
|
-
f"{asg_name}-name",
|
|
601
|
-
value=self.auto_scaling_group.auto_scaling_group_name,
|
|
602
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-name",
|
|
603
|
-
)
|
|
433
|
+
if not target_group_arns:
|
|
434
|
+
logger.warning("No target group ARNs found for Auto Scaling Group")
|
|
435
|
+
return
|
|
604
436
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
f"{asg_name}-arn",
|
|
609
|
-
value=self.auto_scaling_group.auto_scaling_group_arn,
|
|
610
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-arn",
|
|
611
|
-
)
|
|
437
|
+
# Get the underlying CloudFormation resource to add target group ARNs
|
|
438
|
+
cfn_asg = asg.node.default_child
|
|
439
|
+
cfn_asg.add_property_override("TargetGroupARNs", target_group_arns)
|
|
612
440
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
441
|
+
def _get_target_group_arns(self) -> List[str]:
|
|
442
|
+
"""Get target group ARNs using standardized SSM approach"""
|
|
443
|
+
target_group_arns = []
|
|
444
|
+
|
|
445
|
+
# Use standardized SSM imports
|
|
446
|
+
ssm_imports = self._get_ssm_imports()
|
|
447
|
+
if "target_group_arns" in ssm_imports:
|
|
448
|
+
imported_arns = ssm_imports["target_group_arns"]
|
|
449
|
+
if isinstance(imported_arns, list):
|
|
450
|
+
target_group_arns.extend(imported_arns)
|
|
451
|
+
else:
|
|
452
|
+
target_group_arns.append(imported_arns)
|
|
453
|
+
|
|
454
|
+
# Fallback: Direct configuration
|
|
455
|
+
elif self.asg_config.target_group_arns:
|
|
456
|
+
target_group_arns.extend(self.asg_config.target_group_arns)
|
|
457
|
+
|
|
458
|
+
return target_group_arns
|
|
621
459
|
|
|
622
|
-
|
|
623
460
|
def _add_scaling_policies(self) -> None:
|
|
624
461
|
"""Add scaling policies to the Auto Scaling Group"""
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
pass
|
|
462
|
+
if not self.asg_config.scaling_policies:
|
|
463
|
+
return
|
|
628
464
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
465
|
+
for policy_config in self.asg_config.scaling_policies:
|
|
466
|
+
if policy_config.get("type") == "target_tracking":
|
|
467
|
+
# Create a target tracking scaling policy for CPU utilization
|
|
468
|
+
scaling_policy = autoscaling.CfnScalingPolicy(
|
|
469
|
+
self,
|
|
470
|
+
"CPUScalingPolicy",
|
|
471
|
+
auto_scaling_group_name=self.auto_scaling_group.auto_scaling_group_name,
|
|
472
|
+
policy_type="TargetTrackingScaling",
|
|
473
|
+
target_tracking_configuration=autoscaling.CfnScalingPolicy.TargetTrackingConfigurationProperty(
|
|
474
|
+
target_value=policy_config.get("target_cpu", 70),
|
|
475
|
+
predefined_metric_specification=autoscaling.CfnScalingPolicy.PredefinedMetricSpecificationProperty(
|
|
476
|
+
predefined_metric_type="ASGAverageCPUUtilization"
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
logger.info("Added CPU utilization scaling policy")
|
|
634
481
|
|
|
635
|
-
def
|
|
636
|
-
"""
|
|
637
|
-
|
|
638
|
-
This method only handles SSM imports for cluster name injection.
|
|
639
|
-
"""
|
|
640
|
-
# Check if ECS cluster name is available via SSM imports
|
|
641
|
-
if self.has_ssm_import("ecs_cluster_name"):
|
|
642
|
-
logger.info(f"ECS cluster name available via SSM imports")
|
|
643
|
-
# Inject cluster name into user data if available
|
|
644
|
-
if self.user_data and self.user_data_commands:
|
|
645
|
-
self._inject_cluster_name_into_user_data()
|
|
646
|
-
return
|
|
482
|
+
def _add_update_policy(self) -> None:
|
|
483
|
+
"""Add update policy to the Auto Scaling Group"""
|
|
484
|
+
update_policy = self.asg_config.update_policy
|
|
647
485
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
"Use the dedicated EcsClusterStack module to create ECS clusters."
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
def _inject_cluster_name_into_user_data(self) -> None:
|
|
654
|
-
"""Inject the ECS cluster name into user data commands using SSM imports"""
|
|
655
|
-
# Check if ECS cluster name is available via SSM imports
|
|
656
|
-
if self.has_ssm_import("ecs_cluster_name"):
|
|
657
|
-
cluster_name = self.get_ssm_imported_value("ecs_cluster_name")
|
|
658
|
-
logger.info(f"Using ECS cluster name from SSM: {cluster_name}")
|
|
659
|
-
else:
|
|
660
|
-
logger.warning("No ECS cluster name found in SSM imports, skipping cluster name injection")
|
|
486
|
+
if not update_policy:
|
|
487
|
+
# No update policy configured, don't add one
|
|
661
488
|
return
|
|
489
|
+
|
|
490
|
+
# Get the underlying CloudFormation resource to add update policy
|
|
491
|
+
cfn_asg = self.auto_scaling_group.node.default_child
|
|
662
492
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
for command in self.user_data_commands:
|
|
667
|
-
# If this command already sets ECS_CLUSTER, replace it
|
|
668
|
-
if 'ECS_CLUSTER=' in command:
|
|
669
|
-
# Replace existing ECS_CLUSTER setting with our cluster name
|
|
670
|
-
parts = command.split('ECS_CLUSTER=')
|
|
671
|
-
if len(parts) > 1:
|
|
672
|
-
# Keep everything before ECS_CLUSTER=, add our cluster name, then add the rest
|
|
673
|
-
before = parts[0]
|
|
674
|
-
after_parts = parts[1].split(None, 1) # Split on first whitespace
|
|
675
|
-
after = after_parts[1] if len(after_parts) > 1 else ''
|
|
676
|
-
new_command = f"{before}ECS_CLUSTER={cluster_name} {after}".strip()
|
|
677
|
-
injected_commands.append(new_command)
|
|
678
|
-
cluster_name_injected = True
|
|
679
|
-
else:
|
|
680
|
-
injected_commands.append(f"{command}ECS_CLUSTER={cluster_name}")
|
|
681
|
-
cluster_name_injected = True
|
|
682
|
-
else:
|
|
683
|
-
injected_commands.append(command)
|
|
493
|
+
# Get CDK's default policy first (if any)
|
|
494
|
+
default_policy = getattr(cfn_asg, 'update_policy', {})
|
|
684
495
|
|
|
685
|
-
#
|
|
686
|
-
|
|
687
|
-
|
|
496
|
+
# Merge with defaults, then use the robust add_override method
|
|
497
|
+
merged_policy = {
|
|
498
|
+
**default_policy, # Preserve CDK defaults
|
|
499
|
+
"AutoScalingRollingUpdate": {
|
|
500
|
+
"MinInstancesInService": update_policy.get("min_instances_in_service", 1),
|
|
501
|
+
"MaxBatchSize": update_policy.get("max_batch_size", 1),
|
|
502
|
+
"PauseTime": f"PT{update_policy.get('pause_time', 300)}S"
|
|
503
|
+
}
|
|
504
|
+
}
|
|
688
505
|
|
|
689
|
-
#
|
|
690
|
-
|
|
506
|
+
# Use the robust CDK-documented approach
|
|
507
|
+
cfn_asg.add_override("UpdatePolicy", merged_policy)
|
|
691
508
|
|
|
692
|
-
|
|
693
|
-
if hasattr(self, 'user_data') and self.user_data:
|
|
694
|
-
self.user_data = self._recreate_user_data_with_commands(injected_commands)
|
|
509
|
+
logger.info("Added rolling update policy to Auto Scaling Group")
|
|
695
510
|
|
|
696
|
-
def
|
|
697
|
-
"""
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
511
|
+
def _export_ssm_parameters(self) -> None:
|
|
512
|
+
"""Export SSM parameters using standardized approach"""
|
|
513
|
+
if not self.auto_scaling_group:
|
|
514
|
+
logger.warning("No Auto Scaling Group to export")
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
# Prepare resource values for export
|
|
518
|
+
resource_values = {
|
|
519
|
+
"auto_scaling_group_name": self.auto_scaling_group.auto_scaling_group_name,
|
|
520
|
+
"auto_scaling_group_arn": self.auto_scaling_group.auto_scaling_group_arn,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# Export using standardized SSM mixin
|
|
524
|
+
exported_params = self.export_ssm_parameters(resource_values)
|
|
702
525
|
|
|
703
|
-
|
|
526
|
+
logger.info(f"Exported SSM parameters: {exported_params}")
|
|
704
527
|
|
|
705
|
-
def _export_resources(self, asg_name: str) -> None:
|
|
706
|
-
"""Export stack resources to SSM and CloudFormation outputs"""
|
|
707
|
-
# Export ASG name
|
|
708
|
-
cdk.CfnOutput(
|
|
709
|
-
self,
|
|
710
|
-
f"{asg_name}-name",
|
|
711
|
-
value=self.auto_scaling_group.auto_scaling_group_name,
|
|
712
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-name",
|
|
713
|
-
)
|
|
714
528
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
self,
|
|
718
|
-
f"{asg_name}-arn",
|
|
719
|
-
value=self.auto_scaling_group.auto_scaling_group_arn,
|
|
720
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-arn",
|
|
721
|
-
)
|
|
529
|
+
# Backward compatibility alias
|
|
530
|
+
AutoScalingStackStandardized = AutoScalingStack
|