Skip to content

Instantly share code, notes, and snippets.

@alexnederlof
Last active June 27, 2020 07:57
Show Gist options
  • Save alexnederlof/809680a2a2fdb59ed6339db3c7e67ab3 to your computer and use it in GitHub Desktop.
Save alexnederlof/809680a2a2fdb59ed6339db3c7e67ab3 to your computer and use it in GitHub Desktop.
Cloudformation template for S3, Cloudfront, Letsencrypt stack
# Deploy with
# aws cloudformation update-stack --stack-name your-static-site --template-body file://cloudfront.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Stack for static website hosting
Parameters:
IamCertificateId:
Type: String
Description: Use the ID from from the create-certificate.sh script
Default: xslidfjlskdfj
DomainName:
Type: String
Default: yoursite.yourdomain.com.
Route53HostedZone:
Type: String
Default: yourdomain.com.
Resources:
# Demo bucket
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref DomainName
AccessControl: "PublicRead"
WebsiteConfiguration:
# If you do javascript routing
# the URLs do exist on the server, so
# we always return index.html for any route
# the server doesn't have as a file
ErrorDocument: index.html
IndexDocument: index.html
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: "*"
Action: "s3:GetObject"
Resource: !Join
- ""
- - "arn:aws:s3:::"
- !Ref WebsiteBucket
- /*
Bucket: !Ref WebsiteBucket
CloudFront:
Type: AWS::CloudFront::Distribution
DependsOn:
- WebsiteBucket
Properties:
DistributionConfig:
Origins:
- DomainName:
!Join [".", [!Ref DomainName, "s3.amazonaws.com"]]
Id: !Ref DomainName
S3OriginConfig: {}
Enabled: "true"
HttpVersion: "http2"
DefaultRootObject: index.html
# This price class is enough for EU and US, but not the rest of the world
PriceClass: "PriceClass_100"
Aliases:
- !Ref DomainName
DefaultCacheBehavior:
# Disclaimer: probably, there's better ways to configure cloudformation
# But I'm not cloudformtion expert. If you have any recommendations,
# please leave them in the comments!
Compress: true
AllowedMethods:
- GET
- HEAD
TargetOriginId: !Ref DomainName
ForwardedValues:
QueryString: "true"
Cookies:
Forward: none
# Make the world a better place: force the usage of HTTPS:
ViewerProtocolPolicy: redirect-to-https
ViewerCertificate:
IamCertificateId: !Ref IamCertificateId
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2018
CustomErrorResponses:
- ErrorCachingMinTTL: 86400
# again, setup the 404 paths to return
# a 200 with index.html to allow javascript routing
ErrorCode: 404
ResponseCode: 200
ResponsePagePath: "/index.html"
WebsiteDNSName:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Ref Route53HostedZone
RecordSets:
- Name: !Ref DomainName
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2 # This means Cloudfront
DNSName: !GetAtt [CloudFront, DomainName]
Outputs:
BucketLocation:
Description: Sync the web-ui code here
Value: !Ref WebsiteBucket
Domain:
Value: !Ref DomainName
CloudFrontName:
Description: "Cloudfront's internal URL"
Value: !GetAtt [CloudFront, DomainName]
CloudFrontId:
Description: "Cloudfront ID"
Value: !Ref CloudFront
# For this script to work you need the python packages:
# pip install certbot certbot-dns-route53
# If you don't have python/pip installed, or if you are
# on windows, you can also just run this from a python
# docker container! Just don't forget to add your .aws
# credentials to the docker container so you can deploy
# the script
domain=your.website.com # Or whatever domain you desire
[email protected] # You will receive domain expire messages on this email
# This command will take a couple of minutes, because it uses
# DNS from route-53 to prove you own the domain
certbot certonly \
--config-dir /tmp/cert/config \
--work-dir /tmp/cert/work \
--logs-dir /tmp/cert/logs \
-n --agree-tos --email $email \
--dns-route53 \
-d $domain
# Upload the certificate to AWS. Save the response! You'll need the ServerCertificateId!
aws iam upload-server-certificate \
--server-certificate-name $domain \
--certificate-body file:///tmp/cert/config/live/$domain/cert.pem \
--private-key file:///tmp/cert/config/live/$domain/privkey.pem \
--certificate-chain file:///tmp/cert/config/live/$domain/chain.pem \
--path /cloudfront/
target=/path/to/your/website
bucket=s3://your-bucket-name
distribution=distirbutionid # Is returned by the cloudformation stack
# This script assumes a `create-react-app` where static resources
# (resources that can be cached "forever" are in the /static/ folder.
# Other resources should not be cached forever, hence we deploy like this:
# First the non-static assets. Clients should not cache these.
aws s3 sync $target/ $bucket \
--cache-control "no-cache" \
--exclude "static/*" \
--exclude ".DS_Store" \
--delete
# The static assests can be cached for a long time
aws s3 sync $target/static $bucket/static \
--cache-control "public, max-age=2.592000, must-revalidate" \
--exclude ".DS_Store" \
--delete
# CloudFront has an internal cache as well, to be sure people get the latest
# Index.html, force cloudformation to evict its caches for index.html.
# You can add other resources here if you notice any caching problems.
aws cloudfront create-invalidation --distribution-id $distribution --paths / /index.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment