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
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 capabilitiesaws-s3-bucket
: manages a S3 buckets3-policy
: manages S3 bucket policys3-sync
: sync a local folder to a S3 buckets3-website-config
: configures a S3 bucket for website hostingaws-cloudfront
: configures and manages CloudFront distributionaws-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.