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 https://github.com/ndrarmstrong/hugo-terraform-aws.git
$ cd hugo-terraform-aws
$ git submodule update --init --recursive
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.
To build and watch the site:
$ hugo serve
You will need to add --bind=0.0.0.0
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.
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:
[deployment]
[[deployment.targets]]
name = "S3"
URL = "s3://static-example.nicholasarmstrong.com?region=ca-central-1" # 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.
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:
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
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/variables.tf
to match your site, choose your region in infra/state/terraform.tf
, 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.
Outputs:
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.tf
:
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.
Terraform files for the static site infrastructure are stored in infra/site
. Edit infra/site/variables.tf
to match
your site and set your primary region in infra/site/terraform.tf
. 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 -target=aws_acm_certificate.site
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:
Outputs:
cf_distribution_id = ABC12345
cf_website_endpoint = d1234567.cloudfront.net
iam_publish_access_key = AKIAxxxxxxx
iam_publish_secret_key = xxxxxxxxxxx
s3_bucket = static-example.nicholasarmstrong.com.s3.ca-central-1.amazonaws.com
s3_redirect_endpoint = www.static-example.nicholasarmstrong.com.s3-website.ca-central-1.amazonaws.com
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 outputcf_website_endpoint
- Your
www
domain is a CNAME/ALIAS to the value Terraform gave in outputs3_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.
I used the following resources when creating this repository:
- Guides
- AWS
- Terraform
- Hugo
- DNS