Skip to content


Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?


Failed to load latest commit information.
Latest commit message
Commit time

HTTPS static site with Hugo and Terraform

Source code for my post by the same name on using Hugo together with Terraform, S3, and CloudFront to host high-performance static websites.


As of early 2020, I couldn't find a single source that did everything I wanted when making a static site -- which meant pulling bits from a lot different sources. Some of the limitations I encountered were:

  • No Terraform state store
  • Broken redirects when using S3 origin for CloudFront
  • Missing public bucket policies, or do not disable the public access block
  • Not using Hugo's new deployment features
  • No HTTPS, or certificate in wrong region
  • No logs for buckets and distributions
  • Old Terraform syntax
  • Put credentials in Terraform vars, rather than aws configure
  • Requiring Route53 for DNS

This repo attempts to rectify that problem. The contents were created when building the static website for DexManus, a startup I am currently advising.

More background can be found in the blog post.


To checkout:

$ git clone
$ cd hugo-terraform-aws
$ git submodule update --init --recursive

Development container

The included development container includes all of the software needed to build and deploy the site. To build the development container:

$ docker-compose build

To run the development container:

$ docker-compose run --rm static-dev

The last command starts an instance of the container, and runs a shell within it. The Hugo site directory is mounted at ~/site, and all of the Terraform scripts are mounted at ~/infra.

If you don't have Docker installed, follow the official instructions for installing Docker and installing Docker Compose before running the steps above.


An example Hugo site is stored in /site; you may want to follow the Hugo quick start to create a new site instead.

Build website

To build and watch the site:

$ hugo serve

You will need to add --bind= to the above command if you are accessing from a machine different from the one Hugo is running on.

To build the site into static files:

$ hugo

The resulting static site will be placed in the generated public directory.

Site settings

Hugo needs to be configured to deploy to the AWS infrastructure you will set up below. Adjust site/config.toml with the S3 bucket URL and Cloudfront distribution ID from your Terraform deployment:

    name = "S3"
    URL = "s3://" # Terraform output 's3_bucket_url'
    cloudFrontDistributionID = "ABC12345" # Terraform output 'cf_distribution_id'

If you created a new Hugo site, copy the entire block above into your config.toml and adjust for your deployment.

Deploy website

You can use Hugo's built-in capability to upload the static site files to S3 and invalidate the CloudFront distribution that serves them. You do this whenever you want to update the content of your website.

To deploy, start by setting the AWS IAM user you use for publishing:

$ aws configure

Then clear the output directory, build, and deploy:

$ cd site
$ rm -rf public
$ hugo
$ hugo deploy --verbose --maxDeletes -1 --invalidateCDN --confirm

You should see the updated website on your CloudFront endpoint (Terraform output cf_website_endpoint) shortly after -- in my experience, CloudFront can take upward of 5 minutes to complete a deployment, but you'll often see the new version before it completes.


Before you deploy the website with Hugo for the first time, you'll need to set up the AWS infrastructure for the site. You only need to do this once, not on every site deploy. The resulting architecture is shown in the diagram below:

Site infrastructure diagram


You will need an AWS IAM user with permissions to manage S3, CloudFront, ACM, and IAM to create the site infrastructure.

Before executing the steps below, start by setting the AWS IAM user you use for infrastructure changes:

$ aws configure

Terraform state bucket

We use Terraform's S3 backend to store Terraform state in an S3 bucket. We must create the bucket before using it; we can do so using a separate Terraform project.

Edit infra/state/ to match your site, choose your region in infra/state/, then:

$ cd infra/state/
$ terraform init
$ terraform apply

Terraform will create your state bucket:

aws_s3_bucket.tf_state: Creating...
aws_s3_bucket.tf_state: Creation complete after 4s [id=example-static-site-state]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


s3_backend_bucket = example-static-site-state
s3_backend_key = global/s3/terraform.tfstate
s3_backend_region = us-east-1

Update the site Terraform configuration to use your new state bucket by copying the values output by Terraform in the previous step into infra/site/

terraform {
  backend "s3" {
    # Replace with your state bucket name and region
    bucket = "example-static-site-state"
    region = "ca-central-1"
    key    = "global/s3/terraform.tfstate"

Every resource created in the infra/site directory will now use the new state bucket.

Site infrastructure

Terraform files for the static site infrastructure are stored in infra/site. Edit infra/site/ to match your site and set your primary region in infra/site/ Then, initialize Terraform:

$ cd infra/site
$ terraform init

Once Terraform is initialized, create the ACM certificate only. You'll need to manually verify this using the AWS console before applying the rest of the infrastructure.

$ terraform apply

Once your certificate is show as Issued in the AWS console, you can apply the rest of the infrastructure with:

$ terraform apply

The output from Terraform contains all of the values you need to configure Hugo and the rest of your infrastructure:


cf_distribution_id = ABC12345
cf_website_endpoint =
iam_publish_access_key = AKIAxxxxxxx
iam_publish_secret_key = xxxxxxxxxxx
s3_bucket =
s3_redirect_endpoint =


Update your DNS nameserver so that:

  • Your root domain (either @ or the subdomain you are using) is a CNAME/ALIAS to the value Terraform gave in output cf_website_endpoint
  • Your www domain is a CNAME/ALIAS to the value Terraform gave in output s3_redirect_endpoint

Whether you use CNAME or ALIAS depends on whether your nameserver supports ALIAS or CNAME flattening. See the discussion in the blog post for more information.

More information

I used the following resources when creating this repository:


Example of a static site hosted on AWS with Terraform, and generated with Hugo







No releases published


No packages published