Deploy Static Website on Aws Using Cloudformation Template
A step by step guide to deploy static website on aws s3 and cloudfront using cloudformation as devops infrastructure as code.
AWS provides a very efficient, performant and cheap way to host the website both static and Single page application aka SPA. When I started to create my blogging website, I decided to automate everything, right from infrastructure to deployment. Here I am sharing my experience with automation of static website deployment.
To start with , you have to have a domain registered. Once you buy a domain, you can set the name server of AWS. Once this is set we are ready to go.
Step 1
First , create an account on AWS. Once done , create a user with programmatic access required to run cloudformation. Keep note of aws_access_key_id
and aws_secret_access_key
Step 2
Create a hosted zone on AWS Rout53
Service. Once done , you will get a Hosted Zone ID
. Note this Hosted Zone ID
, this will be needed for cloudformation.(see last column in below screenshot)
That's all, Now are ready to automate whole infrastructure.
Solution Overview
As a part of this solution we will use the apex domain example.com
and subdomain www.example.com
.
www.example.com
will be main domain to serve data.example.com
will be redirected towww.example.com
Below are the resources that will be created in AWS :
- ACM Certificate for
www.example.com
&example.com
(It's free in AWS. Hurray!!) - S3 buckets to hold cloudformation template data and website data.
- S3 static website for serving default content and redirection. e.g.
example.com
towww.example.com
- Route entries for
www.example.com
&example.com
- Cloudformation as CDN
- Lambda Edge to add Secure headers via
ContentSecurityPolicy
header.
See the flow in solution diagrams below
In Figure 2
, if users type example.com
, we are just redirecting the user to our main website www.example.com
. This is achieved by
creating a s3 website named example.com
(name of the bucket) and addiing the redirection rule there. Now In cloudfront, we are setting exampel.com
s3 website as origin.
example.com
domain is mapped to cloudfront, which in turn forward request to static website origin.
Why we need cloudfront for example.com
, why not directly map domain example.com
to s3 website which is doing the redirection ?
Well, We can do that too. Only problem is this works well for http
and not for https
.
e.g.
http://example.com ---> www.example.com - will work
https://example.com ---> www.example.com - will not work, actually browser will not be able to reach https://example.com
To make the issue worse, browsers automatically add https
to URL.
To solve this issue, we need cloudfront in between, so that we can handle both http
and https
.
In Figure 3
, user make a request to www.example.com
, which will first come to cloudfront edge location. If endpoint is cached,
then content will be server from edge location else go to the s3 static website origin. Content from origin will be fetched and secure lamda
function will get execued to add security headers , mainly ContentSecurityPolicy
header, which returned to the calee as well as cached on
edge locations.
Why we need static website as origin here, why not choose simple s3 bucket as Origin ?
Well, When we fetch data for endpoint, we are actually fetching the index.html
(excluding SPA) and cloudfront only support root index.html
.
That's why we need s3 website as origin, because we can set the default object in s3 website as index.html
for each folder.
e.g.
http://www.example.com/my-post actually means http://www.example.com/my-post/index.html
And S3 static website help us here to server default object as index.html
Cloudformation Template code
List of files:
- main.yaml
- buckets.yaml
- acm-certificates.yaml
- cloudfront.yaml
- routes-records.yaml
main.yaml This will main file and entry point. This will create all stacks step by step.
1AWSTemplateFormatVersion: 2010-09-09
2Description: ACFS3 - S3 Static site with CF and ACM (uksb-1qnk6ni7b) (version:v0.5)
3
4Metadata:
5 AWS::CloudFormation::Interface:
6 ParameterGroups:
7 - Label:
8 default: Domain
9 Parameters:
10 - SubDomain
11 - DomainName
12
13Mappings:
14 Solution:
15 Constants:
16 Version: 'v0.7'
17
18Rules:
19 OnlyUsEast1:
20 Assertions:
21 - Assert:
22 Fn::Equals:
23 - !Ref AWS::Region
24 - us-east-1
25 AssertDescription: |
26 This template can only be deployed in the us-east-1 region.
27 This is because the ACM Certificate must be created in us-east-1
28
29Parameters:
30 SubDomain:
31 Description: The part of a website address before your DomainName - e.g. www or img
32 Type: String
33 Default: www
34 AllowedPattern: ^[^.]*$
35 DomainName:
36 Description: The part of a website address after your SubDomain - e.g. example.com
37 Type: String
38 HostedZoneId:
39 Description: HostedZoneId for the domain e.g. Z23ABC4XYZL05B
40 Type: String
41
42Resources:
43 BucketStack:
44 Type: AWS::CloudFormation::Stack
45 Properties:
46 TemplateURL: ./buckets.yaml
47 Parameters:
48 DomainName: !Ref DomainName
49 SubDomain: !Ref SubDomain
50
51 AcmCertificateStack:
52 Type: AWS::CloudFormation::Stack
53 Properties:
54 TemplateURL: ./acm-certificate.yaml
55 Parameters:
56 SubDomain: !Ref SubDomain
57 DomainName: !Ref DomainName
58 HostedZoneId: !Ref HostedZoneId
59
60 CloudFrontStack:
61 Type: AWS::CloudFormation::Stack
62 Properties:
63 TemplateURL: ./cloudfront.yaml
64 Parameters:
65 SubDomainCertificateArn: !GetAtt AcmCertificateStack.Outputs.SubDomainCertificateArn
66 ApexDomainCertificateArn: !GetAtt AcmCertificateStack.Outputs.ApexDomainCertificateArn
67 DomainName: !Ref DomainName
68 SubDomain: !Ref SubDomain
69 S3BucketLogsName: !GetAtt BucketStack.Outputs.S3BucketLogsName
70
71 RouteRecordsStack:
72 Type: AWS::CloudFormation::Stack
73 Properties:
74 TemplateURL: ./routes-records.yaml
75 Parameters:
76 DomainName: !Ref DomainName
77 SubDomain: !Ref SubDomain
78 CfSubDomainName: !GetAtt CloudFrontStack.Outputs.SubDomainCloudFrontDistribution
79 CfApexDomainName: !GetAtt CloudFrontStack.Outputs.ApexDomainCloudFrontDistribution
80
81Outputs:
82 SolutionVersion:
83 Value: !FindInMap [Solution, Constants, Version]
84 S3BucketLogs:
85 Description: Logging bucket
86 Value: !GetAtt BucketStack.Outputs.S3BucketLogs
87 S3SubDomainBucket:
88 Description: Website bucket
89 Value: !GetAtt BucketStack.Outputs.S3SubDomainBucket
90 S3BucketLogsName:
91 Description: Logging bucket name
92 Value: !GetAtt BucketStack.Outputs.S3BucketLogsName
93 S3SubDomainBucketName:
94 Description: Website bucket name
95 Value: !GetAtt BucketStack.Outputs.S3SubDomainBucketName
96 SubDomainCertificateArn:
97 Description: Issued certificate
98 Value: !GetAtt AcmCertificateStack.Outputs.SubDomainCertificateArn
99 ApexDomainCertificateArn:
100 Description: Issued certificate
101 Value: !GetAtt AcmCertificateStack.Outputs.ApexDomainCertificateArn
102 CFSubDomainDistributionName:
103 Description: CloudFront distribution
104 Value: !GetAtt CloudFrontStack.Outputs.SubDomainCloudFrontDistribution
105 CFApexDomainDistributionName:
106 Description: CloudFront distribution
107 Value: !GetAtt CloudFrontStack.Outputs.ApexDomainCloudFrontDistribution
108 CloudFrontDomainName:
109 Description: Website address
110 Value: !GetAtt CloudFrontStack.Outputs.CloudFrontDomainName
buckets.yaml
This will create bucket example.com
and www.example.com
, create static website and also create Logs bucket.
1AWSTemplateFormatVersion: '2010-09-09'
2Description: ACFS3 - Cert Provider with DNS validation
3Transform: AWS::Serverless-2016-10-31
4
5Parameters:
6 DomainName:
7 Type: String
8 SubDomain:
9 Type: String
10
11Resources:
12 S3BucketLogs:
13 Type: AWS::S3::Bucket
14 DeletionPolicy: Retain
15 Properties:
16 AccessControl: LogDeliveryWrite
17 BucketEncryption:
18 ServerSideEncryptionConfiguration:
19 - ServerSideEncryptionByDefault:
20 SSEAlgorithm: AES256
21 Tags:
22 - Key: Solution
23 Value: ACFS3
24
25 S3SubDomainBucket:
26 Type: AWS::S3::Bucket
27 DeletionPolicy: Retain
28 Properties:
29 BucketName: !Sub '${SubDomain}.${DomainName}'
30 AccessControl: PublicRead
31 LoggingConfiguration:
32 DestinationBucketName: !Ref 'S3BucketLogs'
33 LogFilePrefix: 'cdn/'
34 WebsiteConfiguration:
35 ErrorDocument: '404.html'
36 IndexDocument: 'index.html'
37
38 S3DomainBucket:
39 Type: AWS::S3::Bucket
40 DeletionPolicy: Retain
41 Properties:
42 BucketName: !Ref DomainName
43 AccessControl: PublicRead
44 LoggingConfiguration:
45 DestinationBucketName: !Ref 'S3BucketLogs'
46 LogFilePrefix: 'rdb/'
47 WebsiteConfiguration:
48 RedirectAllRequestsTo:
49 HostName: !Sub '${SubDomain}.${DomainName}'
50
51 S3BucketPolicy:
52 Type: AWS::S3::BucketPolicy
53 Properties:
54 Bucket: !Ref S3SubDomainBucket
55 PolicyDocument:
56 Version: '2012-10-17'
57 Statement:
58 - Effect: 'Allow'
59 Action: 's3:GetObject'
60 Principal: '*'
61 Resource: !Sub
62 - '${S3BucketRootArn}/*'
63 - {S3BucketRootArn : !GetAtt S3SubDomainBucket.Arn}
64
65
66Outputs:
67 S3DomainBucket:
68 Description: Website bucket
69 Value: !Ref S3DomainBucket
70 S3DomainBucketName:
71 Description: Website bucket name
72 Value: !GetAtt S3DomainBucket.DomainName
73 S3DomainBucketArn:
74 Description: Website bucket locator
75 Value: !GetAtt S3DomainBucket.Arn
76 S3SubDomainBucket:
77 Description: Website bucket
78 Value: !Ref S3SubDomainBucket
79 S3SubDomainBucketName:
80 Description: Website bucket name
81 Value: !Sub '${SubDomain}.${DomainName}'
82 S3SubDomainBucketArn:
83 Description: Website bucket locator
84 Value: !GetAtt S3SubDomainBucket.Arn
85 S3BucketLogs:
86 Description: Logging bucket
87 Value: !Ref S3BucketLogs
88 S3BucketLogsName:
89 Description: Logging bucket Name
90 Value: !GetAtt S3BucketLogs.DomainName
91
acm-certificate.yaml
This will generate the certificates for example.com
and www.example.com
1AWSTemplateFormatVersion: '2010-09-09'
2Description: ACFS3 - Certificate creation
3
4Parameters:
5 DomainName:
6 Type: String
7 SubDomain:
8 Type: String
9 HostedZoneId:
10 Type: String
11
12Resources:
13 SubDomainCertificate:
14 Type: AWS::CertificateManager::Certificate
15 Properties:
16 DomainName: !Sub '${SubDomain}.${DomainName}'
17 SubjectAlternativeNames:
18 - Ref: AWS::NoValue
19 DomainValidationOptions:
20 - DomainName: !Sub '${SubDomain}.${DomainName}'
21 HostedZoneId: !Ref HostedZoneId
22 ValidationMethod: DNS
23
24 ApexDomainCertificate:
25 Type: AWS::CertificateManager::Certificate
26 Properties:
27 DomainName: !Sub '${DomainName}'
28 SubjectAlternativeNames:
29 - Ref: AWS::NoValue
30 DomainValidationOptions:
31 - DomainName: !Sub '${DomainName}'
32 HostedZoneId: !Ref HostedZoneId
33 ValidationMethod: DNS
34
35Outputs:
36 SubDomainCertificateArn:
37 Description: Issued certificate
38 Value: !Ref SubDomainCertificate
39 ApexDomainCertificateArn:
40 Description: Issued certificate
41 Value: !Ref ApexDomainCertificate
42
cloudfront.yaml
This stack will deploy the cloudformation, set origin, secure lambda headers. You can modify the ContentSecurityPolicy
header as per your need
1ContentSecurityPolicy:
2 ContentSecurityPolicy: !Join
3 - "; "
4 - - "default-src 'self'"
5 - "connect-src 'self' links.services.disqus.com www.google-analytics.com googleads.g.doubleclick.net static.doubleclick.net savjee.report-uri.com c.disquscdn.com disqus.com"
6 - "font-src 'self' fonts.gstatic.com"
7 - "frame-src 'self' disqus.com c.disquscdn.com www.google.com www.youtube.com accounts.google.com"
8 - "img-src 'self' 'unsafe-inline' cdn.viglink.com c.disquscdn.com referrer.disqus.com https://*.disquscdn.com www.google-analytics.com www.gstatic.com ssl.gstatic.com i.ytimg.com i.imgur.com images.gr-assets.com s.gr-assets.com data:"
9 - "script-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
10 - "prefetch-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
11 - "style-src 'self' 'unsafe-inline' c.disquscdn.com https://*.disquscdn.com fonts.googleapis.com https://fonts.googleapis.com/ https://tagmanager.google.com/ "
12 - "object-src 'none'"
13 Override: true
1AWSTemplateFormatVersion: '2010-09-09'
2Description: ACFS3 - CloudFront with Header Security and site content
3Transform: 'AWS::Serverless-2016-10-31'
4
5Parameters:
6 SubDomainCertificateArn:
7 Description: Certificate locater for sub domain
8 Type: String
9 ApexDomainCertificateArn:
10 Description: Certificate locater for apex domain
11 Type: String
12 DomainName:
13 Description: Apex domain
14 Type: String
15 SubDomain:
16 Description: Subdomain
17 Type: String
18 S3BucketLogsName:
19 Description: Logging Bucket
20 Type: String
21
22Resources:
23 ApexDomainCloudFrontDistribution:
24 Type: AWS::CloudFront::Distribution
25 Properties:
26 DistributionConfig:
27 Aliases:
28 - !Sub '${DomainName}'
29 Comment: 'apex domain to redirect to sub domain'
30 DefaultCacheBehavior:
31 Compress: true
32 DefaultTTL: 86400
33 ForwardedValues:
34 QueryString: true
35 MaxTTL: 31536000
36 TargetOriginId: !Sub 'S3-${AWS::StackName}-apexdomain'
37 ViewerProtocolPolicy: 'redirect-to-https'
38 Enabled: true
39 HttpVersion: 'http2'
40 IPV6Enabled: true
41 Logging:
42 Bucket: !Ref 'S3BucketLogsName'
43 IncludeCookies: false
44 Prefix: 'cdn/'
45 Origins:
46 - CustomOriginConfig:
47 HTTPPort: 80
48 HTTPSPort: 443
49 OriginKeepaliveTimeout: 5
50 OriginProtocolPolicy: 'http-only'
51 OriginReadTimeout: 30
52 OriginSSLProtocols:
53 - TLSv1
54 - TLSv1.1
55 - TLSv1.2
56 DomainName: !Sub '${DomainName}.s3-website.${AWS::Region}.amazonaws.com'
57 Id: !Sub 'S3-${AWS::StackName}-apexdomain'
58 PriceClass: 'PriceClass_All'
59 ViewerCertificate:
60 AcmCertificateArn: !Ref 'ApexDomainCertificateArn'
61 MinimumProtocolVersion: 'TLSv1.1_2016'
62 SslSupportMethod: 'sni-only'
63
64 SubDomainCloudFrontDistribution:
65 Type: AWS::CloudFront::Distribution
66 Properties:
67 DistributionConfig:
68 Aliases:
69 - !Sub '${SubDomain}.${DomainName}'
70 Comment: 'sub domain to handle real traffic'
71 DefaultCacheBehavior:
72 Compress: true
73 DefaultTTL: 86400
74 ForwardedValues:
75 QueryString: true
76 MaxTTL: 31536000
77 TargetOriginId: !Sub 'S3-${AWS::StackName}-subdomain'
78 ViewerProtocolPolicy: 'redirect-to-https'
79 ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy
80 CustomErrorResponses:
81 - ErrorCachingMinTTL: 60
82 ErrorCode: 404
83 ResponseCode: 404
84 ResponsePagePath: '/404.html'
85 - ErrorCachingMinTTL: 60
86 ErrorCode: 403
87 ResponseCode: 403
88 ResponsePagePath: '/403.html'
89 Enabled: true
90 HttpVersion: 'http2'
91 DefaultRootObject: 'index.html'
92 IPV6Enabled: true
93 Logging:
94 Bucket: !Ref 'S3BucketLogsName'
95 IncludeCookies: false
96 Prefix: 'cdn/'
97 Origins:
98 - CustomOriginConfig:
99 HTTPPort: 80
100 HTTPSPort: 443
101 OriginKeepaliveTimeout: 5
102 OriginProtocolPolicy: 'http-only'
103 OriginReadTimeout: 30
104 OriginSSLProtocols:
105 - TLSv1
106 - TLSv1.1
107 - TLSv1.2
108 DomainName: !Sub '${SubDomain}.${DomainName}.s3-website.${AWS::Region}.amazonaws.com'
109 Id: !Sub 'S3-${AWS::StackName}-subdomain'
110 PriceClass: 'PriceClass_All'
111 ViewerCertificate:
112 AcmCertificateArn: !Ref 'SubDomainCertificateArn'
113 MinimumProtocolVersion: 'TLSv1.1_2016'
114 SslSupportMethod: 'sni-only'
115
116 CloudFrontOriginAccessIdentity:
117 Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
118 Properties:
119 CloudFrontOriginAccessIdentityConfig:
120 Comment: !Sub 'CloudFront OAI for ${SubDomain}.${DomainName}'
121
122 ResponseHeadersPolicy:
123 Type: AWS::CloudFront::ResponseHeadersPolicy
124 Properties:
125 ResponseHeadersPolicyConfig:
126 Name: !Sub "${AWS::StackName}-static-site-security-headers"
127 SecurityHeadersConfig:
128 StrictTransportSecurity:
129 AccessControlMaxAgeSec: 63072000
130 IncludeSubdomains: true
131 Override: true
132 Preload: true
133 ContentSecurityPolicy:
134 ContentSecurityPolicy: !Join
135 - "; "
136 - - "default-src 'self'"
137 - "connect-src 'self' links.services.disqus.com www.google-analytics.com googleads.g.doubleclick.net static.doubleclick.net savjee.report-uri.com c.disquscdn.com disqus.com"
138 - "font-src 'self' fonts.gstatic.com"
139 - "frame-src 'self' disqus.com c.disquscdn.com www.google.com www.youtube.com accounts.google.com"
140 - "img-src 'self' 'unsafe-inline' cdn.viglink.com c.disquscdn.com referrer.disqus.com https://*.disquscdn.com www.google-analytics.com www.gstatic.com ssl.gstatic.com i.ytimg.com i.imgur.com images.gr-assets.com s.gr-assets.com data:"
141 - "script-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
142 - "prefetch-src 'self' 'unsafe-eval' 'unsafe-inline' c.disquscdn.com disqus.com arika-dev.disqus.com https://*.disquscdn.com www.google.com www.google-analytics.com www.gstatic.com apis.google.com https://tagmanager.google.com/ https://www.googletagmanager.com"
143 - "style-src 'self' 'unsafe-inline' c.disquscdn.com https://*.disquscdn.com fonts.googleapis.com https://fonts.googleapis.com/ https://tagmanager.google.com/ "
144 - "object-src 'none'"
145 Override: true
146 ContentTypeOptions:
147 Override: true
148 FrameOptions:
149 FrameOption: DENY
150 Override: true
151 ReferrerPolicy:
152 ReferrerPolicy: "same-origin"
153 Override: true
154 XSSProtection:
155 ModeBlock: true
156 Override: true
157 Protection: true
158
159Outputs:
160 SubDomainCloudFrontDistribution:
161 Description: CloudFront distribution
162 Value: !GetAtt SubDomainCloudFrontDistribution.DomainName
163
164 ApexDomainCloudFrontDistribution:
165 Description: CloudFront distribution
166 Value: !GetAtt ApexDomainCloudFrontDistribution.DomainName
167
168 CloudFrontDomainName:
169 Description: Website address
170 Value: !Sub '${SubDomain}.${DomainName}'
171
routes-records.yaml This will create and verify Route53 Records and point them to cloudfront.
1AWSTemplateFormatVersion: '2010-09-09'
2Description: ACFS3 - CloudFront with Header Security and site content
3Transform: 'AWS::Serverless-2016-10-31'
4
5Parameters:
6 DomainName:
7 Description: Root Domain
8 Type: String
9 SubDomain:
10 Description: Subdomain
11 Type: String
12 CfSubDomainName:
13 Description: Cloudfront domain name
14 Type: String
15 CfApexDomainName:
16 Description: Cloudfront domain name
17 Type: String
18
19Resources:
20 SubDomainRoute53RecordSetGroup:
21 Type: AWS::Route53::RecordSetGroup
22 Properties:
23 HostedZoneName: !Sub '${DomainName}.'
24 RecordSets:
25 - Name: !Sub '${SubDomain}.${DomainName}'
26 Type: 'A'
27 AliasTarget:
28 DNSName: !Ref CfSubDomainName
29 EvaluateTargetHealth: false
30 # The following HosteZoneId is always used for alias records pointing to CF.
31 HostedZoneId: 'Z2FDTNDATAQYW2'
32
33 ApexDomainRoute53RecordSetGroup:
34 Type: AWS::Route53::RecordSetGroup
35 Properties:
36 HostedZoneName: !Sub '${DomainName}.'
37 RecordSets:
38 - Name: !Sub '${DomainName}'
39 Type: 'A'
40 AliasTarget:
41 DNSName: !Ref CfApexDomainName
42 EvaluateTargetHealth: false
43 # The following HosteZoneId is always used for alias records pointing to CF.
44 HostedZoneId: 'Z2FDTNDATAQYW2'
45
46Outputs:
47 SubDomainRoute53RecordSetGroup:
48 Description: sub domain route 53
49 Value: !Ref SubDomainRoute53RecordSetGroup
50
51 ApexDomainRoute53RecordSetGroup:
52 Description: domain route 53
53 Value: !Ref ApexDomainRoute53RecordSetGroup
How to execute
Step 1 Create a bucket to store cloudformation templates
1 aws s3 mb s3://cf-example-dev
2
Step 2 Package template
1aws --region us-east-1 cloudformation package \
2 --template-file ./infrastructure/main.yaml \
3 --s3-bucket cf-example-dev \
4 --output-template-file packaged.template
Step 3 Deploy
1aws --region us-east-1 cloudformation deploy \
2 --stack-name cf-example-dev-stack \
3 --template-file packaged.template \
4 --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \
5 --parameter-overrides DomainName=example.com SubDomain=www HostedZoneId=<ROUTE_53_HOSTED_ZONE_ID>
Deploying website content
Assuming public
folder contains your artefacts, you can deploy the content using
1aws s3 sync ./public s3://www.example.com
Check complete code on github
You can check the complete code at GitHub.