Hosting Hugo on AWS with S3 and CloudFront
Introduction
Hugo is a static site generator that is a great tool for creating blogs, documentation, and other types of websites. This site is also built with Hugo and hosted on AWS using S3 and CloudFront. It is a simple, fast, flexible way to host a static site. I didn’t find a good guide on how to host a Hugo site on AWS in their documentation, so I decided to write one.
In this article, we will learn how to build and deploy a Hugo site on newly created AWS S3 bucket and CloudFront distribution using AWS CDK.
Prerequisites
Step 1: Create a new Hugo site
I assume that that you already have a Hugo site created. If not, let’s create a dummy one for this example:
hugo new site mysite
cd mysite
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
echo "theme = 'ananke'" >> hugo.toml
hugo server
Follow the instructions on the Hugo website for more details
Step 2: Build the Hugo site
Build the Hugo site by running the following command:
cd mysite
hugo --minify --environment production
This will create a public
folder with the static site files. This is the folder we will deploy to S3.
Step 3: Use AWS CDK to create infrastructure and deploy the site
I have created a simple CDK project in C# that will create an S3 bucket and CloudFront distribution together with all the necessary permissions, roles and configurations. Here is what we are going to create:
- S3 bucket for the static site files
- CloudFront distribution to serve the site
- CloudFront Function on Viewer Request event to:
- Rewrite URLs to support Hugo’s pretty URLs (e.g.
/about/
instead of/about/index.html
) - Redirect
www
tonon-www
URLs - Redirect non-trailing slash URLs to trailing slash URLs for consistency (e.g.
/about
to/about/
)
- Rewrite URLs to support Hugo’s pretty URLs (e.g.
Let’s take a look at the CDK code:
using Amazon.CDK;
using Amazon.CDK.AWS.CertificateManager;
using Amazon.CDK.AWS.CloudFront;
using Amazon.CDK.AWS.CloudFront.Origins;
using Amazon.CDK.AWS.IAM;
using Amazon.CDK.AWS.S3;
using Amazon.CDK.AWS.S3.Deployment;
using Constructs;
using System.Collections.Generic;
using static Amazon.CDK.AWS.CloudFront.CfnDistribution;
using static Amazon.CDK.AWS.CloudFront.CfnOriginAccessControl;
namespace Cdk
{
public class HugoCdkStack : Stack
{
internal HugoCdkStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
{
var bucketNameParam = new CfnParameter(this, "BucketName", new CfnParameterProps
{
Type = "String",
Description = "The name of the bucket to host the HUGO website",
Default = "fastfoodcoding-hugo"
});
// uncomment the following lines to enable custom domain and ACM certificate
//var domainNameParam = new CfnParameter(this, "DomainName", new CfnParameterProps
//{
// Type = "String",
// Description = "The domain name to use for the CloudFront distribution. Default is null",
//});
//var certificateArnParam = new CfnParameter(this, "CertificateArn", new CfnParameterProps
//{
// Type = "String",
// Description = "The ARN of the ACM certificate to use for the CloudFront distribution. Default is null",
//});
var s3Bucket = new Bucket(this, "HugoSiteBucket", new BucketProps
{
BucketName = bucketNameParam.ValueAsString,
RemovalPolicy = RemovalPolicy.DESTROY,
BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
});
var cfnOriginAccessControl = new CfnOriginAccessControl(this, "OriginAccessControl", new CfnOriginAccessControlProps
{
OriginAccessControlConfig = new OriginAccessControlConfigProperty
{
Name = "HugoSiteBucket-OriginAccessControl",
OriginAccessControlOriginType = "s3",
SigningBehavior = "always",
SigningProtocol = "sigv4"
}
});
// Hugo uses pretty URLs instead of index.html for every path
// S3 or CloudFront does not support this out of the box
// Let's use a CloudFront lightweight function on Viewer Request event to achieve that
// As a bonus, for consistency we can redirect www to non-www and fix trailing slashes in URLs
var redirectFunction = new Function(this, "RedirectFunction", new FunctionProps
{
FunctionName = "HugoSiteViewerRequestFunction",
Runtime = FunctionRuntime.JS_2_0,
Comment = "Redirect to index.html if the request is for a directory",
Code = FunctionCode.FromInline(GetFunctionCode()),
});
var cfnDistribution = new Distribution(this, "HugoSiteDistribution", new DistributionProps
{
DefaultRootObject = "index.html",
DefaultBehavior = new BehaviorOptions
{
Origin = new S3OacOrigin(s3Bucket, new S3OriginProps
{
OriginAccessIdentity = null,
ConnectionAttempts = 3,
ConnectionTimeout = Duration.Seconds(10)
}),
CachedMethods = CachedMethods.CACHE_GET_HEAD,
CachePolicy = CachePolicy.CACHING_OPTIMIZED,
ViewerProtocolPolicy = ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
FunctionAssociations = new[]
{
new FunctionAssociation
{
EventType = FunctionEventType.VIEWER_REQUEST,
Function = redirectFunction
}
}
},
// uncomment the following lines to enable custom domain and ACM certificate
//DomainNames = bucketNameParam.ValueAsString,
//Certificate = Certificate.FromCertificateArn(this, "Certificate", certificateArnParam.ValueAsString)
});
s3Bucket.AddToResourcePolicy(new PolicyStatement(new PolicyStatementProps
{
Actions = ["s3:GetObject"],
Principals = [new ServicePrincipal("cloudfront.amazonaws.com")],
Effect = Effect.ALLOW,
Resources = [s3Bucket.ArnForObjects("*")],
Conditions = new Dictionary<string, object>
{
["StringEquals"] = new Dictionary<string, object>
{
["AWS:SourceArn"] = $"arn:aws:cloudfront::{this.Account}:distribution/{cfnDistribution.DistributionId}"
}
}
}));
// workaround using the L1 construct to attach the OriginAccessControl to the CloudFront Distribution
var l1CfnDistribution = cfnDistribution.Node.DefaultChild as CfnDistribution;
l1CfnDistribution.AddPropertyOverride("DistributionConfig.Origins.0.OriginAccessControlId", cfnOriginAccessControl.AttrId);
// sync our Hugo website public/ folder with the S3 bucket
// don't forget to run "hugo" before deploying the CDK stack
var _ = new BucketDeployment(this, "DeployWebSite", new BucketDeploymentProps
{
// sync the contents of the public/ folder with the S3 bucket
Sources = [Source.Asset("../mysite/public")],
DestinationBucket = s3Bucket,
// invalidate the cache on the CloudFront distribution when the website is updated
Distribution = cfnDistribution,
DistributionPaths = ["/*"],
});
}
private static string GetFunctionCode()
{
return @"
async function handler(event) {
const request = event.request;
const host = request.headers.host.value;
const uri = request.uri;
if (!request.uri.endsWith('/') && !request.uri.includes('.')) {
request.uri += '/';
return redirectResponse('https://' + host + request.uri);
}
if (host.startsWith('www.')) {
return redirectResponse('https://' + host.replace('www.', '') + request.uri);
}
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
return request;
}
function redirectResponse(newurl) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: { 'location': { 'value': newurl } }
};
}";
}
}
}
public class S3OacOrigin : OriginBase
{
public S3OacOrigin(IBucket bucket, IOriginProps props = null) : base(bucket.BucketRegionalDomainName, props)
{
}
// workaround to avoid the "OriginAccessIdentity" property to be rendered in the CloudFormation template
protected override IS3OriginConfigProperty RenderS3OriginConfig()
{
return new S3OriginConfigProperty
{
OriginAccessIdentity = ""
};
}
}
Step 4: Deploy the CDK stack
To deploy the CDK stack, simply run the following commands:
cdk deploy --parameters BucketName=fastfoodcoding-hugo
Conclusion
That’s it! You have successfully deployed a Hugo site on AWS using S3 and CloudFront. You can now access your site using the CloudFront distribution URL. You can also use a custom domain and ACM certificate by uncommenting the respective lines in the CDK code. But this requires some additional setup in Route 53 and ACM.