11from aws_cdk import (
22 Duration ,
33 Stack ,
4+ Size ,
45 aws_logs as logs ,
56 aws_cloudfront as cloudfront ,
7+ aws_cloudfront_origins as origins ,
68 aws_iam as iam ,
79 aws_s3 as s3 ,
810 aws_kinesisfirehose as firehose ,
911 aws_s3_deployment as s3_deployment ,
1012 RemovalPolicy ,
1113 CfnOutput ,
1214 CfnParameter ,
13- CfnMapping
1415)
16+ # Import the destinations module from aws-cdk-lib
17+ from aws_cdk .aws_kinesisfirehose import S3Bucket , Compression
1518from constructs import Construct
16- from cdk_nag import NagSuppressions
17-
18- import json
1919
2020class CloudfrontV2LoggingStack (Stack ):
2121 """
@@ -30,7 +30,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
3030 super ().__init__ (scope , construct_id , ** kwargs )
3131
3232 # CloudFormation parameters for customization
33- log_retention_days = CfnParameter (
33+ s3_log_retention_days = CfnParameter (
3434 self , "LogRetentionDays" ,
3535 type = "Number" ,
3636 default = 30 ,
@@ -51,8 +51,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
5151 ]
5252 )
5353
54- # Create the logging bucket for CloudFront
55- # This bucket will store logs in Parquet format and from Firehose
54+ # Create the S3 logging bucket for CloudFront
55+ # This bucket will store logs from the S3 output in Parquet format and also be the target for our Firehose delivery
5656 logging_bucket = s3 .Bucket (
5757 self , "CFLoggingBucket" ,
5858 removal_policy = RemovalPolicy .DESTROY ,
@@ -63,7 +63,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
6363 enforce_ssl = True ,
6464 lifecycle_rules = [
6565 s3 .LifecycleRule (
66- expiration = Duration .days (log_retention_days .value_as_number ), # Configurable log retention
66+ expiration = Duration .days (s3_log_retention_days .value_as_number ), # Configurable log retention
6767 )
6868 ]
6969 )
@@ -79,43 +79,54 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
7979 auto_delete_objects = True # Clean up objects when stack is deleted
8080 )
8181
82- # Deploy the static website content to the S3 bucket
83- s3_deploy = s3_deployment .BucketDeployment (
82+ # Deploy the static website content to the S3 bucket with improved options
83+ s3_deployment .BucketDeployment (
8484 self , "DeployWebsite" ,
8585 sources = [s3_deployment .Source .asset ("website" )], # Directory containing your website files
86- destination_bucket = main_bucket
86+ destination_bucket = main_bucket ,
87+ content_type = "text/html" , # Set content type for HTML files
88+ cache_control = [s3_deployment .CacheControl .max_age (Duration .days (7 ))], # Cache for 7 days
89+ prune = False
8790 )
8891
89- # Add bucket policy to deny direct access to S3 objects
90- # This ensures content is only accessed through CloudFront
91- main_bucket .add_to_resource_policy (
92- iam .PolicyStatement (
93- effect = iam .Effect .DENY ,
94- actions = ["s3:GetObject" ],
95- principals = [iam .AnyPrincipal ()],
96- resources = [main_bucket .arn_for_objects ("*" )],
97- conditions = {
98- "StringNotEquals" : {
99- "aws:PrincipalServiceName" : "cloudfront.amazonaws.com"
100- }
101- }
102- )
92+ # Create CloudWatch Logs group with configurable retention
93+ log_group = logs .LogGroup (
94+ self ,
95+ "DistributionLogGroup" ,
96+ retention = self ._get_log_retention (cloudwatch_log_retention_days .value_as_number )
10397 )
104-
105- # Grant CloudFront permission to write logs to the S3 bucket
106- cloudfront_distribution_arn = Stack .of (self ).format_arn (
107- service = "cloudfront" ,
108- region = "" , # CloudFront is a global service
109- resource = "distribution" ,
110- resource_name = "*" # Wildcard for all distributions in the account
98+
99+ # Create Kinesis Firehose delivery stream to buffer and deliver logs to S3 using L2 construct
100+ # Define S3 destination for Firehose with dynamic prefixes
101+ s3_destination = S3Bucket (
102+ bucket = logging_bucket ,
103+ data_output_prefix = "firehose_delivery/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/" ,
104+ error_output_prefix = "errors/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/!{firehose:error-output-type}/" ,
105+ buffering_interval = Duration .seconds (300 ), # Buffer for 5 minutes
106+ buffering_size = Size .mebibytes (5 ), # Or until 5MB is reached
107+ compression = Compression .HADOOP_SNAPPY # Compress data for efficiency
111108 )
112109
110+ # Create Kinesis Firehose delivery stream using L2 construct
111+ firehose_stream = firehose .DeliveryStream (
112+ self , "LoggingFirehose" ,
113+ delivery_stream_name = "cloudfront-logs-stream" ,
114+ destination = s3_destination ,
115+ encryption = firehose .StreamEncryption .aws_owned_key ()
116+ )
117+
118+ # Grant permissions for the delivery service to write logs to the S3 bucket
113119 logging_bucket .add_to_resource_policy (
114120 iam .PolicyStatement (
115121 sid = "AllowCloudFrontLogDelivery" ,
116122 actions = ["s3:PutObject" ],
117123 principals = [iam .ServicePrincipal ("delivery.logs.amazonaws.com" )],
118- resources = [f"{ logging_bucket .bucket_arn } /*" ]
124+ resources = [f"{ logging_bucket .bucket_arn } /*" ],
125+ conditions = {
126+ "StringEquals" : {
127+ "aws:SourceAccount" : Stack .of (self ).account
128+ }
129+ }
119130 )
120131 )
121132
@@ -125,68 +136,31 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
125136 sid = "AllowCloudFrontLogDeliveryAcl" ,
126137 actions = ["s3:GetBucketAcl" ],
127138 principals = [iam .ServicePrincipal ("delivery.logs.amazonaws.com" )],
128- resources = [logging_bucket .bucket_arn ]
129- )
130- )
131-
132- # Create Origin Access Control for CloudFront to access S3
133- # This is the recommended approach instead of Origin Access Identity (OAI)
134- cloudfront_oac = cloudfront .CfnOriginAccessControl (
135- self , "CloudFrontOAC" ,
136- origin_access_control_config = cloudfront .CfnOriginAccessControl .OriginAccessControlConfigProperty (
137- name = "S3OAC" ,
138- origin_access_control_origin_type = "s3" ,
139- signing_behavior = "always" ,
140- signing_protocol = "sigv4"
141- )
142- )
143-
144- # Configure the S3 origin for CloudFront
145- s3_origin_config = cloudfront .CfnDistribution .OriginProperty (
146- domain_name = main_bucket .bucket_regional_domain_name ,
147- id = "S3Origin" ,
148- s3_origin_config = cloudfront .CfnDistribution .S3OriginConfigProperty (
149- origin_access_identity = ""
150- ),
151- origin_access_control_id = cloudfront_oac .ref
152- )
153-
154- # Create CloudFront distribution to serve the website
155- distribution = cloudfront .CfnDistribution (
156- self , "LoggedDistribution" ,
157- distribution_config = cloudfront .CfnDistribution .DistributionConfigProperty (
158- enabled = True ,
159- default_root_object = "index.html" ,
160- origins = [s3_origin_config ],
161- default_cache_behavior = cloudfront .CfnDistribution .DefaultCacheBehaviorProperty (
162- target_origin_id = "S3Origin" ,
163- viewer_protocol_policy = "redirect-to-https" ,
164- cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" , # CachingOptimized policy ID
165- compress = True
166- ),
167- viewer_certificate = cloudfront .CfnDistribution .ViewerCertificateProperty (
168- cloud_front_default_certificate = True ,
169- minimum_protocol_version = "TLSv1.2_2021" ,
170- ),
171- http_version = "http2"
172- )
173- )
174-
175- # Add bucket policy to allow CloudFront access to S3 objects
176- main_bucket .add_to_resource_policy (
177- iam .PolicyStatement (
178- effect = iam .Effect .ALLOW ,
179- actions = ["s3:GetObject" ],
180- principals = [iam .ServicePrincipal ("cloudfront.amazonaws.com" )],
181- resources = [main_bucket .arn_for_objects ("*" )],
139+ resources = [logging_bucket .bucket_arn ],
182140 conditions = {
183141 "StringEquals" : {
184- "AWS:SourceArn " : f"arn:aws:cloudfront:: { self .account } :distribution/ { distribution . ref } "
142+ "aws:SourceAccount " : Stack . of ( self ) .account
185143 }
186144 }
187145 )
188146 )
189147
148+ # Create CloudFront distribution with S3BucketOrigin
149+ distribution = cloudfront .Distribution (
150+ self , "LoggedDistribution" ,
151+ comment = "CloudFront distribution with STD Logging V2 Configuration Examples" ,
152+ default_behavior = cloudfront .BehaviorOptions (
153+ origin = origins .S3BucketOrigin .with_origin_access_control (main_bucket ),
154+ viewer_protocol_policy = cloudfront .ViewerProtocolPolicy .REDIRECT_TO_HTTPS ,
155+ cache_policy = cloudfront .CachePolicy .CACHING_OPTIMIZED ,
156+ compress = True
157+ ),
158+ default_root_object = "index.html" ,
159+ minimum_protocol_version = cloudfront .SecurityPolicyProtocol .TLS_V1_2_2021 , # Uses TLS 1.2 as minimum
160+ http_version = cloudfront .HttpVersion .HTTP2 ,
161+ enable_logging = False # We're using CloudFront V2 logging instead of traditional logging
162+ )
163+
190164 # SECTION: CLOUDFRONT STANDARD LOGGING V2 CONFIGURATION
191165
192166 # 1. Create the delivery source for CloudFront distribution logs
@@ -200,30 +174,17 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
200174 service = "cloudfront" ,
201175 region = "" , # CloudFront is a global service
202176 resource = "distribution" ,
203- resource_name = distribution .attr_id
177+ resource_name = distribution .distribution_id
204178 )
205179 )
206180
207181 # 2. CLOUDWATCH LOGS DESTINATION
208- # Create a CloudWatch Logs group
209- # First, create the log group without specifying retention
210- log_group = logs .LogGroup (
211- self ,
212- "DistributionLogGroup"
213- )
214-
215- # Convert the CloudWatch log retention parameter to RetentionDays enum
216- cfn_log_group = log_group .node .default_child
217- cfn_log_group .add_property_override (
218- "RetentionInDays" ,
219- cloudwatch_log_retention_days .value_as_number
220- )
221182
222183 # Create a CloudWatch delivery destination
223184 cf_distribution_delivery_destination = logs .CfnDeliveryDestination (
224185 self ,
225186 "CloudWatchDeliveryDestination" ,
226- name = "cloudwatch-destination" ,
187+ name = "cloudwatch-logs- destination" ,
227188 destination_resource_arn = log_group .log_group_arn ,
228189 output_format = "json"
229190 )
@@ -236,6 +197,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
236197 delivery_destination_arn = cf_distribution_delivery_destination .attr_arn
237198 )
238199 cf_delivery .node .add_dependency (distribution_delivery_source )
200+ cf_delivery .node .add_dependency (cf_distribution_delivery_destination )
239201
240202 # 3. S3 PARQUET DESTINATION
241203 # Configure S3 as a delivery destination with Parquet format
@@ -257,72 +219,33 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
257219 s3_suffix_path = "s3_delivery/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}"
258220 )
259221 s3_delivery .node .add_dependency (distribution_delivery_source )
222+ s3_delivery .node .add_dependency (s3_distribution_delivery_destination )
223+ s3_delivery .node .add_dependency (cf_delivery ) # Make S3 delivery depend on CloudWatch delivery
260224
261225 # 4. KINESIS DATA FIREHOSE DESTINATION
262- # Create IAM role for Kinesis Firehose with permissions to write to S3
263- firehose_role = iam .Role (
264- self , "FirehoseRole" ,
265- assumed_by = iam .ServicePrincipal ("firehose.amazonaws.com" )
266- )
267-
268- # Add required permissions for Firehose to write to S3
269- firehose_role .add_to_policy (iam .PolicyStatement (
270- actions = [
271- "s3:AbortMultipartUpload" ,
272- "s3:GetBucketLocation" ,
273- "s3:GetObject" ,
274- "s3:ListBucket" ,
275- "s3:ListBucketMultipartUploads" ,
276- "s3:PutObject"
277- ],
278- resources = [
279- logging_bucket .bucket_arn ,
280- f"{ logging_bucket .bucket_arn } /*"
281- ]
282- ))
283-
284- # Create Kinesis Firehose delivery stream to buffer and deliver logs to S3
285- firehose_stream = firehose .CfnDeliveryStream (
286- self , "LoggingFirehose" ,
287- delivery_stream_name = "cloudfront-logs-stream" ,
288- delivery_stream_type = "DirectPut" ,
289- s3_destination_configuration = firehose .CfnDeliveryStream .S3DestinationConfigurationProperty (
290- bucket_arn = logging_bucket .bucket_arn ,
291- role_arn = firehose_role .role_arn ,
292- buffering_hints = firehose .CfnDeliveryStream .BufferingHintsProperty (
293- interval_in_seconds = 300 , # Buffer for 5 minutes
294- size_in_m_bs = 5 # Or until 5MB is reached
295- ),
296- compression_format = "HADOOP_SNAPPY" , # Compress data for efficiency
297- prefix = "firehose_delivery/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/" ,
298- error_output_prefix = "errors/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/!{firehose:error-output-type}/"
299- ),
300- delivery_stream_encryption_configuration_input = firehose .CfnDeliveryStream .DeliveryStreamEncryptionConfigurationInputProperty (
301- key_type = "AWS_OWNED_CMK"
302- )
303- )
304-
305226 # Configure Firehose as a delivery destination for CloudFront logs
306227 firehose_delivery_destination = logs .CfnDeliveryDestination (
307228 self , "FirehoseDeliveryDestination" ,
308229 name = "cloudfront-logs-destination" ,
309- destination_resource_arn = firehose_stream .attr_arn ,
230+ destination_resource_arn = firehose_stream .delivery_stream_arn ,
310231 output_format = "json"
311232 )
312233
313234 # Create the Firehose delivery configuration
314235 delivery = logs .CfnDelivery (
315236 self ,
316- "KinesisDelivery " ,
237+ "Delivery " ,
317238 delivery_source_name = distribution_delivery_source .name ,
318239 delivery_destination_arn = firehose_delivery_destination .attr_arn
319240 )
320241 delivery .node .add_dependency (distribution_delivery_source )
242+ delivery .node .add_dependency (firehose_delivery_destination )
243+ delivery .node .add_dependency (s3_delivery ) # Make Firehose delivery depend on S3 delivery
321244
322245 # Output the CloudFront distribution domain name for easy access
323246 CfnOutput (
324247 self , "DistributionDomainName" ,
325- value = distribution .attr_domain_name ,
248+ value = distribution .distribution_domain_name ,
326249 description = "CloudFront distribution domain name"
327250 )
328251
@@ -339,4 +262,27 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
339262 value = f"{ log_group .log_group_name } (retention: { cloudwatch_log_retention_days .value_as_number } days)" ,
340263 description = "CloudWatch log group for CloudFront logs"
341264 )
342-
265+
266+ def _get_log_retention (self , days ):
267+ """Convert numeric days to logs.RetentionDays enum value"""
268+ retention_map = {
269+ 0 : logs .RetentionDays .INFINITE ,
270+ 1 : logs .RetentionDays .ONE_DAY ,
271+ 3 : logs .RetentionDays .THREE_DAYS ,
272+ 5 : logs .RetentionDays .FIVE_DAYS ,
273+ 7 : logs .RetentionDays .ONE_WEEK ,
274+ 14 : logs .RetentionDays .TWO_WEEKS ,
275+ 30 : logs .RetentionDays .ONE_MONTH ,
276+ 60 : logs .RetentionDays .TWO_MONTHS ,
277+ 90 : logs .RetentionDays .THREE_MONTHS ,
278+ 120 : logs .RetentionDays .FOUR_MONTHS ,
279+ 150 : logs .RetentionDays .FIVE_MONTHS ,
280+ 180 : logs .RetentionDays .SIX_MONTHS ,
281+ 365 : logs .RetentionDays .ONE_YEAR ,
282+ 400 : logs .RetentionDays .THIRTEEN_MONTHS ,
283+ 545 : logs .RetentionDays .EIGHTEEN_MONTHS ,
284+ 731 : logs .RetentionDays .TWO_YEARS ,
285+ 1827 : logs .RetentionDays .FIVE_YEARS ,
286+ 3653 : logs .RetentionDays .TEN_YEARS
287+ }
288+ return retention_map .get (int (days ), logs .RetentionDays .ONE_MONTH )
0 commit comments