Create a blog using pre-built Serverless Components and Hugo

May 14, 2018

Update: This post is based on the beta version of Serverless Components, which is not compatible with the latest, and much faster, GA version. Please check out the latest docs for more up to date information.

Most of us blog, and a very common dilemma is deciding how to host the blog site. You want something easy to use, produce content with, and maintain. Bonus points if it's also easy to port elsewhere in case you ever want to move it.

Static site generators are a good option in this regard; they help keep the authoring part simple. They use Markdown as a document format, spruce up the look & feel with themes, and provide a simple workflow for a fully deployable HTML/CSS/JS-based blog site.

The deployment part is, however, up to you. In this post, we will create a serverless, static blog site. We'll generate it with Hugo, deploy it with pre-built Serverless Components, and host it on AWS.

Why serverless? Hosting static websites with serverless is a key use case: not only easy to deploy, but also very cost-effective.

Here is what we will cover:

  • Generate a static blog site
  • Deploy the site using Serverless Components
  • Deep dive into configuration and implementation

Generating a static blog site

Although we will be using Hugo to generate our blog site, you can use your favorite static generator. As long as you can build the final site into a local folder, you are good to go. We'll see how that can be done with Hugo.

Note: Since working with Hugo is well documented on their site, I'll leave the exercise of creating the site to you. However, to get you started, I've created a sample blog site and shared it below. You can use that with the solution I present here.

First, make sure to get Hugo installed and working.

Then, run the following commands on your terminal to get going:

# get the site code
$ git clone https://github.com/rupakg/sls-blog

# test the site locally at localhost:1313/
hugo server -D

# publish to the local 'site' folder
hugo -d site

Note: The sample sls-blog site code is on Github.

If you are following along, you should have a working blog site that you previwed locally at localhost://1313, and you should have the static files ready to be deployed in the site folder on your machine.

Wrapping it up in a component

You might have already heard about our latest project, Serverless Components, and how you can use them. Our goal was to encapsulate common functionality into so-called "components", which could then be easily re-used, extended and shared with other developers and other serverless applications.

We built all the functionality needed to take a set of static files, host it on S3 with appropriate permissions & configuration, set it up on a CDN, map a domain name, and finally deploy it to AWS.

The blog application

The blog application references the static website component that encapsulates all the functionality we need.

Here's the serverless.yml file:

type: blog-app
version: 0.0.1

components:
  blogSite:
    type: static-website
    inputs:
      name: blog-site
      contentPath: ${self.path}/site
      templateValues: {}
      contentIndex: index.html
      contentError: error.html
      hostingRegion: us-east-1
      hostingDomain: sls-blog-${self.serviceId}.example.com
      aliasDomain: www.sls-blog-${self.serviceId}.example.com

The type identifies the application. The components block is the gut of the application. It simply references the static-website component using the type attribute. It used the inputs block to supply the input parameters required by the static-website component to customize it behavior.

The contentPath specifies the path where the content of the site belongs. In the above section, we generated the static files for our blog site using Hugo. This location is what we specify here.

Although the static-website component can make use of templateValues using Mustache templates, our blog site does not use it.

The hostingDomain and aliasDomain attributes are used to configure the CDN and map it to a domain name.

You will notice the usage of the variable ${self.serviceId} in the above configuration. The serviceId is an unqiue, autogenerated, random identifier that you can use to force uniqueness. In our case, you can copy the same application over and create as many instances of blog sites you want. In all practical purposes, you will probably replace the hostingDomain and aliasDomain attribute values with your own specific domain names.

Deploy

Once you have the serverless.yml set and file path for your static files ready, you can simply deploy the blog application.

Here's how:

$ components deploy

This will detect the component dependencies, run the deployment logic for each, and finally deploy the blog application to AWS.

Here's what you get:

Creating Bucket: 'sls-blog-81jdzsed8u.example.com'
Creating Bucket: 'www.sls-blog-81jdzsed8u.example.com'
Creating Site: 'blog-site'
Setting policy for bucket: 'sls-blog-81jdzsed8u.example.com'
Syncing files from '/var/folders/s1/1hcnm6hx6zgg5nbz9jm16wq80000gn/T/tmp-56625JsN0DiMB4ZOV' to bucket: 'sls-blog-81jdzsed8u.example.com'
Setting website configuration for Bucket: 'sls-blog-81jdzsed8u.example.com'
Creating CloudFront distribution: 'blog-site'
Setting redirection for Bucket: 'www.sls-blog-81jdzsed8u.example.com'
Set policy and CORS for bucket 'sls-blog-81jdzsed8u.example.com'
Uploading file: ...
... snip ...
Objects Found: 0 , Files Found: 35 , Files Deleted: 0
CloudFront distribution 'blog-site' creation initiated
Creating Route53 mapping: 'www.sls-blog-81jdzsed8u.example.com => d2vruw3j75x9bz.cloudfront.net'
Route53 Hosted Zone 'blog-site-site-81jdzsed8u-2x3n7tbu-1525982498' creation initiated
Route53 Record Set 'www.sls-blog-81jdzsed8u.example.com => d2vruw3j75x9bz.cloudfront.net' creation initiated
Static Website resources:
  http://sls-blog-81jdzsed8u.example.com.s3-website-us-east-1.amazonaws.com

:boom: You have a blog site at:

http://sls-blog-81jdzsed8u.example.com.s3-website-us-east-1.amazonaws.com

image

Figure: Blog site built with Hugo and deployed with Serverless Components

Note: If you put in a real domain name, you can access the site via the domain as well. Give CloudFront and Route53 about 15-20 mins to finish the configuration.

You can also get the information about the resources that were deployed by running:

$ components info

And you can always cleanup the resources by running:

$ components remove

The Aha Moment

Let's just think for a moment about what we just did there. You have a blog site, based on your theme, optimised using a CDN, on your domain, using serverless, on AWS. In...really not that much time. And no servers to maintain!

No excuses—get on with those articles you have been meaning to write!

Now that I have piqued your interest in Serverless Components, I wanted to also express that you don't have to be highly technical to use it. The Serverless Components abstracts away a lot of inner workings, and exposes a simplistic view of the behavior you seek. You, as a front-end developer or a full-stack engineer, can benefit from using the serverless technologies without getting into the nuts and bolts of things.

For the curious, let's get into the details and take a peek behind the curtain of how these components work.

The Static Website Component

The blog site we created uses the static-website component.

Let's walk through the static-website component that wraps up the functionality to deploy a static website on AWS S3. It not only configures S3 to host a website, but also configures a CDN using CloudFront and maps a custom domain via Route53 (DNS).

Yes, that's a lot of moving parts, but that's the beauty of encapsulating all that complexity in a reusable and sharable Serverless Component.

The static-website component is composed of several other smaller components. The idea is to build up small, independent blocks of functionality and encapsulate them into reusable chunks.

The static-website component is shared via the registry on Github.

Configuration

Components are declared and customized by its configuration file (i.e. serverless.yml). They are identified by a type and take input parameters to customize it's behavior.

The input parameters are described by an inputTypes block in the component's configuration file. Components can be made up of other Components. The 'composition', or the component's dependencies, are specified in the components block.

Let's take a look at the serverless.yml file for the static-website component.

Type and Metadata

type: static-website
version: 0.2.0
core: 0.2.x

The type attribute is used to reference the component when used from another application or component.

description: "Static Website component."
license: Apache-2.0
author: "Serverless, Inc. <hello@serverless.com> (https://serverless.com)"
repository: "github:serverless/components"

All of the above attributes are pretty self-explanatory, and are metadata about the component.

Input Parameters

The inputTypes block is the specification for inputs that the component exposes. The specs allow for specifying if a parameter is required or not and it's default values. The system will validate the inputs based on these specs.

inputTypes:
  name:
    type: string
    required: true
    displayName: Site Name
    description: Logical name of the site
  contentPath:
    type: string
    default: ./site
    description: Relative path of a folder for the contents of the site like './site'
  templateValues:
    type: object
    default: {}
    required: true
  contentIndex:
    type: string
    default: index.html
    description: The index page for the site like 'index.html'
  contentError:
    type: string
    default: error.html
    description: The error page for the site like 'error.html'
  hostingRegion:
    type: string
    default: us-east-1
    description: The AWS region where the site will be hosted like 'us-east-1'
  hostingDomain:
    type: string
    required: false
    default: site-${self.instanceId}.example.com
    description: The domain name for the site like 'serverless.com'
  aliasDomain:
    type: string
    required: false
    default: www.site-${self.instanceId}.example.com
    description: The alias domain for the site like 'www.serverless.com'

Composition

The components block lists the dependencies that make up the top-level component or an application. In the case of the static-website component, we have several smaller components that build up the functionality:

  • mustache: provides Mustache templating capabilities
  • aws-s3-bucket: manages a S3 bucket
  • s3-policy: manages S3 bucket policy
  • s3-sync: sync a local folder to a S3 bucket
  • s3-website-config: configures a S3 bucket for website hosting
  • aws-cloudfront: configures and manages CloudFront distribution
  • aws-route53: configures and manages Route53 mappings

Note: You can find more details about these components and look at the code in the Components registry.

Each one of these components are independent of each other, but they can be weaved together into a higher-order compomnent.

components:
  renderedFiles:
    type: mustache
    inputs:
      sourcePath: ${input.contentPath}
      values: ${input.templateValues}
  rootDomainBucket:
    type: aws-s3-bucket
    inputs:
      name: ${input.hostingDomain}
  rootDomainBucketPolicy:
    type: s3-policy
    inputs:
      bucketName: ${rootDomainBucket.name}
  siteContentUploader:
    type: s3-sync
    inputs:
      contentPath: ${renderedFiles.renderedFilePath}
      bucketName: ${rootDomainBucket.name}
  wwwDomainBucket:
    type: aws-s3-bucket
    inputs:
      name: ${input.aliasDomain}
  rootDomainBucketConfig:
    type: s3-website-config
    inputs:
      rootBucketName: ${rootDomainBucket.name}
      indexDocument: ${input.contentIndex}
      errorDocument: ${input.contentError}
      redirectBucketName: ${wwwDomainBucket.name}
      redirectToHostName: ${rootDomainBucket.name}
  siteCloudFrontConfig:
    type: aws-cloudfront
    inputs:
      name: ${input.name}
      defaultRootObject: ${input.contentIndex}
      originId: ${input.hostingDomain}
      originDomain: ${rootDomainBucket.name}.s3.amazonaws.com
      aliasDomain: ${input.aliasDomain}
      distributionEnabled: true
  siteRoute53Config:
    type: aws-route53
    inputs:
      name: ${input.name}-site-${self.instanceId}
      domainName: ${input.aliasDomain}
      dnsName: ${siteCloudFrontConfig.distribution.domainName}

Note: Take a look at the entire serverless.yml file here.

Input Variables

Child components can use parent's inputs as input for themselves. This allows sharing of input data and also signifies a dependency.

Here is an example:

  siteCloudFrontConfig:
    type: aws-cloudfront
    inputs:
      ...
      originId: ${input.hostingDomain}
      ...
      aliasDomain: ${input.aliasDomain}
      ...

Here the siteCloudFrontConfig component needs the hosting domain name and the alias domain name to configure the CloudFront distribution. So, it passes the input.hostingDomain to its originId parameter, and input.aliasDomain to its aliasDomain domain parameter.

Recall that the blog site (parent application in our case) had the inputs block as follows:

type: blog-app
version: 0.0.1

components:
  blogSite:
    type: static-website
    inputs:
      ...
      hostingDomain: sls-blog-${self.serviceId}.example.com
      aliasDomain: www.sls-blog-${self.serviceId}.example.com

Output Variables

Components can take input parameters to customize their behavior, but components can also expose output variables. The output variables expose output values that are generated inside the component as part of their implementation.

Here is an example:

  siteRoute53Config:
    type: aws-route53
    inputs:
      ...
      dnsName: ${siteCloudFrontConfig.distribution.domainName}

The dnsName input parameter for the aws-route53 component is provided the output from the aws-cloudfront component. You will notice that the component's instance name, siteCloudFrontConfig, is used to reference the output variable.

Dependency

Components can have dependencies amongst each other; it can be fairly cumbersome for a component or application author to keep track of. To aid with that, the system keeps track of the dependency tree for you, based on the use of input variables and output variables.

For example, since the siteRoute53Config component uses the output variable from the siteCloudFrontConfig component, the siteRoute53Config component waits for the siteCloudFrontConfig component to finish, and then uses its output.

The set of components that do not have any dependencies can execute in parallel, thus improving performance.

Component Behavior

We looked at the configuration for the static-website component. Now, let's look at the code that drives the behavior.

The behavior or implementation of the static-website component is placed in the index.js file, as shown below:

# index.js
const { not, isEmpty } = require('ramda')

const deploy = async (inputs, context) => {
  let outputs = context.state

  const s3url = `http://${inputs.hostingDomain}.s3-website-${inputs.hostingRegion}.amazonaws.com`

  if (!context.state.name && inputs.name) {
    context.log(`Creating Site: '${inputs.name}'`)
    outputs = {
      url: s3url
    }
  } else if (!inputs.name && context.state.name) {
    context.log(`Removing Site: '${context.state.name}'`)
    outputs = {
      url: null
    }
  } else if (context.state.name !== inputs.name) {
    context.log(`Removing old Site: '${context.state.name}'`)
    context.log(`Creating new Site: '${inputs.name}'`)
    outputs = {
      url: s3url
    }
  }
  context.saveState({ ...inputs, ...outputs })

  return outputs
}

const remove = async (inputs, context) => {
  if (!context.state.name) return {}

  context.log(`Removing Site: '${context.state.name}'`)
  context.saveState({})
  return {}
}

const info = (inputs, context) => {
  let message
  if (not(isEmpty(context.state))) {
    message = [ 'Static Website resources:', `  ${context.state.url}` ].join('\n')
  } else {
    message = 'No Static Website state information available. Have you deployed it?'
  }
  context.log(message)
}

module.exports = {
  deploy,
  remove,
  info
}

Let's walk through the code.

First, note the three methods that provide all the functionality: deploy, remove, and info. At the end of the code block, you will notice that these methods are exported so that they are publicly accessible from outside.

At minimum, all components should follow this pattern and implement these three methods. The core system will build the dependency tree of child components and call these methods down the chain.

However, it is not necessary to provide an implementation via the index.js file if you don't need it. You can see that the blog-app application that we created does not provide any index.js file. It just describes the composition of it's child components via the serverless.yml configuration file.

The deploy method

The deploy method is used to encapsulate the deployment behavior of the component. It inspects & validates input parameters, calls appropriate code to deploy the necessary resources, and saves state in the state.json on disk.

The static-website component completely relies on its child component implementations, and so the deploy method only prints out a message that includes the site url. The system calls the deploy method of the child components down the dependency chain.

The remove method

The remove method is used to encapsulate the cleanup behavior of the component. It reverses the effect and cleans up resources created via the deploy method.

In this case, a message is printed stating that the site has been removed. The component's state in the state.json file is also cleared. The system calls the remove method of the child components down the dependency chain.

The info method

The info method is used to print any resources that were deployed. In this case, the static website url is printed if the component has been deployed.

Summary

We saw how easy and simple it is to use Serverless Components to build and deploy applications, such as the blog site we created. Components gives us the flexibility to compose higher-order applications by combining reusable pieces of code.

What will you build with components? Let us know in the comments below.

More on Components

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.