Skip to content

Commit

Permalink
Add module for building CloudFront frontends to S3 buckets (#5)
Browse files Browse the repository at this point in the history
* Add CloudFrontS3Service

* Add logging bucket

* Add CloudFront Distribution

* Ruff

* Bump version
  • Loading branch information
ryanjjung authored Sep 24, 2024
1 parent 00190e3 commit b9421ab
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 3 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tb_pulumi"
version = "0.0.1"
version = "0.0.2"
description = "Framework and patterns for using Pulumi at Thunderbird"
requires-python = ">3.12"
dynamic = ["dependencies"]
Expand Down
173 changes: 173 additions & 0 deletions tb_pulumi/cloudfront.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import pulumi
import pulumi_aws as aws
import tb_pulumi


CACHE_POLICY_ID_OPTIMIZED = '658327ea-f89d-4fab-a63d-7e88639e58f6' # "Managed-CachingOptimized" policy
CACHE_POLICY_ID_DISABLED = '4135ea2d-6df8-44a3-9df3-4b5a84be39ad' # "Managed-CachingDisabled" policy
ORIGIN_REQUEST_POLICY_ID_ALLVIEWER = '216adef6-5c7f-47e4-b989-5492eafa07d3' # "Managed-AllViewer" policy


class CloudFrontS3Service(tb_pulumi.ThunderbirdComponentResource):
"""Serve the static contents of an S3 bucket over a CloudFront Distribution."""

def __init__(
self,
name: str,
project: tb_pulumi.ThunderbirdPulumiProject,
certificate_arn: str,
service_bucket_name: str,
behaviors: list[dict] = [],
distribution: dict = {},
origins: list[dict] = [],
opts: pulumi.ResourceOptions = None,
**kwargs,
):
"""Construct a CloudFrontS3Service.
Positional arguments:
- name: A string identifying this set of resources.
- project: The ThunderbirdPulumiProject to add these resources to.
- certificate_arn: The ARN of the ACM certificate used for TLS in this distribution.
- service_bucket_name: The name of the S3 bucket to store the static content in. This must be globally
unique within the entire S3 ecosystem.
Keyword arguments:
- behaviors: The default behavior of the CF distribution will always be to look in the S3 bucket. Any other
behaviors should be defined as an entry in this list. These should be DistributionOrderedCacheBehavior
objects. Ref: https://www.pulumi.com/registry/packages/aws/api-docs/cloudfront/distribution/#distributionorderedcachebehavior
- distribution: Additional parameters to pass to the `aws.cloudfront.Distribution` constructor. Ref:
https://www.pulumi.com/registry/packages/aws/api-docs/cloudfront/distribution
- origins: List of DistributionOrigin objects to add. This list should not include any references to the S3
bucket, which is managed by this module. Ref:
https://www.pulumi.com/registry/packages/aws/api-docs/cloudfront/distribution/#distributionorigin
- opts: Additional pulumi.ResourceOptions to apply to these resources.
- kwargs: Any other keyword arguments which will be passed as inputs to the
`tb_pulumi.ThunderbirdComponentResource` constructor.
"""

super().__init__('tb:cloudfront:CloudFrontS3Service', name=name, project=project, opts=opts)

# The function supports supplying the bucket policy at this time, but we have to have the CF distro built first.
# For this reason, we build the bucket without the policy and attach the policy later on.
self.resources['service_bucket'] = aws.s3.Bucket(
f'{name}-servicebucket',
bucket=service_bucket_name,
server_side_encryption_configuration={
'rule': {'applyServerSideEncryptionByDefault': {'sseAlgorithm': 'AES256'}, 'bucket_key_enabled': True}
},
opts=pulumi.ResourceOptions(parent=self),
tags=self.tags,
)

# S3 bucket to store access logs from CloudFront
self.resources['logging_bucket'] = aws.s3.Bucket(
f'{name}-loggingbucket',
bucket=f'{service_bucket_name}-logs',
grants=[
{
'permissions': ['FULL_CONTROL'],
'type': 'CanonicalUser',
'id': aws.s3.get_canonical_user_id().id,
'uri': '',
}
],
server_side_encryption_configuration={
'rule': {'applyServerSideEncryptionByDefault': {'sseAlgorithm': 'AES256'}, 'bucket_key_enabled': True}
},
opts=pulumi.ResourceOptions(parent=self),
tags=self.tags,
)

self.resources['logging_bucket_ownership'] = aws.s3.BucketOwnershipControls(
f'{name}-bucketownership',
bucket=self.resources['logging_bucket'].id,
rule={'object_ownership': 'BucketOwnerPreferred'},
opts=pulumi.ResourceOptions(parent=self, depends_on=[self.resources['logging_bucket']]),
)

# Create an Origin Access Control to use when CloudFront talks to S3
self.resources['oac'] = aws.cloudfront.OriginAccessControl(
f'{name}-oac',
origin_access_control_origin_type='s3',
signing_behavior='always',
signing_protocol='sigv4',
description=f'Serve {service_bucket_name} contents via CDN',
name=service_bucket_name,
opts=pulumi.ResourceOptions(parent=self, depends_on=[self.resources['service_bucket']]),
)

# Define the S3 DistributionOrigin and set up the distribution
# The `bucket_regional_domain_name` output does not actually seem to contain the region. This may be a bug in
# the AWS Pulumi provider. For now, we have to form this domain ourselves or it will be incorrect.
bucket_regional_domain_name = f'{service_bucket_name}.s3.{project.aws_region}.amazonaws.com'
s3_origin = {
'domain_name': bucket_regional_domain_name,
'origin_id': bucket_regional_domain_name,
'origin_access_control_id': self.resources['oac'].id,
}
all_origins = [s3_origin]
all_origins.extend(origins)

# Merge logging settings from the config file with this generated bucket name
logging_config = {'bucket': self.resources['logging_bucket'].bucket_domain_name}
if 'logging_config' in distribution:
logging_config.update(distribution['logging_config'])
# Consume this now so it doesn't create kwarg problems later
del distribution['logging_config']

self.resources['cloudfront_distribution'] = aws.cloudfront.Distribution(
f'{name}-cfdistro',
default_cache_behavior={
'allowed_methods': ['HEAD', 'DELETE', 'POST', 'GET', 'OPTIONS', 'PUT', 'PATCH'],
'cached_methods': ['HEAD', 'GET'],
'cache_policy_id': CACHE_POLICY_ID_OPTIMIZED,
'compress': True,
'target_origin_id': bucket_regional_domain_name,
'viewer_protocol_policy': 'redirect-to-https',
},
enabled=True,
logging_config=logging_config,
ordered_cache_behaviors=behaviors,
origins=all_origins,
restrictions={'geo_restriction': {'restriction_type': 'none'}},
viewer_certificate={
'acm_certificate_arn': certificate_arn,
'minimum_protocol_version': 'TLSv1.2_2021',
'ssl_support_method': 'sni-only',
},
tags=self.tags,
opts=pulumi.ResourceOptions(
parent=self,
ignore_changes=['defaultCacheBehavior.functionAssociations'],
),
**distribution,
)

# Set the policy allowing CloudFront access to the service bucket
bucket_policy = {
'Version': '2008-10-17',
'Id': 'PolicyForCloudFrontPrivateContent',
'Statement': [
{
'Sid': 'AllowCloudFrontServicePrincipal',
'Effect': 'Allow',
'Principal': {'Service': 'cloudfront.amazonaws.com'},
'Action': 's3:GetObject',
'Resource': f'arn:aws:s3:::{service_bucket_name}/*',
'Condition': {'StringEquals': {'AWS:SourceArn': self.resources['cloudfront_distribution'].arn}},
}
],
}

self.resources['service_bucket_policy'] = aws.s3.BucketPolicy(
f'{name}-bucketpolicy-service',
bucket=self.resources['service_bucket'],
policy=bucket_policy,
opts=pulumi.ResourceOptions(
parent=self,
depends_on=[self.resources['service_bucket'], self.resources['cloudfront_distribution']],
),
)

self.finish()
2 changes: 1 addition & 1 deletion tb_pulumi/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def __init__(
ThunderbirdComponentResource superconstructor.
"""

super().__init__('tb:ec2:SshableInstance', name, project, opts=opts, **kwargs)
super().__init__('tb:ec2:SshableInstance', name=name, project=project, opts=opts, **kwargs)

self.resources['keypair'] = SshKeyPair(f'{name}-keypair', project, public_key=public_key).resources

Expand Down
1 change: 0 additions & 1 deletion tb_pulumi/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def __init__(
),
)


# If we have to build endpoints, we have to have a security group to let local traffic in
if len(endpoint_interfaces + endpoint_gateways) > 0:
self.resources['endpoint_sg'] = tb_pulumi.network.SecurityGroupWithRules(
Expand Down

0 comments on commit b9421ab

Please sign in to comment.