cdk-factory 0.17.6__py3-none-any.whl → 0.20.0__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 (44) hide show
  1. cdk_factory/configurations/deployment.py +12 -0
  2. cdk_factory/configurations/resources/acm.py +9 -2
  3. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  4. cdk_factory/configurations/resources/ecs_cluster.py +5 -0
  5. cdk_factory/configurations/resources/ecs_service.py +24 -2
  6. cdk_factory/configurations/resources/lambda_edge.py +18 -4
  7. cdk_factory/configurations/resources/rds.py +1 -1
  8. cdk_factory/configurations/resources/route53.py +5 -0
  9. cdk_factory/configurations/resources/s3.py +9 -1
  10. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
  11. cdk_factory/constructs/lambdas/policies/policy_docs.py +1 -1
  12. cdk_factory/interfaces/networked_stack_mixin.py +1 -1
  13. cdk_factory/interfaces/standardized_ssm_mixin.py +82 -10
  14. cdk_factory/stack_library/acm/acm_stack.py +5 -15
  15. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +2 -2
  16. cdk_factory/stack_library/auto_scaling/{auto_scaling_stack_standardized.py → auto_scaling_stack.py} +213 -105
  17. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
  18. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +3 -25
  19. cdk_factory/stack_library/cognito/cognito_stack.py +2 -2
  20. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +2 -2
  21. cdk_factory/stack_library/ecs/__init__.py +2 -4
  22. cdk_factory/stack_library/ecs/{ecs_cluster_stack_standardized.py → ecs_cluster_stack.py} +52 -41
  23. cdk_factory/stack_library/ecs/ecs_service_stack.py +49 -26
  24. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
  25. cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
  26. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +238 -81
  27. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +128 -177
  28. cdk_factory/stack_library/rds/rds_stack.py +65 -72
  29. cdk_factory/stack_library/route53/route53_stack.py +244 -38
  30. cdk_factory/stack_library/rum/rum_stack.py +3 -3
  31. cdk_factory/stack_library/security_group/security_group_full_stack.py +1 -31
  32. cdk_factory/stack_library/security_group/security_group_stack.py +1 -8
  33. cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
  34. cdk_factory/stack_library/stack_base.py +5 -0
  35. cdk_factory/stack_library/vpc/{vpc_stack_standardized.py → vpc_stack.py} +6 -109
  36. cdk_factory/stack_library/websites/static_website_stack.py +7 -3
  37. cdk_factory/utilities/api_gateway_integration_utility.py +2 -2
  38. cdk_factory/utilities/environment_services.py +2 -2
  39. cdk_factory/version.py +1 -1
  40. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
  41. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +44 -42
  42. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
  43. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
  44. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.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
@@ -50,7 +52,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
50
52
  self._hosted_zone = None
51
53
  self._record_names = None
52
54
  # SSM imported values
53
- self.ssm_imported_values: Dict[str, str] = {}
55
+ self._ssm_imported_values: Dict[str, str] = {}
54
56
 
55
57
  def build(
56
58
  self,
@@ -77,8 +79,18 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
77
79
  )
78
80
  lb_name = deployment.build_resource_name(self.lb_config.name)
79
81
 
80
- # Process SSM imports first
81
- 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()
82
94
 
83
95
  self._prep_dns()
84
96
 
@@ -156,16 +168,22 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
156
168
 
157
169
  # If subnets is None, check if we have SSM-imported subnet_ids as a token
158
170
  # We need to use Fn.Split to convert the comma-separated string to an array
159
- if subnets is None and "subnet_ids" in self.ssm_imported_values:
160
- subnet_ids_value = self.ssm_imported_values["subnet_ids"]
161
- if cdk.Token.is_unresolved(subnet_ids_value):
162
- logger.info("Using Fn.Split to convert comma-separated subnet IDs token to array")
163
- # Use CloudFormation escape hatch to set Subnets property with Fn.Split
164
- cfn_lb = load_balancer.node.default_child
165
- cfn_lb.add_property_override(
166
- "Subnets",
167
- cdk.Fn.split(",", subnet_ids_value)
168
- )
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
+ )
169
187
 
170
188
  # Add tags
171
189
  for key, value in self.lb_config.tags.items():
@@ -187,63 +205,13 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
187
205
  )
188
206
  return self._vpc
189
207
 
190
- def _process_ssm_imports(self) -> None:
191
- """
192
- Process SSM imports from configuration.
193
- Follows the same pattern as RDS and Security Group stacks.
194
- """
195
- from aws_cdk import aws_ssm as ssm
196
-
197
- ssm_imports = self.lb_config.ssm_imports
198
-
199
- if not ssm_imports:
200
- logger.debug("No SSM imports configured for Load Balancer")
201
- return
202
-
203
- logger.info(f"Processing {len(ssm_imports)} SSM imports for Load Balancer")
204
-
205
- for param_key, param_value in ssm_imports.items():
206
- try:
207
- # Handle list values (like security_groups)
208
- if isinstance(param_value, list):
209
- imported_list = []
210
- for idx, param_path in enumerate(param_value):
211
- if not param_path.startswith('/'):
212
- param_path = f"/{param_path}"
213
-
214
- construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
215
- param = ssm.StringParameter.from_string_parameter_name(
216
- self, construct_id, param_path
217
- )
218
- imported_list.append(param.string_value)
219
-
220
- self.ssm_imported_values[param_key] = imported_list
221
- logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
222
- else:
223
- # Handle string values
224
- param_path = param_value
225
- if not param_path.startswith('/'):
226
- param_path = f"/{param_path}"
227
-
228
- construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
229
- param = ssm.StringParameter.from_string_parameter_name(
230
- self, construct_id, param_path
231
- )
232
-
233
- self.ssm_imported_values[param_key] = param.string_value
234
- logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
235
-
236
- except Exception as e:
237
- logger.error(f"Failed to import SSM parameter {param_key}: {e}")
238
- raise
239
-
240
208
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
241
209
  """Get security groups for the Load Balancer"""
242
210
  security_groups = []
243
211
 
244
212
  # Check SSM imported values first
245
- if "security_groups" in self.ssm_imported_values:
246
- 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"]
247
215
  if not isinstance(sg_ids, list):
248
216
  sg_ids = [sg_ids]
249
217
  else:
@@ -261,9 +229,16 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
261
229
  """Get subnets for the Load Balancer"""
262
230
  subnets = []
263
231
 
264
- # Check SSM imported values first
265
- if "subnet_ids" in self.ssm_imported_values:
266
- 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"]
267
242
 
268
243
  # Check if this is a CDK token (unresolved SSM parameter)
269
244
  if cdk.Token.is_unresolved(subnet_ids_value):
@@ -272,25 +247,40 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
272
247
  # The ALB construct will handle the token-based subnet IDs
273
248
  logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
274
249
  return None
275
- elif isinstance(subnet_ids_value, str):
276
- # If it's a resolved string, split it
277
- subnet_ids = [s.strip() for s in subnet_ids_value.split(',')]
278
- elif isinstance(subnet_ids_value, list):
279
- subnet_ids = subnet_ids_value
280
- else:
281
- subnet_ids = [subnet_ids_value]
282
- else:
283
- subnet_ids = self.lb_config.subnets
284
-
285
- if not subnet_ids:
286
- return None
287
250
 
251
+ # Convert subnet IDs to subnet objects
288
252
  for idx, subnet_id in enumerate(subnet_ids):
289
253
  subnets.append(
290
254
  ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
291
255
  )
292
256
  return subnets
293
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
+
294
284
  def _create_target_groups(self, lb_name: str) -> None:
295
285
  """Create target groups for the Load Balancer"""
296
286
 
@@ -298,6 +288,9 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
298
288
  tg_name = tg_config.get("name", f"tg-{idx}")
299
289
  tg_id = f"{lb_name}-{tg_name}"
300
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
+
301
294
  # Configure health check
302
295
  health_check = self._configure_health_check(
303
296
  tg_config.get("health_check", {})
@@ -308,7 +301,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
308
301
  target_group = elbv2.ApplicationTargetGroup(
309
302
  self,
310
303
  tg_id,
311
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
304
+ target_group_name=tg_name_sanitized,
312
305
  vpc=self.vpc,
313
306
  port=tg_config.get("port", 80),
314
307
  protocol=elbv2.ApplicationProtocol(
@@ -323,7 +316,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
323
316
  target_group = elbv2.NetworkTargetGroup(
324
317
  self,
325
318
  tg_id,
326
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
319
+ target_group_name=tg_name_sanitized,
327
320
  vpc=self.vpc,
328
321
  port=tg_config.get("port", 80),
329
322
  protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
@@ -333,6 +326,8 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
333
326
  health_check=health_check,
334
327
  )
335
328
 
329
+
330
+
336
331
  # Store target group for later use
337
332
  self.target_groups[tg_name] = target_group
338
333
 
@@ -373,6 +368,11 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
373
368
  if protocol.upper() == "HTTPS":
374
369
  certificates = self._get_certificates()
375
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
+
376
376
  listener = elbv2.ApplicationListener(
377
377
  self,
378
378
  listener_id,
@@ -425,8 +425,9 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
425
425
  certificates = []
426
426
 
427
427
  # Check SSM imported values first (takes priority)
428
- if "certificate_arns" in self.ssm_imported_values:
429
- 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"]
430
431
  if not isinstance(cert_arns, list):
431
432
  cert_arns = [cert_arns]
432
433
  for cert_arn in cert_arns:
@@ -455,44 +456,42 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
455
456
  # Configure conditions
456
457
  conditions = []
457
458
 
458
- # Path patterns
459
- path_patterns = rule_config.get("path_patterns", [])
460
- if path_patterns:
461
- conditions.append(elbv2.ListenerCondition.path_patterns(path_patterns))
462
-
463
- # Host headers
464
- host_headers = rule_config.get("host_headers", [])
465
- if host_headers:
466
- conditions.append(elbv2.ListenerCondition.host_headers(host_headers))
467
-
468
- # HTTP headers
469
- http_headers = rule_config.get("http_headers", {})
470
- for header_name, header_values in http_headers.items():
471
- conditions.append(
472
- elbv2.ListenerCondition.http_header(header_name, header_values)
473
- )
474
-
475
- # Query strings
476
- query_strings = rule_config.get("query_strings", [])
477
- if query_strings:
478
- query_string_conditions = []
479
- for qs in query_strings:
480
- query_string_conditions.append(
481
- elbv2.QueryStringCondition(
482
- key=qs.get("key"), value=qs.get("value")
459
+ # Parse AWS ALB conditions format
460
+ aws_conditions = rule_config.get("conditions", [])
461
+ for condition in aws_conditions:
462
+ field = condition.get("field")
463
+ if field == "http-header" and "http_header_config" in condition:
464
+ header_config = condition["http_header_config"]
465
+ header_name = header_config.get("header_name")
466
+ header_values = header_config.get("values", [])
467
+ if header_name and header_values:
468
+ conditions.append(
469
+ elbv2.ListenerCondition.http_header(header_name, header_values)
483
470
  )
484
- )
485
- conditions.append(
486
- elbv2.ListenerCondition.query_strings(query_string_conditions)
487
- )
471
+ elif field == "path-pattern" and "values" in condition:
472
+ path_patterns = condition.get("values", [])
473
+ if path_patterns:
474
+ conditions.append(elbv2.ListenerCondition.path_patterns(path_patterns))
475
+ elif field == "host-header" and "values" in condition:
476
+ host_headers = condition.get("values", [])
477
+ if host_headers:
478
+ conditions.append(elbv2.ListenerCondition.host_headers(host_headers))
488
479
 
489
480
  # Configure actions
490
- target_group_name = rule_config.get("target_group")
491
- target_group = (
492
- self.target_groups.get(target_group_name) if target_group_name else None
493
- )
481
+ target_group = None
482
+
483
+ # Parse AWS ALB actions format
484
+ aws_actions = rule_config.get("actions", [])
485
+ for action in aws_actions:
486
+ if action.get("type") == "forward":
487
+ target_group_name = action.get("target_group")
488
+ target_group = (
489
+ self.target_groups.get(target_group_name) if target_group_name else None
490
+ )
491
+ break # Use first forward action
494
492
 
495
- if target_group:
493
+ # Validate that we have both conditions and target group before creating rule
494
+ if target_group and conditions:
496
495
  # Create rule with forward action
497
496
  elbv2.ApplicationListenerRule(
498
497
  self,
@@ -502,6 +501,15 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
502
501
  conditions=conditions,
503
502
  target_groups=[target_group],
504
503
  )
504
+ elif not conditions:
505
+ logger.warning(
506
+ f"Skipping listener rule '{rule_id}' - no conditions defined. "
507
+ f"CDK requires at least one condition for every rule."
508
+ )
509
+ elif not target_group:
510
+ logger.warning(
511
+ f"Skipping listener rule '{rule_id}' - no valid target group found."
512
+ )
505
513
 
506
514
  def _add_ip_whitelist_rules(
507
515
  self,
@@ -648,36 +656,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
648
656
 
649
657
  def _export_cfn_outputs(self, lb_name: str) -> None:
650
658
  """Add CloudFormation outputs for the Load Balancer"""
651
- if self.load_balancer:
652
- # Load Balancer DNS Name
653
- cdk.CfnOutput(
654
- self,
655
- f"{lb_name}-dns-name",
656
- value=self.load_balancer.load_balancer_dns_name,
657
- export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
658
- )
659
-
660
- # Load Balancer ARN
661
- cdk.CfnOutput(
662
- self,
663
- f"{lb_name}-arn",
664
- value=self.load_balancer.load_balancer_arn,
665
- export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
666
- )
667
-
668
- # Target Group ARNs
669
- for tg_name, target_group in self.target_groups.items():
670
- # Normalize target group name for consistent CloudFormation export naming
671
- normalized_tg_name = self.normalize_resource_name(
672
- tg_name, for_export=True
673
- )
674
- cdk.CfnOutput(
675
- self,
676
- f"{lb_name}-{normalized_tg_name}-arn",
677
- value=target_group.target_group_arn,
678
- export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
679
- )
680
-
659
+ return
681
660
  def _export_ssm_parameters(self, lb_name: str) -> None:
682
661
  """Export Load Balancer resources to SSM Parameter Store if configured"""
683
662
  if not self.load_balancer:
@@ -708,32 +687,4 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
708
687
 
709
688
  def _export_cfn_outputs(self, lb_name: str) -> None:
710
689
  """Add CloudFormation outputs for the Load Balancer"""
711
- if self.load_balancer:
712
- # Load Balancer DNS Name
713
- cdk.CfnOutput(
714
- self,
715
- f"{lb_name}-dns-name",
716
- value=self.load_balancer.load_balancer_dns_name,
717
- export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
718
- )
719
-
720
- # Load Balancer ARN
721
- cdk.CfnOutput(
722
- self,
723
- f"{lb_name}-arn",
724
- value=self.load_balancer.load_balancer_arn,
725
- export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
726
- )
727
-
728
- # Target Group ARNs
729
- for tg_name, target_group in self.target_groups.items():
730
- # Normalize target group name for consistent CloudFormation export naming
731
- normalized_tg_name = self.normalize_resource_name(
732
- tg_name, for_export=True
733
- )
734
- cdk.CfnOutput(
735
- self,
736
- f"{lb_name}-{normalized_tg_name}-arn",
737
- value=target_group.target_group_arn,
738
- export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
739
- )
690
+ return
@@ -69,8 +69,18 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
69
69
  self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
70
70
  db_name = deployment.build_resource_name(self.rds_config.name)
71
71
 
72
- # Process SSM imports first
73
- 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()
74
84
 
75
85
  # Get VPC and security groups
76
86
  self.security_groups = self._get_security_groups()
@@ -87,40 +97,13 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
87
97
  # Export to SSM Parameter Store
88
98
  self._export_ssm_parameters(db_name)
89
99
 
90
- def _process_ssm_imports(self) -> None:
91
- """Process SSM imports from configuration"""
92
- ssm_imports = self.rds_config.ssm_imports
93
-
94
- if not ssm_imports:
95
- logger.debug("No SSM imports configured for RDS")
96
- return
97
-
98
- logger.info(f"Processing {len(ssm_imports)} SSM imports for RDS")
99
-
100
- for param_key, param_path in ssm_imports.items():
101
- try:
102
- if not param_path.startswith('/'):
103
- param_path = f"/{param_path}"
104
-
105
- construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
106
- param = ssm.StringParameter.from_string_parameter_name(
107
- self, construct_id, param_path
108
- )
109
-
110
- self.ssm_imported_values[param_key] = param.string_value
111
- logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
112
-
113
- except Exception as e:
114
- logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
115
- raise
116
-
117
100
  @property
118
101
  def vpc(self) -> ec2.IVpc:
119
102
  """Get the VPC for the RDS instance using centralized VPC provider mixin."""
120
- if self._vpc:
103
+ if hasattr(self, '_vpc') and self._vpc:
121
104
  return self._vpc
122
105
 
123
- # Use the centralized VPC resolution from VPCProviderMixin
106
+ # Resolve VPC using the centralized VPC provider mixin
124
107
  self._vpc = self.resolve_vpc(
125
108
  config=self.rds_config,
126
109
  deployment=self.deployment,
@@ -133,8 +116,9 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
133
116
  security_groups = []
134
117
 
135
118
  # Check SSM imports first for security group ID
136
- if "security_group_rds_id" in self.ssm_imported_values:
137
- 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"]
138
122
  security_groups.append(
139
123
  ec2.SecurityGroup.from_security_group_id(
140
124
  self, "RDSSecurityGroup", sg_id
@@ -151,27 +135,60 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
151
135
 
152
136
  return security_groups
153
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
+
154
165
  def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
155
166
  """Create a new RDS instance"""
156
167
  # Configure subnet group
157
168
  # If we have subnet IDs from SSM, create a DB subnet group explicitly
158
169
  db_subnet_group = None
159
- if "subnet_ids" in self.ssm_imported_values:
160
- subnet_ids_str = self.ssm_imported_values["subnet_ids"]
161
- # Split the comma-separated token into a list
162
- subnet_ids_list = cdk.Fn.split(",", subnet_ids_str)
163
-
164
- # Create DB subnet group with the token-based subnet list
165
- db_subnet_group = rds.CfnDBSubnetGroup(
166
- self,
167
- "DBSubnetGroup",
168
- db_subnet_group_description=f"Subnet group for {db_name}",
169
- subnet_ids=subnet_ids_list,
170
- db_subnet_group_name=f"{db_name}-subnet-group"
171
- )
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
+ )
172
189
 
173
190
  # Configure subnet selection for VPC (when not using SSM imports)
174
- 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()
175
192
 
176
193
  # Configure engine
177
194
  engine_version = None
@@ -268,31 +285,7 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
268
285
 
269
286
  def _add_outputs(self, db_name: str) -> None:
270
287
  """Add CloudFormation outputs for the RDS instance"""
271
- if self.db_instance:
272
- # Database endpoint
273
- cdk.CfnOutput(
274
- self,
275
- f"{db_name}-endpoint",
276
- value=self.db_instance.db_instance_endpoint_address,
277
- export_name=f"{self.deployment.build_resource_name(db_name)}-endpoint",
278
- )
279
-
280
- # Database port
281
- cdk.CfnOutput(
282
- self,
283
- f"{db_name}-port",
284
- value=self.db_instance.db_instance_endpoint_port,
285
- export_name=f"{self.deployment.build_resource_name(db_name)}-port",
286
- )
287
-
288
- # Secret ARN (if available)
289
- if hasattr(self.db_instance, "secret") and self.db_instance.secret:
290
- cdk.CfnOutput(
291
- self,
292
- f"{db_name}-secret-arn",
293
- value=self.db_instance.secret.secret_arn,
294
- export_name=f"{self.deployment.build_resource_name(db_name)}-secret-arn",
295
- )
288
+ return
296
289
 
297
290
  def _export_ssm_parameters(self, db_name: str) -> None:
298
291
  """Export RDS connection info and credentials to SSM Parameter Store"""