This post was initially going to be the first post on this blog, but during writing it became apparent that it was
necessary to describe the development environment first. This post will describe the process used to create the blog
and publish it to AWS. Each step will describe the process for an Apple Mac, but links
will be included to enable the process to be completed on any common operating system. This post assumes that all
pre-requisites from macOS Development Environment have been installed.
A decision was made early in the creation of this blog to utilise a static site generator for the majority of pages in
this site, to use AWS S3 with AWS CloudFormation
for hosting and to run deployments through Serverless Framework to enable the easy
addition of AWS Lambda functions at a later date if required.
After trialing a number of different static site generators it was eventually decided to use Hexo
due to its simplicity.
The installation of Hexo is simple; however, there are two options for installing it. The remainder
of this post will assume Hexo has been installed globally.
The first step is to create the Hexo project. The initialisation command will create the project
directory, so if you keep all of your projects in ~/code you will need to be within this directory when you run the
command.
Serverless Framework’s initialisation command can only be run to create an empty
project. For this project the configuration is simple enough that the files can be created manually.
1 2 3 4 5 6 7 8 9 10 11
# package directories node_modules jspm_packages
# Serverless directories .serverless
# Hexo files and directories db.json public/ .deploy*/
# Configuration for AWS provider: name: aws runtime: nodejs8.10 profile: serverless # Some future functionality requires us to use us-east-1 at this time region: us-east-1
# Custom configuration options custom: # This enables us to use the default stage definition, but override it from the command line stage: ${opt:stage, self:provider.stage} # This enables us to prepend the stage name for non-production environments domain: fulldomain: prod: ${self:custom.domain.domain} other: ${self:custom.stage}.${self:custom.domain.domain} domain: **domain** domainname: ${self:custom.domain.fulldomain.${self:custom.stage}, self:custom.domain.fulldomain.other} # Define the resources we will need to host the site resources: Resources: # Set-up an S3 bucket to store the site WebsiteS3Bucket: Type: AWS::S3::Bucket Properties: AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html ErrorDocument: error.html BucketName: ${self:custom.domain.domainname} # Set-up a policy on the bucket so it can be used as a website WebsiteBucketPolicy: Type: AWS::S3::BucketPolicy Properties: PolicyDocument: Id: Fn::Join: - "" - - ${self:service.name} - BucketPolicy Statement: - Sid: PublicReadForGetBucketObjects Effect: Allow Principal: '*' Action: 's3:GetObject' Resource: Fn::Join: - '' - - 'arn:aws:s3:::' - Ref: WebsiteS3Bucket - /* Bucket: Ref: WebsiteS3Bucket Outputs: WebsiteURL: Value: Fn::GetAtt: - WebsiteS3Bucket - WebsiteURL Description: URL for my website hosted on S3 S3BucketSecureURL: Value: Fn::Join: - '' - - 'https://' - Fn::GetAtt: - WebsiteS3Bucket - DomainName Description: Secure URL of S3 bucket to hold website content
The package.json file will default to having all Hexo as required when the site is published. As
the dependencies are only required during development change them to be devDependencies.
assets: targets: # Configuration for HTML files (overriding the default cache control age) - bucket: Ref: WebsiteS3Bucket files: - source: ./public/ headers: CacheControl: max-age=${self:custom.domain.cacheControlMaxAgeHTML} empty: true globs: - '**/*.html' # Configuration for all assets - bucket: Ref: WebsiteS3Bucket files: - source: ./public/ empty: true globs: - '**/*.js' - '**/*.css' - '**/*.jpg' - '**/*.png' - '**/*.gif'
Also update the serverless.yml file to include the plugin
1 2 3
# Plugins for additional Serverless functionality plugins: - serverless-s3-deploy
The previous snippet includes a reference for dynamic cache control. This needs to be added to the serverless.yml
file within the custom.domain section.
1 2 3 4 5 6
cacheControlMaxAgeHTMLByStage: # HTML Cache time for production environment prod: 3600 # HTML Cache time for other environments other: 0 cacheControlMaxAgeHTML: ${self:custom.domain.cacheControlMaxAgeHTMLByStage.${self:custom.stage}, self:custom.domain.cacheControlMaxAgeHTMLByStage.other}
That’s it! A basic Hexo static site with deployment to AWS S3 has
been configured. The default Hexo initialisation includes a sample post, and
Serverless Framework has been configured to upload everything to S3. Now it’s time
to test everything.
In a commercial situation it makes sense to keep the content and infrastructure deployment processes independent.
However, for a blog that is maintained by a single person it can be easier to combine both processes; this is especially
true with Serverless Framework and Hexo as updates will only be
deployed when required. It was determined that Serverless Framework should be the
controlling system for deployments.
To integrate the two systems it was first necessary to install the
Serverless Plugin Scripts plugin.
1
npm i serverless-plugin-scripts --save-dev
Add the plugin to the serverless.yml file
1
- serverless-plugin-scripts
Configure the Serverless Plugin Scripts plugin to run
hexo generate when building the deployment and sls s3deploy after infrastructure has been deployed.
1 2 3 4 5 6 7 8 9
scripts: hooks: # Run these commands when creating the deployment artifacts package:createDeploymentArtifacts: > hexo clean && hexo generate # Run these commands after infrastructure changes have been completed deploy:finalize: > sls s3deploy -s ${self:custom.stage}
Although a static site is now online it isn’t suitable for public release yet. The URLs provided by
AWS S3 are not suitable for production use; SSL is not available; the site hasn’t been
customised to look nice; and apart from static content further customisations are limited.
If you want SSL and/or a CDN for your site, skip this step.
This step will assume a domain has already been registered and is hosted with AWS Route53.
If you need to register a domain name this can be done through Route53 and will automatically have appropriate DNS
management set-up.
If you set-up a real domain in the serverless.yml file while configuring the
Serverless Framework you can continue with this step; otherwise you will need to
adjust the value of custom.domain.domain on line 25 of the serverless.yml file.
To configure the domain to point to AWS S3 two record sets need to be added to the
serverless.yml file. You will need to lookup the correct
Hosted Zone ID prior to
completing this step.
# DNS Record for the domain WebsiteDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteS3Bucket - WebsiteURL HostedZoneId: **hosted_zone_id** HostedZoneName: ${self:custom.domain.domain}. Name: ${self:custom.domain.domainname} Type: 'A' # DNS Record for www.domain WebsiteWWWDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteS3Bucket - WebsiteURL HostedZoneId: **hosted_zone_id** HostedZoneName: ${self:custom.domain.domain}. Name: www.${self:custom.domain.domainname} Type: 'A'
That’s all the configuration required. Deploy the changes and you should be able to access the development version of
your site at http://dev.<your_domain>/.
As with the previous section for adding a domain name to your site you will require a domain name to be registered and
for DNS management to be set to AWS Route53.
You will also require an SSL certificate registered through
AWS Certificate Manager. To use a certificate with
AWS CloudFront it MUST be registered through the us-east-1 region.
At a later date a link will be provided here to demonstrate how to register a certificate.
If you set-up a real domain in the serverless.yml file while configuring the
Serverless Framework you can continue with this step; otherwise you will need to
adjust the value of custom.domain.domain on line 25 of the serverless.yml file.
To configure SSL for the CDN the ARN for the Certificate will need to be added to the serverless.yml file within the
custom.domain element.
1
sslCertificateARN: **ARN**
The next step is to add the configuration for the AWS CloudFront distribution and
to add the DNS records for our domain. To prevent having to lookup the S3 URL required for the configuration add a new
element under custom within the serverless.yml file to store all the S3 mappings.
# Configure CloudFront to get all content from S3 WebsiteCloudFrontDistribution: Type: 'AWS::CloudFront::Distribution' Properties: DistributionConfig: Aliases: - ${self:custom.domain.domainname} - www.${self:custom.domain.domainname} CustomErrorResponses: - ErrorCode: '404' ResponsePagePath: "/error.html" ResponseCode: '200' ErrorCachingMinTTL: '30' DefaultCacheBehavior: Compress: true ForwardedValues: QueryString: false Cookies: Forward: all SmoothStreaming: false TargetOriginId: defaultOrigin ViewerProtocolPolicy: redirect-to-https DefaultRootObject: index.html Enabled: true Origins: - DomainName: ${self:custom.domain.domainname}.${self:custom.s3DNSName.${self:provider.region}} Id: defaultOrigin CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 OriginProtocolPolicy: http-only PriceClass: PriceClass_All ViewerCertificate: AcmCertificateArn: ${self:custom.domain.sslCertificateARN} SslSupportMethod: sni-only # DNS Record for the domain WebsiteDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteCloudFrontDistribution - DomainName HostedZoneId: Z2FDTNDATAQYW2 HostedZoneName: ${self:custom.domain.domain}. Name: ${self:custom.domain.domainname} Type: 'A' # DNS Record for www.domain WebsiteWWWDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteCloudFrontDistribution - DomainName HostedZoneId: Z2FDTNDATAQYW2 HostedZoneName: ${self:custom.domain.domain}. Name: www.${self:custom.domain.domainname} Type: 'A'
Everything has now been configured, so the changes can be deployed. When an
AWS CloudFront distribution is first deployed there is a long spin-up time;
Serverless Framework will wait for this process to complete and it can take in
excess of 30 minutes.
At any time you can validate your site locally by running hexo serve within your project directory and navigate to
http://localhost:4000/ in your web browser. Once you’re happy with you changes simply deploy
the site to you development environment by running sls deploy
Now the development site is online you can access it by going to https://dev.<your_domain>. Verify that it is working
as expected and you’re almost ready to publish your site.
Finally, it looks like everything is ready. You’ve configured Hexo to manage and generate your
static site; you’ve configured Serverless Framework to manage all your
AWS infrastructure; you’ve set-up a custom domain name and a CDN; and you’ve got your first
post ready. Everything works on the test environment.
Now it’s time to publish your blog to production. Thankfully this bit is really easy.
# Plugins for additional Serverless functionality plugins: -serverless-s3-deploy -serverless-plugin-scripts
# Configuration for AWS provider: name:aws runtime:nodejs8.10 profile:serverless # Some future functionality requires us to use us-east-1 at this time region:us-east-1
# Custom configuration options custom: # This enables us to use the default stage definition, but override it from the command line stage:${opt:stage,self:provider.stage} # This enables us to prepend the stage name for non-production environments domain: fulldomain: prod:${self:custom.domain.domain} other:${self:custom.stage}.${self:custom.domain.domain} domain:**domain** domainname:${self:custom.domain.fulldomain.${self:custom.stage},self:custom.domain.fulldomain.other} cacheControlMaxAgeHTMLByStage: # HTML Cache time for production environment prod:3600 # HTML Cache time for other environments other:0 cacheControlMaxAgeHTML:${self:custom.domain.cacheControlMaxAgeHTMLByStage.${self:custom.stage},self:custom.domain.cacheControlMaxAgeHTMLByStage.other} sslCertificateARN:**ARN** assets: targets: # Configuration for HTML files (overriding the default cache control age) -bucket: Ref:WebsiteS3Bucket files: -source:./public/ headers: CacheControl:max-age=${self:custom.domain.cacheControlMaxAgeHTML} empty:true globs: -'**/*.html' # Configuration for all assets -bucket: Ref:WebsiteS3Bucket files: -source:./public/ empty:true globs: -'**/*.js' -'**/*.css' -'**/*.jpg' -'**/*.png' -'**/*.gif' scripts: hooks: # Run these commands when creating the deployment artifacts package:createDeploymentArtifacts:> hexo clean && hexo generate # Run these commands after infrastructure changes have been completed deploy:finalize:> sls s3deploy -s ${self:custom.stage} # AWS Region to S3 website hostname mapping s3DNSName: us-east-2:s3-website.us-east-2.amazonaws.com us-east-1:s3-website-us-east-1.amazonaws.com us-west-1:s3-website-us-west-1.amazonaws.com us-west-2:s3-website-us-west-2.amazonaws.com ap-south-1:s3-website.ap-south-1.amazonaws.com ap-northeast-3:s3-website.ap-northeast-3.amazonaws.com ap-northeast-2:s3-website.ap-northeast-2.amazonaws.com ap-southeast-1:s3-website-ap-southeast-1.amazonaws.com ap-southeast-2:s3-website-ap-southeast-2.amazonaws.com ap-northeast-1:s3-website-ap-northeast-1.amazonaws.com ca-central-1:s3-website.ca-central-1.amazonaws.com eu-central-1:s3-website.eu-central-1.amazonaws.com eu-west-1:s3-website-eu-west-1.amazonaws.com eu-west-2:s3-website.eu-west-2.amazonaws.com eu-west-3:s3-website.eu-west-3.amazonaws.com eu-north-1:s3-website.eu-north-1.amazonaws.com sa-east-1:s3-website-sa-east-1.amazonaws.com
# Define the resources we will need to host the site resources: Resources: # Set-up an S3 bucket to store the site WebsiteS3Bucket: Type:AWS::S3::Bucket Properties: AccessControl:PublicRead WebsiteConfiguration: IndexDocument:index.html ErrorDocument:error.html BucketName:${self:custom.domain.domainname} # Set-up a policy on the bucket so it can be used as a website WebsiteBucketPolicy: Type:AWS::S3::BucketPolicy Properties: PolicyDocument: Id: Fn::Join: -"" --${self:service.name} -BucketPolicy Statement: -Sid:PublicReadForGetBucketObjects Effect:Allow Principal:'*' Action:'s3:GetObject' Resource: Fn::Join: -'' - -'arn:aws:s3:::' -Ref:WebsiteS3Bucket -/* Bucket: Ref:WebsiteS3Bucket # Configure CloudFront to get all content from S3 WebsiteCloudFrontDistribution: Type:'AWS::CloudFront::Distribution' Properties: DistributionConfig: Aliases: -${self:custom.domain.domainname} -www.${self:custom.domain.domainname} CustomErrorResponses: -ErrorCode:'404' ResponsePagePath:"/error.html" ResponseCode:'200' ErrorCachingMinTTL:'30' DefaultCacheBehavior: Compress:true ForwardedValues: QueryString:false Cookies: Forward:all SmoothStreaming:false TargetOriginId:defaultOrigin ViewerProtocolPolicy:redirect-to-https DefaultRootObject:index.html Enabled:true Origins: -DomainName:${self:custom.domain.domainname}.${self:custom.s3DNSName.${self:provider.region}} Id:defaultOrigin CustomOriginConfig: HTTPPort:80 HTTPSPort:443 OriginProtocolPolicy:http-only PriceClass:PriceClass_All ViewerCertificate: AcmCertificateArn:${self:custom.domain.sslCertificateARN} SslSupportMethod:sni-only # DNS Record for the domain WebsiteDNSRecord: Type:"AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: -WebsiteCloudFrontDistribution -DomainName HostedZoneId:Z2FDTNDATAQYW2 HostedZoneName:${self:custom.domain.domain}. Name:${self:custom.domain.domainname} Type:'A' # DNS Record for www.domain WebsiteWWWDNSRecord: Type:"AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: -WebsiteCloudFrontDistribution -DomainName HostedZoneId:Z2FDTNDATAQYW2 HostedZoneName:${self:custom.domain.domain}. Name:www.${self:custom.domain.domainname} Type:'A' Outputs: WebsiteURL: Value: Fn::GetAtt: -WebsiteS3Bucket -WebsiteURL Description:URLformywebsitehostedonS3 S3BucketSecureURL: Value: Fn::Join: -'' - -'https://' -Fn::GetAtt: -WebsiteS3Bucket -DomainName Description:SecureURLofS3buckettoholdwebsitecontent