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
@@ -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
@@ -285,31 +285,7 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
285
285
 
286
286
  def _add_outputs(self, db_name: str) -> None:
287
287
  """Add CloudFormation outputs for the RDS instance"""
288
- if self.db_instance:
289
- # Database endpoint
290
- cdk.CfnOutput(
291
- self,
292
- f"{db_name}-endpoint",
293
- value=self.db_instance.db_instance_endpoint_address,
294
- export_name=f"{self.deployment.build_resource_name(db_name)}-endpoint",
295
- )
296
-
297
- # Database port
298
- cdk.CfnOutput(
299
- self,
300
- f"{db_name}-port",
301
- value=self.db_instance.db_instance_endpoint_port,
302
- export_name=f"{self.deployment.build_resource_name(db_name)}-port",
303
- )
304
-
305
- # Secret ARN (if available)
306
- if hasattr(self.db_instance, "secret") and self.db_instance.secret:
307
- cdk.CfnOutput(
308
- self,
309
- f"{db_name}-secret-arn",
310
- value=self.db_instance.secret.secret_arn,
311
- export_name=f"{self.deployment.build_resource_name(db_name)}-secret-arn",
312
- )
288
+ return
313
289
 
314
290
  def _export_ssm_parameters(self, db_name: str) -> None:
315
291
  """Export RDS connection info and credentials to SSM Parameter Store"""
@@ -7,10 +7,15 @@ MIT License. See Project Root for the license information.
7
7
  from typing import Dict, Any, List, Optional
8
8
 
9
9
  import aws_cdk as cdk
10
- from aws_cdk import aws_route53 as route53
11
- from aws_cdk import aws_route53_targets as targets
12
- from aws_cdk import aws_certificatemanager as acm
13
- from aws_cdk import aws_elasticloadbalancingv2 as elbv2
10
+ from aws_cdk import (
11
+ aws_route53 as route53,
12
+ aws_route53_targets as targets,
13
+ aws_certificatemanager as acm,
14
+ aws_elasticloadbalancingv2 as elbv2,
15
+ aws_cloudfront as cloudfront,
16
+ Duration,
17
+ CfnOutput,
18
+ )
14
19
  from aws_lambda_powertools import Logger
15
20
  from constructs import Construct
16
21
 
@@ -18,7 +23,7 @@ from cdk_factory.configurations.deployment import DeploymentConfig
18
23
  from cdk_factory.configurations.stack import StackConfig
19
24
  from cdk_factory.configurations.resources.route53 import Route53Config
20
25
  from cdk_factory.interfaces.istack import IStack
21
- from cdk_factory.interfaces.enhanced_ssm_parameter_mixin import EnhancedSsmParameterMixin
26
+ from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
22
27
  from cdk_factory.stack.stack_module_registry import register_stack
23
28
  from cdk_factory.workload.workload_factory import WorkloadConfig
24
29
 
@@ -27,7 +32,7 @@ logger = Logger(service="Route53Stack")
27
32
 
28
33
  @register_stack("route53_library_module")
29
34
  @register_stack("route53_stack")
30
- class Route53Stack(IStack, EnhancedSsmParameterMixin):
35
+ class Route53Stack(IStack, StandardizedSsmMixin):
31
36
  """
32
37
  Reusable stack for AWS Route53.
33
38
  Supports creating hosted zones, DNS records, and certificate validation.
@@ -35,6 +40,7 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
35
40
 
36
41
  def __init__(self, scope: Construct, id: str, **kwargs) -> None:
37
42
  super().__init__(scope, id, **kwargs)
43
+
38
44
  self.route53_config = None
39
45
  self.stack_config = None
40
46
  self.deployment = None
@@ -42,6 +48,7 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
42
48
  self.hosted_zone = None
43
49
  self.certificate = None
44
50
  self.records = {}
51
+ self._distribution_cache = {} # Cache for reusing distributions
45
52
 
46
53
  def build(self, stack_config: StackConfig, deployment: DeploymentConfig, workload: WorkloadConfig) -> None:
47
54
  """Build the Route53 stack"""
@@ -112,6 +119,232 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
112
119
  return certificate
113
120
 
114
121
  def _create_dns_records(self) -> None:
122
+ self._create_dns_records_old()
123
+ self._create_dns_records_new()
124
+
125
+
126
+ def _get_or_create_cloudfront_distribution(self, distribution_domain: str, distribution_id: str) -> cloudfront.Distribution:
127
+ """Get or create a CloudFront distribution, reusing if already created"""
128
+ # Create a unique cache key from distribution domain and ID
129
+ cache_key = f"{distribution_domain}-{distribution_id}"
130
+
131
+ if cache_key not in self._distribution_cache:
132
+ # Create the distribution construct with a unique ID
133
+ unique_id = f"CF-{distribution_domain.replace('.', '-').replace('*', 'wildcard')}-{hash(cache_key) % 10000}"
134
+ distribution = cloudfront.Distribution.from_distribution_attributes(
135
+ self, unique_id,
136
+ domain_name=distribution_domain,
137
+ distribution_id=distribution_id
138
+ )
139
+ self._distribution_cache[cache_key] = distribution
140
+ logger.info(f"Created CloudFront distribution construct for {distribution_domain}")
141
+
142
+ return self._distribution_cache[cache_key]
143
+
144
+ def _create_dns_records_new(self) -> None:
145
+ """Create DNS records based on configuration - generic implementation"""
146
+
147
+ missing_configurations = []
148
+
149
+ for record in self.route53_config.records:
150
+ record_name = record.get("name", "")
151
+ record_type = record.get("type", "")
152
+
153
+ if not record_name or not record_type:
154
+ message = f"Record missing name or type: {record}"
155
+ logger.warning(message)
156
+ missing_configurations.append(message)
157
+ continue
158
+
159
+ # Handle alias records
160
+ if "alias" in record:
161
+ alias_config = record["alias"]
162
+ target_type = alias_config.get("target_type", "")
163
+ target_value = alias_config.get("target_value", "")
164
+ hosted_zone_id = alias_config.get("hosted_zone_id", "")
165
+
166
+ unique_id = f"{record_name}-{record_type}"
167
+ # Handle SSM parameter references in target_value
168
+ target_value = self.resolve_ssm_value(self, target_value, unique_id=unique_id)
169
+
170
+ if not target_type or not target_value:
171
+ message = f"Alias record missing target_type or target_value: {record}"
172
+ logger.warning(message)
173
+ missing_configurations.append(message)
174
+ continue
175
+
176
+ # Create appropriate target based on type
177
+ alias_target = None
178
+ if target_type == "cloudfront":
179
+ # CloudFront distribution target
180
+ distribution_domain = target_value
181
+ distribution_id = alias_config.get("distribution_id", "")
182
+ if not distribution_id:
183
+ message = f"Alias record missing distribution_id: {record}"
184
+ logger.warning(message)
185
+ missing_configurations.append(message)
186
+ continue
187
+
188
+ # Get or create the distribution (reuses if already created)
189
+ distribution = self._get_or_create_cloudfront_distribution(distribution_domain, distribution_id)
190
+ alias_target = route53.RecordTarget.from_alias(
191
+ targets.CloudFrontTarget(distribution)
192
+ )
193
+ elif target_type == "loadbalancer" or target_type == "alb":
194
+ # Load Balancer target
195
+ alias_target = route53.RecordTarget.from_alias(
196
+ targets.LoadBalancerTarget(
197
+ elbv2.ApplicationLoadBalancer.from_load_balancer_attributes(
198
+ self, f"ALB-{record_name}",
199
+ load_balancer_dns_name=target_value,
200
+ load_balancer_canonical_hosted_zone_id=hosted_zone_id
201
+ )
202
+ )
203
+ )
204
+ elif target_type == "elbv2":
205
+ # Generic ELBv2 target
206
+ alias_target = route53.RecordTarget.from_alias(
207
+ targets.LoadBalancerTarget(
208
+ elbv2.ApplicationLoadBalancer.from_load_balancer_attributes(
209
+ self, f"ELB-{record_name}",
210
+ load_balancer_dns_name=target_value,
211
+ load_balancer_canonical_hosted_zone_id=hosted_zone_id
212
+ )
213
+ )
214
+ )
215
+ else:
216
+ message = f"Unsupported alias target type: {target_type}"
217
+ logger.warning(message)
218
+ missing_configurations.append(message)
219
+ continue
220
+
221
+ # Create the alias record
222
+ route53.ARecord(
223
+ self,
224
+ f"AliasRecord-{record_name}-{record_type}",
225
+ zone=self.hosted_zone,
226
+ record_name=record_name,
227
+ target=alias_target,
228
+ ttl=cdk.Duration.seconds(record.get("ttl", 300))
229
+ ) if record_type == "A" else route53.AaaaRecord(
230
+ self,
231
+ f"AliasRecord-{record_name}-{record_type}",
232
+ zone=self.hosted_zone,
233
+ record_name=record_name,
234
+ target=alias_target,
235
+ ttl=cdk.Duration.seconds(record.get("ttl", 300))
236
+ )
237
+
238
+ # Handle standard records with values
239
+ elif "values" in record:
240
+ values = record["values"]
241
+ if not isinstance(values, list):
242
+ values = [values]
243
+
244
+ # Handle SSM parameter references in values
245
+ processed_values = []
246
+ for value in values:
247
+ if "{{ssm:" in str(value) and "}}" in str(value):
248
+ # Extract SSM parameter path from template like {{ssm:/path/to/parameter}}
249
+ ssm_path = str(value).split("{{ssm:")[1].split("}}")[0]
250
+ resolved_value = self.get_ssm_imported_value(ssm_path)
251
+ processed_values.append(resolved_value)
252
+ else:
253
+ processed_values.append(value)
254
+
255
+ values = processed_values
256
+ ttl = record.get("ttl", 300)
257
+
258
+ # Create standard record based on type
259
+ if record_type == "A":
260
+ route53.ARecord(
261
+ self,
262
+ f"Record-{record_name}",
263
+ zone=self.hosted_zone,
264
+ record_name=record_name,
265
+ target=route53.RecordTarget.from_ip_addresses(*values),
266
+ ttl=cdk.Duration.seconds(ttl)
267
+ )
268
+ elif record_type == "AAAA":
269
+ route53.AaaaRecord(
270
+ self,
271
+ f"Record-{record_name}",
272
+ zone=self.hosted_zone,
273
+ record_name=record_name,
274
+ target=route53.RecordTarget.from_ip_addresses(*values),
275
+ ttl=cdk.Duration.seconds(ttl)
276
+ )
277
+ elif record_type == "CNAME":
278
+ route53.CnameRecord(
279
+ self,
280
+ f"Record-{record_name}",
281
+ zone=self.hosted_zone,
282
+ record_name=record_name,
283
+ domain_name=values[0], # CNAME only supports single value
284
+ ttl=cdk.Duration.seconds(ttl)
285
+ )
286
+ elif record_type == "MX":
287
+ # MX records need special handling for preference values
288
+ mx_targets = []
289
+ for value in values:
290
+ if isinstance(value, str) and " " in value:
291
+ preference, domain = value.split(" ", 1)
292
+ mx_targets.append(route53.MxRecordValue(
293
+ domain_name=domain.strip(),
294
+ preference=int(preference.strip())
295
+ ))
296
+ else:
297
+ logger.warning(f"Invalid MX record format: {value}")
298
+
299
+ if mx_targets:
300
+ route53.MxRecord(
301
+ self,
302
+ f"Record-{record_name}",
303
+ zone=self.hosted_zone,
304
+ record_name=record_name,
305
+ values=mx_targets,
306
+ ttl=cdk.Duration.seconds(ttl)
307
+ )
308
+ elif record_type == "TXT":
309
+ route53.TxtRecord(
310
+ self,
311
+ f"Record-{record_name}",
312
+ zone=self.hosted_zone,
313
+ record_name=record_name,
314
+ values=values,
315
+ ttl=cdk.Duration.seconds(ttl)
316
+ )
317
+ elif record_type == "NS":
318
+ route53.NsRecord(
319
+ self,
320
+ f"Record-{record_name}",
321
+ zone=self.hosted_zone,
322
+ record_name=record_name,
323
+ values=values,
324
+ ttl=cdk.Duration.seconds(ttl)
325
+ )
326
+ else:
327
+ message = f"Unsupported record type: {record_type}"
328
+ logger.warning(message)
329
+ missing_configurations.append(message)
330
+ continue
331
+
332
+ else:
333
+ message = f"Record missing 'alias' or 'values' configuration: {record}"
334
+ logger.warning(message)
335
+ missing_configurations.append(message)
336
+ continue
337
+
338
+ if missing_configurations and len(missing_configurations) > 0:
339
+ # print all missing configurations
340
+ print("Missing configurations:")
341
+ for message in missing_configurations:
342
+ print(message)
343
+
344
+ messages = "\n".join(missing_configurations)
345
+ raise ValueError(f"Missing Configurations:\n{messages}")
346
+
347
+ def _create_dns_records_old(self) -> None:
115
348
  """Create DNS records based on configuration"""
116
349
  # Create alias records
117
350
  for alias_record in self.route53_config.aliases:
@@ -119,6 +352,12 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
119
352
  target_type = alias_record.get("target_type", "")
120
353
  target_value = alias_record.get("target_value", "")
121
354
 
355
+ # target value needs to handle SSM parameters
356
+ if "{{ssm:" in target_value and "}}" in target_value:
357
+ # Extract SSM parameter path from template like {{ssm:/path/to/parameter}}
358
+ ssm_path = target_value.split("{{ssm:")[1].split("}}")[0]
359
+ target_value = self.get_ssm_imported_value(ssm_path)
360
+
122
361
  if not record_name or not target_type or not target_value:
123
362
  continue
124
363
 
@@ -174,37 +413,4 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
174
413
  def _add_outputs(self) -> None:
175
414
  """Add CloudFormation outputs for the Route53 resources"""
176
415
  # Hosted Zone ID
177
- if self.hosted_zone:
178
- cdk.CfnOutput(
179
- self,
180
- "HostedZoneId",
181
- value=self.hosted_zone.hosted_zone_id,
182
- export_name=f"{self.deployment.build_resource_name('hosted-zone')}-id"
183
- )
184
-
185
- # Hosted Zone Name Servers
186
- if hasattr(self.hosted_zone, "name_servers") and self.hosted_zone.name_servers:
187
- cdk.CfnOutput(
188
- self,
189
- "NameServers",
190
- value=",".join(self.hosted_zone.name_servers),
191
- export_name=f"{self.deployment.build_resource_name('hosted-zone')}-name-servers"
192
- )
193
-
194
- # Certificate ARN
195
- if self.certificate:
196
- cdk.CfnOutput(
197
- self,
198
- "CertificateArn",
199
- value=self.certificate.certificate_arn,
200
- export_name=f"{self.deployment.build_resource_name('certificate')}-arn"
201
- )
202
-
203
- # Record names
204
- for name, record in self.records.items():
205
- cdk.CfnOutput(
206
- self,
207
- f"Record-{name}",
208
- value=name,
209
- export_name=f"{self.deployment.build_resource_name('record')}-{name}"
210
- )
416
+ return