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.

Files changed (59) hide show
  1. cdk_factory/configurations/base_config.py +23 -24
  2. cdk_factory/configurations/cdk_config.py +1 -1
  3. cdk_factory/configurations/deployment.py +12 -0
  4. cdk_factory/configurations/devops.py +1 -1
  5. cdk_factory/configurations/resources/acm.py +9 -2
  6. cdk_factory/configurations/resources/auto_scaling.py +2 -5
  7. cdk_factory/configurations/resources/cloudfront.py +7 -2
  8. cdk_factory/configurations/resources/ecr.py +1 -1
  9. cdk_factory/configurations/resources/ecs_cluster.py +12 -5
  10. cdk_factory/configurations/resources/ecs_service.py +7 -2
  11. cdk_factory/configurations/resources/load_balancer.py +8 -9
  12. cdk_factory/configurations/resources/monitoring.py +8 -3
  13. cdk_factory/configurations/resources/rds.py +7 -8
  14. cdk_factory/configurations/resources/rum.py +7 -2
  15. cdk_factory/configurations/resources/s3.py +1 -1
  16. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  17. cdk_factory/configurations/resources/vpc.py +19 -0
  18. cdk_factory/configurations/workload.py +32 -2
  19. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  20. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  21. cdk_factory/interfaces/istack.py +4 -4
  22. cdk_factory/interfaces/networked_stack_mixin.py +6 -6
  23. cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
  24. cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
  25. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  26. cdk_factory/pipeline/pipeline_factory.py +3 -3
  27. cdk_factory/stack_library/__init__.py +3 -2
  28. cdk_factory/stack_library/acm/acm_stack.py +2 -2
  29. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  30. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +344 -535
  31. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
  32. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  33. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  34. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  35. cdk_factory/stack_library/ecs/__init__.py +1 -3
  36. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +157 -73
  37. cdk_factory/stack_library/ecs/ecs_service_stack.py +10 -26
  38. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
  39. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +96 -119
  40. cdk_factory/stack_library/rds/rds_stack.py +73 -73
  41. cdk_factory/stack_library/route53/route53_stack.py +2 -2
  42. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  43. cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
  44. cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
  45. cdk_factory/stack_library/stack_base.py +5 -0
  46. cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
  47. cdk_factory/stack_library/websites/static_website_stack.py +1 -1
  48. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  49. cdk_factory/utilities/environment_services.py +5 -5
  50. cdk_factory/utilities/json_loading_utility.py +1 -1
  51. cdk_factory/validation/config_validator.py +483 -0
  52. cdk_factory/version.py +1 -1
  53. {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
  54. {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +57 -57
  55. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  56. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
  57. {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
  58. {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
  59. {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="AutoScalingStack")
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
- Uses enhanced SsmParameterMixin (via IStack) to eliminate SSM code duplication.
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 class properly - IStack inherits from enhanced SsmParameterMixin
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
- # Process SSM imports using enhanced SsmParameterMixin
89
- self.process_ssm_imports(self.asg_config, deployment, "Auto Scaling Group")
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
- # Get security groups
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 user data
98
- self.user_data = self._create_user_data()
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
- # This must happen before launch template creation so user data can be updated
102
- self._create_ecs_cluster_if_needed(asg_name)
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
- # Add scheduled actions
114
- self._add_scheduled_actions()
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
- # see if we have any directly defined in the config
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
- return target_group_arns
151
-
152
- def _attach_target_groups(self, asg: autoscaling.AutoScalingGroup) -> None:
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
- """Get security groups for the Auto Scaling Group"""
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
- for sg_id in self.asg_config.security_group_ids:
171
- # if the security group id contains a comma, it is a list of security group ids
172
- if "," in sg_id:
173
- blocks = sg_id.split(",")
174
- for block in blocks:
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-{block}", block
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-{sg_id}", sg_id
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
- # Add inline policies (for custom permissions like S3 bucket access)
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
- # Store raw commands for ECS cluster detection
246
- self.user_data_commands = ["set -euxo pipefail"]
247
-
248
- # Add base commands
249
- user_data.add_commands("set -euxo pipefail")
250
-
251
- # Add custom commands from config (with variable substitution)
252
- for command in self.asg_config.user_data_commands:
253
- # Perform variable substitution on the command
254
- substituted_command = self._substitute_variables(command)
255
- user_data.add_commands(substituted_command)
256
- self.user_data_commands.append(substituted_command)
257
-
258
- # Add user data scripts from files (with variable substitution)
259
- if self.asg_config.user_data_scripts:
260
- self._add_user_data_scripts_from_files(user_data)
261
-
262
- # Add container configuration if specified
263
- container_config = self.asg_config.container_config
264
- if container_config:
265
- self._add_container_user_data(user_data, container_config)
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 _add_user_data_scripts_from_files(self, user_data: ec2.UserData) -> None:
270
- """
271
- Add user data scripts from external files with variable substitution.
272
- Supports loading shell scripts and injecting them into user data with
273
- placeholder replacement.
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
- # Add script to user data
322
- # Split by lines and add each line as a command
323
- for line in script_content.split('\n'):
324
- if line.strip(): # Skip empty lines
325
- user_data.add_commands(line)
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
- # Start with the original command
338
- substituted_command = command
339
-
340
- # Define available variables for substitution
341
- variables = {}
342
-
343
- # Add workload variables
344
- if self.workload:
345
- variables.update({
346
- "WORKLOAD_NAME": getattr(self.workload, 'name', ''),
347
- "ENVIRONMENT": getattr(self.workload, 'environment', ''),
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
- # Perform substitution
365
- for var_name, var_value in variables.items():
366
- if var_value is not None:
367
- placeholder = f"{{{{{var_name}}}}}" # {{VAR_NAME}}
368
- substituted_command = substituted_command.replace(placeholder, str(var_value))
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
- return substituted_command
371
-
372
- def _add_container_user_data(
373
- self, user_data: ec2.UserData, container_config: Dict[str, Any]
374
- ) -> None:
375
- """Add container-specific user data commands"""
376
- # Install Docker
377
- user_data.add_commands(
378
- "dnf -y update", "dnf -y install docker jq", "systemctl enable --now docker"
379
- )
380
-
381
- # ECR configuration
382
- if "ecr" in container_config:
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
- # Run container
405
- if "run_command" in container_config:
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 the Auto Scaling Group"""
417
- # Get AMI
418
- ami = None
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
- ami = ec2.MachineImage.generic_linux({self.region: self.asg_config.ami_id})
421
- else:
422
- if self.asg_config.ami_type == "amazon-linux-2023":
423
- ami = ec2.MachineImage.latest_amazon_linux2023()
424
- elif self.asg_config.ami_type == "amazon-linux-2":
425
- ami = ec2.MachineImage.latest_amazon_linux2()
426
- else:
427
- ami = ec2.MachineImage.latest_amazon_linux2023()
428
-
429
- # Parse instance type
430
- instance_type_str = self.asg_config.instance_type
431
- instance_type = None
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
- instance_type = ec2.InstanceType(instance_type_str)
364
+ # Default to latest Amazon Linux
365
+ machine_image = ec2.MachineImage.latest_amazon_linux2023()
444
366
  else:
445
- instance_type = ec2.InstanceType(instance_type_str)
446
-
447
- # Create block device mappings
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
- machine_image=ami,
469
- instance_type=instance_type,
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=block_devices if block_devices else None,
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 the Auto Scaling Group"""
481
- # Configure subnet selection
482
- subnet_group_name = self.asg_config.subnet_group_name
483
- subnets = ec2.SubnetSelection(subnet_group_name=subnet_group_name)
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
- # Create Auto Scaling Group
493
- asg = autoscaling.AutoScalingGroup(
402
+ auto_scaling_group = autoscaling.AutoScalingGroup(
494
403
  self,
495
- asg_name,
496
- vpc=self.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
- launch_template=self.launch_template,
502
- health_check=health_check_type,
503
- cooldown=Duration.seconds(self.asg_config.cooldown),
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(policy)
418
+ getattr(autoscaling.TerminationPolicy, policy.upper())
506
419
  for policy in self.asg_config.termination_policies
507
420
  ],
508
421
  )
509
422
 
510
- # Attach to target groups after ASG creation
511
- self._attach_target_groups(asg)
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
- scaling_intervals.append(autoscaling.ScalingInterval(**interval_kwargs))
426
+ logger.info(f"Created Auto Scaling Group: {asg_name}")
427
+ return auto_scaling_group
591
428
 
592
- return scaling_intervals
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
- def _add_outputs(self, asg_name: str) -> None:
595
- """Add CloudFormation outputs for the Auto Scaling Group"""
596
- if self.auto_scaling_group:
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
- # Auto Scaling Group ARN
606
- cdk.CfnOutput(
607
- self,
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
- # Launch Template ID
614
- if self.launch_template:
615
- cdk.CfnOutput(
616
- self,
617
- f"{asg_name}-launch-template-id",
618
- value=self.launch_template.launch_template_id,
619
- export_name=f"{self.deployment.build_resource_name(asg_name)}-launch-template-id",
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
- for policy_config in self.asg_config.scaling_policies:
626
- # Scaling policy implementation would go here
627
- pass
462
+ if not self.asg_config.scaling_policies:
463
+ return
628
464
 
629
- def _add_scheduled_actions(self) -> None:
630
- """Add scheduled actions to the Auto Scaling Group"""
631
- for action_config in self.asg_config.scheduled_actions:
632
- # Scheduled action implementation would go here
633
- pass
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 _create_ecs_cluster_if_needed(self, asg_name: str):
636
- """
637
- ECS cluster creation should be handled by the dedicated EcsClusterStack module.
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
- logger.warning(
649
- "No ECS cluster name found in SSM imports. "
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
- injected_commands = []
664
- cluster_name_injected = False
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
- # If no ECS_CLUSTER was found in existing commands, add it
686
- if not cluster_name_injected:
687
- injected_commands.append(f"echo ECS_CLUSTER={cluster_name} >> /etc/ecs/ecs.config")
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
- # Update the user data with the injected commands
690
- self.user_data_commands = injected_commands
506
+ # Use the robust CDK-documented approach
507
+ cfn_asg.add_override("UpdatePolicy", merged_policy)
691
508
 
692
- # If user data object exists, we need to recreate it with the updated commands
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 _recreate_user_data_with_commands(self, commands: List[str]) -> ec2.UserData:
697
- """Recreate user data with updated commands"""
698
- user_data = ec2.UserData.for_linux()
699
-
700
- for command in commands:
701
- user_data.add_commands(command)
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
- return user_data
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
- # Export ASG ARN
716
- cdk.CfnOutput(
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