cdk-factory 0.15.0__py3-none-any.whl → 0.15.2__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.

@@ -130,3 +130,8 @@ class RdsConfig(EnhancedBaseConfig):
130
130
  def vpc_id(self, value: str):
131
131
  """Sets the VPC ID for the Security Group"""
132
132
  self.__config["vpc_id"] = value
133
+
134
+ @property
135
+ def ssm_imports(self) -> Dict[str, str]:
136
+ """SSM parameter imports for the RDS instance"""
137
+ return self.__config.get("ssm_imports", {})
@@ -26,11 +26,16 @@ class Route53Config:
26
26
  def hosted_zone_id(self) -> Optional[str]:
27
27
  """Hosted zone ID"""
28
28
  return self.__config.get("hosted_zone_id")
29
+
30
+ @property
31
+ def existing_hosted_zone_id(self) -> Optional[str]:
32
+ """Existing hosted zone ID (alias for hosted_zone_id)"""
33
+ return self.__config.get("existing_hosted_zone_id") or self.__config.get("hosted_zone_id")
29
34
 
30
35
  @property
31
36
  def domain_name(self) -> Optional[str]:
32
- """Domain name"""
33
- return self.__config.get("domain_name")
37
+ """Domain name (also checks hosted_zone_name)"""
38
+ return self.__config.get("domain_name") or self.__config.get("hosted_zone_name")
34
39
 
35
40
  @property
36
41
  def record_names(self) -> List[str]:
@@ -61,3 +61,8 @@ class SecurityGroupFullStackConfig:
61
61
  def tags(self) -> Dict[str, str]:
62
62
  """Tags to apply to the Security Group"""
63
63
  return self.__config.get("tags", {})
64
+
65
+ @property
66
+ def ssm_imports(self) -> Dict[str, str]:
67
+ """SSM parameter imports for the Security Group"""
68
+ return self.__config.get("ssm_imports", {})
@@ -22,6 +22,8 @@ from constructs import Construct
22
22
  from cdk_factory.interfaces.istack import IStack
23
23
  from cdk_factory.stack.stack_module_registry import register_stack
24
24
  from cdk_factory.configurations.stack import StackConfig
25
+ from cdk_factory.configurations.deployment import DeploymentConfig
26
+ from cdk_factory.configurations.workload import WorkloadConfig
25
27
  from cdk_factory.configurations.resources.cloudfront import CloudFrontConfig
26
28
 
27
29
  logger = logging.getLogger(__name__)
@@ -39,110 +41,153 @@ class CloudFrontStack(IStack):
39
41
  - Custom error responses
40
42
  """
41
43
 
42
- def __init__(
43
- self,
44
- scope: Construct,
45
- id: str,
46
- stack_config: StackConfig,
47
- deployment,
48
- **kwargs,
49
- ) -> None:
44
+ def __init__(self, scope: Construct, id: str, **kwargs) -> None:
50
45
  super().__init__(scope, id, **kwargs)
51
46
 
52
- self.stack_config = stack_config
53
- self.deployment = deployment
54
-
55
- # CloudFront config
56
- cloudfront_dict = stack_config.dictionary.get("cloudfront", {})
57
- self.cf_config = CloudFrontConfig(cloudfront_dict, deployment)
58
-
47
+ self.stack_config = None
48
+ self.deployment = None
49
+ self.cf_config = None
50
+
59
51
  # Resources
60
52
  self.distribution: Optional[cloudfront.Distribution] = None
61
53
  self.certificate: Optional[acm.ICertificate] = None
62
54
  self.origins_map: Dict[str, cloudfront.IOrigin] = {}
63
55
 
56
+ # SSM imported values
57
+ self.ssm_imported_values: Dict[str, str] = {}
58
+
64
59
  def build(
65
60
  self,
61
+ stack_config: StackConfig,
62
+ deployment: DeploymentConfig,
63
+ workload: WorkloadConfig,
66
64
  vpc=None,
67
65
  target_groups=None,
68
66
  security_groups=None,
69
67
  shared=None,
70
68
  ):
71
69
  """Build the CloudFront distribution"""
72
-
70
+
71
+ self.stack_config = stack_config
72
+ self.deployment = deployment
73
+
74
+ # CloudFront config
75
+ cloudfront_dict = stack_config.dictionary.get("cloudfront", {})
76
+ self.cf_config = CloudFrontConfig(cloudfront_dict, deployment)
77
+
73
78
  logger.info(f"Building CloudFront distribution: {self.cf_config.name}")
74
-
79
+
80
+ # Process SSM imports first
81
+ self._process_ssm_imports()
82
+
75
83
  # Create certificate if needed
76
84
  if self.cf_config.certificate and self.cf_config.aliases:
77
85
  self._create_certificate()
78
-
86
+
79
87
  # Create origins
80
88
  self._create_origins()
81
-
89
+
82
90
  # Create distribution
83
91
  self._create_distribution()
84
-
92
+
85
93
  # Export SSM parameters
86
94
  self._export_ssm_parameters()
87
-
95
+
88
96
  # Create CloudFormation outputs
89
97
  self._create_outputs()
90
-
98
+
91
99
  return self
92
100
 
101
+ def _process_ssm_imports(self) -> None:
102
+ """
103
+ Process SSM imports from configuration.
104
+ Follows the same pattern as API Gateway stack - imports SSM parameters as CDK tokens.
105
+ """
106
+ ssm_imports = self.cf_config.ssm_imports
107
+
108
+ if not ssm_imports:
109
+ logger.debug("No SSM imports configured for CloudFront")
110
+ return
111
+
112
+ logger.info(f"Processing {len(ssm_imports)} SSM imports for CloudFront")
113
+
114
+ for param_key, param_path in ssm_imports.items():
115
+ try:
116
+ # Ensure parameter path starts with /
117
+ if not param_path.startswith("/"):
118
+ param_path = f"/{param_path}"
119
+
120
+ # Create unique construct ID from parameter path
121
+ construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
122
+
123
+ # Import SSM parameter - this creates a CDK token that resolves at deployment time
124
+ param = ssm.StringParameter.from_string_parameter_name(
125
+ self, construct_id, param_path
126
+ )
127
+
128
+ # Store the token value for use in configuration
129
+ self.ssm_imported_values[param_key] = param.string_value
130
+ logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
131
+
132
+ except Exception as e:
133
+ logger.error(
134
+ f"Failed to import SSM parameter {param_key} from {param_path}: {e}"
135
+ )
136
+ raise
137
+
93
138
  def _create_certificate(self) -> None:
94
139
  """Create or import ACM certificate for CloudFront (must be in us-east-1)"""
95
140
  cert_config = self.cf_config.certificate
96
-
141
+
97
142
  if not cert_config:
98
143
  return
99
-
144
+
100
145
  # Check if certificate ARN is provided
101
146
  cert_arn = cert_config.get("arn")
102
147
  if cert_arn:
103
148
  self.certificate = acm.Certificate.from_certificate_arn(
104
- self,
105
- "Certificate",
106
- certificate_arn=cert_arn
149
+ self, "Certificate", certificate_arn=cert_arn
107
150
  )
108
151
  logger.info(f"Using existing certificate: {cert_arn}")
109
152
  return
110
-
153
+
111
154
  # Check if we should import from SSM
112
155
  ssm_param = cert_config.get("ssm_parameter")
113
156
  if ssm_param:
114
157
  cert_arn = ssm.StringParameter.value_from_lookup(self, ssm_param)
115
158
  self.certificate = acm.Certificate.from_certificate_arn(
116
- self,
117
- "Certificate",
118
- certificate_arn=cert_arn
159
+ self, "Certificate", certificate_arn=cert_arn
119
160
  )
120
161
  logger.info(f"Using certificate from SSM: {ssm_param}")
121
162
  return
122
-
123
- logger.warning("No certificate ARN provided - CloudFront will use default certificate")
163
+
164
+ logger.warning(
165
+ "No certificate ARN provided - CloudFront will use default certificate"
166
+ )
124
167
 
125
168
  def _create_origins(self) -> None:
126
169
  """Create CloudFront origins (custom, S3, etc.)"""
127
170
  origins_config = self.cf_config.origins
128
-
171
+
129
172
  if not origins_config:
130
- raise ValueError("At least one origin is required for CloudFront distribution")
131
-
173
+ raise ValueError(
174
+ "At least one origin is required for CloudFront distribution"
175
+ )
176
+
132
177
  for origin_config in origins_config:
133
178
  origin_id = origin_config.get("id")
134
179
  origin_type = origin_config.get("type", "custom")
135
-
180
+
136
181
  if not origin_id:
137
182
  raise ValueError("Origin ID is required")
138
-
183
+
139
184
  if origin_type == "custom":
140
185
  origin = self._create_custom_origin(origin_config)
141
186
  elif origin_type == "s3":
142
187
  origin = self._create_s3_origin(origin_config)
143
188
  else:
144
189
  raise ValueError(f"Unsupported origin type: {origin_type}")
145
-
190
+
146
191
  self.origins_map[origin_id] = origin
147
192
  logger.info(f"Created {origin_type} origin: {origin_id}")
148
193
 
@@ -150,30 +195,41 @@ class CloudFrontStack(IStack):
150
195
  """Create custom origin (ALB, API Gateway, etc.)"""
151
196
  domain_name = config.get("domain_name")
152
197
  origin_id = config.get("id")
153
-
198
+
154
199
  if not domain_name:
155
200
  raise ValueError("domain_name is required for custom origin")
156
-
157
- # Check if domain name is an SSM parameter reference
158
- if domain_name.startswith("{{ssm:") and domain_name.endswith("}}"):
201
+
202
+ # Check if domain name is a placeholder from ssm_imports
203
+ if domain_name.startswith("{{") and domain_name.endswith("}}"):
204
+ placeholder_key = domain_name[2:-2] # Remove {{ and }}
205
+ if placeholder_key in self.ssm_imported_values:
206
+ domain_name = self.ssm_imported_values[placeholder_key]
207
+ logger.info(f"Resolved domain from SSM import: {placeholder_key}")
208
+ else:
209
+ logger.warning(f"Placeholder {domain_name} not found in SSM imports")
210
+
211
+ # Legacy support: Check if domain name is an SSM parameter reference
212
+ elif domain_name.startswith("{{ssm:") and domain_name.endswith("}}"):
159
213
  # Extract SSM parameter name
160
214
  ssm_param = domain_name[6:-2] # Remove {{ssm: and }}
161
215
  domain_name = ssm.StringParameter.value_from_lookup(self, ssm_param)
162
- logger.info(f"Resolved domain from SSM {ssm_param}: {domain_name}")
163
-
216
+ logger.info(f"Resolved domain from SSM lookup {ssm_param}: {domain_name}")
217
+
164
218
  # Build custom headers (e.g., X-Origin-Secret)
165
219
  custom_headers = {}
166
220
  custom_headers_config = config.get("custom_headers", {})
167
-
221
+
168
222
  for header_name, header_value in custom_headers_config.items():
169
223
  # Check if value is from Secrets Manager
170
224
  if isinstance(header_value, str) and header_value.startswith("{{secrets:"):
171
225
  # For now, just log a warning - Secrets Manager integration needs IAM permissions
172
- logger.warning(f"Secrets Manager references not yet supported in custom headers: {header_value}")
226
+ logger.warning(
227
+ f"Secrets Manager references not yet supported in custom headers: {header_value}"
228
+ )
173
229
  continue
174
-
230
+
175
231
  custom_headers[header_name] = header_value
176
-
232
+
177
233
  # Protocol policy
178
234
  protocol_policy_str = config.get("protocol_policy", "https-only")
179
235
  protocol_policy_map = {
@@ -181,12 +237,18 @@ class CloudFrontStack(IStack):
181
237
  "https-only": cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
182
238
  "match-viewer": cloudfront.OriginProtocolPolicy.MATCH_VIEWER,
183
239
  }
184
- protocol_policy = protocol_policy_map.get(protocol_policy_str, cloudfront.OriginProtocolPolicy.HTTPS_ONLY)
185
-
240
+ protocol_policy = protocol_policy_map.get(
241
+ protocol_policy_str, cloudfront.OriginProtocolPolicy.HTTPS_ONLY
242
+ )
243
+
186
244
  # SSL protocols
187
245
  origin_ssl_protocols_list = config.get("origin_ssl_protocols", ["TLSv1.2"])
188
- ssl_protocols = [cloudfront.OriginSslPolicy.TLS_V1_2] if "TLSv1.2" in origin_ssl_protocols_list else []
189
-
246
+ ssl_protocols = (
247
+ [cloudfront.OriginSslPolicy.TLS_V1_2]
248
+ if "TLSv1.2" in origin_ssl_protocols_list
249
+ else []
250
+ )
251
+
190
252
  # Create custom origin with explicit origin ID
191
253
  return origins.HttpOrigin(
192
254
  domain_name,
@@ -207,37 +269,41 @@ class CloudFrontStack(IStack):
207
269
  """Create S3 origin"""
208
270
  # S3 origin implementation
209
271
  # This would use origins.S3Origin
210
- raise NotImplementedError("S3 origin support - use existing static_website_stack for S3")
272
+ raise NotImplementedError(
273
+ "S3 origin support - use existing static_website_stack for S3"
274
+ )
211
275
 
212
276
  def _create_distribution(self) -> None:
213
277
  """Create CloudFront distribution"""
214
-
278
+
215
279
  # Get default origin
216
280
  default_behavior_config = self.cf_config.default_cache_behavior
217
281
  target_origin_id = default_behavior_config.get("target_origin_id")
218
-
282
+
219
283
  if not target_origin_id or target_origin_id not in self.origins_map:
220
284
  raise ValueError(f"Default cache behavior must reference a valid origin ID")
221
-
285
+
222
286
  default_origin = self.origins_map[target_origin_id]
223
-
287
+
224
288
  # Build default behavior
225
- default_behavior = self._build_cache_behavior(default_behavior_config, default_origin)
226
-
289
+ default_behavior = self._build_cache_behavior(
290
+ default_behavior_config, default_origin
291
+ )
292
+
227
293
  # Build additional behaviors
228
294
  additional_behaviors = {}
229
295
  for behavior_config in self.cf_config.cache_behaviors:
230
296
  path_pattern = behavior_config.get("path_pattern")
231
297
  origin_id = behavior_config.get("target_origin_id")
232
-
298
+
233
299
  if not path_pattern or not origin_id or origin_id not in self.origins_map:
234
300
  logger.warning(f"Invalid cache behavior config, skipping")
235
301
  continue
236
-
302
+
237
303
  origin = self.origins_map[origin_id]
238
304
  behavior = self._build_cache_behavior_options(behavior_config)
239
305
  additional_behaviors[path_pattern] = behavior
240
-
306
+
241
307
  # Build error responses
242
308
  error_responses = []
243
309
  for error_config in self.cf_config.custom_error_responses:
@@ -248,10 +314,12 @@ class CloudFrontStack(IStack):
248
314
  http_status=error_code,
249
315
  response_http_status=error_config.get("response_http_status"),
250
316
  response_page_path=error_config.get("response_page_path"),
251
- ttl=Duration.seconds(error_config.get("error_caching_min_ttl", 10)),
317
+ ttl=Duration.seconds(
318
+ error_config.get("error_caching_min_ttl", 10)
319
+ ),
252
320
  )
253
321
  )
254
-
322
+
255
323
  # HTTP version
256
324
  http_version_map = {
257
325
  "http1_1": cloudfront.HttpVersion.HTTP1_1,
@@ -265,12 +333,11 @@ class CloudFrontStack(IStack):
265
333
  except AttributeError:
266
334
  # Fall back to HTTP2 if HTTP2_AND_3/HTTP3 not available in this CDK version
267
335
  default_version = cloudfront.HttpVersion.HTTP2
268
-
336
+
269
337
  http_version = http_version_map.get(
270
- self.cf_config.http_version.lower(),
271
- default_version
338
+ self.cf_config.http_version.lower(), default_version
272
339
  )
273
-
340
+
274
341
  # Price class
275
342
  price_class_map = {
276
343
  "PriceClass_100": cloudfront.PriceClass.PRICE_CLASS_100,
@@ -278,10 +345,9 @@ class CloudFrontStack(IStack):
278
345
  "PriceClass_All": cloudfront.PriceClass.PRICE_CLASS_ALL,
279
346
  }
280
347
  price_class = price_class_map.get(
281
- self.cf_config.price_class,
282
- cloudfront.PriceClass.PRICE_CLASS_100
348
+ self.cf_config.price_class, cloudfront.PriceClass.PRICE_CLASS_100
283
349
  )
284
-
350
+
285
351
  # Create distribution
286
352
  self.distribution = cloudfront.Distribution(
287
353
  self,
@@ -298,16 +364,16 @@ class CloudFrontStack(IStack):
298
364
  default_root_object=self.cf_config.default_root_object,
299
365
  web_acl_id=self.cf_config.waf_web_acl_id,
300
366
  )
301
-
302
- logger.info(f"Created CloudFront distribution: {self.distribution.distribution_id}")
367
+
368
+ logger.info(
369
+ f"Created CloudFront distribution: {self.distribution.distribution_id}"
370
+ )
303
371
 
304
372
  def _build_cache_behavior(
305
- self,
306
- config: Dict[str, Any],
307
- origin: cloudfront.IOrigin
373
+ self, config: Dict[str, Any], origin: cloudfront.IOrigin
308
374
  ) -> cloudfront.BehaviorOptions:
309
375
  """Build cache behavior with origin"""
310
-
376
+
311
377
  # Viewer protocol policy
312
378
  viewer_protocol_str = config.get("viewer_protocol_policy", "redirect-to-https")
313
379
  viewer_protocol_map = {
@@ -316,35 +382,43 @@ class CloudFrontStack(IStack):
316
382
  "https-only": cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
317
383
  }
318
384
  viewer_protocol_policy = viewer_protocol_map.get(
319
- viewer_protocol_str,
320
- cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
385
+ viewer_protocol_str, cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
321
386
  )
322
-
387
+
323
388
  # Allowed methods
324
389
  allowed_methods_str = config.get("allowed_methods", ["GET", "HEAD"])
325
- if "DELETE" in allowed_methods_str or "PUT" in allowed_methods_str or "PATCH" in allowed_methods_str or "POST" in allowed_methods_str:
390
+ if (
391
+ "DELETE" in allowed_methods_str
392
+ or "PUT" in allowed_methods_str
393
+ or "PATCH" in allowed_methods_str
394
+ or "POST" in allowed_methods_str
395
+ ):
326
396
  allowed_methods = cloudfront.AllowedMethods.ALLOW_ALL
327
397
  elif "OPTIONS" in allowed_methods_str:
328
398
  allowed_methods = cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
329
399
  else:
330
400
  allowed_methods = cloudfront.AllowedMethods.ALLOW_GET_HEAD
331
-
401
+
332
402
  # Cached methods
333
403
  cached_methods_str = config.get("cached_methods", ["GET", "HEAD"])
334
404
  if "OPTIONS" in cached_methods_str:
335
405
  cached_methods = cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
336
406
  else:
337
407
  cached_methods = cloudfront.CachedMethods.CACHE_GET_HEAD
338
-
408
+
339
409
  # Cache policy
340
410
  cache_policy = self._build_cache_policy(config.get("cache_policy", {}))
341
-
411
+
342
412
  # Origin request policy
343
- origin_request_policy = self._build_origin_request_policy(config.get("origin_request_policy", {}))
344
-
413
+ origin_request_policy = self._build_origin_request_policy(
414
+ config.get("origin_request_policy", {})
415
+ )
416
+
345
417
  # Lambda@Edge associations
346
- edge_lambdas = self._build_lambda_edge_associations(config.get("lambda_edge_associations", []))
347
-
418
+ edge_lambdas = self._build_lambda_edge_associations(
419
+ config.get("lambda_edge_associations", [])
420
+ )
421
+
348
422
  return cloudfront.BehaviorOptions(
349
423
  origin=origin,
350
424
  viewer_protocol_policy=viewer_protocol_policy,
@@ -362,24 +436,26 @@ class CloudFrontStack(IStack):
362
436
  # For now, return basic structure
363
437
  return {}
364
438
 
365
- def _build_cache_policy(self, config: Dict[str, Any]) -> Optional[cloudfront.ICachePolicy]:
439
+ def _build_cache_policy(
440
+ self, config: Dict[str, Any]
441
+ ) -> Optional[cloudfront.ICachePolicy]:
366
442
  """Build or reference cache policy"""
367
443
  if not config:
368
444
  # Use managed caching disabled policy for dynamic content
369
445
  return cloudfront.CachePolicy.CACHING_DISABLED
370
-
446
+
371
447
  policy_name = config.get("name")
372
-
448
+
373
449
  # Check for managed policies
374
450
  managed_policies = {
375
451
  "CachingOptimized": cloudfront.CachePolicy.CACHING_OPTIMIZED,
376
452
  "CachingDisabled": cloudfront.CachePolicy.CACHING_DISABLED,
377
453
  "CachingOptimizedForUncompressedObjects": cloudfront.CachePolicy.CACHING_OPTIMIZED_FOR_UNCOMPRESSED_OBJECTS,
378
454
  }
379
-
455
+
380
456
  if policy_name in managed_policies:
381
457
  return managed_policies[policy_name]
382
-
458
+
383
459
  # Create custom cache policy
384
460
  return cloudfront.CachePolicy(
385
461
  self,
@@ -390,47 +466,65 @@ class CloudFrontStack(IStack):
390
466
  min_ttl=Duration.seconds(config.get("min_ttl", 0)),
391
467
  max_ttl=Duration.seconds(config.get("max_ttl", 31536000)),
392
468
  enable_accept_encoding_gzip=config.get("enable_accept_encoding_gzip", True),
393
- enable_accept_encoding_brotli=config.get("enable_accept_encoding_brotli", True),
394
- header_behavior=self._build_cache_header_behavior(config.get("headers_config", {})),
395
- query_string_behavior=self._build_cache_query_string_behavior(config.get("query_strings_config", {})),
396
- cookie_behavior=self._build_cache_cookie_behavior(config.get("cookies_config", {})),
469
+ enable_accept_encoding_brotli=config.get(
470
+ "enable_accept_encoding_brotli", True
471
+ ),
472
+ header_behavior=self._build_cache_header_behavior(
473
+ config.get("headers_config", {})
474
+ ),
475
+ query_string_behavior=self._build_cache_query_string_behavior(
476
+ config.get("query_strings_config", {})
477
+ ),
478
+ cookie_behavior=self._build_cache_cookie_behavior(
479
+ config.get("cookies_config", {})
480
+ ),
397
481
  )
398
482
 
399
- def _build_origin_request_policy(self, config: Dict[str, Any]) -> Optional[cloudfront.IOriginRequestPolicy]:
483
+ def _build_origin_request_policy(
484
+ self, config: Dict[str, Any]
485
+ ) -> Optional[cloudfront.IOriginRequestPolicy]:
400
486
  """Build or reference origin request policy"""
401
487
  if not config:
402
488
  # Use managed all viewer policy
403
489
  return cloudfront.OriginRequestPolicy.ALL_VIEWER
404
-
490
+
405
491
  policy_name = config.get("name")
406
-
492
+
407
493
  # Check for managed policies
408
494
  managed_policies = {
409
495
  "AllViewer": cloudfront.OriginRequestPolicy.ALL_VIEWER,
410
496
  "AllViewerExceptHostHeader": cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
411
- "AllViewerAndCloudFrontHeaders2022": cloudfront.OriginRequestPolicy.ALL_VIEWER_AND_CLOUDFRONT_HEADERS_2022,
497
+ "AllViewerAndCloudFrontHeaders2022": cloudfront.OriginRequestPolicy.ALL_VIEWER_AND_CLOUDFRONT_2022,
412
498
  "CORS-CustomOrigin": cloudfront.OriginRequestPolicy.CORS_CUSTOM_ORIGIN,
413
499
  "CORS-S3Origin": cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
414
500
  }
415
-
501
+
416
502
  if policy_name in managed_policies:
417
503
  return managed_policies[policy_name]
418
-
504
+
419
505
  # Create custom origin request policy
420
506
  return cloudfront.OriginRequestPolicy(
421
507
  self,
422
508
  f"OriginRequestPolicy-{policy_name}",
423
509
  origin_request_policy_name=policy_name,
424
510
  comment=config.get("comment", ""),
425
- header_behavior=self._build_origin_header_behavior(config.get("headers_config", {})),
426
- query_string_behavior=self._build_origin_query_string_behavior(config.get("query_strings_config", {})),
427
- cookie_behavior=self._build_origin_cookie_behavior(config.get("cookies_config", {})),
511
+ header_behavior=self._build_origin_header_behavior(
512
+ config.get("headers_config", {})
513
+ ),
514
+ query_string_behavior=self._build_origin_query_string_behavior(
515
+ config.get("query_strings_config", {})
516
+ ),
517
+ cookie_behavior=self._build_origin_cookie_behavior(
518
+ config.get("cookies_config", {})
519
+ ),
428
520
  )
429
521
 
430
- def _build_cache_header_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheHeaderBehavior:
522
+ def _build_cache_header_behavior(
523
+ self, config: Dict[str, Any]
524
+ ) -> cloudfront.CacheHeaderBehavior:
431
525
  """Build cache header behavior"""
432
526
  behavior = config.get("behavior", "none")
433
-
527
+
434
528
  if behavior == "none":
435
529
  return cloudfront.CacheHeaderBehavior.none()
436
530
  elif behavior == "whitelist":
@@ -439,10 +533,12 @@ class CloudFrontStack(IStack):
439
533
  else:
440
534
  return cloudfront.CacheHeaderBehavior.none()
441
535
 
442
- def _build_cache_query_string_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheQueryStringBehavior:
536
+ def _build_cache_query_string_behavior(
537
+ self, config: Dict[str, Any]
538
+ ) -> cloudfront.CacheQueryStringBehavior:
443
539
  """Build cache query string behavior"""
444
540
  behavior = config.get("behavior", "none")
445
-
541
+
446
542
  if behavior == "none":
447
543
  return cloudfront.CacheQueryStringBehavior.none()
448
544
  elif behavior == "all":
@@ -456,10 +552,12 @@ class CloudFrontStack(IStack):
456
552
  else:
457
553
  return cloudfront.CacheQueryStringBehavior.none()
458
554
 
459
- def _build_cache_cookie_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheCookieBehavior:
555
+ def _build_cache_cookie_behavior(
556
+ self, config: Dict[str, Any]
557
+ ) -> cloudfront.CacheCookieBehavior:
460
558
  """Build cache cookie behavior"""
461
559
  behavior = config.get("behavior", "none")
462
-
560
+
463
561
  if behavior == "none":
464
562
  return cloudfront.CacheCookieBehavior.none()
465
563
  elif behavior == "all":
@@ -473,44 +571,52 @@ class CloudFrontStack(IStack):
473
571
  else:
474
572
  return cloudfront.CacheCookieBehavior.none()
475
573
 
476
- def _build_origin_header_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestHeaderBehavior:
574
+ def _build_origin_header_behavior(
575
+ self, config: Dict[str, Any]
576
+ ) -> cloudfront.OriginRequestHeaderBehavior:
477
577
  """Build origin request header behavior"""
478
578
  behavior = config.get("behavior", "none")
479
-
579
+
480
580
  if behavior == "none":
481
581
  return cloudfront.OriginRequestHeaderBehavior.none()
482
582
  elif behavior == "all":
483
- return cloudfront.OriginRequestHeaderBehavior.all_viewer()
583
+ return cloudfront.OriginRequestHeaderBehavior.all()
484
584
  elif behavior == "whitelist":
485
585
  headers = config.get("headers", [])
486
586
  return cloudfront.OriginRequestHeaderBehavior.allow_list(*headers)
487
587
  elif behavior == "allViewerAndWhitelistCloudFront":
488
- headers = config.get("headers", [])
489
- return cloudfront.OriginRequestHeaderBehavior.all_viewer_and_whitelist_cloud_front(*headers)
588
+ # For now, just forward all headers - this matches the intent
589
+ return cloudfront.OriginRequestHeaderBehavior.all()
490
590
  else:
491
591
  return cloudfront.OriginRequestHeaderBehavior.none()
492
592
 
493
- def _build_origin_query_string_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestQueryStringBehavior:
593
+ def _build_origin_query_string_behavior(
594
+ self, config: Dict[str, Any]
595
+ ) -> cloudfront.OriginRequestQueryStringBehavior:
494
596
  """Build origin request query string behavior"""
495
597
  behavior = config.get("behavior", "none")
496
-
598
+
497
599
  if behavior == "none":
498
600
  return cloudfront.OriginRequestQueryStringBehavior.none()
499
601
  elif behavior == "all":
500
602
  return cloudfront.OriginRequestQueryStringBehavior.all()
501
603
  elif behavior == "whitelist":
502
604
  query_strings = config.get("query_strings", [])
503
- return cloudfront.OriginRequestQueryStringBehavior.allow_list(*query_strings)
605
+ return cloudfront.OriginRequestQueryStringBehavior.allow_list(
606
+ *query_strings
607
+ )
504
608
  elif behavior == "allExcept":
505
609
  query_strings = config.get("query_strings", [])
506
610
  return cloudfront.OriginRequestQueryStringBehavior.deny_list(*query_strings)
507
611
  else:
508
612
  return cloudfront.OriginRequestQueryStringBehavior.none()
509
613
 
510
- def _build_origin_cookie_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestCookieBehavior:
614
+ def _build_origin_cookie_behavior(
615
+ self, config: Dict[str, Any]
616
+ ) -> cloudfront.OriginRequestCookieBehavior:
511
617
  """Build origin request cookie behavior"""
512
618
  behavior = config.get("behavior", "none")
513
-
619
+
514
620
  if behavior == "none":
515
621
  return cloudfront.OriginRequestCookieBehavior.none()
516
622
  elif behavior == "all":
@@ -521,26 +627,39 @@ class CloudFrontStack(IStack):
521
627
  else:
522
628
  return cloudfront.OriginRequestCookieBehavior.none()
523
629
 
524
- def _build_lambda_edge_associations(self, associations: List[Dict[str, Any]]) -> Optional[List[cloudfront.EdgeLambda]]:
630
+ def _build_lambda_edge_associations(
631
+ self, associations: List[Dict[str, Any]]
632
+ ) -> Optional[List[cloudfront.EdgeLambda]]:
525
633
  """Build Lambda@Edge associations"""
526
634
  if not associations:
527
635
  return None
528
-
636
+
529
637
  edge_lambdas = []
530
-
638
+
531
639
  for assoc in associations:
532
640
  event_type_str = assoc.get("event_type", "origin-request")
533
641
  lambda_arn = assoc.get("lambda_arn")
534
-
642
+
535
643
  if not lambda_arn:
536
644
  continue
537
-
538
- # Check if ARN is an SSM parameter reference
539
- if lambda_arn.startswith("{{ssm:") and lambda_arn.endswith("}}"):
645
+
646
+ # Check if ARN is a placeholder from ssm_imports
647
+ if lambda_arn.startswith("{{") and lambda_arn.endswith("}}"):
648
+ placeholder_key = lambda_arn[2:-2] # Remove {{ and }}
649
+ if placeholder_key in self.ssm_imported_values:
650
+ lambda_arn = self.ssm_imported_values[placeholder_key]
651
+ logger.info(
652
+ f"Resolved Lambda ARN from SSM import: {placeholder_key}"
653
+ )
654
+ else:
655
+ logger.warning(f"Placeholder {lambda_arn} not found in SSM imports")
656
+
657
+ # Legacy support: Check if ARN is an SSM parameter reference
658
+ elif lambda_arn.startswith("{{ssm:") and lambda_arn.endswith("}}"):
540
659
  ssm_param = lambda_arn[6:-2]
541
660
  lambda_arn = ssm.StringParameter.value_from_lookup(self, ssm_param)
542
- logger.info(f"Resolved Lambda ARN from SSM {ssm_param}")
543
-
661
+ logger.info(f"Resolved Lambda ARN from SSM lookup {ssm_param}")
662
+
544
663
  # Map event type
545
664
  event_type_map = {
546
665
  "viewer-request": cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
@@ -548,56 +667,65 @@ class CloudFrontStack(IStack):
548
667
  "origin-response": cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
549
668
  "viewer-response": cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE,
550
669
  }
551
-
552
- event_type = event_type_map.get(event_type_str, cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST)
553
-
670
+
671
+ event_type = event_type_map.get(
672
+ event_type_str, cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST
673
+ )
674
+
554
675
  # Import Lambda version
555
676
  lambda_version = _lambda.Version.from_version_arn(
556
- self,
557
- f"LambdaEdge-{event_type_str}",
558
- version_arn=lambda_arn
677
+ self, f"LambdaEdge-{event_type_str}", version_arn=lambda_arn
559
678
  )
560
-
679
+
561
680
  edge_lambdas.append(
562
681
  cloudfront.EdgeLambda(
563
682
  function_version=lambda_version,
564
683
  event_type=event_type,
565
- include_body=assoc.get("include_body", False)
684
+ include_body=assoc.get("include_body", False),
566
685
  )
567
686
  )
568
-
687
+
569
688
  return edge_lambdas if edge_lambdas else None
570
689
 
571
690
  def _export_ssm_parameters(self) -> None:
572
691
  """Export distribution info to SSM Parameter Store"""
573
692
  ssm_exports = self.cf_config.ssm_exports
574
-
693
+
575
694
  if not ssm_exports:
576
695
  return
577
-
696
+
578
697
  if "distribution_id" in ssm_exports:
698
+ param_name = ssm_exports["distribution_id"]
699
+ if not param_name.startswith("/"):
700
+ param_name = f"/{param_name}"
579
701
  ssm.StringParameter(
580
702
  self,
581
703
  "DistributionIdParam",
582
- parameter_name=ssm_exports["distribution_id"],
704
+ parameter_name=param_name,
583
705
  string_value=self.distribution.distribution_id,
584
706
  description=f"CloudFront Distribution ID for {self.cf_config.name}",
585
707
  )
586
-
708
+
587
709
  if "distribution_domain" in ssm_exports:
710
+ param_name = ssm_exports["distribution_domain"]
711
+ if not param_name.startswith("/"):
712
+ param_name = f"/{param_name}"
588
713
  ssm.StringParameter(
589
714
  self,
590
715
  "DistributionDomainParam",
591
- parameter_name=ssm_exports["distribution_domain"],
716
+ parameter_name=param_name,
592
717
  string_value=self.distribution.distribution_domain_name,
593
718
  description=f"CloudFront Distribution Domain for {self.cf_config.name}",
594
719
  )
595
-
720
+
596
721
  if "distribution_arn" in ssm_exports:
722
+ param_name = ssm_exports["distribution_arn"]
723
+ if not param_name.startswith("/"):
724
+ param_name = f"/{param_name}"
597
725
  ssm.StringParameter(
598
726
  self,
599
727
  "DistributionArnParam",
600
- parameter_name=ssm_exports["distribution_arn"],
728
+ parameter_name=param_name,
601
729
  string_value=f"arn:aws:cloudfront::{self.account}:distribution/{self.distribution.distribution_id}",
602
730
  description=f"CloudFront Distribution ARN for {self.cf_config.name}",
603
731
  )
@@ -610,14 +738,14 @@ class CloudFrontStack(IStack):
610
738
  value=self.distribution.distribution_id,
611
739
  description="CloudFront Distribution ID",
612
740
  )
613
-
741
+
614
742
  CfnOutput(
615
743
  self,
616
744
  "DistributionDomain",
617
745
  value=self.distribution.distribution_domain_name,
618
746
  description="CloudFront Distribution Domain Name",
619
747
  )
620
-
748
+
621
749
  if self.cf_config.aliases:
622
750
  CfnOutput(
623
751
  self,
@@ -9,6 +9,7 @@ from typing import Dict, Any, List, Optional
9
9
  import aws_cdk as cdk
10
10
  from aws_cdk import aws_rds as rds
11
11
  from aws_cdk import aws_ec2 as ec2
12
+ from aws_cdk import aws_ssm as ssm
12
13
  from aws_cdk import Duration
13
14
  from aws_lambda_powertools import Logger
14
15
  from constructs import Construct
@@ -41,6 +42,8 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
41
42
  self.db_instance = None
42
43
  self.security_groups = []
43
44
  self._vpc = None
45
+ # SSM imported values
46
+ self.ssm_imported_values: Dict[str, str] = {}
44
47
 
45
48
  def build(
46
49
  self,
@@ -65,6 +68,9 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
65
68
  self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
66
69
  db_name = deployment.build_resource_name(self.rds_config.name)
67
70
 
71
+ # Process SSM imports first
72
+ self._process_ssm_imports()
73
+
68
74
  # Get VPC and security groups
69
75
  self.security_groups = self._get_security_groups()
70
76
 
@@ -77,18 +83,58 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
77
83
  # Add outputs
78
84
  self._add_outputs(db_name)
79
85
 
86
+ def _process_ssm_imports(self) -> None:
87
+ """Process SSM imports from configuration"""
88
+ ssm_imports = self.rds_config.ssm_imports
89
+
90
+ if not ssm_imports:
91
+ logger.debug("No SSM imports configured for RDS")
92
+ return
93
+
94
+ logger.info(f"Processing {len(ssm_imports)} SSM imports for RDS")
95
+
96
+ for param_key, param_path in ssm_imports.items():
97
+ try:
98
+ if not param_path.startswith('/'):
99
+ param_path = f"/{param_path}"
100
+
101
+ construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
102
+ param = ssm.StringParameter.from_string_parameter_name(
103
+ self, construct_id, param_path
104
+ )
105
+
106
+ self.ssm_imported_values[param_key] = param.string_value
107
+ logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
108
+
109
+ except Exception as e:
110
+ logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
111
+ raise
112
+
80
113
  @property
81
114
  def vpc(self) -> ec2.IVpc:
82
115
  """Get the VPC for the RDS instance"""
83
- # Assuming VPC is provided by the workload
84
116
  if self._vpc:
85
117
  return self._vpc
86
- if self.rds_config.vpc_id:
118
+
119
+ # Check SSM imported values first (tokens from SSM parameters)
120
+ if "vpc_id" in self.ssm_imported_values:
121
+ vpc_id = self.ssm_imported_values["vpc_id"]
122
+
123
+ # When using tokens, we can't provide subnet lists to from_vpc_attributes
124
+ # because CDK validates subnet count against AZ count at synthesis time
125
+ # We'll create a DB subnet group separately instead
126
+ vpc_attrs = {
127
+ "vpc_id": vpc_id,
128
+ "availability_zones": ["us-east-1a", "us-east-1b"]
129
+ }
130
+
131
+ # Use from_vpc_attributes() for SSM tokens
132
+ self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
133
+ elif self.rds_config.vpc_id:
87
134
  self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.rds_config.vpc_id)
88
- if self.workload.vpc_id:
135
+ elif self.workload.vpc_id:
89
136
  self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
90
137
  else:
91
- # Use default VPC if not provided
92
138
  raise ValueError(
93
139
  "VPC is not defined in the configuration. "
94
140
  "You can provide it a the rds.vpc_id in the configuration "
@@ -99,19 +145,47 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
99
145
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
100
146
  """Get security groups for the RDS instance"""
101
147
  security_groups = []
102
- for sg_id in self.rds_config.security_group_ids:
148
+
149
+ # Check SSM imports first for security group ID
150
+ if "security_group_rds_id" in self.ssm_imported_values:
151
+ sg_id = self.ssm_imported_values["security_group_rds_id"]
152
+ security_groups.append(
153
+ ec2.SecurityGroup.from_security_group_id(
154
+ self, "RDSSecurityGroup", sg_id
155
+ )
156
+ )
157
+
158
+ # Also check config for any additional security group IDs
159
+ for idx, sg_id in enumerate(self.rds_config.security_group_ids):
103
160
  security_groups.append(
104
161
  ec2.SecurityGroup.from_security_group_id(
105
- self, f"SecurityGroup-{sg_id}", sg_id
162
+ self, f"SecurityGroup-{idx}", sg_id
106
163
  )
107
164
  )
165
+
108
166
  return security_groups
109
167
 
110
168
  def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
111
169
  """Create a new RDS instance"""
112
- # Configure subnet selection
113
- # Use private subnets for database placement
114
- subnets = ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS)
170
+ # Configure subnet group
171
+ # If we have subnet IDs from SSM, create a DB subnet group explicitly
172
+ db_subnet_group = None
173
+ if "subnet_ids" in self.ssm_imported_values:
174
+ subnet_ids_str = self.ssm_imported_values["subnet_ids"]
175
+ # Split the comma-separated token into a list
176
+ subnet_ids_list = cdk.Fn.split(",", subnet_ids_str)
177
+
178
+ # Create DB subnet group with the token-based subnet list
179
+ db_subnet_group = rds.CfnDBSubnetGroup(
180
+ self,
181
+ "DBSubnetGroup",
182
+ db_subnet_group_description=f"Subnet group for {db_name}",
183
+ subnet_ids=subnet_ids_list,
184
+ db_subnet_group_name=f"{db_name}-subnet-group"
185
+ )
186
+
187
+ # Configure subnet selection for VPC (when not using SSM imports)
188
+ subnets = None if db_subnet_group else ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_ISOLATED)
115
189
 
116
190
  # Configure engine
117
191
  engine_version = None
@@ -147,28 +221,36 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
147
221
  removal_policy = cdk.RemovalPolicy.RETAIN
148
222
 
149
223
  # Create the database instance
150
- db_instance = rds.DatabaseInstance(
151
- self,
152
- db_name,
153
- engine=engine,
154
- vpc=self.vpc,
155
- vpc_subnets=subnets,
156
- instance_type=instance_type,
157
- credentials=rds.Credentials.from_generated_secret(
224
+ # Build common properties
225
+ db_props = {
226
+ "engine": engine,
227
+ "vpc": self.vpc,
228
+ "instance_type": instance_type,
229
+ "credentials": rds.Credentials.from_generated_secret(
158
230
  username=self.rds_config.username,
159
231
  secret_name=self.rds_config.secret_name,
160
232
  ),
161
- database_name=self.rds_config.database_name,
162
- multi_az=self.rds_config.multi_az,
163
- allocated_storage=self.rds_config.allocated_storage,
164
- storage_encrypted=self.rds_config.storage_encrypted,
165
- security_groups=self.security_groups if self.security_groups else None,
166
- deletion_protection=self.rds_config.deletion_protection,
167
- backup_retention=Duration.days(self.rds_config.backup_retention),
168
- cloudwatch_logs_exports=self.rds_config.cloudwatch_logs_exports,
169
- enable_performance_insights=self.rds_config.enable_performance_insights,
170
- removal_policy=removal_policy,
171
- )
233
+ "database_name": self.rds_config.database_name,
234
+ "multi_az": self.rds_config.multi_az,
235
+ "allocated_storage": self.rds_config.allocated_storage,
236
+ "storage_encrypted": self.rds_config.storage_encrypted,
237
+ "security_groups": self.security_groups if self.security_groups else None,
238
+ "deletion_protection": self.rds_config.deletion_protection,
239
+ "backup_retention": Duration.days(self.rds_config.backup_retention),
240
+ "cloudwatch_logs_exports": self.rds_config.cloudwatch_logs_exports,
241
+ "enable_performance_insights": self.rds_config.enable_performance_insights,
242
+ "removal_policy": removal_policy,
243
+ }
244
+
245
+ # Use either subnet group or vpc_subnets depending on what's available
246
+ if db_subnet_group:
247
+ db_props["subnet_group"] = rds.SubnetGroup.from_subnet_group_name(
248
+ self, "ImportedSubnetGroup", db_subnet_group.ref
249
+ )
250
+ else:
251
+ db_props["vpc_subnets"] = subnets
252
+
253
+ db_instance = rds.DatabaseInstance(self, db_name, **db_props)
172
254
 
173
255
  # Add tags
174
256
  for key, value in self.rds_config.tags.items():
@@ -1,7 +1,7 @@
1
1
  from typing import Dict, Any, List, Optional
2
2
 
3
3
  import aws_cdk as cdk
4
- from aws_cdk import aws_ec2 as ec2
4
+ from aws_cdk import aws_ec2 as ec2, aws_ssm as ssm
5
5
  from aws_lambda_powertools import Logger
6
6
  from constructs import Construct
7
7
 
@@ -31,6 +31,8 @@ class SecurityGroupsStack(IStack):
31
31
  # Flag to determine if we're in test mode
32
32
  self._test_mode = False
33
33
  self._vpc = None
34
+ # SSM imported values
35
+ self.ssm_imported_values: Dict[str, str] = {}
34
36
 
35
37
  def build(
36
38
  self,
@@ -57,6 +59,9 @@ class SecurityGroupsStack(IStack):
57
59
  deployment=deployment,
58
60
  )
59
61
 
62
+ # Process SSM imports first
63
+ self._process_ssm_imports()
64
+
60
65
  env_name = self.deployment.environment
61
66
 
62
67
  # =========================================================
@@ -220,17 +225,65 @@ class SecurityGroupsStack(IStack):
220
225
  export_name=f"{self.deployment.environment}-{self.workload.name}-WebMonitoringSecurityGroup",
221
226
  )
222
227
 
228
+ def _process_ssm_imports(self) -> None:
229
+ """
230
+ Process SSM imports from configuration.
231
+ Follows the same pattern as API Gateway and CloudFront stacks.
232
+ """
233
+ ssm_imports = self.sg_config.ssm_imports
234
+
235
+ if not ssm_imports:
236
+ logger.debug("No SSM imports configured for Security Groups")
237
+ return
238
+
239
+ logger.info(f"Processing {len(ssm_imports)} SSM imports for Security Groups")
240
+
241
+ for param_key, param_path in ssm_imports.items():
242
+ try:
243
+ # Ensure parameter path starts with /
244
+ if not param_path.startswith('/'):
245
+ param_path = f"/{param_path}"
246
+
247
+ # Create unique construct ID from parameter path
248
+ construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
249
+
250
+ # Import SSM parameter - this creates a CDK token that resolves at deployment time
251
+ param = ssm.StringParameter.from_string_parameter_name(
252
+ self, construct_id, param_path
253
+ )
254
+
255
+ # Store the token value for use in configuration
256
+ self.ssm_imported_values[param_key] = param.string_value
257
+ logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
258
+
259
+ except Exception as e:
260
+ logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
261
+ raise
262
+
223
263
  @property
224
264
  def vpc(self) -> ec2.IVpc:
225
265
  """Get the VPC for the Security Group"""
226
266
  if self._vpc:
227
267
  return self._vpc
228
- if self.sg_config.vpc_id:
268
+
269
+ # Check SSM imported values first (tokens from SSM parameters)
270
+ if "vpc_id" in self.ssm_imported_values:
271
+ vpc_id = self.ssm_imported_values["vpc_id"]
272
+
273
+ # Build VPC attributes
274
+ vpc_attrs = {
275
+ "vpc_id": vpc_id,
276
+ "availability_zones": ["us-east-1a", "us-east-1b"]
277
+ }
278
+
279
+ # Use from_vpc_attributes() instead of from_lookup() because SSM imports return tokens
280
+ # from_lookup() requires concrete values and queries AWS during synthesis
281
+ self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
282
+ elif self.sg_config.vpc_id:
229
283
  self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.sg_config.vpc_id)
230
284
  elif self.workload.vpc_id:
231
285
  self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
232
-
233
286
  else:
234
- raise ValueError("VPC ID is not defined in the configuration.")
287
+ raise ValueError("VPC ID is not defined in the configuration or SSM imports.")
235
288
 
236
289
  return self._vpc
@@ -88,6 +88,23 @@ class VpcStack(IStack, EnhancedSsmParameterMixin):
88
88
  # Configure NAT gateways
89
89
  nat_gateway_count = self.vpc_config.nat_gateways.get("count", 1)
90
90
 
91
+ # Get explicit availability zones to avoid dummy AZs in pipeline synthesis
92
+ # When CDK synthesizes in a pipeline context, it doesn't have access to real AZs
93
+ # So we explicitly specify them based on the deployment region
94
+ availability_zones = None
95
+ if self.deployment:
96
+ region = self.deployment.region or "us-east-1"
97
+ # Explicitly list AZs for the region to avoid dummy values
98
+ max_azs = self.vpc_config.max_azs or 2
99
+ if region == "us-east-1":
100
+ availability_zones = [f"us-east-1{chr(97+i)}" for i in range(max_azs)] # us-east-1a, us-east-1b, etc.
101
+ elif region == "us-east-2":
102
+ availability_zones = [f"us-east-2{chr(97+i)}" for i in range(max_azs)]
103
+ elif region == "us-west-1":
104
+ availability_zones = [f"us-west-1{chr(97+i)}" for i in range(max_azs)]
105
+ elif region == "us-west-2":
106
+ availability_zones = [f"us-west-2{chr(97+i)}" for i in range(max_azs)]
107
+
91
108
  # Create the VPC
92
109
  vpc = ec2.Vpc(
93
110
  self,
@@ -95,6 +112,7 @@ class VpcStack(IStack, EnhancedSsmParameterMixin):
95
112
  vpc_name=vpc_name,
96
113
  cidr=self.vpc_config.cidr,
97
114
  max_azs=self.vpc_config.max_azs,
115
+ availability_zones=availability_zones, # Explicitly specify AZs
98
116
  nat_gateways=nat_gateway_count,
99
117
  subnet_configuration=subnet_configuration,
100
118
  enable_dns_hostnames=self.vpc_config.enable_dns_hostnames,
cdk_factory/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.15.0"
1
+ __version__ = "0.15.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cdk_factory
3
- Version: 0.15.0
3
+ Version: 0.15.2
4
4
  Summary: CDK Factory. A QuickStarter and best practices setup for CDK projects
5
5
  Author-email: Eric Wilson <eric.wilson@geekcafe.com>
6
6
  License: MIT License
@@ -2,7 +2,7 @@ cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cdk_factory/app.py,sha256=RnX0-pwdTAPAdKJK_j13Zl8anf9zYKBwboR0KA8K8xM,10346
3
3
  cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
4
4
  cdk_factory/cli.py,sha256=FGbCTS5dYCNsfp-etshzvFlGDCjC28r6rtzYbe7KoHI,6407
5
- cdk_factory/version.py,sha256=wGIgxINRfcIKyk0LjIbc9UF9UwuclyCQZv_axTUzwNw,23
5
+ cdk_factory/version.py,sha256=BOC5JUFbzkhCG96hrOtH7kEDXF3aNOg4kZ97OzGzruI,23
6
6
  cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
7
7
  cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
8
8
  cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
@@ -38,16 +38,16 @@ cdk_factory/configurations/resources/lambda_layers.py,sha256=gVeP_-LC3Eq0lkPaG_J
38
38
  cdk_factory/configurations/resources/lambda_triggers.py,sha256=MD7cdMNKEulNBhtMLIFnWJuJ5R-yyIqa0LHUgbSQerA,834
39
39
  cdk_factory/configurations/resources/load_balancer.py,sha256=DHVKuEDaTfbB0UKYBt7UQQCPCM4FY-ThT1T52lcwg_E,4897
40
40
  cdk_factory/configurations/resources/monitoring.py,sha256=zsfDMa7yph33Ql8iP7lIqqLAyixh-Mesi0imtZJFdcE,2310
41
- cdk_factory/configurations/resources/rds.py,sha256=-ccbd_rcOBIcI1ipZz2W3aRVRGyjQ-MSOaoznPNl_UI,4459
41
+ cdk_factory/configurations/resources/rds.py,sha256=NhXOPTqjfOZhJWyNd0yhBnkk1J0VOPgNLe6VldaJe6k,4628
42
42
  cdk_factory/configurations/resources/resource_mapping.py,sha256=cwv3n63RJ6E59ErsmSTdkW4i-g8huhHtKI0ExbRhJxA,2182
43
43
  cdk_factory/configurations/resources/resource_naming.py,sha256=VE9S2cpzp11qqPL2z1sX79wXH0o1SntO2OG74nEmWC8,5508
44
44
  cdk_factory/configurations/resources/resource_types.py,sha256=1WQHyDoErb-M-tETZZzyLDtbq_jdC85-I403dM48pgE,2317
45
- cdk_factory/configurations/resources/route53.py,sha256=hmKKjn1Elyj1iz58cnhgMBPPN4d0RKUuz9dNwFFuwik,3142
45
+ cdk_factory/configurations/resources/route53.py,sha256=FwfAL90amoQqn7DSGcIprlfNhbJ08T80joplxLg99Ko,3453
46
46
  cdk_factory/configurations/resources/route53_hosted_zone.py,sha256=qjEYPCSxSOx5blr9EULv892ezxkCs--yrLa1ngWbyXM,880
47
47
  cdk_factory/configurations/resources/rum.py,sha256=5aNLhyJEl97spby2gEV59RsMIQpUto2hGh1DeSyIp_I,5149
48
48
  cdk_factory/configurations/resources/s3.py,sha256=LBwTOZ4tOxNbgiu1fFGHOTyF5jlzeVphc_9VAqNw8zA,6042
49
49
  cdk_factory/configurations/resources/security_group.py,sha256=8kQtaaRVEn2aDm8XoC7QFh2mDOFbPbgobmssIuqU8MA,2259
50
- cdk_factory/configurations/resources/security_group_full_stack.py,sha256=x5MIMCa_olO7prFBKx9zVOfvsVdKo-2mWyhrCy27dFw,2031
50
+ cdk_factory/configurations/resources/security_group_full_stack.py,sha256=J56ui5cR4ULcT-20LdK43UNXhcicB2M45Wl8Y9SIWCA,2202
51
51
  cdk_factory/configurations/resources/sqs.py,sha256=fAh2dqttJ6PX46enFRULuiLEu3TEj0Vb2xntAOgUpYE,4346
52
52
  cdk_factory/configurations/resources/vpc.py,sha256=sNn6w76bHFwmt6N76gZZhqpsuNB9860C1SZu6tebaXY,3835
53
53
  cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py,sha256=gFQw96rfSX7n3-YaK4AWyF2NNzJezgZpmnAcxZpmgxs,22036
@@ -86,7 +86,7 @@ cdk_factory/stack_library/aws_lambdas/lambda_stack.py,sha256=SFbBPvvCopbyiuYtq-O
86
86
  cdk_factory/stack_library/buckets/README.md,sha256=XkK3UNVtRLE7NtUvbhCOBBYUYi8hlrrSaI1s3GJVrqI,78
87
87
  cdk_factory/stack_library/buckets/bucket_stack.py,sha256=SLoZqSffAqmeBBEVUQg54D_8Ad5UKdkjEAmKAVgAqQo,1778
88
88
  cdk_factory/stack_library/cloudfront/__init__.py,sha256=Zfx50q4xIJ4ZEoVIzUBDTKbRE9DKDM6iyVIFhtQXvww,153
89
- cdk_factory/stack_library/cloudfront/cloudfront_stack.py,sha256=-mw24FbvwLUzAa1NNKCKGFILZpNXtsuSdxNSNtEz6D8,26804
89
+ cdk_factory/stack_library/cloudfront/cloudfront_stack.py,sha256=kBMWxHlB48aRcFGIX71mRso0tEuEaQxcTBr3YCJOr4s,30210
90
90
  cdk_factory/stack_library/code_artifact/code_artifact_stack.py,sha256=vySYIjWGTdVfMcUOyJdW6gTL1maHWq9ThzfrN_rVL5A,6290
91
91
  cdk_factory/stack_library/cognito/cognito_stack.py,sha256=zEHkKVCIeyZywPs_GIMXCXyCux9RAKdl5kba3wy8wtQ,24608
92
92
  cdk_factory/stack_library/dynamodb/dynamodb_stack.py,sha256=TVyOrUhgaSuN8uymkpaQcpOaSA0lkYJ8QUMgakTCKus,6771
@@ -101,17 +101,17 @@ cdk_factory/stack_library/load_balancer/load_balancer_stack.py,sha256=t5JUe5lMUb
101
101
  cdk_factory/stack_library/monitoring/__init__.py,sha256=k1G_KDx47Aw0UugaL99PN_TKlyLK4nkJVApCaAK7GJg,153
102
102
  cdk_factory/stack_library/monitoring/monitoring_stack.py,sha256=N_1YvEXE7fboH_S3kv_dSKZsufxMuPdFMjGzlNFpuSo,19283
103
103
  cdk_factory/stack_library/rds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
- cdk_factory/stack_library/rds/rds_stack.py,sha256=-IQFmacO_UgedAN72Uo0ZGlcmqGcDinr3tquBdhQZXw,8559
104
+ cdk_factory/stack_library/rds/rds_stack.py,sha256=jvG3mcz5CQHv2NV-KwjGX8XgxtPiixRQTdBtaLb6sw4,12161
105
105
  cdk_factory/stack_library/route53/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
106
  cdk_factory/stack_library/route53/route53_stack.py,sha256=R-6DW7gIjeg25uBT5ZMLNDiQUOSZMipc-Tw6f8POVvI,8081
107
107
  cdk_factory/stack_library/rum/__init__.py,sha256=gUrWQdzd4rZ2J0YzAQC8PsEGAS7QgyYjB2ZCUKWasy4,90
108
108
  cdk_factory/stack_library/rum/rum_stack.py,sha256=OvQ6tsjYcXS8adqU_Xh0A_VKdnPtQnij4cG67nNqSVo,13611
109
109
  cdk_factory/stack_library/security_group/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
- cdk_factory/stack_library/security_group/security_group_full_stack.py,sha256=x3gpSi-DzV8aOqFYpPVDWqpCk-236xX2RVKCANpaNLI,8530
110
+ cdk_factory/stack_library/security_group/security_group_full_stack.py,sha256=zu-xrz2KuojJGoN4-sTzD14sT9DwYaJpgwFl3wPiNXw,10907
111
111
  cdk_factory/stack_library/security_group/security_group_stack.py,sha256=2zxd5ozgQ4GP0xi-Ni7SyChtEAOzC0nXeGz78DPXwPg,14445
112
112
  cdk_factory/stack_library/simple_queue_service/sqs_stack.py,sha256=jJksWrvrvgZUMM01RZ317DOIxqIJbkYYSYu38w0jHpc,6039
113
113
  cdk_factory/stack_library/vpc/__init__.py,sha256=7pIqP97Gf2AJbv9Ebp1WbQGHYhgEbWJ52L1MzeXBybA,42
114
- cdk_factory/stack_library/vpc/vpc_stack.py,sha256=zdDiGilf03esxuya5Z8zVYSVMAIuZBeD-ZKgfnEd6aw,10077
114
+ cdk_factory/stack_library/vpc/vpc_stack.py,sha256=xZj_CzEF-41qgAIdHwsc_PUZaYPDuA6ZKY_rHumUMXY,11187
115
115
  cdk_factory/stack_library/websites/static_website_stack.py,sha256=hcdZQxyhupCy7n7UpNaX8egc2oI9TrssyOufj-oJuo8,10343
116
116
  cdk_factory/stages/websites/static_website_stage.py,sha256=X4fpKXkhb0zIbSHx3QyddBhVSLBryb1vf1Cg2fMTqog,755
117
117
  cdk_factory/templates/README.md,sha256=ATBEjG6beYvbEAdLtZ_8xnxgFD5X0cgZoI_6pToqH90,2679
@@ -129,8 +129,8 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
129
129
  cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
130
130
  cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
131
131
  cdk_factory/workload/workload_factory.py,sha256=mM8GU_5mKq_0OyK060T3JrUSUiGAcKf0eqNlT9mfaws,6028
132
- cdk_factory-0.15.0.dist-info/METADATA,sha256=Da_9MBQTbK6XRX6OrC1ooR-ixBJLcfuTqmyg-dqQeVc,2451
133
- cdk_factory-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- cdk_factory-0.15.0.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
- cdk_factory-0.15.0.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
- cdk_factory-0.15.0.dist-info/RECORD,,
132
+ cdk_factory-0.15.2.dist-info/METADATA,sha256=JZqZG24TGyyJmJ6pWhDVifi1pONKJD38RAxFS4V6WTc,2451
133
+ cdk_factory-0.15.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
+ cdk_factory-0.15.2.dist-info/entry_points.txt,sha256=S1DPe0ORcdiwEALMN_WIo3UQrW_g4YdQCLEsc_b0Swg,53
135
+ cdk_factory-0.15.2.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
136
+ cdk_factory-0.15.2.dist-info/RECORD,,