cdk-factory 0.9.11__py3-none-any.whl → 0.10.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 (24) hide show
  1. cdk_factory/app.py +39 -8
  2. cdk_factory/configurations/resources/auto_scaling.py +27 -0
  3. cdk_factory/configurations/resources/cloudfront.py +101 -11
  4. cdk_factory/configurations/resources/ecs_service.py +12 -0
  5. cdk_factory/configurations/resources/lambda_edge.py +92 -0
  6. cdk_factory/configurations/resources/monitoring.py +74 -0
  7. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +51 -1
  8. cdk_factory/lambdas/edge/ip_gate/handler.py +104 -0
  9. cdk_factory/pipeline/pipeline_factory.py +1 -0
  10. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +99 -0
  11. cdk_factory/stack_library/cloudfront/__init__.py +6 -0
  12. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +627 -0
  13. cdk_factory/stack_library/ecs/ecs_service_stack.py +90 -0
  14. cdk_factory/stack_library/lambda_edge/__init__.py +6 -0
  15. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +217 -0
  16. cdk_factory/stack_library/monitoring/__init__.py +6 -0
  17. cdk_factory/stack_library/monitoring/monitoring_stack.py +492 -0
  18. cdk_factory/version.py +1 -1
  19. cdk_factory/workload/workload_factory.py +2 -0
  20. {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/METADATA +1 -1
  21. {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/RECORD +24 -15
  22. {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/WHEEL +0 -0
  23. {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/entry_points.txt +0 -0
  24. {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,627 @@
1
+ """
2
+ CloudFront Stack for ALB and Custom Origins
3
+ Geek Cafe, LLC
4
+ Maintainers: Eric Wilson
5
+ MIT License. See Project Root for the license information.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, List, Any, Optional
10
+
11
+ from aws_cdk import (
12
+ Duration,
13
+ aws_cloudfront as cloudfront,
14
+ aws_cloudfront_origins as origins,
15
+ aws_certificatemanager as acm,
16
+ aws_lambda as _lambda,
17
+ aws_ssm as ssm,
18
+ CfnOutput,
19
+ )
20
+ from constructs import Construct
21
+
22
+ from cdk_factory.interfaces.istack import IStack
23
+ from cdk_factory.stack.stack_module_registry import register_stack
24
+ from cdk_factory.configurations.stack import StackConfig
25
+ from cdk_factory.configurations.resources.cloudfront import CloudFrontConfig
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @register_stack("cloudfront_library_module")
31
+ class CloudFrontStack(IStack):
32
+ """
33
+ CloudFront Distribution Stack with support for:
34
+ - Custom origins (ALB, API Gateway, etc.)
35
+ - S3 origins
36
+ - Lambda@Edge associations
37
+ - Cache and origin request policies
38
+ - ACM certificates
39
+ - Custom error responses
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ scope: Construct,
45
+ id: str,
46
+ stack_config: StackConfig,
47
+ deployment,
48
+ **kwargs,
49
+ ) -> None:
50
+ super().__init__(scope, id, **kwargs)
51
+
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
+
59
+ # Resources
60
+ self.distribution: Optional[cloudfront.Distribution] = None
61
+ self.certificate: Optional[acm.ICertificate] = None
62
+ self.origins_map: Dict[str, cloudfront.IOrigin] = {}
63
+
64
+ def build(
65
+ self,
66
+ vpc=None,
67
+ target_groups=None,
68
+ security_groups=None,
69
+ shared=None,
70
+ ):
71
+ """Build the CloudFront distribution"""
72
+
73
+ logger.info(f"Building CloudFront distribution: {self.cf_config.name}")
74
+
75
+ # Create certificate if needed
76
+ if self.cf_config.certificate and self.cf_config.aliases:
77
+ self._create_certificate()
78
+
79
+ # Create origins
80
+ self._create_origins()
81
+
82
+ # Create distribution
83
+ self._create_distribution()
84
+
85
+ # Export SSM parameters
86
+ self._export_ssm_parameters()
87
+
88
+ # Create CloudFormation outputs
89
+ self._create_outputs()
90
+
91
+ return self
92
+
93
+ def _create_certificate(self) -> None:
94
+ """Create or import ACM certificate for CloudFront (must be in us-east-1)"""
95
+ cert_config = self.cf_config.certificate
96
+
97
+ if not cert_config:
98
+ return
99
+
100
+ # Check if certificate ARN is provided
101
+ cert_arn = cert_config.get("arn")
102
+ if cert_arn:
103
+ self.certificate = acm.Certificate.from_certificate_arn(
104
+ self,
105
+ "Certificate",
106
+ certificate_arn=cert_arn
107
+ )
108
+ logger.info(f"Using existing certificate: {cert_arn}")
109
+ return
110
+
111
+ # Check if we should import from SSM
112
+ ssm_param = cert_config.get("ssm_parameter")
113
+ if ssm_param:
114
+ cert_arn = ssm.StringParameter.value_from_lookup(self, ssm_param)
115
+ self.certificate = acm.Certificate.from_certificate_arn(
116
+ self,
117
+ "Certificate",
118
+ certificate_arn=cert_arn
119
+ )
120
+ logger.info(f"Using certificate from SSM: {ssm_param}")
121
+ return
122
+
123
+ logger.warning("No certificate ARN provided - CloudFront will use default certificate")
124
+
125
+ def _create_origins(self) -> None:
126
+ """Create CloudFront origins (custom, S3, etc.)"""
127
+ origins_config = self.cf_config.origins
128
+
129
+ if not origins_config:
130
+ raise ValueError("At least one origin is required for CloudFront distribution")
131
+
132
+ for origin_config in origins_config:
133
+ origin_id = origin_config.get("id")
134
+ origin_type = origin_config.get("type", "custom")
135
+
136
+ if not origin_id:
137
+ raise ValueError("Origin ID is required")
138
+
139
+ if origin_type == "custom":
140
+ origin = self._create_custom_origin(origin_config)
141
+ elif origin_type == "s3":
142
+ origin = self._create_s3_origin(origin_config)
143
+ else:
144
+ raise ValueError(f"Unsupported origin type: {origin_type}")
145
+
146
+ self.origins_map[origin_id] = origin
147
+ logger.info(f"Created {origin_type} origin: {origin_id}")
148
+
149
+ def _create_custom_origin(self, config: Dict[str, Any]) -> cloudfront.IOrigin:
150
+ """Create custom origin (ALB, API Gateway, etc.)"""
151
+ domain_name = config.get("domain_name")
152
+ origin_id = config.get("id")
153
+
154
+ if not domain_name:
155
+ 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("}}"):
159
+ # Extract SSM parameter name
160
+ ssm_param = domain_name[6:-2] # Remove {{ssm: and }}
161
+ domain_name = ssm.StringParameter.value_from_lookup(self, ssm_param)
162
+ logger.info(f"Resolved domain from SSM {ssm_param}: {domain_name}")
163
+
164
+ # Build custom headers (e.g., X-Origin-Secret)
165
+ custom_headers = {}
166
+ custom_headers_config = config.get("custom_headers", {})
167
+
168
+ for header_name, header_value in custom_headers_config.items():
169
+ # Check if value is from Secrets Manager
170
+ if isinstance(header_value, str) and header_value.startswith("{{secrets:"):
171
+ # 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}")
173
+ continue
174
+
175
+ custom_headers[header_name] = header_value
176
+
177
+ # Protocol policy
178
+ protocol_policy_str = config.get("protocol_policy", "https-only")
179
+ protocol_policy_map = {
180
+ "http-only": cloudfront.OriginProtocolPolicy.HTTP_ONLY,
181
+ "https-only": cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
182
+ "match-viewer": cloudfront.OriginProtocolPolicy.MATCH_VIEWER,
183
+ }
184
+ protocol_policy = protocol_policy_map.get(protocol_policy_str, cloudfront.OriginProtocolPolicy.HTTPS_ONLY)
185
+
186
+ # SSL protocols
187
+ 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
+
190
+ # Create custom origin with explicit origin ID
191
+ return origins.HttpOrigin(
192
+ domain_name,
193
+ origin_id=origin_id,
194
+ protocol_policy=protocol_policy,
195
+ origin_ssl_protocols=ssl_protocols,
196
+ http_port=config.get("http_port", 80),
197
+ https_port=config.get("https_port", 443),
198
+ origin_path=config.get("origin_path", ""),
199
+ connection_attempts=config.get("connection_attempts", 3),
200
+ connection_timeout=Duration.seconds(config.get("connection_timeout", 10)),
201
+ read_timeout=Duration.seconds(config.get("response_timeout", 30)),
202
+ keepalive_timeout=Duration.seconds(config.get("keepalive_timeout", 5)),
203
+ custom_headers=custom_headers if custom_headers else None,
204
+ )
205
+
206
+ def _create_s3_origin(self, config: Dict[str, Any]) -> cloudfront.IOrigin:
207
+ """Create S3 origin"""
208
+ # S3 origin implementation
209
+ # This would use origins.S3Origin
210
+ raise NotImplementedError("S3 origin support - use existing static_website_stack for S3")
211
+
212
+ def _create_distribution(self) -> None:
213
+ """Create CloudFront distribution"""
214
+
215
+ # Get default origin
216
+ default_behavior_config = self.cf_config.default_cache_behavior
217
+ target_origin_id = default_behavior_config.get("target_origin_id")
218
+
219
+ if not target_origin_id or target_origin_id not in self.origins_map:
220
+ raise ValueError(f"Default cache behavior must reference a valid origin ID")
221
+
222
+ default_origin = self.origins_map[target_origin_id]
223
+
224
+ # Build default behavior
225
+ default_behavior = self._build_cache_behavior(default_behavior_config, default_origin)
226
+
227
+ # Build additional behaviors
228
+ additional_behaviors = {}
229
+ for behavior_config in self.cf_config.cache_behaviors:
230
+ path_pattern = behavior_config.get("path_pattern")
231
+ origin_id = behavior_config.get("target_origin_id")
232
+
233
+ if not path_pattern or not origin_id or origin_id not in self.origins_map:
234
+ logger.warning(f"Invalid cache behavior config, skipping")
235
+ continue
236
+
237
+ origin = self.origins_map[origin_id]
238
+ behavior = self._build_cache_behavior_options(behavior_config)
239
+ additional_behaviors[path_pattern] = behavior
240
+
241
+ # Build error responses
242
+ error_responses = []
243
+ for error_config in self.cf_config.custom_error_responses:
244
+ error_code = error_config.get("error_code")
245
+ if error_code:
246
+ error_responses.append(
247
+ cloudfront.ErrorResponse(
248
+ http_status=error_code,
249
+ response_http_status=error_config.get("response_http_status"),
250
+ response_page_path=error_config.get("response_page_path"),
251
+ ttl=Duration.seconds(error_config.get("error_caching_min_ttl", 10)),
252
+ )
253
+ )
254
+
255
+ # HTTP version
256
+ http_version_map = {
257
+ "http1_1": cloudfront.HttpVersion.HTTP1_1,
258
+ "http2": cloudfront.HttpVersion.HTTP2,
259
+ }
260
+ # Try to use HTTP2_AND_3 and HTTP3 if available in the CDK version
261
+ try:
262
+ http_version_map["http2_and_3"] = cloudfront.HttpVersion.HTTP2_AND_3
263
+ http_version_map["http3"] = cloudfront.HttpVersion.HTTP3
264
+ default_version = cloudfront.HttpVersion.HTTP2_AND_3
265
+ except AttributeError:
266
+ # Fall back to HTTP2 if HTTP2_AND_3/HTTP3 not available in this CDK version
267
+ default_version = cloudfront.HttpVersion.HTTP2
268
+
269
+ http_version = http_version_map.get(
270
+ self.cf_config.http_version.lower(),
271
+ default_version
272
+ )
273
+
274
+ # Price class
275
+ price_class_map = {
276
+ "PriceClass_100": cloudfront.PriceClass.PRICE_CLASS_100,
277
+ "PriceClass_200": cloudfront.PriceClass.PRICE_CLASS_200,
278
+ "PriceClass_All": cloudfront.PriceClass.PRICE_CLASS_ALL,
279
+ }
280
+ price_class = price_class_map.get(
281
+ self.cf_config.price_class,
282
+ cloudfront.PriceClass.PRICE_CLASS_100
283
+ )
284
+
285
+ # Create distribution
286
+ self.distribution = cloudfront.Distribution(
287
+ self,
288
+ "Distribution",
289
+ comment=self.cf_config.comment or f"{self.cf_config.name} distribution",
290
+ enabled=self.cf_config.enabled,
291
+ domain_names=self.cf_config.aliases if self.cf_config.aliases else None,
292
+ certificate=self.certificate,
293
+ default_behavior=default_behavior,
294
+ additional_behaviors=additional_behaviors if additional_behaviors else None,
295
+ error_responses=error_responses if error_responses else None,
296
+ http_version=http_version,
297
+ price_class=price_class,
298
+ default_root_object=self.cf_config.default_root_object,
299
+ web_acl_id=self.cf_config.waf_web_acl_id,
300
+ )
301
+
302
+ logger.info(f"Created CloudFront distribution: {self.distribution.distribution_id}")
303
+
304
+ def _build_cache_behavior(
305
+ self,
306
+ config: Dict[str, Any],
307
+ origin: cloudfront.IOrigin
308
+ ) -> cloudfront.BehaviorOptions:
309
+ """Build cache behavior with origin"""
310
+
311
+ # Viewer protocol policy
312
+ viewer_protocol_str = config.get("viewer_protocol_policy", "redirect-to-https")
313
+ viewer_protocol_map = {
314
+ "allow-all": cloudfront.ViewerProtocolPolicy.ALLOW_ALL,
315
+ "redirect-to-https": cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
316
+ "https-only": cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
317
+ }
318
+ viewer_protocol_policy = viewer_protocol_map.get(
319
+ viewer_protocol_str,
320
+ cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
321
+ )
322
+
323
+ # Allowed methods
324
+ 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:
326
+ allowed_methods = cloudfront.AllowedMethods.ALLOW_ALL
327
+ elif "OPTIONS" in allowed_methods_str:
328
+ allowed_methods = cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
329
+ else:
330
+ allowed_methods = cloudfront.AllowedMethods.ALLOW_GET_HEAD
331
+
332
+ # Cached methods
333
+ cached_methods_str = config.get("cached_methods", ["GET", "HEAD"])
334
+ if "OPTIONS" in cached_methods_str:
335
+ cached_methods = cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
336
+ else:
337
+ cached_methods = cloudfront.CachedMethods.CACHE_GET_HEAD
338
+
339
+ # Cache policy
340
+ cache_policy = self._build_cache_policy(config.get("cache_policy", {}))
341
+
342
+ # Origin request policy
343
+ origin_request_policy = self._build_origin_request_policy(config.get("origin_request_policy", {}))
344
+
345
+ # Lambda@Edge associations
346
+ edge_lambdas = self._build_lambda_edge_associations(config.get("lambda_edge_associations", []))
347
+
348
+ return cloudfront.BehaviorOptions(
349
+ origin=origin,
350
+ viewer_protocol_policy=viewer_protocol_policy,
351
+ allowed_methods=allowed_methods,
352
+ cached_methods=cached_methods,
353
+ cache_policy=cache_policy,
354
+ origin_request_policy=origin_request_policy,
355
+ compress=config.get("compress", True),
356
+ edge_lambdas=edge_lambdas if edge_lambdas else None,
357
+ )
358
+
359
+ def _build_cache_behavior_options(self, config: Dict[str, Any]) -> Dict[str, Any]:
360
+ """Build cache behavior options dict (for additional behaviors)"""
361
+ # This is a simplified version - reuse logic from _build_cache_behavior
362
+ # For now, return basic structure
363
+ return {}
364
+
365
+ def _build_cache_policy(self, config: Dict[str, Any]) -> Optional[cloudfront.ICachePolicy]:
366
+ """Build or reference cache policy"""
367
+ if not config:
368
+ # Use managed caching disabled policy for dynamic content
369
+ return cloudfront.CachePolicy.CACHING_DISABLED
370
+
371
+ policy_name = config.get("name")
372
+
373
+ # Check for managed policies
374
+ managed_policies = {
375
+ "CachingOptimized": cloudfront.CachePolicy.CACHING_OPTIMIZED,
376
+ "CachingDisabled": cloudfront.CachePolicy.CACHING_DISABLED,
377
+ "CachingOptimizedForUncompressedObjects": cloudfront.CachePolicy.CACHING_OPTIMIZED_FOR_UNCOMPRESSED_OBJECTS,
378
+ }
379
+
380
+ if policy_name in managed_policies:
381
+ return managed_policies[policy_name]
382
+
383
+ # Create custom cache policy
384
+ return cloudfront.CachePolicy(
385
+ self,
386
+ f"CachePolicy-{policy_name}",
387
+ cache_policy_name=policy_name,
388
+ comment=config.get("comment", ""),
389
+ default_ttl=Duration.seconds(config.get("default_ttl", 0)),
390
+ min_ttl=Duration.seconds(config.get("min_ttl", 0)),
391
+ max_ttl=Duration.seconds(config.get("max_ttl", 31536000)),
392
+ 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", {})),
397
+ )
398
+
399
+ def _build_origin_request_policy(self, config: Dict[str, Any]) -> Optional[cloudfront.IOriginRequestPolicy]:
400
+ """Build or reference origin request policy"""
401
+ if not config:
402
+ # Use managed all viewer policy
403
+ return cloudfront.OriginRequestPolicy.ALL_VIEWER
404
+
405
+ policy_name = config.get("name")
406
+
407
+ # Check for managed policies
408
+ managed_policies = {
409
+ "AllViewer": cloudfront.OriginRequestPolicy.ALL_VIEWER,
410
+ "AllViewerExceptHostHeader": cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
411
+ "AllViewerAndCloudFrontHeaders2022": cloudfront.OriginRequestPolicy.ALL_VIEWER_AND_CLOUDFRONT_HEADERS_2022,
412
+ "CORS-CustomOrigin": cloudfront.OriginRequestPolicy.CORS_CUSTOM_ORIGIN,
413
+ "CORS-S3Origin": cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
414
+ }
415
+
416
+ if policy_name in managed_policies:
417
+ return managed_policies[policy_name]
418
+
419
+ # Create custom origin request policy
420
+ return cloudfront.OriginRequestPolicy(
421
+ self,
422
+ f"OriginRequestPolicy-{policy_name}",
423
+ origin_request_policy_name=policy_name,
424
+ 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", {})),
428
+ )
429
+
430
+ def _build_cache_header_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheHeaderBehavior:
431
+ """Build cache header behavior"""
432
+ behavior = config.get("behavior", "none")
433
+
434
+ if behavior == "none":
435
+ return cloudfront.CacheHeaderBehavior.none()
436
+ elif behavior == "whitelist":
437
+ headers = config.get("headers", [])
438
+ return cloudfront.CacheHeaderBehavior.allow_list(*headers)
439
+ else:
440
+ return cloudfront.CacheHeaderBehavior.none()
441
+
442
+ def _build_cache_query_string_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheQueryStringBehavior:
443
+ """Build cache query string behavior"""
444
+ behavior = config.get("behavior", "none")
445
+
446
+ if behavior == "none":
447
+ return cloudfront.CacheQueryStringBehavior.none()
448
+ elif behavior == "all":
449
+ return cloudfront.CacheQueryStringBehavior.all()
450
+ elif behavior == "whitelist":
451
+ query_strings = config.get("query_strings", [])
452
+ return cloudfront.CacheQueryStringBehavior.allow_list(*query_strings)
453
+ elif behavior == "allExcept":
454
+ query_strings = config.get("query_strings", [])
455
+ return cloudfront.CacheQueryStringBehavior.deny_list(*query_strings)
456
+ else:
457
+ return cloudfront.CacheQueryStringBehavior.none()
458
+
459
+ def _build_cache_cookie_behavior(self, config: Dict[str, Any]) -> cloudfront.CacheCookieBehavior:
460
+ """Build cache cookie behavior"""
461
+ behavior = config.get("behavior", "none")
462
+
463
+ if behavior == "none":
464
+ return cloudfront.CacheCookieBehavior.none()
465
+ elif behavior == "all":
466
+ return cloudfront.CacheCookieBehavior.all()
467
+ elif behavior == "whitelist":
468
+ cookies = config.get("cookies", [])
469
+ return cloudfront.CacheCookieBehavior.allow_list(*cookies)
470
+ elif behavior == "allExcept":
471
+ cookies = config.get("cookies", [])
472
+ return cloudfront.CacheCookieBehavior.deny_list(*cookies)
473
+ else:
474
+ return cloudfront.CacheCookieBehavior.none()
475
+
476
+ def _build_origin_header_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestHeaderBehavior:
477
+ """Build origin request header behavior"""
478
+ behavior = config.get("behavior", "none")
479
+
480
+ if behavior == "none":
481
+ return cloudfront.OriginRequestHeaderBehavior.none()
482
+ elif behavior == "all":
483
+ return cloudfront.OriginRequestHeaderBehavior.all_viewer()
484
+ elif behavior == "whitelist":
485
+ headers = config.get("headers", [])
486
+ return cloudfront.OriginRequestHeaderBehavior.allow_list(*headers)
487
+ elif behavior == "allViewerAndWhitelistCloudFront":
488
+ headers = config.get("headers", [])
489
+ return cloudfront.OriginRequestHeaderBehavior.all_viewer_and_whitelist_cloud_front(*headers)
490
+ else:
491
+ return cloudfront.OriginRequestHeaderBehavior.none()
492
+
493
+ def _build_origin_query_string_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestQueryStringBehavior:
494
+ """Build origin request query string behavior"""
495
+ behavior = config.get("behavior", "none")
496
+
497
+ if behavior == "none":
498
+ return cloudfront.OriginRequestQueryStringBehavior.none()
499
+ elif behavior == "all":
500
+ return cloudfront.OriginRequestQueryStringBehavior.all()
501
+ elif behavior == "whitelist":
502
+ query_strings = config.get("query_strings", [])
503
+ return cloudfront.OriginRequestQueryStringBehavior.allow_list(*query_strings)
504
+ elif behavior == "allExcept":
505
+ query_strings = config.get("query_strings", [])
506
+ return cloudfront.OriginRequestQueryStringBehavior.deny_list(*query_strings)
507
+ else:
508
+ return cloudfront.OriginRequestQueryStringBehavior.none()
509
+
510
+ def _build_origin_cookie_behavior(self, config: Dict[str, Any]) -> cloudfront.OriginRequestCookieBehavior:
511
+ """Build origin request cookie behavior"""
512
+ behavior = config.get("behavior", "none")
513
+
514
+ if behavior == "none":
515
+ return cloudfront.OriginRequestCookieBehavior.none()
516
+ elif behavior == "all":
517
+ return cloudfront.OriginRequestCookieBehavior.all()
518
+ elif behavior == "whitelist":
519
+ cookies = config.get("cookies", [])
520
+ return cloudfront.OriginRequestCookieBehavior.allow_list(*cookies)
521
+ else:
522
+ return cloudfront.OriginRequestCookieBehavior.none()
523
+
524
+ def _build_lambda_edge_associations(self, associations: List[Dict[str, Any]]) -> Optional[List[cloudfront.EdgeLambda]]:
525
+ """Build Lambda@Edge associations"""
526
+ if not associations:
527
+ return None
528
+
529
+ edge_lambdas = []
530
+
531
+ for assoc in associations:
532
+ event_type_str = assoc.get("event_type", "origin-request")
533
+ lambda_arn = assoc.get("lambda_arn")
534
+
535
+ if not lambda_arn:
536
+ continue
537
+
538
+ # Check if ARN is an SSM parameter reference
539
+ if lambda_arn.startswith("{{ssm:") and lambda_arn.endswith("}}"):
540
+ ssm_param = lambda_arn[6:-2]
541
+ lambda_arn = ssm.StringParameter.value_from_lookup(self, ssm_param)
542
+ logger.info(f"Resolved Lambda ARN from SSM {ssm_param}")
543
+
544
+ # Map event type
545
+ event_type_map = {
546
+ "viewer-request": cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
547
+ "origin-request": cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
548
+ "origin-response": cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
549
+ "viewer-response": cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE,
550
+ }
551
+
552
+ event_type = event_type_map.get(event_type_str, cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST)
553
+
554
+ # Import Lambda version
555
+ lambda_version = _lambda.Version.from_version_arn(
556
+ self,
557
+ f"LambdaEdge-{event_type_str}",
558
+ version_arn=lambda_arn
559
+ )
560
+
561
+ edge_lambdas.append(
562
+ cloudfront.EdgeLambda(
563
+ function_version=lambda_version,
564
+ event_type=event_type,
565
+ include_body=assoc.get("include_body", False)
566
+ )
567
+ )
568
+
569
+ return edge_lambdas if edge_lambdas else None
570
+
571
+ def _export_ssm_parameters(self) -> None:
572
+ """Export distribution info to SSM Parameter Store"""
573
+ ssm_exports = self.cf_config.ssm_exports
574
+
575
+ if not ssm_exports:
576
+ return
577
+
578
+ if "distribution_id" in ssm_exports:
579
+ ssm.StringParameter(
580
+ self,
581
+ "DistributionIdParam",
582
+ parameter_name=ssm_exports["distribution_id"],
583
+ string_value=self.distribution.distribution_id,
584
+ description=f"CloudFront Distribution ID for {self.cf_config.name}",
585
+ )
586
+
587
+ if "distribution_domain" in ssm_exports:
588
+ ssm.StringParameter(
589
+ self,
590
+ "DistributionDomainParam",
591
+ parameter_name=ssm_exports["distribution_domain"],
592
+ string_value=self.distribution.distribution_domain_name,
593
+ description=f"CloudFront Distribution Domain for {self.cf_config.name}",
594
+ )
595
+
596
+ if "distribution_arn" in ssm_exports:
597
+ ssm.StringParameter(
598
+ self,
599
+ "DistributionArnParam",
600
+ parameter_name=ssm_exports["distribution_arn"],
601
+ string_value=f"arn:aws:cloudfront::{self.account}:distribution/{self.distribution.distribution_id}",
602
+ description=f"CloudFront Distribution ARN for {self.cf_config.name}",
603
+ )
604
+
605
+ def _create_outputs(self) -> None:
606
+ """Create CloudFormation outputs"""
607
+ CfnOutput(
608
+ self,
609
+ "DistributionId",
610
+ value=self.distribution.distribution_id,
611
+ description="CloudFront Distribution ID",
612
+ )
613
+
614
+ CfnOutput(
615
+ self,
616
+ "DistributionDomain",
617
+ value=self.distribution.distribution_domain_name,
618
+ description="CloudFront Distribution Domain Name",
619
+ )
620
+
621
+ if self.cf_config.aliases:
622
+ CfnOutput(
623
+ self,
624
+ "DistributionAliases",
625
+ value=",".join(self.cf_config.aliases),
626
+ description="CloudFront Distribution Aliases",
627
+ )