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.

Files changed (63) hide show
  1. cdk_factory/configurations/base_config.py +23 -24
  2. cdk_factory/configurations/cdk_config.py +6 -4
  3. cdk_factory/configurations/deployment.py +12 -0
  4. cdk_factory/configurations/devops.py +1 -1
  5. cdk_factory/configurations/pipeline_stage.py +29 -5
  6. cdk_factory/configurations/resources/acm.py +85 -0
  7. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  8. cdk_factory/configurations/resources/cloudfront.py +7 -2
  9. cdk_factory/configurations/resources/ecr.py +1 -1
  10. cdk_factory/configurations/resources/ecs_cluster.py +108 -0
  11. cdk_factory/configurations/resources/ecs_service.py +17 -2
  12. cdk_factory/configurations/resources/load_balancer.py +17 -4
  13. cdk_factory/configurations/resources/monitoring.py +8 -3
  14. cdk_factory/configurations/resources/rds.py +305 -19
  15. cdk_factory/configurations/resources/rum.py +7 -2
  16. cdk_factory/configurations/resources/s3.py +1 -1
  17. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  18. cdk_factory/configurations/resources/vpc.py +19 -0
  19. cdk_factory/configurations/workload.py +32 -2
  20. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  21. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  22. cdk_factory/interfaces/istack.py +6 -3
  23. cdk_factory/interfaces/networked_stack_mixin.py +75 -0
  24. cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
  25. cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
  26. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  27. cdk_factory/pipeline/pipeline_factory.py +222 -27
  28. cdk_factory/stack/stack_factory.py +34 -0
  29. cdk_factory/stack_library/__init__.py +3 -2
  30. cdk_factory/stack_library/acm/__init__.py +6 -0
  31. cdk_factory/stack_library/acm/acm_stack.py +169 -0
  32. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  33. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
  34. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
  35. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  36. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  37. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  38. cdk_factory/stack_library/ecs/__init__.py +12 -0
  39. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
  40. cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
  41. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
  42. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
  43. cdk_factory/stack_library/rds/rds_stack.py +85 -74
  44. cdk_factory/stack_library/route53/route53_stack.py +8 -3
  45. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  46. cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
  47. cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
  48. cdk_factory/stack_library/stack_base.py +5 -0
  49. cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
  50. cdk_factory/stack_library/websites/static_website_stack.py +1 -1
  51. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  52. cdk_factory/utilities/environment_services.py +5 -5
  53. cdk_factory/utilities/json_loading_utility.py +12 -3
  54. cdk_factory/validation/config_validator.py +483 -0
  55. cdk_factory/version.py +1 -1
  56. cdk_factory/workload/workload_factory.py +1 -0
  57. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
  58. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
  59. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  60. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
  61. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
  62. {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
  63. {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 Duration
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.enhanced_ssm_parameter_mixin import EnhancedSsmParameterMixin
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="AutoScalingStack")
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, EnhancedSsmParameterMixin):
34
+ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
33
35
  """
34
- Reusable stack for AWS Auto Scaling Groups.
35
- Supports creating EC2 Auto Scaling Groups with customizable configurations.
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._vpc = None
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
- # Get VPC and security groups
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 user data
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 auto scaling group
125
+ # Create Auto Scaling Group
89
126
  self.auto_scaling_group = self._create_auto_scaling_group(asg_name)
90
127
 
91
- # Configure scaling policies
92
- self._configure_scaling_policies()
93
-
94
- # Add outputs
95
- self._add_outputs(asg_name)
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
- # Look for target group ARN imports
132
- for key, value in imported_values.items():
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
- # see if we have any directly defined in the config
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
- return target_group_arns
143
-
144
- def _attach_target_groups(self, asg: autoscaling.AutoScalingGroup) -> None:
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
- """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
+ """
161
150
  security_groups = []
162
- for sg_id in self.asg_config.security_group_ids:
163
- # if the security group id contains a comma, it is a list of security group ids
164
- if "," in sg_id:
165
- blocks = sg_id.split(",")
166
- 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):
167
158
  security_groups.append(
168
159
  ec2.SecurityGroup.from_security_group_id(
169
- self, f"SecurityGroup-{block}", block
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-{sg_id}", sg_id
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
- # Add inline policies (for custom permissions like S3 bucket access)
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 base commands
238
- user_data.add_commands("set -euxo pipefail")
239
-
240
- # Add custom commands from config
241
- for command in self.asg_config.user_data_commands:
242
- user_data.add_commands(command)
243
-
244
- # Add user data scripts from files (with variable substitution)
245
- if self.asg_config.user_data_scripts:
246
- self._add_user_data_scripts_from_files(user_data)
247
-
248
- # Add container configuration if specified
249
- container_config = self.asg_config.container_config
250
- if container_config:
251
- 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)
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 _add_user_data_scripts_from_files(self, user_data: ec2.UserData) -> None:
256
- """
257
- Add user data scripts from external files with variable substitution.
258
- Supports loading shell scripts and injecting them into user data with
259
- placeholder replacement.
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
- if script_type == "file":
267
- # Load script from file
268
- script_path = script_config.get("path")
269
- if not script_path:
270
- logger.warning("Script path not specified, skipping")
271
- continue
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
- logger.info(f"Added user data script from {script_type}: {script_config.get('path', 'inline')}")
314
-
315
- def _add_container_user_data(
316
- self, user_data: ec2.UserData, container_config: Dict[str, Any]
317
- ) -> None:
318
- """Add container-specific user data commands"""
319
- # Install Docker
320
- user_data.add_commands(
321
- "dnf -y update", "dnf -y install docker jq", "systemctl enable --now docker"
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
- # ECR configuration
325
- if "ecr" in container_config:
326
- ecr_config = container_config["ecr"]
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
- # Database configuration
337
- if "database" in container_config:
338
- db_config = container_config["database"]
339
- secret_arn = db_config.get("secret_arn", "")
340
- if secret_arn:
341
- user_data.add_commands(
342
- f"DB_SECRET_ARN={secret_arn}",
343
- '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',
344
- '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',
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
- # Run container
348
- if "run_command" in container_config:
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 the Auto Scaling Group"""
360
- # Get AMI
361
- ami = None
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
- ami = ec2.MachineImage.generic_linux({self.region: self.asg_config.ami_id})
364
- else:
365
- if self.asg_config.ami_type == "amazon-linux-2023":
366
- ami = ec2.MachineImage.latest_amazon_linux2023()
367
- elif self.asg_config.ami_type == "amazon-linux-2":
368
- ami = ec2.MachineImage.latest_amazon_linux2()
369
- else:
370
- ami = ec2.MachineImage.latest_amazon_linux2023()
371
-
372
- # Parse instance type
373
- instance_type_str = self.asg_config.instance_type
374
- instance_type = None
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
- instance_type = ec2.InstanceType(instance_type_str)
364
+ # Default to latest Amazon Linux
365
+ machine_image = ec2.MachineImage.latest_amazon_linux2023()
387
366
  else:
388
- instance_type = ec2.InstanceType(instance_type_str)
389
-
390
- # Create block device mappings
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
- machine_image=ami,
412
- instance_type=instance_type,
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=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,
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 the Auto Scaling Group"""
424
- # Configure subnet selection
425
- subnet_group_name = self.asg_config.subnet_group_name
426
- subnets = ec2.SubnetSelection(subnet_group_name=subnet_group_name)
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
- # Create Auto Scaling Group
436
- asg = autoscaling.AutoScalingGroup(
402
+ auto_scaling_group = autoscaling.AutoScalingGroup(
437
403
  self,
438
- asg_name,
439
- vpc=self.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
- launch_template=self.launch_template,
445
- health_check=health_check_type,
446
- 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),
447
417
  termination_policies=[
448
- autoscaling.TerminationPolicy(policy)
418
+ getattr(autoscaling.TerminationPolicy, policy.upper())
449
419
  for policy in self.asg_config.termination_policies
450
420
  ],
451
421
  )
452
422
 
453
- # Attach to target groups after ASG creation
454
- self._attach_target_groups(asg)
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
- # Add tags
476
- for key, value in self.asg_config.tags.items():
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
- return asg
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
- def _configure_scaling_policies(self) -> None:
482
- """Configure scaling policies for the Auto Scaling Group"""
483
- for policy in self.asg_config.scaling_policies:
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
- if policy_type == "target_tracking":
487
- self.auto_scaling_group.scale_on_metric(
488
- f"{self.asg_config.name}-{policy.get('name', 'scaling-policy')}",
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 _get_metric(self, policy: Dict[str, Any]) -> cloudwatch.Metric:
502
- """Get metric for scaling policy"""
503
- # This is a simplified implementation
504
- # In a real-world scenario, you would use CloudWatch metrics
505
- return cloudwatch.Metric(
506
- namespace="AWS/EC2",
507
- metric_name=policy.get("metric_name", "CPUUtilization"),
508
- dimensions_map={
509
- "AutoScalingGroupName": self.auto_scaling_group.auto_scaling_group_name
510
- },
511
- statistic=policy.get("statistic", "Average"),
512
- period=Duration.seconds(policy.get("period", 60)),
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 _get_scaling_steps(
516
- self, policy: Dict[str, Any]
517
- ) -> List[autoscaling.ScalingInterval]:
518
- """Get scaling steps for scaling policy"""
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
- # Only set upper if it's explicitly provided
530
- if "upper" in step:
531
- interval_kwargs["upper"] = step["upper"]
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
- scaling_intervals.append(autoscaling.ScalingInterval(**interval_kwargs))
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
- return scaling_intervals
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
- def _add_outputs(self, asg_name: str) -> None:
538
- """Add CloudFormation outputs for the Auto Scaling Group"""
539
- if self.auto_scaling_group:
540
- # Auto Scaling Group Name
541
- cdk.CfnOutput(
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
- # Auto Scaling Group ARN
549
- cdk.CfnOutput(
550
- self,
551
- f"{asg_name}-arn",
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
- # Instance Role ARN
566
- if self.instance_role:
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