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
@@ -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
 
@@ -112,34 +125,65 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
112
125
  # Get subnets
113
126
  subnets = self._get_subnets()
114
127
 
128
+ # Prepare vpc_subnets parameter
129
+ # If subnets is None, we'll handle it via escape hatch after creation
130
+ vpc_subnets_param = ec2.SubnetSelection(subnets=subnets) if subnets else None
131
+
115
132
  # Create the Load Balancer based on type
116
133
  if self.lb_config.type == "APPLICATION":
117
- load_balancer = elbv2.ApplicationLoadBalancer(
118
- self,
119
- lb_name,
120
- load_balancer_name=lb_name,
121
- vpc=self.vpc,
122
- internet_facing=self.lb_config.internet_facing,
123
- security_group=(
134
+ # When vpc_subnets is None and we have token-based subnet_ids,
135
+ # we need to create the ALB without vpc_subnets to avoid VPC subnet lookup errors
136
+ alb_props = {
137
+ "load_balancer_name": lb_name,
138
+ "vpc": self.vpc,
139
+ "internet_facing": self.lb_config.internet_facing,
140
+ "security_group": (
124
141
  security_groups[0]
125
142
  if security_groups and len(security_groups) > 0
126
143
  else None
127
144
  ),
128
- deletion_protection=self.lb_config.deletion_protection,
129
- idle_timeout=cdk.Duration.seconds(self.lb_config.idle_timeout),
130
- http2_enabled=self.lb_config.http2_enabled,
131
- vpc_subnets=ec2.SubnetSelection(subnets=subnets) if subnets else None,
132
- )
145
+ "deletion_protection": self.lb_config.deletion_protection,
146
+ "idle_timeout": cdk.Duration.seconds(self.lb_config.idle_timeout),
147
+ "http2_enabled": self.lb_config.http2_enabled,
148
+ }
149
+
150
+ # Only add vpc_subnets if we have concrete subnet objects
151
+ if vpc_subnets_param:
152
+ alb_props["vpc_subnets"] = vpc_subnets_param
153
+
154
+ load_balancer = elbv2.ApplicationLoadBalancer(self, lb_name, **alb_props)
133
155
  else: # NETWORK
134
- load_balancer = elbv2.NetworkLoadBalancer(
135
- self,
136
- lb_name,
137
- load_balancer_name=lb_name,
138
- vpc=self.vpc,
139
- internet_facing=self.lb_config.internet_facing,
140
- deletion_protection=self.lb_config.deletion_protection,
141
- vpc_subnets=ec2.SubnetSelection(subnets=subnets) if subnets else None,
142
- )
156
+ nlb_props = {
157
+ "load_balancer_name": lb_name,
158
+ "vpc": self.vpc,
159
+ "internet_facing": self.lb_config.internet_facing,
160
+ "deletion_protection": self.lb_config.deletion_protection,
161
+ }
162
+
163
+ # Only add vpc_subnets if we have concrete subnet objects
164
+ if vpc_subnets_param:
165
+ nlb_props["vpc_subnets"] = vpc_subnets_param
166
+
167
+ load_balancer = elbv2.NetworkLoadBalancer(self, lb_name, **nlb_props)
168
+
169
+ # If subnets is None, check if we have SSM-imported subnet_ids as a token
170
+ # We need to use Fn.Split to convert the comma-separated string to an array
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
+ )
143
187
 
144
188
  # Add tags
145
189
  for key, value in self.lb_config.tags.items():
@@ -149,93 +193,25 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
149
193
 
150
194
  @property
151
195
  def vpc(self) -> ec2.IVpc:
152
- """Get the VPC for the Load Balancer"""
196
+ """Get the VPC for the Load Balancer using centralized VPC provider mixin."""
153
197
  if self._vpc:
154
198
  return self._vpc
155
-
156
- # Check SSM imported values first (tokens from SSM parameters)
157
- if "vpc_id" in self.ssm_imported_values:
158
- vpc_id = self.ssm_imported_values["vpc_id"]
159
-
160
- # Build VPC attributes
161
- vpc_attrs = {
162
- "vpc_id": vpc_id,
163
- "availability_zones": ["us-east-1a", "us-east-1b"]
164
- }
165
-
166
- # Use from_vpc_attributes() instead of from_lookup() because SSM imports return tokens
167
- self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
168
- elif self.lb_config.vpc_id:
169
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.lb_config.vpc_id)
170
- elif self.workload.vpc_id:
171
- self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
172
- else:
173
- # Use default VPC if not provided
174
- raise ValueError(
175
- "VPC is not defined in the configuration. "
176
- "You can provide it a the load_balancer.vpc_id in the configuration "
177
- "or a top level workload.vpc_id in the workload configuration."
178
- )
179
-
180
- return self._vpc
181
-
182
- def _process_ssm_imports(self) -> None:
183
- """
184
- Process SSM imports from configuration.
185
- Follows the same pattern as RDS and Security Group stacks.
186
- """
187
- from aws_cdk import aws_ssm as ssm
188
-
189
- ssm_imports = self.lb_config.ssm_imports
190
199
 
191
- if not ssm_imports:
192
- logger.debug("No SSM imports configured for Load Balancer")
193
- return
194
-
195
- logger.info(f"Processing {len(ssm_imports)} SSM imports for Load Balancer")
196
-
197
- for param_key, param_value in ssm_imports.items():
198
- try:
199
- # Handle list values (like security_groups)
200
- if isinstance(param_value, list):
201
- imported_list = []
202
- for idx, param_path in enumerate(param_value):
203
- if not param_path.startswith('/'):
204
- param_path = f"/{param_path}"
205
-
206
- construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
207
- param = ssm.StringParameter.from_string_parameter_name(
208
- self, construct_id, param_path
209
- )
210
- imported_list.append(param.string_value)
211
-
212
- self.ssm_imported_values[param_key] = imported_list
213
- logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
214
- else:
215
- # Handle string values
216
- param_path = param_value
217
- if not param_path.startswith('/'):
218
- param_path = f"/{param_path}"
219
-
220
- construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
221
- param = ssm.StringParameter.from_string_parameter_name(
222
- self, construct_id, param_path
223
- )
224
-
225
- self.ssm_imported_values[param_key] = param.string_value
226
- logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
227
-
228
- except Exception as e:
229
- logger.error(f"Failed to import SSM parameter {param_key}: {e}")
230
- 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
231
207
 
232
208
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
233
209
  """Get security groups for the Load Balancer"""
234
210
  security_groups = []
235
211
 
236
212
  # Check SSM imported values first
237
- if "security_groups" in self.ssm_imported_values:
238
- 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"]
239
215
  if not isinstance(sg_ids, list):
240
216
  sg_ids = [sg_ids]
241
217
  else:
@@ -253,23 +229,58 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
253
229
  """Get subnets for the Load Balancer"""
254
230
  subnets = []
255
231
 
256
- # Check SSM imported values first
257
- if "subnet_ids" in self.ssm_imported_values:
258
- subnet_ids = self.ssm_imported_values["subnet_ids"]
259
- # SSM returns comma-separated string for StringList, need to split
260
- if isinstance(subnet_ids, str):
261
- subnet_ids = [s.strip() for s in subnet_ids.split(',')]
262
- elif not isinstance(subnet_ids, list):
263
- subnet_ids = [subnet_ids]
264
- else:
265
- subnet_ids = self.lb_config.subnets
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
266
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"]
242
+
243
+ # Check if this is a CDK token (unresolved SSM parameter)
244
+ if cdk.Token.is_unresolved(subnet_ids_value):
245
+ # For tokens, we can't split at synth time
246
+ # Return None to signal that subnets should be resolved via SubnetSelection
247
+ # The ALB construct will handle the token-based subnet IDs
248
+ logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
249
+ return None
250
+
251
+ # Convert subnet IDs to subnet objects
267
252
  for idx, subnet_id in enumerate(subnet_ids):
268
253
  subnets.append(
269
254
  ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
270
255
  )
271
256
  return subnets
272
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
+
273
284
  def _create_target_groups(self, lb_name: str) -> None:
274
285
  """Create target groups for the Load Balancer"""
275
286
 
@@ -277,6 +288,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
277
288
  tg_name = tg_config.get("name", f"tg-{idx}")
278
289
  tg_id = f"{lb_name}-{tg_name}"
279
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
+
280
294
  # Configure health check
281
295
  health_check = self._configure_health_check(
282
296
  tg_config.get("health_check", {})
@@ -287,7 +301,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
287
301
  target_group = elbv2.ApplicationTargetGroup(
288
302
  self,
289
303
  tg_id,
290
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
304
+ target_group_name=tg_name_sanitized,
291
305
  vpc=self.vpc,
292
306
  port=tg_config.get("port", 80),
293
307
  protocol=elbv2.ApplicationProtocol(
@@ -302,7 +316,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
302
316
  target_group = elbv2.NetworkTargetGroup(
303
317
  self,
304
318
  tg_id,
305
- target_group_name=tg_id[:32], # Ensure name is within AWS limits
319
+ target_group_name=tg_name_sanitized,
306
320
  vpc=self.vpc,
307
321
  port=tg_config.get("port", 80),
308
322
  protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
@@ -312,6 +326,8 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
312
326
  health_check=health_check,
313
327
  )
314
328
 
329
+
330
+
315
331
  # Store target group for later use
316
332
  self.target_groups[tg_name] = target_group
317
333
 
@@ -352,6 +368,11 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
352
368
  if protocol.upper() == "HTTPS":
353
369
  certificates = self._get_certificates()
354
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
+
355
376
  listener = elbv2.ApplicationListener(
356
377
  self,
357
378
  listener_id,
@@ -402,8 +423,20 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
402
423
  def _get_certificates(self) -> List[elbv2.ListenerCertificate]:
403
424
  """Get certificates for HTTPS listeners"""
404
425
  certificates = []
405
- for cert_arn in self.lb_config.certificate_arns:
406
- certificates.append(elbv2.ListenerCertificate.from_arn(cert_arn))
426
+
427
+ # Check SSM imported values first (takes priority)
428
+ ssm_imports = self.get_all_ssm_imports()
429
+ if "certificate_arns" in ssm_imports:
430
+ cert_arns = ssm_imports["certificate_arns"]
431
+ if not isinstance(cert_arns, list):
432
+ cert_arns = [cert_arns]
433
+ for cert_arn in cert_arns:
434
+ certificates.append(elbv2.ListenerCertificate.from_arn(cert_arn))
435
+ logger.info(f"Using {len(cert_arns)} certificate(s) from SSM")
436
+ else:
437
+ # Fall back to config values
438
+ for cert_arn in self.lb_config.certificate_arns:
439
+ certificates.append(elbv2.ListenerCertificate.from_arn(cert_arn))
407
440
 
408
441
  if self.ssl_certificate:
409
442
  certificates.append(
@@ -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
@@ -211,8 +211,10 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
211
211
  raise ValueError(f"Unsupported database engine: {self.rds_config.engine}")
212
212
 
213
213
  # Configure instance type
214
+ # Strip 'db.' prefix if present since ec2.InstanceType expects just the instance family/size
214
215
  instance_class = self.rds_config.instance_class
215
- instance_type = ec2.InstanceType(instance_class)
216
+ instance_class_name = instance_class.replace("db.", "") if instance_class.startswith("db.") else instance_class
217
+ instance_type = ec2.InstanceType(instance_class_name)
216
218
 
217
219
  # Configure removal policy
218
220
  removal_policy = None
@@ -242,9 +244,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
242
244
  "backup_retention": Duration.days(self.rds_config.backup_retention),
243
245
  "cloudwatch_logs_exports": self.rds_config.cloudwatch_logs_exports,
244
246
  "enable_performance_insights": self.rds_config.enable_performance_insights,
247
+ "allow_major_version_upgrade": self.rds_config.allow_major_version_upgrade,
245
248
  "removal_policy": removal_policy,
246
249
  }
247
250
 
251
+ # Add storage auto-scaling if max_allocated_storage is configured
252
+ if self.rds_config.max_allocated_storage:
253
+ db_props["max_allocated_storage"] = self.rds_config.max_allocated_storage
254
+ logger.info(
255
+ f"Storage auto-scaling enabled: {self.rds_config.allocated_storage}GB "
256
+ f"-> {self.rds_config.max_allocated_storage}GB"
257
+ )
258
+
248
259
  # Use either subnet group or vpc_subnets depending on what's available
249
260
  if db_subnet_group:
250
261
  db_props["subnet_group"] = rds.SubnetGroup.from_subnet_group_name(
@@ -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.
@@ -58,8 +58,13 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
58
58
  # Get or create hosted zone
59
59
  self.hosted_zone = self._get_or_create_hosted_zone()
60
60
 
61
- # Create certificate if needed
61
+ # Create certificate if needed (DEPRECATED - use dedicated ACM stack)
62
62
  if self.route53_config.create_certificate:
63
+ logger.warning(
64
+ "Creating certificates in Route53Stack is deprecated. "
65
+ "Please use the dedicated 'acm_stack' module for certificate management. "
66
+ "This feature will be maintained for backward compatibility."
67
+ )
63
68
  self.certificate = self._create_certificate()
64
69
 
65
70
  # Create DNS records