Minimalist static website with AWS S3, Cloud Formation & Route 53
We'll use a minimalist method to deploy a static website using a custom domain name with AWS S3, Route 53 and CloudFormation. Our stack will contain:
- An S3 bucket holding our website content with a public access policy
- The bucket will act as storage for our website data (HTML, CSS, etc.), we'll just need to copy our website content into it.
- A Route 53 DNS record for our domain name pointing to our S3 bucket
- A CloudFormation stack deploying all of the above
Requirements:
- An existing Route53 Hosted Zone for our domain name
- An AWS user with enough permission to manage our stack (see below for minimal IAM policy)
Minimal example
We're gonna deploy our website at http://minimalist-s3-website.devops.crafteo.io using Hosted Zone devops.crafteo.io.
. You can see the final result on the full example on GitHub.
First, let's create our S3 bucket and DNS record with this cloudformation.yml
template:
# CloudFormation template deploying a static S3 bucket with custom DNS
# Based on https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-s3.html
AWSTemplateFormatVersion: 2010-09-09
Parameters:
# Name of the Hosted Zone under which to create DNS record
# Note: the full HZ name ends with a dot ('.') such as 'devops.crafteo.io.'
HostedZoneName:
Type: String
# Desired Fully Qualified Domain Name (FQDN) for our website
WebsiteFQDN:
Type: String
Resources:
# Bucket holding static content
# Bucket name must match desired domain name
S3Bucket:
Type: AWS::S3::Bucket
Properties:
# gitlabci-gitops-s3-static-website.devops.crafteo.io
BucketName: !Ref WebsiteFQDN
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
DeletionPolicy: Retain
# Allow world to read bucket objects (i.e grant public access)
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Id: PublicAccessPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref S3Bucket
- /*
# DNS record pointing to bucket
# Record name using name of S3 bucket (they must match)
Route53DNSRecord:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Ref HostedZoneName
RecordSets:
- Name: !Ref S3Bucket
Type: CNAME
TTL: 900
# Record like gitlabci-gitops-s3-static-website.devops.crafteo.io.s3-website.eu-west-3.amazonaws.com
# We'll use current stack's AWS region to point to S3 bucket website endpoint
# See https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints
ResourceRecords:
- !Join [ '', [ !Ref S3Bucket, '.s3-website.', !Ref "AWS::Region", '.amazonaws.com'] ]
Run command:
# Deploy our stack using given Hosted Zone and domain name
aws cloudformation deploy \
--template-file cloudformation.yml \
--stack-name minimalist-s3-website \
--parameter-overrides \
HostedZoneName='devops.crafteo.io.' \
WebsiteFQDN='minimalist-s3-website.devops.crafteo.io'
Once created, simply copy your website content with:
# Synchronize website content to bucket
# The bucket name matches exactly our FQDN
# /!\ --delete flag will ensure Bucket content matches EXACTLY local website-content/ folder and delete anything else
# For non-destructive action, you can use `aws s3 cp` or remove --delete flag
aws s3 sync --delete /path/to/website-content/ s3://minimalist-s3-website.devops.crafteo.io
That's it! Our website should be available at http://minimalist-s3-website.devops.crafteo.io
Minimal AWS IAM policy
Minimalist CloudFormation is nice and all, but how about a minimalist IAM policy which only grant enough permissions to manage our stack and website content? Thus respecting Principle of Least Privileges (also known as Principle of Minimal Privileges).
The below policy only allow:
- Management of S3 bucket for our website (and content within)
- Management of records on our Route 53 Hosted Zone
- Management of CloudFormation stacks named like
minimalist-s3-website
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3List",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"route53:ListHostedZones"
],
"Resource": "*"
},
{
"Sid": "S3",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*.devops.crafteo.io/*",
"arn:aws:s3:::*.devops.crafteo.io"
]
},
{
"Sid": "Route53Records",
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets",
"route53:GetChange"
],
"Resource": [
"arn:aws:route53:::hostedzone/Z022447923VAXAUFPW2F5",
"arn:aws:route53:::change/*"
]
},
{
"Sid": "CloudFormation",
"Effect": "Allow",
"Action": [
"cloudformation:*"
],
"Resource": [
"arn:aws:cloudformation:*:*:stack/minimalist-s3-website/*",
"arn:aws:cloudformation:*:*:stack/minimalist-s3-website"
]
}
]
}
This policy is an example, you may restrict it further by specifying AWS account ID and region, using wildcard on resource names to manage multiple environments, etc. - but it works with the above example without having to set FullAccess
rights on a bunch of services.
Cleaning-up
You can delete all resources by using:
# Delete CloudFormation stack
# S3 bucket won't be deleted - AWS won't allow it if bucket is not empty
# Bucket must be destroyed manually afterward (or emptied before deletion)
aws cloudformation delete-stack --stack-name minimalist-s3-website
# Force bucket deletion afterward
aws s3 rb --force s3://minimalist-s3-website.devops.crafteo.io
Full example on Github
You can checkout the full example on GitHub