Deploy Multi-Language Static HTML on CDN

Vincent Delacourt
6 min readMar 21, 2019

With Serverless Framework and AWS CloudFront. Delete Cache on redeploying and force redirect according to browser language

Deploy to CloudFront with Serverless

With the rise of Single Page Application (SPA), the use of a Content Delivery Network (CDN) is more than recommended. It allows us to have fast and reliable static hosting with easy HTTPS set up around the world. I love CloudFront, but I was always frustrated with 2 things, that I can do with .htaccess or Nginx configuration:

  • When I deploy a new version, I want it accessible immediately (clean server and browser cache)
  • If browser it’s in French, I want to redirect the user to /fr/index.html(for example)

I think the Serverless Framework is the best way to share an AWS Infrastructure. It also helps to describe textually the AWS Resources. The best things for me it’s that I don’t have to details the creation of all the steps in a multitude of screenshots in this article. If you work with a team and deploy on multiple environments (test, staging, prod), you need to use something which relies on AWS CloudFormation like the Serverless Framework.

Long life to the Infrastructure as Code 😎

Requirements

You need to have an AWS account: https://aws.amazon.com/s/dm/optimization/server-side-test/free-tier/free_np/ (free tier is great to experiment)

You need to have Node.js v6.5.0 or later. Homebrew (mac) or Chocolatey (Windows) are great tools for that.

Install the Serverless Framework by following the instructions here: https://serverless.com/framework/docs/getting-started/

Most complicated of all is to Set-up your AWS Credentials as explained here or on this video

To deploy your files to the S3 Bucket your need to the AWS CLI installed https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html

If you are in a hurry and doesn't want to read the article, just clone the code and launch the script

$ git clone https://github.com/vdelacou/iblis-deploy-static
$ cd iblis-deploy-static
$ chmod +x deploy.sh
$ ./deploy.sh

You can replace the content of the src folder with your site (HTML, CSS, JS) and also rename the project in serverless.yml (don’t forget to change the lambda function languageRedirectCloudFront.js according to your need)

Set up the project

Create your working folder (name it as you wish)

$ mkdir iblis-deploy-static

Create a file name serverless.yml and copy on it

service:
name: iblis-deploy-static

provider:
name: aws
stage: dev
region: us-east-1

Create S3 Resources

We are now going to create the S3 Bucket to store our files

Add at the end of the file serverless.yml

...
resources:
Resources:
S3Bucket: # We use this name as Ref later
Type: AWS::S3::Bucket

If we want to see the name of our newly created Bucket we can add

...
resources:
Resources:
...
Outputs:
BucketSiteName:
Value:
Ref: S3Bucket

Test the deployment of our bucket

$ serverless deploy -v

If all is going well you should see at the end the name of your bucket:

Stack Outputs
BucketName: iblis-deploy-static-dev-s3bucket-xxxxxxxxxx

Create CloudFront Ressources

First, we need to create an origin access identity to allows CloudFront to use our Bucket. Add in the file serverless.yml

...
resources:
Resources:
...
CloudFrontOriginAccessIdentity: # We use this name as Ref later
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: CloudFront origin access identity

Then we create the CloudFront Distribution:

...
resources:
Resources:
...
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
DependsOn:
- S3Bucket
Properties:
DistributionConfig:
Enabled: "true"
Origins:
- Id: !Join ["-", [!Ref S3Bucket, "s3"]]
DomainName:
!Join [".", [!Ref S3Bucket, "s3.amazonaws.com"]]
S3OriginConfig:
OriginAccessIdentity:
!Join [
"/",
[
"origin-access-identity/cloudfront",
!Ref CloudFrontOriginAccessIdentity,
],
]
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
ViewerProtocolPolicy: redirect-to-https
TargetOriginId:
!Join [
"-",
[
!Ref S3Bucket,
"s3",
],
]
ForwardedValues:
QueryString: false
DefaultRootObject: index.html
CustomErrorResponses: # For Single Page App
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html

The last thing to do is add a policy to our bucket to let our CloudFront Distribution to access it (please note that our bucket is not public)

...
resources:
Resources:
...
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: { Ref: S3Bucket }
PolicyDocument:
Statement:
- Action:
- "s3:GetObject"
Effect: Allow
Resource:
{
"Fn::Join":
[
"",
[
"arn:aws:s3:::",
{ Ref: S3Bucket},
"/*",
],
],
}
Principal:
AWS:
{
"Fn::Join":
[
" ",
[
"arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity",
{ Ref: CloudFrontOriginAccessIdentity},
],
],
}

If we want to see the address and the id of our CloudFront we can add

...
resources:
Resources:
...
Outputs:
...
CloudFrontDistributionId:
Value:
Ref: CloudFrontDistribution
CloudFrontDistributionDomain:
Value:
"Fn::GetAtt": [CloudFrontDistribution, DomainName]

Then we can deploy our CloudFront to be sure all is set up correctly:

$ serverless deploy -v

If all is going well you should see at the end the URL of your Distribution:

Stack Outputs
BucketName: iblis-deploy-static-dev-s3bucket-xxxxxxxxxx
CloudFrontDistributionDomain: xxxxxxxxxx.cloudfront.net

Copy your files to the bucket and see your site

You can use your HTML, CSS, and JS.

For this article, I will use the BootstrapMade template NewBiz. So I download the last source code https://bootstrapmade.com/newbiz-bootstrap-business-template/?download_theme=newbiz.zip and copy the files in /src in my working folder.

Now we use the AWS CLI to copy our files into our bucket. Replace the #BucketName# with the name of your bucket created in the previous step.

$ severless info -v (to get the name again)
$ aws s3 sync src/ s3://#BucketName#

So now just go to the CloudFront URL to see your site https://#CloudFrontDistributionDomain# (Replace the #CloudFrontDistributionDomain# with the domain name of the CloudFront created in the previous step)

If you want to use your own domain with HTTPS, it’s quite easy, this article has simple instructions https://deliciousbrains.com/wp-offload-media/doc/custom-domain-https-cloudfront/

Refresh CDN and Browser cache after a deployment

If you change your files and upload it again, you will see that your website is not updated. There are two reasons for that: you need to invalidate the objects from CloudFront edge caches and you need to ask the client browser to refresh its cache.

To force the browser to not use cache for updated files we can launch:

$ severless info -v (to get the name again)
$ aws s3 sync src/ s3://#BucketName# --delete --cache-control max-age=31536000,public
$ aws s3 cp s3://#BucketName#/index.html s3://#BucketName#/index.html --metadata-directive REPLACE --cache-control max-age=0,no-cache,no-store,must-revalidate --content-type text/html

As we asked CloudFront to “Use Origin Cache Header”, it will use the Cache Header from our S3 object. So first when we copy the files, we add a cache for one year. Then for the files, we want to refresh we replace the cache and add the tag must-revalidate . If you use service-worker, I recommend you to also refresh the file service-worker.js

To create an invalidation for the CloudFront, after copy your files you need to launch the following instructions: (Replace #CloudFrontDistributionId# with the name of your bucket created in the previous step)

$ severless info -v (to get the name again)
$ aws configure set preview.cloudfront true
$ aws cloudfront create-invalidation --distribution-id #CloudFrontDistributionId# --paths "/*"

You can go to the CloudFront URL to see your site https://#CloudFrontDistributionDomain# and see if your website has been updated.

Create a Cloud Edge Function to redirect the user according to the browser language

First, we can create a fr folder inside our /src folder and add a index.html file with French content

We need to create a NodeJS function to redirect our user according to browser language to the correct path

As Serverless Framework doesn’t allow us associating a Lambda function with a CloudFront, we use the plugin serverless-plugin-cloudfront-lambda-edge

$ npm init private
$ npm install --save-dev --save-exact @silvermine/serverless-plugin-cloudfront-lambda-edge

Create the function in the file languageRedirectCloudFront.js in root folder

exports.handler = (event, _context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// only if we are at domain root
if (request.uri == "/") {
if (typeof headers["accept-language"] !== "undefined") {
const supportedLanguages = headers["accept-language"][0].value;
console.log("Supported languages:", supportedLanguages);
if (supportedLanguages.startsWith("en")) {
callback(null, redirect("/index.html"));
} else if (supportedLanguages.startsWith("fr")) {
callback(null, redirect("/fr/index.html"));
} else {
callback(null, redirect("/index.html"));
}
} else {
callback(null, redirect("/index.html"));
}
} else {
callback(null, request);
}
};
function redirect(to) {
return {
status: "301",
statusDescription: "redirect to browser language",
headers: {
location: [{ key: "Location", value: to }]
}
};
}

Add in the file serverless.yml

...
provider:
name: aws
stage: dev
region: us-east-1

plugins:
- "@silvermine/serverless-plugin-cloudfront-lambda-edge"

package:
exclude:
- "node_modules/**"
- "src/**"

functions:
languageRedirectCloudFront:
handler: languageRedirectCloudFront.handler
runtime: nodejs8.10
memorySize: 128
timeout: 5
lambdaAtEdge:
distribution: "CloudFrontDistribution"
eventType: "viewer-request"

resources:
...

You can deploy your lambda and associate it to the CloudFront

$ severless deploy -v

You can now use a browser in the French language, go to the root address (without /index.html) and see that you are redirected to /fr/index.html

Deploy on different environment

If you want to deploy on a different environment (prod for example) you just need to launch

$ severless deploy -v --stage prod

Remove and clean all in AWS

$ aws s3 rm s3://#BucketName# --recursive
$ serverless remove

For deleting the Cloud Edge Function you need to read: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html

--

--

Vincent Delacourt

Interesting in start-up or project development in the latest technologies for web and mobile apps