Nickolas Kraus

Hosting a Static Website with Hugo and AWS

This article details the steps for creating and hosting a static website on AWS. It provides both the manual steps (via the Amazon Management Console) and the semi-automated steps using the AWS CLI.

Hugo

Hugo is a static site generator. The purpose of a static site generator is to render content into HTML files before the request for the content is made — increasing performance and reducing load time. To achieve this, Hugo uses a source directory of files and templates as input to create a complete website.

Getting Started

  1. Install Hugo:

    brew install hugo
    
  2. Create a new site:

    hugo new site my-site
    
  3. Add source control:

    cd my-site
    git init
    
  4. Choose a theme:

    Pre-made themes can be found here. This website uses a custom theme that I created, which can be found here.1

    git submodule add git@github.com:<username>/<theme>.git themes/<theme>
    

    Next, copy the config.toml from your chosen template into your own.

  5. Add content:

    hugo new posts/my-first-post.md
    

    NOTE: This will create a new directory, posts, and file, my-first-post.md in the content directory.

  6. Start the Hugo server:

    hugo server -D
    

    This will bootstrap your static site. For a more in-depth look at Hugo and how to use it, check out their documentation.

    NOTE: The -D option will include content marked as draft when running the server or generating static content.

  7. Generate static files:

    hugo
    

Amazon Web Services

Amazon Web Services (AWS) is a collection of digital infrastructure services that developers can leverage when developing their applications. The services include computing, storage, database, and application services (messaging and queuing). For this application, we will use:

  • Amazon S3
  • Amazon CloudFront
  • Amazon Route 53

Before diving in, it is advisable to have at least a cursory understanding of the different AWS services we will be using as well as a general idea of the architecture. The following gives an overview of how each Amazon service will be used to achieve our goal.

Amazon S3

In the most trivial case, to host a static website, you configure an Amazon S3 bucket for website hosting, and then upload your website content to the bucket. The website is then available at the AWS region-specific website endpoint of the bucket. For example:

<bucket-name>.s3-website-<region>.amazonaws.com

or

<bucket-name>.s3-website.<region>.amazonaws.com

Taking this example further, say you create a bucket called my-bucket in the US West (Oregon) Region, and configure it as a website. The following example URLs provide access to your website content:

This URL returns a default index document that you configured for the website.

http://my-bucket.s3-website-us-west-2.amazonaws.com

This URL requests the photo.jpg object stored at the root level of the bucket.

http://my-bucket.s3-website-us-east-1.amazonaws.com/photo.jpg

This URL requests the docs/doc1.html object in your bucket.

http://my-bucket.s3-website-us-east-1.amazonaws.com/docs/doc1.html

In addition, you can use your own domain, such as example.com to serve your content using Amazon S3 with Amazon Route 53.

Amazon CloudFront

Amazon CloudFront is a web service that speeds up distribution of your static web content. CloudFront delivers your content through a worldwide network of data centers called edge locations. When a user requests content that you’re serving with CloudFront, the user is routed to the edge location that provides the lowest latency (time delay), so that content is delivered with the best possible performance.

Amazon Route 53

You can use Amazon Route 53 to help you get your website or web application up and running with a custom domain. Route 53 performs three main functions:

  1. Domain name registration.
  2. Routing traffic to the resources for your domain.
  3. Checking the health of your resources.

Amazon Route 53 can be used to fulfill all three functions, however in this case we will only need to register a domain name, then configure Route 53 to route traffic for this domain.

Setup

The following will provide a painfully detailed walk-through for configuring AWS. It provides both the manual process (via the AWS Management Console) and through the use of the AWS Command Line Interface. It is wise to have the AWS CLI Command Reference handy when executing the commands, so you know what everything is doing. When using the AWS CLI, the goal is to replicate the configuration that we obtained using the AWS Management Console.

To simplify the AWS CLI commands, we will use environment variables set using a shell script. This script as well as the JSON files used to configure the various AWS services can be found in the GitHub repository for this website under .aws.2

Step 0: Prerequisites

  1. Sign up for AWS.

  2. Install awscli:

    pip install --upgrade awscli
    

NOTE: You will need to configure the AWS CLI. To do so, consult the AWS CLI documentation.

Step 1: Create S3 buckets

Overview

  1. Create buckets.
  2. Configure buckets.

AWS Management Console

  1. Go to https://console.aws.amazon.com/s3/.
  2. Choose Create bucket.

S3 Bucket for Log Files

  1. Enter a Bucket name and Region.

    Example: <domain>-logs

  2. Under Manage system permissions, select Grant Amazon S3 Log Delivery group write access to this bucket.

  3. Once the bucket is created, click on the bucket, select Overview, then select Create folder, and name the folder cdn/. This will be the location of log files from Amazon CloudFront.

S3 Bucket for the Root Domain

  1. Enter a Bucket name and Region.

    Example: <domain>-root

  2. Under Server access logging, select Enable logging then choose the S3 bucket for log files you just created for the Target bucket and cdn/ for the Target prefix.

  3. Once the bucket is created, click on the bucket, select Properties, then Static website hosting, then Use this bucket to host a website, enter index.html and 404.html for the Index document and Error document respectively. Take note of the endpoint URL — we will use this when configuring the CloudFront distribution.

  4. When you configure a bucket as a website, you must make the objects that you want to serve publicly readable. To do this, you write a bucket policy that grants everyone s3:GetObject permission. To do this, click on the bucket, select Permissions, then Bucket Policy, then paste the following:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "PublicReadGetObject",
          "Effect": "Allow",
          "Principal": "*",
          "Action": [
            "s3:GetObject"
          ],
          "Resource": [
            "arn:aws:s3:::example-bucket-root/*"
          ]
        }
      ]
    }
    

    NOTE: Change example-bucket-root to the name of your root bucket.

AWS CLI

S3 Bucket for Log Files

  1. Create the S3 bucket for log files:

    aws s3 mb s3://$S3_BUCKET_LOGS --region $REGION
    
  2. Create the cdn/ folder:

    aws s3api put-object --bucket $S3_BUCKET_LOGS --key cdn/
    
  3. Enable Log Delivery:

    aws s3api put-bucket-acl --bucket $S3_BUCKET_LOGS \
    --grant-full-control id=$CANONICAL_USER_ID \
    --grant-read 'URI="http://acs.amazonaws.com/groups/s3/LogDelivery"' \
    --grant-write 'URI="http://acs.amazonaws.com/groups/s3/LogDelivery"' \
    --grant-read-acp 'URI="http://acs.amazonaws.com/groups/s3/LogDelivery"'
    

    NOTE: You cannot use an email address to specify a grantee for any AWS Region that was created after 12/8/2014. The following Regions were created after 12/8/2014: US East (Ohio), Canada (Central), Asia Pacific (Mumbai), Asia Pacific (Seoul), EU (Frankfurt), EU (London), EU (Paris), China (Beijing), China (Ningxia), and AWS GovCloud (US). Instead, pass id with the canonical user ID to the --grant-full-control flag.

S3 Bucket for the Root Domain

  1. Create the S3 bucket for the root domain:

    aws s3 mb s3://$S3_BUCKET_ROOT --region $REGION
    
  2. Enable logging:

    aws s3api put-bucket-logging \
    --bucket $S3_BUCKET_ROOT \
    --bucket-logging-status file://$S3_LOGGING_POLICY
    

    s3_logging_policy.json

    {
      "LoggingEnabled": {
        "TargetBucket": "example-bucket-logs",
        "TargetPrefix": "cdn/"
      }
    }
    

    NOTE: Change example-bucket-logs to the name of your log bucket.

  3. Enable static website hosting:

    aws s3api put-bucket-website \
    --bucket $S3_BUCKET_ROOT \
    --website-configuration file://$S3_WEBSITE_CONFIG
    

    s3_website_config.json

    {
      "IndexDocument": {
        "Suffix": "index.html"
      },
      "ErrorDocument": {
        "Key": "404.html"
      }
    }
    
  4. Apply bucket policy:

    aws s3api put-bucket-policy \
    --bucket $S3_BUCKET_ROOT \
    --policy file://$S3_BUCKET_POLICY
    

    s3_bucket_policy.json

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "PublicReadGetObject",
          "Effect": "Allow",
          "Principal": "*",
          "Action": [
            "s3:GetObject"
          ],
          "Resource": [
            "arn:aws:s3:::example-bucket-root/*"
          ]
        }
      ]
    }
    

    NOTE: Change example-bucket-root to the name of your root bucket.

Step 2: Configure Amazon CloudFront

Overview

  1. Request a SSL/TLS certificate.
  2. Create a CloudFront Distribution.

AWS Management Console

Request a SSL/TLS certificate

WARNING: You must be in region us-east-1 in order to successfully use SSL/TLS certificates. Change your region in the AWS Management Console by selecting US East (N. Virginia) from the drop-down in the upper right corner.

  1. Go to https://console.aws.amazon.com/acm.

  2. Click Get started.

  3. In the Add domain names section add the FQDN for your domain name. Click Add another name to this certificate and add the www subdomain.

    Example:

    example.com
    www.example.com
    
  4. Click Next.

  5. In the Select validation method section, choose DNS validation.

  6. Click Review.

  7. Click Confirm and request.

  8. For each domain, click the carrot, then Create record in Route 53. This will automatically create the record used for DNS validation.

Create a CloudFront Distribution

  1. Go to https://console.aws.amazon.com/cloudfront.
  2. Click Create Distribution.
  3. In the Select a delivery method for your content section, choose Get Started under Web.
  4. Fill out the Create Distribution form (see below).
  5. Click Create Distribution.

Origin Settings

Origin Domain Nameexample-bucket-root.s3-website.<region>.amazonaws.com
Origin PathN/A
Origin IDS3-example-bucket-root
Restrict Bucket AccessNo
Origin Custom HeadersN/A

Default Cache Behavior Settings

Path PatternDefault (\*)
Viewer Protocol PolicyRedirect HTTP to HTTPS
Allowed HTTP MethodsGET, HEAD
Field-level Encryption ConfigN/A
Cached HTTP MethodsGET, HEAD (Cached by default)
Cache Based on Selected Request HeadersNone (Improves Caching)
Object CachingUse Origin Cache Headers
Forward CookiesNone (Improves Caching)
Query String Forwarding and CachingNone (Improves Caching)
Smooth StreamingNo
Restrict Viewer AccessNo
Compress Objects AutomaticallyNo
Lambda Function AssociationsN/A

Distribution Settings

Price ClassUse All Edge Locations (Best Performance)
AWS WAF Web ACLNone
Alternate Domain Namesexample.com, www.example.com
SSL CertificateCustom SSL Certificate
Custom SSL Client SupportOnly Clients that Support SNI
Security PolicyTLSv1.1_2016 (recommended)
Supported HTTP VersionsHTTP/2, HTTP/1.1, HTTP/1.0
Default Root Objectindex.html
LoggingOn
Bucket for Logsexample-bucket-logs.s3.amazonaws.com
Log Prefixcdn/
Cookie LoggingOff
Enable IPv6True
CommentN/A
Distribution StateEnabled

NOTE: Select the SSL certificate created for this domain.

NOTE: To get the default index.html/404.html page functionality, you will need to use the S3 static website URL. If you do not use the S3 static website URL, but instead use the S3 REST endpoint (example-bucket-root.s3.amazonaws.com), you will not get this functionality. CloudFront provides default root object support, such that example.com will return index.html, but this will not work for any subdirectories (e.g., example.com/blog). The solution is to simply use the S3 static website URL and create a custom origin as opposed to a S3 Origin.

AWS CLI

Request a SSL/TLS certificate

WARNING: You must use region us-east-1 in order to successfully use SSL/TLS certificates. Change your region in the AWS CLI or append the --region us-east-1 option when requesting a ticket.

  1. Request a certificate:

    aws acm request-certificate \
    --domain-name $DOMAIN_NAME \
    --validation-method DNS \
    --subject-alternative-names www.$DOMAIN_NAME \
    --idempotency-token 1337
    

    NOTE: The returned CertificateArn must be used for the following commands.

  2. Use DNS to validate domain ownership:

    aws acm describe-certificate --certificate-arn $CF_CERTIFICATE_ARN
    

    NOTE: DNS validation involves creating two CNAME records using the given Name and Value.

    aws route53 list-hosted-zones-by-name --dns-name $DOMAIN_NAME
    

    NOTE: This assumes that you already have a registered domain with Amazon. If you do not, go to Step 3: Configure Amazon Route 53 and register a domain before creating the records.

    aws route53 change-resource-record-sets \
    --hosted-zone-id $R53_HOSTED_ZONE_ID \
    --change-batch file://$CF_DNS_VALIDATION
    

    cf_dns_validation.json

    {
      "Changes": [
        {
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [
              {
                "Value": ""
              }
            ]
          }
        },
        {
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [
              {
                "Value": ""
              }
            ]
          }
        }
      ]
    }
    
    aws acm list-certificates --certificate-statuses ISSUED
    

Create a CloudFront Distribution

  1. Create a new CloudFront distribution:

    aws cloudfront create-distribution \
    --distribution-config file://$CF_DISTRIBUTION
    

    cf_distribution.json

    {
      "CallerReference": "example.com",
      "Aliases": {
        "Quantity": 2,
        "Items": [
          "www.example.com",
          "example.com"
        ]
      },
      "DefaultRootObject": "index.html",
      "Origins": {
        "Quantity": 1,
        "Items": [
          {
            "Id": "S3-example-root",
            "DomainName": "example-bucket-root.s3-website.<region>.amazonaws.com",
            "OriginPath": "",
            "CustomHeaders": {
              "Quantity": 0
            },
            "CustomOriginConfig": {
              "HTTPPort": 80,
              "HTTPSPort": 443,
              "OriginProtocolPolicy": "https-only",
              "OriginSslProtocols": {
                "Quantity": 3,
                "Items": [
                    "TLSv1",
                    "TLSv1.1",
                    "TLSv1.2"
                ]
              },
              "OriginReadTimeout": 30,
              "OriginKeepaliveTimeout": 5
            }
          }
        ]
      },
      "DefaultCacheBehavior": {
        "TargetOriginId": "S3-example-root",
        "ForwardedValues": {
          "QueryString": false,
          "Cookies": {
            "Forward": "none"
          }
        },
        "TrustedSigners": {
          "Enabled": false,
          "Quantity": 0
        },
        "ViewerProtocolPolicy": "redirect-to-https",
        "MinTTL": 0,
        "AllowedMethods": {
          "Quantity": 2,
          "Items": [
            "GET",
            "HEAD"
          ],
          "CachedMethods": {
            "Quantity": 2,
            "Items": [
              "GET",
              "HEAD"
            ]
          }
        },
        "SmoothStreaming": false,
        "DefaultTTL": 86400,
        "MaxTTL": 31536000,
        "Compress": true
      },
      "CustomErrorResponses": {
        "Quantity": 1,
        "Items": [
          {
            "ErrorCode": 404,
            "ResponsePagePath": "/404.html",
            "ResponseCode": "404",
            "ErrorCachingMinTTL": 60
          }
        ]
      },
      "Comment": "",
      "Logging": {
        "Enabled": true,
        "IncludeCookies": false,
        "Bucket": "example-logs.s3.amazonaws.com",
        "Prefix": "cdn/"
      },
      "PriceClass": "PriceClass_All",
      "Enabled": true,
      "ViewerCertificate": {
        "ACMCertificateArn": "",
        "SSLSupportMethod": "sni-only",
        "MinimumProtocolVersion": "TLSv1.1_2016",
        "Certificate": "",
        "CertificateSource": "acm"
      },
      "Restrictions": {
        "GeoRestriction": {
          "RestrictionType": "none",
          "Quantity": 0
        }
      },
      "HttpVersion": "http2",
      "IsIPV6Enabled": true
    }
    

    NOTE: Change example.com to the name of your domain, change example-bucket-root to the name of your root bucket, and finally, change ACMCertificateArn and Certificate to the arn of your newly created certificate.

    NOTE: To get the default index.html/404.html page functionality, you will need to use the S3 static website URL. If you do not use the S3 static website URL, but instead use the S3 REST endpoint (example-bucket-root.s3.amazonaws.com), you will not get this functionality. CloudFront provides default root object support, such that example.com will return index.html, but this will not work for any subdirectories (ex. example.com/blog). The solution is to simply use the S3 static website URL and create a custom origin as opposed to a S3 Origin.

  2. Determine the CloudFront domain name:

    aws cloudfront list-distributions \
    --query 'DistributionList.Items[].
    {Id:Id,DomainName:DomainName,Aliases:Aliases.Items[]}'
    

Step 3: Configure Amazon Route 53

Overview

  1. Register a domain name
  2. Route traffic to the resources for your domain

AWS Management Console

Register a domain name

  1. Go to https://console.aws.amazon.com/route53.
  2. Choose Registered domains in the navigation pane, then Register Domain.
  3. Enter your domain and select a Top-level Domain (TLD), then select Check.
  4. If the domain is available, select Add to cart, then Continue.
  5. Enter the registrant contact information, then Continue.
  6. Check the box for I have read and agree to the AWS Domain Name Registration Agreement, then Complete Purchase.

Route traffic to the resources for your domain

  1. Go to https://console.aws.amazon.com/route53.

  2. Choose Hosted zones in the navigation pane.

    NOTE: If you registered your domain with Amazon, a hosted zone will have been automatically created with the name of your domain. A hosted zone contains information about how you want Route 53 to route traffic for the domain.

  3. Choose the hosted zone for your domain.

  4. Click Go to Record Sets.

  5. Click Create Record Set.

  6. Specify the following values (see below).

  7. Repeat steps 5 and 6 for your www subdomain.

Create Record Set

Name-
TypeA - IPv4 address
AliasYes
Alias TargetCloudFront distribution domain
Routing PolicySimple
Evaluate Target HealthNo

AWS CLI

  1. Check your domain’s availability:

    aws route53domains check-domain-availability --domain-name $DOMAIN_NAME
    
  2. Register the domain name:

    aws route53domains register-domain \
    --domain-name $DOMAIN_NAME \
    --duration-in-years $R53_DOMAIN_DURATION \
    --admin-contact file://$R53_CONTACT_INFO \
    --registrant-contact file://$R53_CONTACT_INFO \
    --tech-contact file://$R53_CONTACT_INFO
    

    NOTE: The Amazon Route 53 API can only be used in the us-east-1 region.

    r53_contact_info.json

    {
      "FirstName": "",
      "LastName": "",
      "ContactType": "PERSON",
      "AddressLine1": "",
      "City": "",
      "State": "",
      "CountryCode": "",
      "ZipCode": "",
      "PhoneNumber": "",
      "Email": ""
    }
    

    NOTE: State must be a valid state abbreviation. Ex. NY, CA, OH, etc.
    NOTE: PhoneNumber must be of the form +999.12345678, where 999 is the country code.

Configure Route 53 to point to the new CloudFront distribution

  1. Determine the hosted zone for the domain:

    aws route53 list-hosted-zones
    
  2. Create A records:

    aws route53 change-resource-record-sets \
    --hosted-zone-id $R53_HOSTED_ZONE_ID \
    --change-batch file://$R53_DNS_CONFIG
    

    r53_dns_config.json

    {
      "Changes": [
        {
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "example.com.",
            "Type": "A",
            "AliasTarget": {
              "HostedZoneId": "Z2FDTNDATAQYW2",
              "DNSName": "",
              "EvaluateTargetHealth": false
            }
          }
        },
        {
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "www.example.com.",
            "Type": "A",
            "AliasTarget": {
              "HostedZoneId": "Z2FDTNDATAQYW2",
              "DNSName": "",
              "EvaluateTargetHealth": false
            }
          }
        }
      ]
    }
    

    NOTE: Change example.com to the name of your domain. Change DNSName to the domain name of your newly created CloudFront Distribution.

    NOTE: The HostZoneId must be set to Z2FDTNDATAQYW2 for CloudFront distributions. Alias resource record sets for CloudFront cannot be created in a private zone.

Step 4: Deploying your website

AWS Management Console

Upload your website

  1. Go to https://console.aws.amazon.com/s3/.
  2. Select Upload.
  3. Select the contents of public/ and click Next.
  4. Under Manage public permissions, select Grant public read access to this object(s) and click Next.
  5. Under Storage class, choose Standard and Encryption None and click Next.
  6. Click Upload.

Invalidate the CloudFront cache

  1. Go to https://console.aws.amazon.com/cloudfront.
  2. Check your distribution and click Distribution Settings.
  3. Click the Invalidations tab, then click Create Invalidation.
  4. Enter /* and click Invalidate.

AWS CLI

  1. Upload your website:

    aws s3 sync --acl "public-read" public/ s3://$S3_BUCKET_ROOT
    
  2. Invalidate the CloudFront cache:

    aws cloudfront create-invalidation \
    --distribution-id $CF_DISTRIBUTION_ID --paths "/*"
    

    NOTE: If you specify a path that includes a * (wildcard), you must use quotes (") around the path. For example, if you wish to invalidate all paths, you would use "/*".

    To remove all files or objects in the root bucket, do the following:

    aws s3 rm s3://$S3_BUCKET_ROOT --recursive
    

FAQ

Question
Why does my Amazon S3 bucket need to be public?

Answer
In order for your customers to access content at the website endpoint, you must make all your content publicly readable. To do so, you can use a bucket policy or an ACL on an object to grant the necessary permissions.

Question
Do I need two buckets in order to host my static website on a root and www subdomain?

Answer
The short answer is not necessarily. Most guides that you will come across, including those found in the official Amazon documentation, instruct the user to create an S3 bucket for both the root and www subdomain. This is an entirely legitimate solution, however, when using Amazon CloudFront to distribute content, this becomes unnecessary.

Question Why am I unable to see all the AWS resources I created, whether they were created through the Amazon Management Console or using the AWS CLI?

Answer
Given that not all services are available in all regions and the region specified in your AWS configuration (~/.aws/config) dictates the region from which resources are requested you may not be able to access, or use for that matter, all your resources. I have found that defaulting to us-east-1 solved many of the problems I encountered when creating and using resources generated via the AWS CLI.


  1. I am currently using a different theme: Paperwhite↩︎

  2. In the years since I first wrote this article, my methods for hosting static websites have evolved greatly. See these other articles for a full history: Hosting a Static Website with Hugo and CloudFormation and Hosting a Static Website with Hugo and Terraform↩︎

Last updated: