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.
- cdk_factory/app.py +39 -8
- cdk_factory/configurations/resources/auto_scaling.py +27 -0
- cdk_factory/configurations/resources/cloudfront.py +101 -11
- cdk_factory/configurations/resources/ecs_service.py +12 -0
- cdk_factory/configurations/resources/lambda_edge.py +92 -0
- cdk_factory/configurations/resources/monitoring.py +74 -0
- cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +51 -1
- cdk_factory/lambdas/edge/ip_gate/handler.py +104 -0
- cdk_factory/pipeline/pipeline_factory.py +1 -0
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +99 -0
- cdk_factory/stack_library/cloudfront/__init__.py +6 -0
- cdk_factory/stack_library/cloudfront/cloudfront_stack.py +627 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +90 -0
- cdk_factory/stack_library/lambda_edge/__init__.py +6 -0
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +217 -0
- cdk_factory/stack_library/monitoring/__init__.py +6 -0
- cdk_factory/stack_library/monitoring/monitoring_stack.py +492 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +2 -0
- {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/METADATA +1 -1
- {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/RECORD +24 -15
- {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/WHEEL +0 -0
- {cdk_factory-0.9.11.dist-info → cdk_factory-0.10.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|