-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstatic_site.py
243 lines (208 loc) · 8.6 KB
/
static_site.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
"""
Two constructs to host static sites in aws using S3, cloudfront and Route53.
StaticSitePrivateS3 creates a private S3 bucket and uses S3 API endpoint as
an origin in cloudfront and Origin Access Identity (OAI) to access the s3 objects.
StaticSitePublicS3 creates a public S3 bucket with website enabled and
uses Origin Custom Header (referer) to limit the access of s3 objects to the
CloudFront only.
"""
from aws_cdk import (
aws_s3 as s3,
aws_s3_deployment as s3deploy,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_certificatemanager as acm,
aws_route53 as route53,
aws_route53_targets as targets,
aws_iam as iam,
aws_ssm as ssm,
RemovalPolicy
)
from constructs import Construct
class StaticSite(Construct):
"""The base class for StaticSite constructs"""
def __init__(
self,
scope,
construct_id,
site_domain_name,
hosted_zone_id,
hosted_zone_name,
domain_certificate_arn=None,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
# Public variables
self.bucket = None
self.certificate = None
self.distribution = None
# Internal variables
self._site_domain_name = site_domain_name
# Instance Variables
self.__domain_certificate_arn = domain_certificate_arn
self.__hosted_zone_id = hosted_zone_id
self.__hosted_zone_name = hosted_zone_name
def _build_site(self):
"""The Template Method for building the site.
It uses hook functions which are implemented in the sub classes
"""
# Create the S3 bucket for the site contents
self._create_site_bucket()
# Get the hosted zone based on the provided domain name
hosted_zone = self.__get_hosted_zone()
# Get an existing or create a new certificate for the site domain
self.__create_certificate(hosted_zone)
# create the cloud front distribution
self._create_cloudfront_distribution()
# Deploy the content of the website
# Deploy files in S3 bucket
self.deployment = s3deploy.BucketDeployment(self, "DeployedWebsite",
sources=[s3deploy.Source.asset("./website/build")],
destination_bucket=self.bucket,
distribution=self.distribution,
distribution_paths=["/*"],
)
# Create a Route53 record
self.__create_route53_record(hosted_zone)
def _create_site_bucket(self):
"""a virtual function to be implemented by the sub classes"""
def _create_cloudfront_distribution(self):
"""a virtual function to be implemented by the sub classes"""
def __get_hosted_zone(self):
return route53.HostedZone.from_hosted_zone_attributes(
self,
"hosted_zone",
zone_name=self.__hosted_zone_name,
hosted_zone_id=self.__hosted_zone_id,
)
def __create_route53_record(self, hosted_zone):
route53.ARecord(
self,
"site-alias-record",
record_name=self._site_domain_name,
zone=hosted_zone,
target=route53.RecordTarget.from_alias(
targets.CloudFrontTarget(self.distribution)
),
)
def __create_certificate(self, hosted_zone):
if self.__domain_certificate_arn:
# If certificate arn is provided, import the certificate
self.certificate = acm.Certificate.from_certificate_arn(
self,
"site_certificate",
certificate_arn=self.__domain_certificate_arn,
)
else:
# If certificate arn is not provided, create a new one.
# ACM certificates that are used with CloudFront must be in
# the us-east-1 region.
self.certificate = acm.DnsValidatedCertificate(
self,
"site_certificate",
domain_name=self._site_domain_name,
hosted_zone=hosted_zone,
region="us-east-1",
)
class StaticSitePrivateS3(StaticSite):
def __init__(
self,
scope,
construct_id,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
self._build_site()
def _create_site_bucket(self):
"""Creates a private S3 bucket for the static site construct"""
self.bucket = s3.Bucket(
self,
"site_bucket",
bucket_name=self._site_domain_name,
encryption=s3.BucketEncryption.S3_MANAGED,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
versioned=True
)
def _create_cloudfront_distribution(self):
"""Create a cloudfront distribution with a private bucket as the origin"""
self.distribution = cloudfront.Distribution(
self,
"cloudfront_distribution",
default_behavior=cloudfront.BehaviorOptions(
origin=origins.S3Origin(self.bucket),
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
),
domain_names=[self._site_domain_name],
certificate=self.certificate,
default_root_object="index.html",
)
class StaticSitePublicS3(StaticSite):
def __init__(
self,
scope,
construct_id,
origin_referer_header_parameter_name,
**kwargs,
):
super().__init__(scope, construct_id, **kwargs)
# Get the origin referer header value
self.__origin_referer_header = self.__get_referer_header(
origin_referer_header_parameter_name,
)
self._build_site()
def __get_referer_header(self, parameter_name):
return ssm.StringParameter.from_string_parameter_attributes(
self, "custom_header", parameter_name=parameter_name
).string_value
def _create_site_bucket(self):
"""Creates a public S3 bucket for the static site construct"""
self.bucket = s3.Bucket(
self,
"site_bucket",
bucket_name=self._site_domain_name,
website_index_document="index.html",
website_error_document="404.html",
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
versioned=True
)
bucket_policy = iam.PolicyStatement(
actions=["s3:GetObject"],
resources=[self.bucket.arn_for_objects("*")],
principals=[iam.AnyPrincipal()],
)
bucket_policy.add_condition(
"StringEquals",
{"aws:Referer": self.__origin_referer_header},
)
self.bucket.add_to_resource_policy(bucket_policy)
def _create_cloudfront_distribution(self):
"""Create a cloudfront distribution with a public bucket as the origin"""
origin_source = cloudfront.CustomOriginConfig(
domain_name=self.bucket.bucket_website_domain_name,
origin_protocol_policy=cloudfront.OriginProtocolPolicy.HTTP_ONLY,
origin_headers={"Referer": self.__origin_referer_header},
)
self.distribution = cloudfront.CloudFrontWebDistribution(
self,
"cloudfront_distribution",
viewer_certificate=cloudfront.ViewerCertificate.from_acm_certificate(self.certificate,
aliases=[self._site_domain_name],
security_policy=cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019,
ssl_method=cloudfront.SSLMethod.SNI
),
origin_configs=[
cloudfront.SourceConfiguration(
custom_origin_source=origin_source,
behaviors=[
cloudfront.Behavior(
is_default_behavior=True,
)
],
)
],
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
price_class=cloudfront.PriceClass.PRICE_CLASS_ALL,
)