cdk-factory 0.15.10__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 +6 -4
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/pipeline_stage.py +29 -5
- cdk_factory/configurations/resources/acm.py +85 -0
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +108 -0
- cdk_factory/configurations/resources/ecs_service.py +17 -2
- cdk_factory/configurations/resources/load_balancer.py +17 -4
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +305 -19
- 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 +6 -3
- cdk_factory/interfaces/networked_stack_mixin.py +75 -0
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +222 -27
- cdk_factory/stack/stack_factory.py +34 -0
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/__init__.py +6 -0
- cdk_factory/stack_library/acm/acm_stack.py +169 -0
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
- 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 +12 -0
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
- cdk_factory/stack_library/rds/rds_stack.py +85 -74
- cdk_factory/stack_library/route53/route53_stack.py +8 -3
- 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 +12 -3
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +1 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.10.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
|
"""
|
|
@@ -12,7 +12,8 @@ from aws_cdk import aws_autoscaling as autoscaling
|
|
|
12
12
|
from aws_cdk import aws_cloudwatch as cloudwatch
|
|
13
13
|
from aws_cdk import aws_iam as iam
|
|
14
14
|
from aws_cdk import aws_ssm as ssm
|
|
15
|
-
from aws_cdk import
|
|
15
|
+
from aws_cdk import aws_ecs as ecs
|
|
16
|
+
from aws_cdk import Duration, Stack
|
|
16
17
|
from aws_lambda_powertools import Logger
|
|
17
18
|
from constructs import Construct
|
|
18
19
|
|
|
@@ -20,23 +21,39 @@ from cdk_factory.configurations.deployment import DeploymentConfig
|
|
|
20
21
|
from cdk_factory.configurations.stack import StackConfig
|
|
21
22
|
from cdk_factory.configurations.resources.auto_scaling import AutoScalingConfig
|
|
22
23
|
from cdk_factory.interfaces.istack import IStack
|
|
23
|
-
from cdk_factory.interfaces.
|
|
24
|
+
from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
|
|
25
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
24
26
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
25
27
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
26
28
|
|
|
27
|
-
logger = Logger(service="
|
|
29
|
+
logger = Logger(service="AutoScalingStackStandardized")
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
@register_stack("auto_scaling_library_module")
|
|
31
33
|
@register_stack("auto_scaling_stack")
|
|
32
|
-
class AutoScalingStack(IStack,
|
|
34
|
+
class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
33
35
|
"""
|
|
34
|
-
Reusable stack for AWS Auto Scaling Groups.
|
|
35
|
-
|
|
36
|
+
Reusable stack for AWS Auto Scaling Groups with standardized SSM integration.
|
|
37
|
+
|
|
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
|
|
36
47
|
"""
|
|
37
48
|
|
|
38
49
|
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
50
|
+
# Initialize parent classes properly
|
|
39
51
|
super().__init__(scope, id, **kwargs)
|
|
52
|
+
|
|
53
|
+
# Initialize VPC cache from mixin
|
|
54
|
+
self._initialize_vpc_cache()
|
|
55
|
+
|
|
56
|
+
# Initialize module attributes
|
|
40
57
|
self.asg_config = None
|
|
41
58
|
self.stack_config = None
|
|
42
59
|
self.deployment = None
|
|
@@ -46,7 +63,8 @@ class AutoScalingStack(IStack, EnhancedSsmParameterMixin):
|
|
|
46
63
|
self.launch_template = None
|
|
47
64
|
self.instance_role = None
|
|
48
65
|
self.user_data = None
|
|
49
|
-
self.
|
|
66
|
+
self.user_data_commands = [] # Store raw commands for ECS cluster detection
|
|
67
|
+
self.ecs_cluster = None
|
|
50
68
|
|
|
51
69
|
def build(
|
|
52
70
|
self,
|
|
@@ -73,111 +91,133 @@ class AutoScalingStack(IStack, EnhancedSsmParameterMixin):
|
|
|
73
91
|
)
|
|
74
92
|
asg_name = deployment.build_resource_name(self.asg_config.name)
|
|
75
93
|
|
|
76
|
-
#
|
|
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
|
+
)
|
|
103
|
+
|
|
104
|
+
# Process SSM imports using standardized method
|
|
105
|
+
self.process_ssm_imports()
|
|
106
|
+
|
|
107
|
+
# Get security groups using standardized approach
|
|
77
108
|
self.security_groups = self._get_security_groups()
|
|
78
109
|
|
|
79
110
|
# Create IAM role for instances
|
|
80
111
|
self.instance_role = self._create_instance_role(asg_name)
|
|
81
112
|
|
|
82
|
-
# Create
|
|
113
|
+
# Create VPC once to be reused by both ECS cluster and ASG
|
|
114
|
+
self._vpc = None # Store VPC for reuse
|
|
115
|
+
|
|
116
|
+
# Create ECS cluster if ECS configuration is detected
|
|
117
|
+
self.ecs_cluster = self._create_ecs_cluster_if_needed()
|
|
118
|
+
|
|
119
|
+
# Create user data (after ECS cluster so it can reference it)
|
|
83
120
|
self.user_data = self._create_user_data()
|
|
84
121
|
|
|
85
122
|
# Create launch template
|
|
86
123
|
self.launch_template = self._create_launch_template(asg_name)
|
|
87
124
|
|
|
88
|
-
# Create
|
|
125
|
+
# Create Auto Scaling Group
|
|
89
126
|
self.auto_scaling_group = self._create_auto_scaling_group(asg_name)
|
|
90
127
|
|
|
91
|
-
#
|
|
92
|
-
self.
|
|
93
|
-
|
|
94
|
-
# Add
|
|
95
|
-
self.
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def vpc(self) -> ec2.IVpc:
|
|
99
|
-
"""Get the VPC for the Auto Scaling Group"""
|
|
100
|
-
# Assuming VPC is provided by the workload
|
|
101
|
-
|
|
102
|
-
if self._vpc:
|
|
103
|
-
return self._vpc
|
|
104
|
-
|
|
105
|
-
elif self.asg_config.vpc_id:
|
|
106
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.asg_config.vpc_id)
|
|
107
|
-
elif self.workload.vpc_id:
|
|
108
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
|
|
109
|
-
else:
|
|
110
|
-
# Use default VPC if not provided
|
|
111
|
-
raise ValueError(
|
|
112
|
-
"VPC is not defined in the configuration. "
|
|
113
|
-
"You can provide it a the auto_scaling.vpc_id in the configuration "
|
|
114
|
-
"or a top level workload.vpc_id in the workload configuration."
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
return self._vpc
|
|
118
|
-
|
|
119
|
-
def _get_target_group_arns(self) -> List[str]:
|
|
120
|
-
"""Get target group ARNs from SSM imports"""
|
|
121
|
-
target_group_arns = []
|
|
122
|
-
|
|
123
|
-
# Import target group ARNs using the SSM import pattern
|
|
124
|
-
imported_values = self.import_resources_from_ssm(
|
|
125
|
-
scope=self,
|
|
126
|
-
config=self.asg_config,
|
|
127
|
-
resource_name=self.asg_config.name,
|
|
128
|
-
resource_type="auto-scaling",
|
|
129
|
-
)
|
|
128
|
+
# Add scaling policies
|
|
129
|
+
self._add_scaling_policies()
|
|
130
|
+
|
|
131
|
+
# Add update policy
|
|
132
|
+
self._add_update_policy()
|
|
130
133
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
if "target_group" in key and "arn" in key:
|
|
134
|
-
target_group_arns.append(value)
|
|
134
|
+
# Export SSM parameters
|
|
135
|
+
self._export_ssm_parameters()
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
if self.asg_config.target_group_arns:
|
|
138
|
-
for arn in self.asg_config.target_group_arns:
|
|
139
|
-
logger.info(f"Adding target group ARN: {arn}")
|
|
140
|
-
target_group_arns.append(arn)
|
|
137
|
+
logger.info(f"Auto Scaling Group {asg_name} built successfully")
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"""Attach the Auto Scaling Group to target groups"""
|
|
146
|
-
target_group_arns = self._get_target_group_arns()
|
|
147
|
-
|
|
148
|
-
if not target_group_arns:
|
|
149
|
-
logger.warning("No target group ARNs found for Auto Scaling Group")
|
|
150
|
-
print(
|
|
151
|
-
"⚠️ No target group ARNs found for Auto Scaling Group. Nothing will be attached."
|
|
152
|
-
)
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
# Get the underlying CloudFormation resource to add target group ARNs
|
|
156
|
-
cfn_asg = asg.node.default_child
|
|
157
|
-
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()
|
|
158
142
|
|
|
159
143
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
160
|
-
"""
|
|
144
|
+
"""
|
|
145
|
+
Get security groups for the Auto Scaling Group using standardized SSM imports.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of security group references
|
|
149
|
+
"""
|
|
161
150
|
security_groups = []
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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):
|
|
167
158
|
security_groups.append(
|
|
168
159
|
ec2.SecurityGroup.from_security_group_id(
|
|
169
|
-
self, f"SecurityGroup-{
|
|
160
|
+
self, f"SecurityGroup-SSM-{idx}", sg_id
|
|
170
161
|
)
|
|
171
162
|
)
|
|
163
|
+
logger.info(f"Added {len(imported_sg_ids)} security groups from SSM imports")
|
|
172
164
|
else:
|
|
173
|
-
# TODO: add some additional checks to make it more robust
|
|
174
165
|
security_groups.append(
|
|
175
166
|
ec2.SecurityGroup.from_security_group_id(
|
|
176
|
-
self, f"SecurityGroup-
|
|
167
|
+
self, f"SecurityGroup-SSM-0", imported_sg_ids
|
|
177
168
|
)
|
|
178
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
|
+
|
|
179
195
|
return security_groups
|
|
180
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
|
+
|
|
181
221
|
def _create_instance_role(self, asg_name: str) -> iam.Role:
|
|
182
222
|
"""Create IAM role for EC2 instances"""
|
|
183
223
|
role = iam.Role(
|
|
@@ -193,380 +233,298 @@ class AutoScalingStack(IStack, EnhancedSsmParameterMixin):
|
|
|
193
233
|
iam.ManagedPolicy.from_aws_managed_policy_name(policy_name)
|
|
194
234
|
)
|
|
195
235
|
|
|
196
|
-
|
|
197
|
-
for policy_config in self.asg_config.iam_inline_policies:
|
|
198
|
-
policy_name = policy_config.get("name", "CustomPolicy")
|
|
199
|
-
statements = policy_config.get("statements", [])
|
|
200
|
-
|
|
201
|
-
if not statements:
|
|
202
|
-
logger.warning(f"No statements found for inline policy {policy_name}, skipping")
|
|
203
|
-
continue
|
|
204
|
-
|
|
205
|
-
# Build policy statements
|
|
206
|
-
policy_statements = []
|
|
207
|
-
for stmt in statements:
|
|
208
|
-
effect = iam.Effect.ALLOW if stmt.get("effect", "Allow") == "Allow" else iam.Effect.DENY
|
|
209
|
-
actions = stmt.get("actions", [])
|
|
210
|
-
resources = stmt.get("resources", [])
|
|
211
|
-
|
|
212
|
-
if not actions or not resources:
|
|
213
|
-
logger.warning(f"Incomplete statement in policy {policy_name}, skipping")
|
|
214
|
-
continue
|
|
215
|
-
|
|
216
|
-
policy_statements.append(
|
|
217
|
-
iam.PolicyStatement(
|
|
218
|
-
effect=effect,
|
|
219
|
-
actions=actions,
|
|
220
|
-
resources=resources
|
|
221
|
-
)
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
if policy_statements:
|
|
225
|
-
role.add_to_principal_policy(policy_statements[0])
|
|
226
|
-
for stmt in policy_statements[1:]:
|
|
227
|
-
role.add_to_principal_policy(stmt)
|
|
228
|
-
|
|
229
|
-
logger.info(f"Added inline policy {policy_name} with {len(policy_statements)} statements")
|
|
230
|
-
|
|
236
|
+
logger.info(f"Created instance role: {role.role_name}")
|
|
231
237
|
return role
|
|
232
238
|
|
|
233
239
|
def _create_user_data(self) -> ec2.UserData:
|
|
234
240
|
"""Create user data for EC2 instances"""
|
|
235
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
|
+
)
|
|
236
249
|
|
|
237
|
-
# Add
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
self.
|
|
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)
|
|
252
285
|
|
|
286
|
+
logger.info(f"Created user data with {len(self.user_data_commands)} custom commands")
|
|
253
287
|
return user_data
|
|
254
288
|
|
|
255
|
-
def
|
|
256
|
-
"""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"""
|
|
261
|
-
from pathlib import Path
|
|
262
|
-
|
|
263
|
-
for script_config in self.asg_config.user_data_scripts:
|
|
264
|
-
script_type = script_config.get("type", "file")
|
|
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()
|
|
265
294
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# Resolve path (relative to project root or absolute)
|
|
274
|
-
path = Path(script_path)
|
|
275
|
-
if not path.is_absolute():
|
|
276
|
-
# Try relative to current working directory
|
|
277
|
-
path = Path.cwd() / script_path
|
|
278
|
-
|
|
279
|
-
if not path.exists():
|
|
280
|
-
logger.warning(f"Script file not found: {path}, skipping")
|
|
281
|
-
continue
|
|
282
|
-
|
|
283
|
-
# Read script content
|
|
284
|
-
try:
|
|
285
|
-
with open(path, 'r') as f:
|
|
286
|
-
script_content = f.read()
|
|
287
|
-
except Exception as e:
|
|
288
|
-
logger.error(f"Failed to read script file {path}: {e}")
|
|
289
|
-
continue
|
|
290
|
-
|
|
291
|
-
elif script_type == "inline":
|
|
292
|
-
# Use inline script content
|
|
293
|
-
script_content = script_config.get("content", "")
|
|
294
|
-
if not script_content:
|
|
295
|
-
logger.warning("Inline script content is empty, skipping")
|
|
296
|
-
continue
|
|
297
|
-
else:
|
|
298
|
-
logger.warning(f"Unknown script type: {script_type}, skipping")
|
|
299
|
-
continue
|
|
300
|
-
|
|
301
|
-
# Perform variable substitution
|
|
302
|
-
variables = script_config.get("variables", {})
|
|
303
|
-
for var_name, var_value in variables.items():
|
|
304
|
-
placeholder = f"{{{{{var_name}}}}}" # {{VAR_NAME}}
|
|
305
|
-
script_content = script_content.replace(placeholder, str(var_value))
|
|
306
|
-
|
|
307
|
-
# Add script to user data
|
|
308
|
-
# Split by lines and add each line as a command
|
|
309
|
-
for line in script_content.split('\n'):
|
|
310
|
-
if line.strip(): # Skip empty lines
|
|
311
|
-
user_data.add_commands(line)
|
|
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
|
+
)
|
|
312
301
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
313
|
+
|
|
314
|
+
return self._vpc
|
|
323
315
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
user_data.add_commands(
|
|
328
|
-
f"ACCOUNT_ID={ecr_config.get('account_id', self.account)}",
|
|
329
|
-
f"REGION={ecr_config.get('region', self.region)}",
|
|
330
|
-
f"REPO={ecr_config.get('repo', 'app')}",
|
|
331
|
-
f"TAG={ecr_config.get('tag', 'latest')}",
|
|
332
|
-
"aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com",
|
|
333
|
-
"docker pull ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:${TAG}",
|
|
334
|
-
)
|
|
316
|
+
def _get_subnets(self) -> List[ec2.Subnet]:
|
|
317
|
+
"""Get the subnets from the shared VPC"""
|
|
318
|
+
return getattr(self, '_subnets', [])
|
|
335
319
|
|
|
336
|
-
|
|
337
|
-
if "
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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)
|
|
326
|
+
|
|
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
|
|
345
340
|
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
user_data.add_commands(container_config["run_command"])
|
|
350
|
-
elif "ecr" in container_config:
|
|
351
|
-
port = container_config.get("port", 8080)
|
|
352
|
-
user_data.add_commands(
|
|
353
|
-
f"docker run -d --name app -p {port}:{port} "
|
|
354
|
-
'-e DB_HOST="$DB_HOST" -e DB_USER="$DB_USER" -e DB_PASS="$DB_PASS" -e DB_NAME="$DB_NAME" '
|
|
355
|
-
"--restart=always ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:${TAG}"
|
|
356
|
-
)
|
|
341
|
+
logger.info(f"Connected to existing ECS cluster: {cluster_name}")
|
|
342
|
+
|
|
343
|
+
return self.ecs_cluster
|
|
357
344
|
|
|
358
345
|
def _create_launch_template(self, asg_name: str) -> ec2.LaunchTemplate:
|
|
359
|
-
"""Create launch template for
|
|
360
|
-
|
|
361
|
-
|
|
346
|
+
"""Create launch template for Auto Scaling Group"""
|
|
347
|
+
|
|
348
|
+
# Use the configured AMI ID or fall back to appropriate lookup
|
|
362
349
|
if self.asg_config.ami_id:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if "." in instance_type_str:
|
|
377
|
-
parts = instance_type_str.split(".")
|
|
378
|
-
if len(parts) == 2:
|
|
379
|
-
try:
|
|
380
|
-
instance_class = ec2.InstanceClass[parts[0].upper()]
|
|
381
|
-
instance_size = ec2.InstanceSize[parts[1].upper()]
|
|
382
|
-
instance_type = ec2.InstanceType.of(instance_class, instance_size)
|
|
383
|
-
except (KeyError, ValueError):
|
|
384
|
-
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")
|
|
385
363
|
else:
|
|
386
|
-
|
|
364
|
+
# Default to latest Amazon Linux
|
|
365
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
387
366
|
else:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
block_devices = []
|
|
392
|
-
for device in self.asg_config.block_devices:
|
|
393
|
-
block_devices.append(
|
|
394
|
-
ec2.BlockDevice(
|
|
395
|
-
device_name=device.get("device_name", "/dev/xvda"),
|
|
396
|
-
volume=ec2.BlockDeviceVolume.ebs(
|
|
397
|
-
volume_size=device.get("volume_size", 8),
|
|
398
|
-
volume_type=ec2.EbsDeviceVolumeType(
|
|
399
|
-
str(device.get("volume_type", "gp3")).upper()
|
|
400
|
-
),
|
|
401
|
-
delete_on_termination=device.get("delete_on_termination", True),
|
|
402
|
-
encrypted=device.get("encrypted", True),
|
|
403
|
-
),
|
|
404
|
-
)
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
# Create launch template
|
|
367
|
+
# Default fallback
|
|
368
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
369
|
+
|
|
408
370
|
launch_template = ec2.LaunchTemplate(
|
|
409
371
|
self,
|
|
410
372
|
f"{asg_name}-LaunchTemplate",
|
|
411
|
-
|
|
412
|
-
|
|
373
|
+
instance_type=ec2.InstanceType(self.asg_config.instance_type),
|
|
374
|
+
machine_image=machine_image,
|
|
413
375
|
role=self.instance_role,
|
|
414
|
-
security_group=self.security_groups[0] if self.security_groups else None,
|
|
415
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,
|
|
416
379
|
detailed_monitoring=self.asg_config.detailed_monitoring,
|
|
417
|
-
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,
|
|
418
391
|
)
|
|
419
392
|
|
|
393
|
+
logger.info(f"Created launch template: {launch_template.launch_template_name}")
|
|
420
394
|
return launch_template
|
|
421
395
|
|
|
422
396
|
def _create_auto_scaling_group(self, asg_name: str) -> autoscaling.AutoScalingGroup:
|
|
423
|
-
"""Create
|
|
424
|
-
#
|
|
425
|
-
|
|
426
|
-
subnets =
|
|
427
|
-
|
|
428
|
-
# Configure health check
|
|
429
|
-
health_check_type = autoscaling.HealthCheck.ec2()
|
|
430
|
-
if self.asg_config.health_check_type.upper() == "ELB":
|
|
431
|
-
health_check_type = autoscaling.HealthCheck.elb(
|
|
432
|
-
grace=Duration.seconds(self.asg_config.health_check_grace_period)
|
|
433
|
-
)
|
|
397
|
+
"""Create Auto Scaling Group"""
|
|
398
|
+
# Use the shared VPC and subnets
|
|
399
|
+
vpc = self._get_or_create_vpc()
|
|
400
|
+
subnets = self._get_subnets()
|
|
434
401
|
|
|
435
|
-
|
|
436
|
-
asg = autoscaling.AutoScalingGroup(
|
|
402
|
+
auto_scaling_group = autoscaling.AutoScalingGroup(
|
|
437
403
|
self,
|
|
438
|
-
asg_name,
|
|
439
|
-
vpc=
|
|
440
|
-
vpc_subnets=subnets,
|
|
404
|
+
f"{asg_name}-ASG",
|
|
405
|
+
vpc=vpc,
|
|
406
|
+
vpc_subnets=ec2.SubnetSelection(subnets=subnets),
|
|
407
|
+
launch_template=self.launch_template,
|
|
441
408
|
min_capacity=self.asg_config.min_capacity,
|
|
442
409
|
max_capacity=self.asg_config.max_capacity,
|
|
443
410
|
desired_capacity=self.asg_config.desired_capacity,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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),
|
|
447
417
|
termination_policies=[
|
|
448
|
-
autoscaling.TerminationPolicy(
|
|
418
|
+
getattr(autoscaling.TerminationPolicy, policy.upper())
|
|
449
419
|
for policy in self.asg_config.termination_policies
|
|
450
420
|
],
|
|
451
421
|
)
|
|
452
422
|
|
|
453
|
-
# Attach
|
|
454
|
-
self._attach_target_groups(
|
|
455
|
-
|
|
456
|
-
# Configure update policy
|
|
457
|
-
# Only apply update policy if it was explicitly configured
|
|
458
|
-
if "update_policy" in self.stack_config.dictionary.get("auto_scaling", {}):
|
|
459
|
-
update_policy = self.asg_config.update_policy
|
|
460
|
-
# Apply the update policy to the ASG's CloudFormation resource
|
|
461
|
-
cfn_asg = asg.node.default_child
|
|
462
|
-
cfn_asg.add_override(
|
|
463
|
-
"UpdatePolicy",
|
|
464
|
-
{
|
|
465
|
-
"AutoScalingRollingUpdate": {
|
|
466
|
-
"MinInstancesInService": update_policy.get(
|
|
467
|
-
"min_instances_in_service", 1
|
|
468
|
-
),
|
|
469
|
-
"MaxBatchSize": update_policy.get("max_batch_size", 1),
|
|
470
|
-
"PauseTime": f"PT{update_policy.get('pause_time', 300) // 60}M",
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
)
|
|
423
|
+
# Attach target groups if configured
|
|
424
|
+
self._attach_target_groups(auto_scaling_group)
|
|
474
425
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
cdk.Tags.of(asg).add(key, value)
|
|
426
|
+
logger.info(f"Created Auto Scaling Group: {asg_name}")
|
|
427
|
+
return auto_scaling_group
|
|
478
428
|
|
|
479
|
-
|
|
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()
|
|
480
432
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
policy_type = policy.get("type", "target_tracking")
|
|
433
|
+
if not target_group_arns:
|
|
434
|
+
logger.warning("No target group ARNs found for Auto Scaling Group")
|
|
435
|
+
return
|
|
485
436
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
metric=self._get_metric(policy),
|
|
490
|
-
scaling_steps=self._get_scaling_steps(policy),
|
|
491
|
-
adjustment_type=autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
|
|
492
|
-
)
|
|
493
|
-
elif policy_type == "step":
|
|
494
|
-
self.auto_scaling_group.scale_on_metric(
|
|
495
|
-
f"{self.asg_config.name}-{policy.get('name', 'scaling-policy')}",
|
|
496
|
-
metric=self._get_metric(policy),
|
|
497
|
-
scaling_steps=self._get_scaling_steps(policy),
|
|
498
|
-
adjustment_type=autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
|
|
499
|
-
)
|
|
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)
|
|
500
440
|
|
|
501
|
-
def
|
|
502
|
-
"""Get
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
514
459
|
|
|
515
|
-
def
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
steps = policy.get("steps", [])
|
|
520
|
-
scaling_intervals = []
|
|
521
|
-
|
|
522
|
-
for step in steps:
|
|
523
|
-
# Handle upper bound - if not specified, don't set it (let CDK handle it)
|
|
524
|
-
interval_kwargs = {
|
|
525
|
-
"lower": step.get("lower", 0),
|
|
526
|
-
"change": step.get("change", 1),
|
|
527
|
-
}
|
|
460
|
+
def _add_scaling_policies(self) -> None:
|
|
461
|
+
"""Add scaling policies to the Auto Scaling Group"""
|
|
462
|
+
if not self.asg_config.scaling_policies:
|
|
463
|
+
return
|
|
528
464
|
|
|
529
|
-
|
|
530
|
-
if "
|
|
531
|
-
|
|
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")
|
|
532
481
|
|
|
533
|
-
|
|
482
|
+
def _add_update_policy(self) -> None:
|
|
483
|
+
"""Add update policy to the Auto Scaling Group"""
|
|
484
|
+
update_policy = self.asg_config.update_policy
|
|
485
|
+
|
|
486
|
+
if not update_policy:
|
|
487
|
+
# No update policy configured, don't add one
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# Get the underlying CloudFormation resource to add update policy
|
|
491
|
+
cfn_asg = self.auto_scaling_group.node.default_child
|
|
492
|
+
|
|
493
|
+
# Get CDK's default policy first (if any)
|
|
494
|
+
default_policy = getattr(cfn_asg, 'update_policy', {})
|
|
495
|
+
|
|
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
|
+
}
|
|
505
|
+
|
|
506
|
+
# Use the robust CDK-documented approach
|
|
507
|
+
cfn_asg.add_override("UpdatePolicy", merged_policy)
|
|
508
|
+
|
|
509
|
+
logger.info("Added rolling update policy to Auto Scaling Group")
|
|
534
510
|
|
|
535
|
-
|
|
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
|
|
536
516
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
self,
|
|
543
|
-
f"{asg_name}-name",
|
|
544
|
-
value=self.auto_scaling_group.auto_scaling_group_name,
|
|
545
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-name",
|
|
546
|
-
)
|
|
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
|
+
}
|
|
547
522
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
value=self.auto_scaling_group.auto_scaling_group_arn,
|
|
553
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-arn",
|
|
554
|
-
)
|
|
523
|
+
# Export using standardized SSM mixin
|
|
524
|
+
exported_params = self.export_ssm_parameters(resource_values)
|
|
525
|
+
|
|
526
|
+
logger.info(f"Exported SSM parameters: {exported_params}")
|
|
555
527
|
|
|
556
|
-
# Launch Template ID
|
|
557
|
-
if self.launch_template:
|
|
558
|
-
cdk.CfnOutput(
|
|
559
|
-
self,
|
|
560
|
-
f"{asg_name}-launch-template-id",
|
|
561
|
-
value=self.launch_template.launch_template_id,
|
|
562
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-launch-template-id",
|
|
563
|
-
)
|
|
564
528
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
cdk.CfnOutput(
|
|
568
|
-
self,
|
|
569
|
-
f"{asg_name}-instance-role-arn",
|
|
570
|
-
value=self.instance_role.role_arn,
|
|
571
|
-
export_name=f"{self.deployment.build_resource_name(asg_name)}-instance-role-arn",
|
|
572
|
-
)
|
|
529
|
+
# Backward compatibility alias
|
|
530
|
+
AutoScalingStackStandardized = AutoScalingStack
|