cdk-factory 0.16.15__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 (66) 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 +7 -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 +30 -3
  11. cdk_factory/configurations/resources/lambda_edge.py +18 -4
  12. cdk_factory/configurations/resources/load_balancer.py +8 -9
  13. cdk_factory/configurations/resources/monitoring.py +8 -3
  14. cdk_factory/configurations/resources/rds.py +8 -9
  15. cdk_factory/configurations/resources/route53.py +5 -0
  16. cdk_factory/configurations/resources/rum.py +7 -2
  17. cdk_factory/configurations/resources/s3.py +10 -2
  18. cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
  19. cdk_factory/configurations/resources/vpc.py +19 -0
  20. cdk_factory/configurations/workload.py +32 -2
  21. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
  22. cdk_factory/constructs/ecr/ecr_construct.py +9 -2
  23. cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
  24. cdk_factory/interfaces/istack.py +4 -4
  25. cdk_factory/interfaces/networked_stack_mixin.py +6 -6
  26. cdk_factory/interfaces/standardized_ssm_mixin.py +684 -0
  27. cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
  28. cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
  29. cdk_factory/pipeline/pipeline_factory.py +3 -3
  30. cdk_factory/stack_library/__init__.py +3 -2
  31. cdk_factory/stack_library/acm/acm_stack.py +7 -17
  32. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
  33. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +454 -537
  34. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
  35. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +5 -27
  36. cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
  37. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
  38. cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
  39. cdk_factory/stack_library/ecs/__init__.py +1 -3
  40. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +159 -75
  41. cdk_factory/stack_library/ecs/ecs_service_stack.py +59 -52
  42. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
  43. cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
  44. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +240 -83
  45. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +139 -212
  46. cdk_factory/stack_library/rds/rds_stack.py +74 -98
  47. cdk_factory/stack_library/route53/route53_stack.py +246 -40
  48. cdk_factory/stack_library/rum/rum_stack.py +108 -91
  49. cdk_factory/stack_library/security_group/security_group_full_stack.py +10 -53
  50. cdk_factory/stack_library/security_group/security_group_stack.py +12 -19
  51. cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
  52. cdk_factory/stack_library/stack_base.py +5 -0
  53. cdk_factory/stack_library/vpc/vpc_stack.py +171 -130
  54. cdk_factory/stack_library/websites/static_website_stack.py +7 -3
  55. cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
  56. cdk_factory/utilities/environment_services.py +5 -5
  57. cdk_factory/utilities/json_loading_utility.py +1 -1
  58. cdk_factory/validation/config_validator.py +483 -0
  59. cdk_factory/version.py +1 -1
  60. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
  61. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +64 -62
  62. cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
  63. cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
  64. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
  65. {cdk_factory-0.16.15.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
  66. {cdk_factory-0.16.15.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
@@ -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
-
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
199
 
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:
@@ -479,44 +456,42 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
479
456
  # Configure conditions
480
457
  conditions = []
481
458
 
482
- # Path patterns
483
- path_patterns = rule_config.get("path_patterns", [])
484
- if path_patterns:
485
- conditions.append(elbv2.ListenerCondition.path_patterns(path_patterns))
486
-
487
- # Host headers
488
- host_headers = rule_config.get("host_headers", [])
489
- if host_headers:
490
- conditions.append(elbv2.ListenerCondition.host_headers(host_headers))
491
-
492
- # HTTP headers
493
- http_headers = rule_config.get("http_headers", {})
494
- for header_name, header_values in http_headers.items():
495
- conditions.append(
496
- elbv2.ListenerCondition.http_header(header_name, header_values)
497
- )
498
-
499
- # Query strings
500
- query_strings = rule_config.get("query_strings", [])
501
- if query_strings:
502
- query_string_conditions = []
503
- for qs in query_strings:
504
- query_string_conditions.append(
505
- elbv2.QueryStringCondition(
506
- 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)
507
470
  )
508
- )
509
- conditions.append(
510
- elbv2.ListenerCondition.query_strings(query_string_conditions)
511
- )
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))
512
479
 
513
480
  # Configure actions
514
- target_group_name = rule_config.get("target_group")
515
- target_group = (
516
- self.target_groups.get(target_group_name) if target_group_name else None
517
- )
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
518
492
 
519
- if target_group:
493
+ # Validate that we have both conditions and target group before creating rule
494
+ if target_group and conditions:
520
495
  # Create rule with forward action
521
496
  elbv2.ApplicationListenerRule(
522
497
  self,
@@ -526,6 +501,15 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
526
501
  conditions=conditions,
527
502
  target_groups=[target_group],
528
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
+ )
529
513
 
530
514
  def _add_ip_whitelist_rules(
531
515
  self,
@@ -672,36 +656,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
672
656
 
673
657
  def _export_cfn_outputs(self, lb_name: str) -> None:
674
658
  """Add CloudFormation outputs for the Load Balancer"""
675
- if self.load_balancer:
676
- # Load Balancer DNS Name
677
- cdk.CfnOutput(
678
- self,
679
- f"{lb_name}-dns-name",
680
- value=self.load_balancer.load_balancer_dns_name,
681
- export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
682
- )
683
-
684
- # Load Balancer ARN
685
- cdk.CfnOutput(
686
- self,
687
- f"{lb_name}-arn",
688
- value=self.load_balancer.load_balancer_arn,
689
- export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
690
- )
691
-
692
- # Target Group ARNs
693
- for tg_name, target_group in self.target_groups.items():
694
- # Normalize target group name for consistent CloudFormation export naming
695
- normalized_tg_name = self.normalize_resource_name(
696
- tg_name, for_export=True
697
- )
698
- cdk.CfnOutput(
699
- self,
700
- f"{lb_name}-{normalized_tg_name}-arn",
701
- value=target_group.target_group_arn,
702
- export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
703
- )
704
-
659
+ return
705
660
  def _export_ssm_parameters(self, lb_name: str) -> None:
706
661
  """Export Load Balancer resources to SSM Parameter Store if configured"""
707
662
  if not self.load_balancer:
@@ -732,32 +687,4 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
732
687
 
733
688
  def _export_cfn_outputs(self, lb_name: str) -> None:
734
689
  """Add CloudFormation outputs for the Load Balancer"""
735
- if self.load_balancer:
736
- # Load Balancer DNS Name
737
- cdk.CfnOutput(
738
- self,
739
- f"{lb_name}-dns-name",
740
- value=self.load_balancer.load_balancer_dns_name,
741
- export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
742
- )
743
-
744
- # Load Balancer ARN
745
- cdk.CfnOutput(
746
- self,
747
- f"{lb_name}-arn",
748
- value=self.load_balancer.load_balancer_arn,
749
- export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
750
- )
751
-
752
- # Target Group ARNs
753
- for tg_name, target_group in self.target_groups.items():
754
- # Normalize target group name for consistent CloudFormation export naming
755
- normalized_tg_name = self.normalize_resource_name(
756
- tg_name, for_export=True
757
- )
758
- cdk.CfnOutput(
759
- self,
760
- f"{lb_name}-{normalized_tg_name}-arn",
761
- value=target_group.target_group_arn,
762
- export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
763
- )
690
+ return