【hugo】使用 AWS CloudFront(CDN)加速 hugo 博客

Posted by 西维蜀黍 on 2020-09-04, Last Modified on 2021-11-06

Achievement

Step by step guide to deploy your Hugo website to Amazon Web Services using:

Knowledge

AWS Route S3

Amazon S3 does not support HTTPS access to the website. If you want to use HTTPS, you can use Amazon CloudFront to serve a static website hosted on Amazon S3.

AWS CloudFront

Create a CloudFront Distribution(Set up Your CDN)

CDN for example.com

Using Cloudfront we use their CDN, enforce only access the bucket throught Cloudfront and HTTPS usage, and handle pretty URLs in combination with a Lambda Edge function.

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

  2. Click Create distribution

  3. In the Delivery method for your content page, select the Web/Get Started button

  4. Enter ONLY the following data:

    • in the Origin Settings section, for Origin Domain Name, enter the Amazon S3 website endpoint for your bucket, for example, example.com.s3-website.us-west-1.amazonaws.com.
    • Restrict Bucket Access: Select Yes; more options will appear
      • Origin Access Identity: Select or create an Access Identity
      • Grant Read Permissions on Bucket: Select Yes, Update Bucket Policy so Amazon automatically handle your S3 bucket permissions
    • In Default Cache Behavior Settings section:
      • *Viewer Protocol Policy: Select Redirect HTTP to HTTPS
      • Compress objects automatically: Select Yes to Compress Content when possible
    • In Distribution Settings section:
      • Leave Price Class set to Use All Edge Locations (Best Performance).
      • Alternate Domain Names (CNAMEs): enter example.com.
    • In Default Root Object: index.html.
    • SSL Certificate
      • Select Custom SSL Certificate (example.com)
        • And press the Request or Import a Certificate with ACM button.
          • You will be redirected to AWS Certificate Manager to create a new certificate, in this page add the two domain names:
            • example.com
            • www.example.com
            • Then click Next and validate your certificate.
      • After you have the certificate, go back to Cloudfront settings page and select your newly created certificate from the list at Distribution Settings/SSL Certificate/Custom SSL Certificate.
  5. Click Create distribution at the bottom of the page

  6. Make CloudFront able to access to your S3 bucket by going to the Origins and Origin Group tab, edit the existing origin, in Grant Read Permissions on Bucket select Yes, Update Bucket Policy, and save changes, it will automatically generate the following policy:

    {
        "Version": "2008-10-17",
        "Id": "PolicyForCloudFrontPrivateContent",
        "Statement": [
            {
                "Sid": "1",
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ..."
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::example.com/*"
            }
        ]
    }
    

Now when you visit the S3 Bucket Policy tab, it will be already generated with the values for your bucket.

This is a very important step or you will get a 404 error page when trying to access your site because CloudFront won’t be able to read your files, read more about it at Granting Permission to an Amazon CloudFront Origin Identity

CDN for www.example.com

Create another distribution for the www.example.com:

  1. Go to https://console.aws.amazon.com/cloudfront/
  2. Click Create distribution
  3. In the Delivery method for your content page, select the Web/Get Started button
  4. Enter ONLY the following data:
    • Origin Domain Name: Click inside the text input and a list of your buckets will appear, select www.example.com.s3.amazonaws.com.
    • In Default Cache Behavior Settings section:
      • *Viewer Protocol Policy: Select Redirect HTTP to HTTPS
    • In Distribution Settings section:
      • Alternate Domain Names (CNAMEs): enter www.example.com.
    • SSL Certificate:
      • Select Custom SSL Certificate (example.com)
        • Select your previously created certificate from the list at the Custom SSL Certificate input box.
  5. Click Create distribution at the bottom of the page

Set up DNS via AWS S3

Now that you have successfully created a CloudFront distribution, update the alias record in Route 53 to point to the new CloudFront distribution.

配置example.com

  1. Go to your hosted zones in Route53 console https://console.aws.amazon.com/route53/home?#hosted-zones:
  2. Click Create Hosted Zone button and enter example.com, point your domain to Route 53 DNS servers showed at Record Set NS type list.
  3. Click Create Record Set button.
  4. Leave Name empty so we are setting up example.com..
  5. Select Type: A - IPv4 address
  6. Select Alias: Yes
    • In Alias target, select the example.com Cloudfront distribution
  7. Click Save record set
  8. Update your DNS nameservers (set by your domain provider) to point to the new name servers.

即将在AWS Route 53中显示的 type 为 NS 的值(即,在你的域名商网站那里,你需要配置你的domain 的Nameservers为这些)

dig 一下,直到确认 DNS Record 被成功更新了

配置 www.example.com

Now we create another one for the www version:

  1. Click Create Hosted Zone button and enter example.com.
  2. Click Create Record Set button.
  3. Enter Name: www
  4. Select Type: A - IPv4 address
  5. Select Alias: Yes
    • In Alias target, select the www.example.com Cloudfront distribution.
  6. Click Save record set

Handle Simple URLs (such as www.example.com/about/ instead of www.example.com/about/index.html)

Background

Hugo by default generates web pages like <content-title>/index.html at its /public directory.

The default root object feature for CloudFront supports only the root of the origin that your distribution points to (such as http://www.example.com > http://www.example.com/index.html).

However, CloudFront does not return default root objects in subdirectories (such as http://www.example.com/about/). If you were to attempt to request this URL through CloudFront, CloudFront would do a S3 GetObject API call against a key that does not exist.

Of course, it is a bad user experience to expect users to always type index.html at the end of every URL (or even know that it should be there). Until now, there has not been an easy way to provide these simpler URLs (equivalent to the DirectoryIndex Directive in an Apache Web Server configuration) to users through CloudFront. Not if you still want to be able to restrict access to the S3 origin using an OAI. However, with the release of Lambda@Edge, you can use a JavaScript function running on the CloudFront edge nodes to look for these patterns and request the appropriate object key from the S3 origin.

To assign a default root object for your CloudFront distribution, be sure to upload the object to the origin that your distribution points to.

Docs at Why isn’t Amazon CloudFront returning my default root object from a subdirectory?

As we have our S3 bucket private, accessing a webpage like example.com/hello/ won’t request our example.com/hello/index.html. To handle this we use a Lambda@Edge function.

Solution

Solution 1 - Use a template Lambda@Edge Function

We use the function standard-redirects-for-cloudfront, to install it via the Serverless Application Repository:

  1. Go to AWS Serverless Application Repository

  2. Press the Deploy button to use the application standard-redirects-for-cloudfront.

  3. It opens a description of the app, hit Deploy again to finish deploying it.

  4. After it has been created, locate the button View CloudFormation stack or go directly to the Cloudformation Console

  5. In the Resources tab, locate the AWS::IAM::Role and open the Physical ID, it will open up the IAM console

  6. Go to Trust Relationship tab and choose Edit the trust relationship to allow CloudFront to execute this function as a Lambda@Edge function. (by adding "edgelambda.amazonaws.com"), set the policy to:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": [
              "lambda.amazonaws.com",
              "edgelambda.amazonaws.com"
            ]
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
    
  7. Go back to the Cloudformation’s Stack Detail page and in the Output tab, locate the key StandardRedirectsForCloudFrontVersionOutput and note down its Value (it will look something like: arn:aws:lambda:us-east-1:XXXXXXXXXXX:function:aws-serverless-repository-StandardRedirectsForClou-XXXXXXXXXXXX:2 ). We will use it in the next steps as this is the ARN (Amazon Resource Name) for the Lambda function that we will use in Cloudfront.

  8. Go back to the CloudFront console, select the example.com distribution

  9. Go to the Behaviour tab and edit the default Behavior.

  10. Now we use the Lambda function, in Lambda Function Association select Origin Request in Event Type and enter the Lambda function’s StandardRedirectsForCloudFrontVersionOutput ARN value from the previous step.

    Note

    If you want to use CloudFront Triggers for Lambda Functions, the region of your S3 bucket must be US East (N. Virginia) Region, as you can add CloudFront Triggers only for Lambda Functions in the US East (N. Virginia) Region(from https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html ).

    And if the region of the Lambda Functions is different from the region of the S3 bucket, we are not allowed to create that Lambda Function (shwo the error below).

  11. Wait for the CloudFront distribution to deploy.

Solution2 - Make it from scratch

In this example, you use the compute power at the CloudFront edge to inspect the request as it’s coming in from the client. Then re-write the request so that CloudFront requests a default index object (index.html in this case) for any request URI that ends in ‘/’.

When a request is made against a web server, the client specifies the object to obtain in the request. You can use this URI and apply a regular expression to it so that these URIs get resolved to a default index object before CloudFront requests the object from the origin. Use the following code:

'use strict';
exports.handler = (event, context, callback) => {
    
    // Extract the request from the CloudFront event that is sent to Lambda@Edge 
    var request = event.Records[0].cf.request;

    // Extract the URI from the request
    var olduri = request.uri;

    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');
    
    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);
    
    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;
    
    // Return to CloudFront
    return callback(null, request);

};

In the Lambda console, choose Create function. On the next screen, skip the blueprint selection and choose Author from scratch, as you’ll use the sample code provided.

Next, configure the trigger. Choosing the empty box shows a list of available triggers. Choose CloudFront and select your CloudFront distribution ID (created earlier). For this example, leave Cache Behavior as ***** and CloudFront Event as Origin Request. Select the Enable trigger and replicate box and choose Next.

如果遇到这个错误

Error page

If we try to access a URL that doesn’t exist on our S3 bucket, like https://example.com/not-existing-page we will get a 403 Forbidden error code because Cloudfront tries to access a object2 that doesn’t exists, so to properly handle this, we should return a 404 error response to the request.

Setting up the error page on S3 wouldn’t have any effect because this is an error that should be handled by Cloudfront.

To do this, we configure CloudFront to respond to requests using Hugo’s custom error page located at /layouts/404.html, when your origin returns an HTTP 403 permission denied.

  1. Go to Cloudfront console: https://console.aws.amazon.com/cloudfront
  2. Select your example.com distribution
  3. Choose the Error Pages tab.
  4. Press Create Custom Error Response button.
  • In HTTP Error Code, select: 403: Forbidden
  • Customize Error Response: Yes
  • Response Page Path: /404.html
  • HTTP Response Codeo: 404: Not Found

Reference