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
@@ -6,6 +6,8 @@ MIT License. See Project Root for the license information.
6
6
 
7
7
  from typing import Dict, Any, List, Optional
8
8
 
9
+ import base64
10
+ import hashlib
9
11
  import aws_cdk as cdk
10
12
  from aws_cdk import aws_elasticloadbalancingv2 as elbv2
11
13
  from aws_cdk import aws_ec2 as ec2
@@ -19,7 +21,8 @@ from cdk_factory.configurations.deployment import DeploymentConfig
19
21
  from cdk_factory.configurations.stack import StackConfig
20
22
  from cdk_factory.configurations.resources.load_balancer import LoadBalancerConfig
21
23
  from cdk_factory.interfaces.istack import IStack
22
- 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
23
26
  from cdk_factory.stack.stack_module_registry import register_stack
24
27
  from cdk_factory.workload.workload_factory import WorkloadConfig
25
28
 
@@ -30,7 +33,7 @@ logger = Logger(service="LoadBalancerStack")
30
33
  @register_stack("alb_stack")
31
34
  @register_stack("load_balancer_library_module")
32
35
  @register_stack("load_balancer_stack")
33
- class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
36
+ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
34
37
  """
35
38
  Reusable stack for AWS Load Balancers.
36
39
  Supports creating Application and Network Load Balancers with customizable configurations.
@@ -49,7 +52,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
49
52
  self._hosted_zone = None
50
53
  self._record_names = None
51
54
  # SSM imported values
52
- self.ssm_imported_values: Dict[str, str] = {}
55
+ self._ssm_imported_values: Dict[str, str] = {}
53
56
 
54
57
  def build(
55
58
  self,
@@ -76,8 +79,18 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
76
79
  )
77
80
  lb_name = deployment.build_resource_name(self.lb_config.name)
78
81
 
79
- # Process SSM imports first
80
- self._process_ssm_imports()
82
+ # Setup standardized SSM integration
83
+ self.setup_ssm_integration(
84
+ scope=self,
85
+ config=self.lb_config,
86
+ resource_type="load_balancer",
87
+ resource_name=self.lb_config.name,
88
+ deployment=deployment,
89
+ workload=workload
90
+ )
91
+
92
+ # Process SSM imports
93
+ self.process_ssm_imports()
81
94
 
82
95
  self._prep_dns()
83
96
 
@@ -155,16 +168,22 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
155
168
 
156
169
  # If subnets is None, check if we have SSM-imported subnet_ids as a token
157
170
  # We need to use Fn.Split to convert the comma-separated string to an array
158
- if subnets is None and "subnet_ids" in self.ssm_imported_values:
159
- subnet_ids_value = self.ssm_imported_values["subnet_ids"]
160
- if cdk.Token.is_unresolved(subnet_ids_value):
161
- logger.info("Using Fn.Split to convert comma-separated subnet IDs token to array")
162
- # Use CloudFormation escape hatch to set Subnets property with Fn.Split
163
- cfn_lb = load_balancer.node.default_child
164
- cfn_lb.add_property_override(
165
- "Subnets",
166
- cdk.Fn.split(",", subnet_ids_value)
167
- )
171
+ if subnets is None:
172
+ subnet_ids = self.get_subnet_ids(self.lb_config)
173
+ if subnet_ids:
174
+ # For CloudFormation token resolution, we still need Fn.split
175
+ # but we use the helper to determine if subnet IDs are available
176
+ ssm_imports = self.get_all_ssm_imports()
177
+ if "subnet_ids" in ssm_imports:
178
+ subnet_ids_value = ssm_imports["subnet_ids"]
179
+ if cdk.Token.is_unresolved(subnet_ids_value):
180
+ logger.info("Using Fn.Split to convert comma-separated subnet IDs token to array")
181
+ # Use CloudFormation escape hatch to set Subnets property with Fn.Split
182
+ cfn_lb = load_balancer.node.default_child
183
+ cfn_lb.add_property_override(
184
+ "Subnets",
185
+ cdk.Fn.split(",", subnet_ids_value)
186
+ )
168
187
 
169
188
  # Add tags
170
189
  for key, value in self.lb_config.tags.items():
@@ -174,100 +193,25 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
174
193
 
175
194
  @property
176
195
  def vpc(self) -> ec2.IVpc:
177
- """Get the VPC for the Load Balancer"""
196
+ """Get the VPC for the Load Balancer using centralized VPC provider mixin."""
178
197
  if self._vpc:
179
198
  return self._vpc
180
-
181
- # Check SSM imported values first (tokens from SSM parameters)
182
- if "vpc_id" in self.ssm_imported_values:
183
- vpc_id = self.ssm_imported_values["vpc_id"]
184
-
185
- # Build VPC attributes
186
- vpc_attrs = {
187
- "vpc_id": vpc_id,
188
- "availability_zones": ["us-east-1a", "us-east-1b"],
189
- }
190
-
191
- # If we have subnet_ids from SSM, provide dummy public subnets
192
- # The actual subnets will be set via CloudFormation escape hatch
193
- if "subnet_ids" in self.ssm_imported_values:
194
- # Provide dummy subnet IDs - these will be overridden by the escape hatch
195
- # We need at least one dummy subnet per AZ to satisfy CDK's validation
196
- vpc_attrs["public_subnet_ids"] = ["subnet-dummy1", "subnet-dummy2"]
197
-
198
- # Use from_vpc_attributes() instead of from_lookup() because SSM imports return tokens
199
- self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
200
- elif self.lb_config.vpc_id:
201
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.lb_config.vpc_id)
202
- elif self.workload.vpc_id:
203
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
204
- else:
205
- # Use default VPC if not provided
206
- raise ValueError(
207
- "VPC is not defined in the configuration. "
208
- "You can provide it a the load_balancer.vpc_id in the configuration "
209
- "or a top level workload.vpc_id in the workload configuration."
210
- )
211
-
212
- return self._vpc
213
-
214
- def _process_ssm_imports(self) -> None:
215
- """
216
- Process SSM imports from configuration.
217
- Follows the same pattern as RDS and Security Group stacks.
218
- """
219
- from aws_cdk import aws_ssm as ssm
220
-
221
- ssm_imports = self.lb_config.ssm_imports
222
199
 
223
- if not ssm_imports:
224
- logger.debug("No SSM imports configured for Load Balancer")
225
- return
226
-
227
- logger.info(f"Processing {len(ssm_imports)} SSM imports for Load Balancer")
228
-
229
- for param_key, param_value in ssm_imports.items():
230
- try:
231
- # Handle list values (like security_groups)
232
- if isinstance(param_value, list):
233
- imported_list = []
234
- for idx, param_path in enumerate(param_value):
235
- if not param_path.startswith('/'):
236
- param_path = f"/{param_path}"
237
-
238
- construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
239
- param = ssm.StringParameter.from_string_parameter_name(
240
- self, construct_id, param_path
241
- )
242
- imported_list.append(param.string_value)
243
-
244
- self.ssm_imported_values[param_key] = imported_list
245
- logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
246
- else:
247
- # Handle string values
248
- param_path = param_value
249
- if not param_path.startswith('/'):
250
- param_path = f"/{param_path}"
251
-
252
- construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
253
- param = ssm.StringParameter.from_string_parameter_name(
254
- self, construct_id, param_path
255
- )
256
-
257
- self.ssm_imported_values[param_key] = param.string_value
258
- logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
259
-
260
- except Exception as e:
261
- logger.error(f"Failed to import SSM parameter {param_key}: {e}")
262
- raise
200
+ # Use the centralized VPC resolution from VPCProviderMixin
201
+ self._vpc = self.resolve_vpc(
202
+ config=self.lb_config,
203
+ deployment=self.deployment,
204
+ workload=self.workload
205
+ )
206
+ return self._vpc
263
207
 
264
208
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
265
209
  """Get security groups for the Load Balancer"""
266
210
  security_groups = []
267
211
 
268
212
  # Check SSM imported values first
269
- if "security_groups" in self.ssm_imported_values:
270
- sg_ids = self.ssm_imported_values["security_groups"]
213
+ if "security_groups" in self._ssm_imported_values:
214
+ sg_ids = self._ssm_imported_values["security_groups"]
271
215
  if not isinstance(sg_ids, list):
272
216
  sg_ids = [sg_ids]
273
217
  else:
@@ -285,9 +229,16 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
285
229
  """Get subnets for the Load Balancer"""
286
230
  subnets = []
287
231
 
288
- # Check SSM imported values first
289
- if "subnet_ids" in self.ssm_imported_values:
290
- subnet_ids_value = self.ssm_imported_values["subnet_ids"]
232
+ # Use the standardized helper function to get subnet IDs
233
+ subnet_ids = self.get_subnet_ids(self.lb_config)
234
+
235
+ if not subnet_ids:
236
+ return None
237
+
238
+ # Check if we have unresolved tokens from SSM
239
+ ssm_imports = self.get_all_ssm_imports()
240
+ if "subnet_ids" in ssm_imports:
241
+ subnet_ids_value = ssm_imports["subnet_ids"]
291
242
 
292
243
  # Check if this is a CDK token (unresolved SSM parameter)
293
244
  if cdk.Token.is_unresolved(subnet_ids_value):
@@ -296,25 +247,40 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
296
247
  # The ALB construct will handle the token-based subnet IDs
297
248
  logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
298
249
  return None
299
- elif isinstance(subnet_ids_value, str):
300
- # If it's a resolved string, split it
301
- subnet_ids = [s.strip() for s in subnet_ids_value.split(',')]
302
- elif isinstance(subnet_ids_value, list):
303
- subnet_ids = subnet_ids_value
304
- else:
305
- subnet_ids = [subnet_ids_value]
306
- else:
307
- subnet_ids = self.lb_config.subnets
308
-
309
- if not subnet_ids:
310
- return None
311
250
 
251
+ # Convert subnet IDs to subnet objects
312
252
  for idx, subnet_id in enumerate(subnet_ids):
313
253
  subnets.append(
314
254
  ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
315
255
  )
316
256
  return subnets
317
257
 
258
+ def _generate_target_group_name(self, lb_name: str, tg_name: str, max_length: int = 32) -> str:
259
+ """Generate a unique target group name that doesn't begin/end with hyphens"""
260
+ full_name = f"{lb_name}-{tg_name}"
261
+
262
+ if len(full_name) <= max_length:
263
+ # No truncation needed, just ensure no leading/trailing hyphens
264
+ return full_name.strip('-')
265
+
266
+ # Need to truncate - use hash suffix for uniqueness
267
+ # Reserve space for hash (typically 8 chars) and separator
268
+ hash_length = 8
269
+ separator_length = 1
270
+ max_name_length = max_length - hash_length - separator_length
271
+
272
+ # Take the prefix and ensure it doesn't end with hyphen
273
+ prefix = full_name[:max_name_length].rstrip('-')
274
+
275
+ # Generate hash of the full name for uniqueness
276
+ hash_bytes = hashlib.sha256(full_name.encode()).digest()
277
+ hash_suffix = base64.urlsafe_b64encode(hash_bytes).decode()[:hash_length]
278
+
279
+ # Ensure hash doesn't start with hyphen (replace any non-alphanumeric chars)
280
+ hash_suffix = ''.join(c for c in hash_suffix if c.isalnum())[:hash_length]
281
+
282
+ return f"{prefix}-{hash_suffix}"
283
+
318
284
  def _create_target_groups(self, lb_name: str) -> None:
319
285
  """Create target groups for the Load Balancer"""
320
286
 
@@ -322,6 +288,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
322
288
  tg_name = tg_config.get("name", f"tg-{idx}")
323
289
  tg_id = f"{lb_name}-{tg_name}"
324
290
 
291
+ # Generate a unique target group name that doesn't begin/end with hyphens
292
+ tg_name_sanitized = self._generate_target_group_name(lb_name, tg_name)
293
+
325
294
  # Configure health check
326
295
  health_check = self._configure_health_check(
327
296
  tg_config.get("health_check", {})
@@ -332,7 +301,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
332
301
  target_group = elbv2.ApplicationTargetGroup(
333
302
  self,
334
303
  tg_id,
335
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
304
+ target_group_name=tg_name_sanitized,
336
305
  vpc=self.vpc,
337
306
  port=tg_config.get("port", 80),
338
307
  protocol=elbv2.ApplicationProtocol(
@@ -347,7 +316,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
347
316
  target_group = elbv2.NetworkTargetGroup(
348
317
  self,
349
318
  tg_id,
350
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
319
+ target_group_name=tg_name_sanitized,
351
320
  vpc=self.vpc,
352
321
  port=tg_config.get("port", 80),
353
322
  protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
@@ -357,6 +326,8 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
357
326
  health_check=health_check,
358
327
  )
359
328
 
329
+
330
+
360
331
  # Store target group for later use
361
332
  self.target_groups[tg_name] = target_group
362
333
 
@@ -397,6 +368,11 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
397
368
  if protocol.upper() == "HTTPS":
398
369
  certificates = self._get_certificates()
399
370
 
371
+ if not certificates and protocol.upper() == "HTTPS":
372
+ message = "No certificates found for HTTPS listener. Please attach a certificate or create a certificate stack."
373
+ logger.warning(message)
374
+ raise ValueError(message)
375
+
400
376
  listener = elbv2.ApplicationListener(
401
377
  self,
402
378
  listener_id,
@@ -449,8 +425,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
449
425
  certificates = []
450
426
 
451
427
  # Check SSM imported values first (takes priority)
452
- if "certificate_arns" in self.ssm_imported_values:
453
- cert_arns = self.ssm_imported_values["certificate_arns"]
428
+ ssm_imports = self.get_all_ssm_imports()
429
+ if "certificate_arns" in ssm_imports:
430
+ cert_arns = ssm_imports["certificate_arns"]
454
431
  if not isinstance(cert_arns, list):
455
432
  cert_arns = [cert_arns]
456
433
  for cert_arn in cert_arns:
@@ -18,7 +18,8 @@ from cdk_factory.configurations.deployment import DeploymentConfig
18
18
  from cdk_factory.configurations.stack import StackConfig
19
19
  from cdk_factory.configurations.resources.rds import RdsConfig
20
20
  from cdk_factory.interfaces.istack import IStack
21
- from cdk_factory.interfaces.enhanced_ssm_parameter_mixin import EnhancedSsmParameterMixin
21
+ from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
22
+ from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
22
23
  from cdk_factory.stack.stack_module_registry import register_stack
23
24
  from cdk_factory.workload.workload_factory import WorkloadConfig
24
25
 
@@ -27,7 +28,7 @@ logger = Logger(service="RdsStack")
27
28
 
28
29
  @register_stack("rds_library_module")
29
30
  @register_stack("rds_stack")
30
- class RdsStack(IStack, EnhancedSsmParameterMixin):
31
+ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
31
32
  """
32
33
  Reusable stack for AWS RDS.
33
34
  Supports creating RDS instances with customizable configurations.
@@ -68,8 +69,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
68
69
  self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
69
70
  db_name = deployment.build_resource_name(self.rds_config.name)
70
71
 
71
- # Process SSM imports first
72
- self._process_ssm_imports()
72
+ # Setup standardized SSM integration
73
+ self.setup_ssm_integration(
74
+ scope=self,
75
+ config=self.rds_config,
76
+ resource_type="rds",
77
+ resource_name=self.rds_config.name,
78
+ deployment=deployment,
79
+ workload=workload
80
+ )
81
+
82
+ # Process SSM imports
83
+ self.process_ssm_imports()
73
84
 
74
85
  # Get VPC and security groups
75
86
  self.security_groups = self._get_security_groups()
@@ -86,63 +97,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
86
97
  # Export to SSM Parameter Store
87
98
  self._export_ssm_parameters(db_name)
88
99
 
89
- def _process_ssm_imports(self) -> None:
90
- """Process SSM imports from configuration"""
91
- ssm_imports = self.rds_config.ssm_imports
92
-
93
- if not ssm_imports:
94
- logger.debug("No SSM imports configured for RDS")
95
- return
96
-
97
- logger.info(f"Processing {len(ssm_imports)} SSM imports for RDS")
98
-
99
- for param_key, param_path in ssm_imports.items():
100
- try:
101
- if not param_path.startswith('/'):
102
- param_path = f"/{param_path}"
103
-
104
- construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
105
- param = ssm.StringParameter.from_string_parameter_name(
106
- self, construct_id, param_path
107
- )
108
-
109
- self.ssm_imported_values[param_key] = param.string_value
110
- logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
111
-
112
- except Exception as e:
113
- logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
114
- raise
115
-
116
100
  @property
117
101
  def vpc(self) -> ec2.IVpc:
118
- """Get the VPC for the RDS instance"""
119
- if self._vpc:
102
+ """Get the VPC for the RDS instance using centralized VPC provider mixin."""
103
+ if hasattr(self, '_vpc') and self._vpc:
120
104
  return self._vpc
121
105
 
122
- # Check SSM imported values first (tokens from SSM parameters)
123
- if "vpc_id" in self.ssm_imported_values:
124
- vpc_id = self.ssm_imported_values["vpc_id"]
125
-
126
- # When using tokens, we can't provide subnet lists to from_vpc_attributes
127
- # because CDK validates subnet count against AZ count at synthesis time
128
- # We'll create a DB subnet group separately instead
129
- vpc_attrs = {
130
- "vpc_id": vpc_id,
131
- "availability_zones": ["us-east-1a", "us-east-1b"]
132
- }
133
-
134
- # Use from_vpc_attributes() for SSM tokens
135
- self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
136
- elif self.rds_config.vpc_id:
137
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.rds_config.vpc_id)
138
- elif self.workload.vpc_id:
139
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
140
- else:
141
- raise ValueError(
142
- "VPC is not defined in the configuration. "
143
- "You can provide it a the rds.vpc_id in the configuration "
144
- "or a top level workload.vpc_id in the workload configuration."
145
- )
106
+ # Resolve VPC using the centralized VPC provider mixin
107
+ self._vpc = self.resolve_vpc(
108
+ config=self.rds_config,
109
+ deployment=self.deployment,
110
+ workload=self.workload
111
+ )
146
112
  return self._vpc
147
113
 
148
114
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
@@ -150,8 +116,9 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
150
116
  security_groups = []
151
117
 
152
118
  # Check SSM imports first for security group ID
153
- if "security_group_rds_id" in self.ssm_imported_values:
154
- sg_id = self.ssm_imported_values["security_group_rds_id"]
119
+ ssm_imports = self.get_all_ssm_imports()
120
+ if "security_group_rds_id" in ssm_imports:
121
+ sg_id = ssm_imports["security_group_rds_id"]
155
122
  security_groups.append(
156
123
  ec2.SecurityGroup.from_security_group_id(
157
124
  self, "RDSSecurityGroup", sg_id
@@ -168,27 +135,60 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
168
135
 
169
136
  return security_groups
170
137
 
138
+ def _get_subnet_selection(self) -> ec2.SubnetSelection:
139
+ """
140
+ Get subnet selection based on available subnet types in the VPC.
141
+
142
+ RDS instances require private subnets for security, but we'll fall back
143
+ to available subnets if the preferred types aren't available.
144
+ """
145
+ vpc = self.vpc
146
+
147
+ # Check for isolated subnets first (most secure for RDS)
148
+ if vpc.isolated_subnets:
149
+ logger.info("Using isolated subnets for RDS instance")
150
+ return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_ISOLATED)
151
+
152
+ # Check for private subnets next
153
+ elif vpc.private_subnets:
154
+ logger.info("Using private subnets for RDS instance")
155
+ return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS)
156
+
157
+ # Fall back to public subnets (not recommended for production)
158
+ elif vpc.public_subnets:
159
+ logger.warning("Using public subnets for RDS instance - not recommended for production")
160
+ return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC)
161
+
162
+ else:
163
+ raise ValueError("No subnets available in VPC for RDS instance")
164
+
171
165
  def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
172
166
  """Create a new RDS instance"""
173
167
  # Configure subnet group
174
168
  # If we have subnet IDs from SSM, create a DB subnet group explicitly
175
169
  db_subnet_group = None
176
- if "subnet_ids" in self.ssm_imported_values:
177
- subnet_ids_str = self.ssm_imported_values["subnet_ids"]
178
- # Split the comma-separated token into a list
179
- subnet_ids_list = cdk.Fn.split(",", subnet_ids_str)
180
-
181
- # Create DB subnet group with the token-based subnet list
182
- db_subnet_group = rds.CfnDBSubnetGroup(
183
- self,
184
- "DBSubnetGroup",
185
- db_subnet_group_description=f"Subnet group for {db_name}",
186
- subnet_ids=subnet_ids_list,
187
- db_subnet_group_name=f"{db_name}-subnet-group"
188
- )
170
+ subnet_ids = self.get_subnet_ids(self.rds_config)
171
+
172
+ if subnet_ids:
173
+ # For CloudFormation token resolution, we need to get the raw SSM value
174
+ # Use the standardized SSM imports
175
+ ssm_imports = self.get_all_ssm_imports()
176
+ if "subnet_ids" in ssm_imports:
177
+ subnet_ids_str = ssm_imports["subnet_ids"]
178
+ # Split the comma-separated token into a list for CloudFormation
179
+ subnet_ids_list = cdk.Fn.split(",", subnet_ids_str)
180
+
181
+ # Create DB subnet group with the token-based subnet list
182
+ db_subnet_group = rds.CfnDBSubnetGroup(
183
+ self,
184
+ "DBSubnetGroup",
185
+ db_subnet_group_description=f"Subnet group for {db_name}",
186
+ subnet_ids=subnet_ids_list,
187
+ db_subnet_group_name=f"{db_name}-subnet-group"
188
+ )
189
189
 
190
190
  # Configure subnet selection for VPC (when not using SSM imports)
191
- subnets = None if db_subnet_group else ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_ISOLATED)
191
+ subnets = None if db_subnet_group else self._get_subnet_selection()
192
192
 
193
193
  # Configure engine
194
194
  engine_version = None
@@ -18,7 +18,7 @@ from cdk_factory.configurations.deployment import DeploymentConfig
18
18
  from cdk_factory.configurations.stack import StackConfig
19
19
  from cdk_factory.configurations.resources.route53 import Route53Config
20
20
  from cdk_factory.interfaces.istack import IStack
21
- from cdk_factory.interfaces.enhanced_ssm_parameter_mixin import EnhancedSsmParameterMixin
21
+ from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
22
22
  from cdk_factory.stack.stack_module_registry import register_stack
23
23
  from cdk_factory.workload.workload_factory import WorkloadConfig
24
24
 
@@ -27,7 +27,7 @@ logger = Logger(service="Route53Stack")
27
27
 
28
28
  @register_stack("route53_library_module")
29
29
  @register_stack("route53_stack")
30
- class Route53Stack(IStack, EnhancedSsmParameterMixin):
30
+ class Route53Stack(IStack, StandardizedSsmMixin):
31
31
  """
32
32
  Reusable stack for AWS Route53.
33
33
  Supports creating hosted zones, DNS records, and certificate validation.