Minimalist static website with AWS S3, CloudFormation & Route 53

Minimalist static website with AWS S3, CloudFormation & Route 53

A minimalist way to deploy a static website with a custom domain name

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

Minimalist 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 source code

You can checkout the the full example on GitHub and the static website at http://minimalist-s3-website.devops.crafteo.io

 
Share this