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
@@ -28,6 +28,18 @@ class DeploymentConfig:
28
28
  self.__load()
29
29
 
30
30
  def __load(self):
31
+ # Validate environment consistency
32
+ deployment_env = self.__deployment.get("environment")
33
+ workload_env = self.__workload.get("environment")
34
+
35
+ if deployment_env and workload_env and deployment_env != workload_env:
36
+ from aws_lambda_powertools import Logger
37
+ logger = Logger()
38
+ logger.warning(
39
+ f"Environment mismatch: deployment.environment='{deployment_env}' != workload.environment='{workload_env}'. "
40
+ f"Using workload.environment for consistency."
41
+ )
42
+
31
43
  self.__load_pipeline()
32
44
  self.__load_stacks()
33
45
 
@@ -59,15 +59,22 @@ class AcmConfig:
59
59
  """Certificate transparency logging preference (ENABLED or DISABLED)"""
60
60
  return self.__config.get("certificate_transparency_logging_preference")
61
61
 
62
+ @property
63
+ def ssm(self) -> Dict[str, Any]:
64
+ """SSM configuration for importing/exporting resources"""
65
+ return self.__config.get("ssm", {})
66
+
62
67
  @property
63
68
  def ssm_exports(self) -> Dict[str, str]:
64
69
  """SSM parameter paths to export certificate details"""
65
- exports = self.__config.get("ssm_exports", {})
70
+ exports = self.ssm.get("exports", {})
66
71
 
67
72
  # Provide default SSM export path if not specified
68
73
  if not exports and self.__deployment:
74
+ workload_env = self.__deployment.workload.get("environment", self.__deployment.environment)
75
+ workload_name = self.__deployment.workload.get("name", self.__deployment.workload_name)
69
76
  exports = {
70
- "certificate_arn": f"/{self.__deployment.environment}/{self.__deployment.workload_name}/certificate/arn"
77
+ "certificate_arn": f"/{workload_env}/{workload_name}/certificate/arn"
71
78
  }
72
79
 
73
80
  return exports
@@ -70,12 +70,14 @@ class AutoScalingConfig(EnhancedBaseConfig):
70
70
  return self.__config.get("termination_policies", ["DEFAULT"])
71
71
 
72
72
  @property
73
- def update_policy(self) -> Dict[str, Any]:
73
+ def update_policy(self) -> Optional[Dict[str, Any]]:
74
74
  """Update policy configuration"""
75
- return self.__config.get(
76
- "update_policy",
77
- {"min_instances_in_service": 1, "max_batch_size": 1, "pause_time": 300},
78
- )
75
+ return self.__config.get("update_policy")
76
+
77
+ @property
78
+ def instance_refresh(self) -> Optional[Dict[str, Any]]:
79
+ """Instance refresh configuration for rolling updates"""
80
+ return self.__config.get("instance_refresh")
79
81
 
80
82
  @property
81
83
  def user_data_commands(self) -> List[str]:
@@ -18,6 +18,11 @@ class EcsClusterConfig:
18
18
  def __init__(self, config: Dict[str, Any]) -> None:
19
19
  self._config = config or {}
20
20
 
21
+ @property
22
+ def dictionary(self) -> Dict[str, Any]:
23
+ """Access to the underlying configuration dictionary (for compatibility with SSM mixin)"""
24
+ return self._config
25
+
21
26
  @property
22
27
  def name(self) -> str:
23
28
  """Name of the ECS cluster. Supports template variables like {{WORKLOAD_NAME}}-{{ENVIRONMENT}}-cluster"""
@@ -83,14 +83,27 @@ class EcsServiceConfig:
83
83
  """Whether to assign public IP addresses"""
84
84
  return self._config.get("assign_public_ip", False)
85
85
 
86
+ @property
87
+ def load_balancer_config(self) -> Dict[str, Any]:
88
+ """Load balancer configuration"""
89
+ return self._config.get("load_balancer", {})
90
+
86
91
  @property
87
92
  def target_group_arns(self) -> List[str]:
88
93
  """Target group ARNs for load balancing"""
94
+ # Check if load_balancer config has target_group_arn
95
+ if self.load_balancer_config and self.load_balancer_config.get("target_group_arn"):
96
+ arn = self.load_balancer_config["target_group_arn"]
97
+ if arn and arn != "arn:aws:elasticloadbalancing:placeholder":
98
+ return [arn]
89
99
  return self._config.get("target_group_arns", [])
90
100
 
91
101
  @property
92
102
  def container_port(self) -> int:
93
103
  """Container port for load balancer"""
104
+ # Check load_balancer config first
105
+ if self.load_balancer_config and self.load_balancer_config.get("container_port"):
106
+ return self.load_balancer_config["container_port"]
94
107
  return self._config.get("container_port", 80)
95
108
 
96
109
  @property
@@ -138,8 +151,17 @@ class EcsServiceConfig:
138
151
  """SSM parameter imports"""
139
152
  # Check both nested and flat structures for backwards compatibility
140
153
  if "ssm" in self._config and "imports" in self._config["ssm"]:
141
- return self._config["ssm"]["imports"]
142
- return self.ssm.get("imports", {})
154
+ imports = self._config["ssm"]["imports"]
155
+ else:
156
+ imports = self.ssm.get("imports", {})
157
+
158
+ # Add load_balancer SSM imports if they exist
159
+ if self.load_balancer_config and "ssm" in self.load_balancer_config:
160
+ lb_ssm = self.load_balancer_config["ssm"]
161
+ if "imports" in lb_ssm:
162
+ imports.update(lb_ssm["imports"])
163
+
164
+ return imports
143
165
 
144
166
  @property
145
167
  def deployment_type(self) -> str:
@@ -49,16 +49,30 @@ class LambdaEdgeConfig(EnhancedBaseConfig):
49
49
 
50
50
  @property
51
51
  def timeout(self) -> int:
52
- """Timeout in seconds (max 5 for origin-request)"""
52
+ """Timeout in seconds
53
+ viewer-request: 5s
54
+ viewer-response: 5s
55
+ ---
56
+ origin-request: 30s
57
+ origin-response: 30s
58
+
59
+
60
+ """
53
61
  timeout = int(self._config.get("timeout", 5))
54
- if timeout > 5:
55
- raise ValueError("Lambda@Edge origin-request timeout cannot exceed 5 seconds")
62
+
63
+ event_type = self.event_type
64
+ if event_type == "viewer-request" or event_type == "viewer-response":
65
+ if timeout > 5:
66
+ raise ValueError("Lambda@Edge viewer timeout cannot exceed 5 seconds. Value was set to {}".format(timeout))
67
+ else:
68
+ if timeout > 30:
69
+ raise ValueError("Lambda@Edge origin timeout cannot exceed 30 seconds. Value was set to {}".format(timeout))
56
70
  return timeout
57
71
 
58
72
  @property
59
73
  def code_path(self) -> str:
60
74
  """Path to Lambda function code directory"""
61
- return self._config.get("code_path", "./lambdas/edge/ip_gate")
75
+ return self._config.get("code_path", "./lambdas/cloudfront/ip_gate")
62
76
 
63
77
  @property
64
78
  def environment(self) -> Dict[str, str]:
@@ -29,7 +29,7 @@ class RdsConfig(EnhancedBaseConfig):
29
29
  @property
30
30
  def name(self) -> str:
31
31
  """RDS instance name"""
32
- return self.__config.get("name", "database")
32
+ return self.__config.get("name", self.database_name)
33
33
 
34
34
  @property
35
35
  def identifier(self) -> str:
@@ -106,3 +106,8 @@ class Route53Config:
106
106
  def tags(self) -> Dict[str, str]:
107
107
  """Tags to apply to the Route53 resources"""
108
108
  return self.__config.get("tags", {})
109
+
110
+ @property
111
+ def records(self) -> List[Dict[str, Any]]:
112
+ """Records to create"""
113
+ return self.__config.get("records", [])
@@ -158,7 +158,15 @@ class S3BucketConfig(EnhancedBaseConfig):
158
158
  value = self.__config.get("block_public_access")
159
159
 
160
160
  if value and isinstance(value, str):
161
- if value.lower() == "block_acls":
161
+ if value.lower() == "disabled":
162
+ # For public website hosting, disable block public access
163
+ return s3.BlockPublicAccess(
164
+ block_public_acls=False,
165
+ block_public_policy=False,
166
+ ignore_public_acls=False,
167
+ restrict_public_buckets=False
168
+ )
169
+ elif value.lower() == "block_acls":
162
170
  return s3.BlockPublicAccess.BLOCK_ACLS
163
171
  # elif value.lower() == "block_public_acls":
164
172
  # return s3.BlockPublicAccess.block_public_acls
@@ -558,7 +558,7 @@ class CloudFrontDistributionConstruct(Construct):
558
558
  """
559
559
  bucket_policy = s3.BucketPolicy(
560
560
  self,
561
- id=f"CloudFrontBucketPolicy-{self.source_bucket.bucket_name}",
561
+ id="CloudFrontBucketPolicy",
562
562
  bucket=self.source_bucket,
563
563
  )
564
564
 
@@ -46,7 +46,7 @@ class ResourceResolver:
46
46
  ssm_config = lambda_dict.get("ssm", {})
47
47
 
48
48
  if ssm_config.get("enabled", False):
49
- self._ssm_mixin.setup_standardized_ssm_integration(
49
+ self._ssm_mixin.setup_ssm_integration(
50
50
  scope=self.scope,
51
51
  config=lambda_dict,
52
52
  resource_type="lambda",
@@ -26,7 +26,7 @@ class NetworkedStackMixin(StandardizedSsmMixin, VPCProviderMixin):
26
26
  # SSM initialization is handled automatically by StandardizedSsmMixin.__init__
27
27
 
28
28
  def _build(self, stack_config, deployment, workload):
29
- self.setup_standardized_ssm_integration(scope=self, config=stack_config.dictionary, resource_type="my-resource", resource_name="my-name")
29
+ self.setup_ssm_integration(scope=self, config=stack_config.dictionary, resource_type="my-resource", resource_name="my-name")
30
30
  self.vpc = self.resolve_vpc(stack_config, deployment, workload)
31
31
  """
32
32
 
@@ -24,6 +24,8 @@ import os
24
24
  import re
25
25
  from typing import Dict, Any, Optional, List, Union
26
26
  from aws_cdk import aws_ssm as ssm
27
+ from aws_cdk import aws_ec2 as ec2
28
+ from aws_cdk import aws_ecs as ecs
27
29
  from constructs import Construct
28
30
  from aws_lambda_powertools import Logger
29
31
  from cdk_factory.configurations.deployment import DeploymentConfig
@@ -117,6 +119,7 @@ class StandardizedSsmMixin:
117
119
  """Export multiple resource values to SSM Parameter Store."""
118
120
  params = {}
119
121
 
122
+ invalid_export_keys = []
120
123
  # Only export parameters that are explicitly configured in ssm_exports
121
124
  if not hasattr(config, 'ssm_exports') or not config.ssm_exports:
122
125
  logger.debug("No SSM exports configured")
@@ -136,8 +139,17 @@ class StandardizedSsmMixin:
136
139
  )
137
140
  params[key] = param
138
141
  else:
142
+ invalid_export_keys.append(key)
139
143
  logger.warning(f"SSM export configured for '{key}' but no value found in resource_values")
140
144
 
145
+ if invalid_export_keys:
146
+ message = f"Export SSM Error\n🚨 SSM exports configured for '{invalid_export_keys}' but no values found in resource_values"
147
+ available_keys = list(resource_values.keys())
148
+ message = f"{message}\n✅ Available keys: {available_keys}"
149
+ message = f"{message}\n👉 Please update to the correct key or remove from the export list."
150
+ logger.warning(message)
151
+ raise ValueError(message)
152
+
141
153
  return params
142
154
 
143
155
  def normalize_resource_name(self, name: str, for_export: bool = False) -> str:
@@ -151,7 +163,7 @@ class StandardizedSsmMixin:
151
163
  normalized = normalized.strip('-')
152
164
  return normalized
153
165
 
154
- def setup_standardized_ssm_integration(
166
+ def setup_ssm_integration(
155
167
  self,
156
168
  scope: Construct,
157
169
  config: Any,
@@ -200,7 +212,7 @@ class StandardizedSsmMixin:
200
212
  logger.info(f"SSM imports: {len(self.ssm_config.get('imports', {}))}")
201
213
  logger.info(f"SSM exports: {len(self.ssm_config.get('exports', {}))}")
202
214
 
203
- def process_standardized_ssm_imports(self) -> None:
215
+ def process_ssm_imports(self) -> None:
204
216
  """
205
217
  Process SSM imports using standardized approach.
206
218
 
@@ -228,7 +240,7 @@ class StandardizedSsmMixin:
228
240
  logger.error(error_msg)
229
241
  raise ValueError(error_msg)
230
242
 
231
- def export_standardized_ssm_parameters(self, resource_values: Dict[str, Any]) -> Dict[str, str]:
243
+ def export_ssm_parameters(self, resource_values: Dict[str, Any]) -> Dict[str, str]:
232
244
  """
233
245
  Export SSM parameters using standardized approach.
234
246
 
@@ -267,6 +279,23 @@ class StandardizedSsmMixin:
267
279
 
268
280
  return exported_params
269
281
 
282
+ def resolve_ssm_value(self, scope: Construct, value: str, unique_id: str)-> str:
283
+ if isinstance(value, str) and value.startswith("{{ssm:") and value.endswith("}}"):
284
+ # Extract SSM parameter path
285
+ ssm_param_path = value[6:-2] # Remove {{ssm: and }}
286
+
287
+ # Import SSM parameter - this creates a token that resolves at deployment time
288
+ param = ssm.StringParameter.from_string_parameter_name(
289
+ scope=scope,
290
+ id=f"{unique_id}-env-{hash(ssm_param_path) % 10000}",
291
+ string_parameter_name=ssm_param_path
292
+ )
293
+ resolved_value = param.string_value
294
+ logger.info(f"Resolved SSM parameter {ssm_param_path}")
295
+ return resolved_value
296
+ else:
297
+ return value
298
+
270
299
  def _resolve_ssm_import(self, import_value: Union[str, List[str]], import_key: str) -> Union[str, List[str]]:
271
300
  """
272
301
  Resolve SSM import value with proper error handling and validation.
@@ -336,16 +365,18 @@ class StandardizedSsmMixin:
336
365
  # Prepare template variables
337
366
  variables = {}
338
367
 
339
- if self.deployment:
368
+ # Always prioritize workload environment for consistency
369
+ if self.workload:
370
+ variables["ENVIRONMENT"] = self.workload.dictionary.get("environment", "test")
371
+ variables["WORKLOAD_NAME"] = self.workload.dictionary.get("name", "test-workload")
372
+ variables["AWS_REGION"] = os.getenv("AWS_REGION", "us-east-1")
373
+ elif self.deployment:
374
+ # Fallback to deployment only if workload not available
340
375
  variables["ENVIRONMENT"] = self.deployment.environment
341
376
  variables["WORKLOAD_NAME"] = self.deployment.workload_name
342
377
  variables["AWS_REGION"] = getattr(self.deployment, 'region', None) or os.getenv("AWS_REGION", "us-east-1")
343
- elif self.workload:
344
- variables["ENVIRONMENT"] = getattr(self.workload, 'environment', 'test')
345
- variables["WORKLOAD_NAME"] = getattr(self.workload, 'name', 'test-workload')
346
- variables["AWS_REGION"] = os.getenv("AWS_REGION", "us-east-1")
347
378
  else:
348
- # Fallback to environment variables
379
+ # Final fallback to environment variables
349
380
  variables["ENVIRONMENT"] = os.getenv("ENVIRONMENT", "test")
350
381
  variables["WORKLOAD_NAME"] = os.getenv("WORKLOAD_NAME", "test-workload")
351
382
  variables["AWS_REGION"] = os.getenv("AWS_REGION", "us-east-1")
@@ -396,7 +427,7 @@ class StandardizedSsmMixin:
396
427
  resource_type = segments[3]
397
428
 
398
429
  # Check for valid environment patterns
399
- if environment not in ["dev", "staging", "prod", "test"]:
430
+ if environment not in ["dev", "staging", "prod", "test", "alpha", "beta", "sandbox"]:
400
431
  logger.warning(f"{context}: Unusual environment segment: {environment}")
401
432
 
402
433
  # Check for valid resource type patterns
@@ -529,6 +560,44 @@ class StandardizedSsmMixin:
529
560
  return self._ssm_exported_values.copy()
530
561
 
531
562
 
563
+ def get_subnet_ids(self, config) -> List[str]:
564
+ """
565
+ Helper function to parse subnet IDs from SSM imports.
566
+
567
+ This common pattern handles:
568
+ 1. Comma-separated subnet ID strings from SSM
569
+ 2. List of subnet IDs from SSM
570
+ 3. Fallback to config attributes
571
+
572
+ Args:
573
+ config: Configuration object that might have subnet_ids attribute
574
+
575
+ Returns:
576
+ List of subnet IDs (empty list if not found or invalid format)
577
+ """
578
+ # Use the standardized SSM imports
579
+ ssm_imports = self.get_all_ssm_imports()
580
+ if "subnet_ids" in ssm_imports:
581
+ subnet_ids = ssm_imports["subnet_ids"]
582
+
583
+ # Handle comma-separated string or list
584
+ if isinstance(subnet_ids, str):
585
+ # Split comma-separated string
586
+ parsed_ids = [sid.strip() for sid in subnet_ids.split(',') if sid.strip()]
587
+ return parsed_ids
588
+ elif isinstance(subnet_ids, list):
589
+ return subnet_ids
590
+ else:
591
+ logger.warning(f"Unexpected subnet_ids type: {type(subnet_ids)}")
592
+ return []
593
+
594
+ # Fallback: Check config attributes
595
+ elif hasattr(config, 'subnet_ids') and config.subnet_ids:
596
+ return config.subnet_ids
597
+
598
+ else:
599
+ logger.warning("No subnet IDs found, using default behavior")
600
+ return []
532
601
 
533
602
  class ValidationResult:
534
603
  """Result of configuration validation."""
@@ -610,3 +679,6 @@ class SsmStandardValidator:
610
679
  errors.append(f"{context}: SSM path should use template variables: {path}")
611
680
 
612
681
  return errors
682
+
683
+
684
+
@@ -25,6 +25,7 @@ logger = Logger(service="AcmStack")
25
25
 
26
26
  @register_stack("acm_stack")
27
27
  @register_stack("certificate_stack")
28
+ @register_stack("certificate_library_module")
28
29
  class AcmStack(IStack, StandardizedSsmMixin):
29
30
  """
30
31
  Reusable stack for AWS Certificate Manager.
@@ -152,18 +153,7 @@ class AcmStack(IStack, StandardizedSsmMixin):
152
153
 
153
154
  def _add_outputs(self, cert_name: str) -> None:
154
155
  """Add CloudFormation outputs"""
155
- cdk.CfnOutput(
156
- self,
157
- "CertificateArn",
158
- value=self.certificate.certificate_arn,
159
- description=f"Certificate ARN for {self.acm_config.domain_name}",
160
- export_name=f"{cert_name}-arn",
161
- )
162
-
163
- cdk.CfnOutput(
164
- self,
165
- "DomainName",
166
- value=self.acm_config.domain_name,
167
- description="Primary domain name for the certificate",
168
- export_name=f"{cert_name}-domain",
169
- )
156
+
157
+ return
158
+
159
+
@@ -744,7 +744,7 @@ class ApiGatewayStack(IStack, StandardizedSsmMixin):
744
744
  # Setup enhanced SSM integration with proper resource type and name
745
745
  api_name = self.api_config.name or "api-gateway"
746
746
 
747
- self.setup_standardized_ssm_integration(
747
+ self.setup_ssm_integration(
748
748
  scope=self,
749
749
  config=self.stack_config.dictionary.get("api_gateway", {}),
750
750
  resource_type="api-gateway",
@@ -775,7 +775,7 @@ class ApiGatewayStack(IStack, StandardizedSsmMixin):
775
775
  resource_values["authorizer_id"] = authorizer.authorizer_id
776
776
 
777
777
  # Use enhanced SSM parameter export
778
- exported_params = self.export_standardized_ssm_parameters(resource_values)
778
+ exported_params = self.export_ssm_parameters(resource_values)
779
779
 
780
780
  if exported_params:
781
781
  logger.info(