Skip to content

Commit 7a67a24

Browse files
author
Chris Fane
committed
Updated to use higher level constructs where possible.
1 parent 1462aea commit 7a67a24

File tree

2 files changed

+97
-151
lines changed

2 files changed

+97
-151
lines changed

python/cloudfront-v2-logging/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
{
4848
"id": "AwsSolutions-CFR4",
49-
"reason": "Using TLSv1.2_2021 security policy which is the latest supported version."
49+
"reason": "We're making use of the highest currently available viewer certificate. This flag is due to our use of the default viewer certificate which is not an issue in this demonstration case."
5050
}
5151
]
5252
)
Lines changed: 96 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
from 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
1518
from constructs import Construct
16-
from cdk_nag import NagSuppressions
17-
18-
import json
1919

2020
class 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

Comments
 (0)