Deploy Multi-Language Static HTML on CDN
With Serverless Framework and AWS CloudFront. Delete Cache on redeploying and force redirect according to browser language
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