CloudFront is a great option to host Hugo websites. Previously I showed how to host a hugo blog in AWS App Runner but App Runner is shutting down.
CloudFront is a great option and provides a few benefits over App Runner
- More performant leveraging CDN
- Cheaper for serving static context
Here are steps we’ll go through
- Create S3 bucket to store Hugo static files
- Create CloudFront disribution
- CloudFront function to rewrite URLs
The CloudFormation template creates an S3 bucket and assoicated policy for a CloudFront distribution to access it (see further below for the CloudFront distribution).
Few items to note
- The bucket name must be unique across all S3, not just the account!
- Public access to the public is blocked
- The policy allows only CloudFront to access the bucket
1Resources:
2 SiteBucket:
3 Type: AWS::S3::Bucket
4 Properties:
5 BucketName: !Sub "${BucketNamePrefix}-${Environment}-SiteBucket"
6 AccessControl: Private
7 PublicAccessBlockConfiguration:
8 BlockPublicAcls: true
9 BlockPublicPolicy: true
10 IgnorePublicAcls: true
11 RestrictPublicBuckets: true
12 VersioningConfiguration:
13 Status: Suspended
14
15 SiteBucketPolicy:
16 Type: AWS::S3::BucketPolicy
17 Properties:
18 Bucket: !Ref SiteBucket
19 PolicyDocument:
20 Version: '2012-10-17'
21 Statement:
22 - Sid: AllowCloudFrontReadOnly
23 Effect: Allow
24 Principal:
25 Service: cloudfront.amazonaws.com
26 Action: s3:GetObject
27 Resource: !Sub "${SiteBucket.Arn}/*"
28 Condition:
29 StringEquals:
30 AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"
Next we’ll create the CloudFront distribution and the Origin Access Control to allow CloudFront to access files in the S3 bucket.
- The default cache and security headers policies are used
- The S3 bucket is set as origin for the distribution
1CloudFrontOAC:
2 Type: AWS::CloudFront::OriginAccessControl
3 Properties:
4 OriginAccessControlConfig:
5 Name: !Sub "${BucketNamePrefix}-${Environment}-oac"
6 Description: "OAC for S3 origin"
7 SigningProtocol: sigv4
8 SigningBehavior: always
9 OriginAccessControlOriginType: s3
10
11SiteCachePolicy:
12 Type: AWS::CloudFront::CachePolicy
13 Properties:
14 CachePolicyConfig:
15 Name: !Sub "${BucketNamePrefix}-${Environment}-cache-policy"
16 Comment: "Cache policy for site"
17 DefaultTTL: 86400
18 MinTTL: 0
19 MaxTTL: 31536000
20 ParametersInCacheKeyAndForwardedToOrigin:
21 EnableAcceptEncodingGzip: true
22 EnableAcceptEncodingBrotli: true
23 CookiesConfig:
24 CookieBehavior: none
25 HeadersConfig:
26 HeaderBehavior: none
27 QueryStringsConfig:
28 QueryStringBehavior: none
29
30
31 CloudFrontDistribution:
32 Type: AWS::CloudFront::Distribution
33 Properties:
34 DistributionConfig:
35 Enabled: true
36 Comment: !Sub "CloudFront distribution for ${DomainName} (${Environment})"
37 DefaultRootObject: index.html
38 Origins:
39 - Id: S3Origin
40 DomainName: !GetAtt SiteBucket.RegionalDomainName
41 S3OriginConfig: {}
42 OriginAccessControlId: !Ref CloudFrontOAC
43 DefaultCacheBehavior:
44 TargetOriginId: S3Origin
45 ViewerProtocolPolicy: redirect-to-https
46 AllowedMethods:
47 - GET
48 - HEAD
49 CachedMethods:
50 - GET
51 - HEAD
52 CachePolicyId: !Ref SiteCachePolicy
53 Compress: true
54 # Using AWS-managed Response Headers Policy (managed ID is region-specific).
55 # This is the managed security headers policy; hardcoded here because CloudFront
56 # does not provide a stable logical name across accounts/regions. Verify the
57 # policy ID exists in the target region before deploying.
58 ResponseHeadersPolicyId: "67f7725c-6f97-4210-82d7-5512b31e9d03"
59 FunctionAssociations:
60 - EventType: viewer-request
61 FunctionARN: !GetAtt TrailingSlashFunction.FunctionMetadata.FunctionARN
62 ViewerCertificate:
63 AcmCertificateArn: !Ref CertificateArn
64 SslSupportMethod: sni-only
65 MinimumProtocolVersion: TLSv1.2_2021
66 PriceClass: PriceClass_100
67 Aliases:
68 - !Ref DomainName
An ‘A’ record is created to host the distribution on a custom URL. This assumes a hosted zone exists as well as a certificate in the Certificate Manager.
1ApexRecord:
2 Type: AWS::Route53::RecordSet
3 Properties:
4 HostedZoneId: !Ref HostedZoneId
5 Name: !Ref DomainName
6 Type: A
7 AliasTarget:
8 DNSName: !GetAtt CloudFrontDistribution.DomainName
9 # Hardcode this id when creating an A record to a CloudFront distribution
10 HostedZoneId: Z2FDTNDATAQYW2
Hugo assumes index.html is served when using paths without an extension. The below viewer request CloudFront function redirects all paths without a period nor an extension to an index.html in the current path.
1TrailingSlashFunction:
2 Type: AWS::CloudFront::Function
3 Properties:
4 Name: !Sub "trailing-slash-fn-${Environment}"
5 AutoPublish: true
6 FunctionConfig:
7 Comment: "Rewrite URIs ending with / to /index.html for Hugo sites"
8 Runtime: cloudfront-js-1.0
9 FunctionCode: |
10 function handler(event) {
11 const request = event.request;
12 const uri = request.uri;
13
14 // If URI ends with '/', append index.html
15 if (uri.endsWith('/')) {
16 request.uri = uri + 'index.html';
17 return request;
18 }
19
20 // If URI has no file extension, treat as directory and serve /index.html
21 if (uri.indexOf('.') === -1) {
22 request.uri += '/index.html';
23 return request;
24 }
25
26 return request;
27 }
For next time, we’ll discuss deploying changes through CloudFormation and using shell scripts to help.