How have I built this site?

I get a fair amount of questions about how I built this site, enough so that I decided to write a post about it.

When I started my company I knew I wanted a web presence. I also had a pretty good idea about what I wanted to achieve with it. The primary objective is of course to make myself and my company visible. The secondary objective was to have a place where I could publish articles.

The secondary objective makes things slightly more complex. A static web page with contact details that are never updated is simple enough but writing articles makes the content of the site dynamic. I wanted the site to be low maintenance, low cost and easy to update so I had a bit of a challenge ahead of me.

As soon as you deploy something that has a backend you need a server or, in the AWS case, a Lambda. Regardless of how you deploy the backend it adds costs and complexity that I really wanted to avoid. I just wanted a simple, static web site but I wanted to be able to update the content easily and I wanted to avoid code duplication as much as possible.

The challenge

The challenge ahead of me was to create a web site that had zero capex, minimal opex, no code duplication and wouldn't take me days to implement. I will admit that I felt discouraged at first. These requirements, given the time constraint, felt a bit much.

I had a straight forward plan for hosting the website. Amazon S3, Amazon CloudFront, AWS Certificate Manager (CM) and a handful of records in Amazon Route53 was all I needed, I'll go into more detail on that later. The problem was the code duplication.

So, how did I set about to solve this challenge? I won't go into detail on markup, css and the like but rather focus of three topics:

  1. What infrastructure do I use to host the site and how do I provision it?
  2. How do I generate the site?
  3. How do I deploy the site?

Infrastructure

There was never any question about where to host the site, AWS is the obvious choice. As for what services to use, that was straight forward too. S3 is great for hosting static websites and with a CloudFront cache in front not only can I use my own domain name via Route53 but I can also cache the website on global edge locations which will drastically reduce the latency. On top of this, CloudFront is integrated with Amazon Certificate Manager which enables me to use https for free.

As for provisioning, I always talk about automation and I'd be damned if I didn't automate my own setup. That said I wrote a CloudFormation template, you can download a copy if you want to take a look. Feel free to re-use the concepts, or even the full template if you want to set up something similar to this website. I manage the CloudFormation stack via my own open source tool aws-cloudformation-simple-cli.

Generating the site

If the infrastructure setup was the easy, straight-forward part, solving the code duplication issue was more of a challenge. As it turned out, there was a simple solution to this too. A former colleague had stumbled across a framework called Metalsmith which is a tiny framework for generating static websites. It's plugin based so you can do pretty much anything and there is a rich plugin supply readily available. I won't go into depths on how Metalsmith works, they explain that rather well themselves.

For this website, I created a layout template and then went to work on the content with each page being a separate html file. I then wrote a tiny nodejs script to generate the web site from the template and content files using metalsmith.

Deployment

With my infrastructure in place and my website generated, the last piece of the puzzle was deployment. I had several options here: I could do it myself using the AWS CLI or I could use Metalsmith. I opted for the latter so that I would have only one script to manage both site generation and site deployment.

There is a Metalsmith plugin for uploading files to S3 so that part was quickly resolved. What was missing was a clean way of invalidating the CloudFront cache. Since I don't want to change the urls of the pages on my site with every release I have to tell CloudFront to invalidate everything after I release an update. There wasn't a Metalsmith plugin that could do this for me so I wrote one myself. It was surprisingly simple and didn't take long. With that done, I had all the pieces I needed.

The script I wrote to generate and deploy the website wasn't big:

let Metalsmith = require('metalsmith');
let layouts    = require('metalsmith-layouts');
let permalinks = require('metalsmith-permalinks');
let s3         = require('metalsmith-s3');
let cloudfront = require('metalsmith-cloudfront');
let moveRemove = require('metalsmith-move-remove');

let config = {
    mode: process.env.MODE,
    metalsmith: {},
    s3: {
        action: 'write',
        region: 'eu-west-1'
    },
    cloudfront: {
        paths: ['/*']
    },
    misc: {}
};

switch (process.env.MODE) {
    case 'production':
        config.misc.localDestination = './build/prod';
        config.metalsmith.base = '/';
        config.s3.bucket = 'bynordenfelt.se';
	config.cloudfront.dist =  'E3GC3M7FZB2J1S';
        break;
    case 'test':
	throw new Error('Not implemented');
    default:
	config.metalsmith.base = '/bynordenfelt.se/build/dev/'; 
        config.misc.localDestination = './build/dev';
	break;
}

console.log('Configuration', config);

let metalsmith = Metalsmith(__dirname)
    .metadata(config.metalsmith)
    .source('./src')
    .destination(config.misc.localDestination)
    .clean(true)
    .use(permalinks())
    .use(layouts({
        engine: 'handlebars'
    }))
    .use(moveRemove({
        move: [{ source: 'not-found/index.html', target: 'not-found.html' }],
        remove: ['^not-found/']
    }));

if (process.env.MODE === 'production') {
    console.log('Adding S3 & CloudFront plugins to execution');
    metalsmith.use(s3(config.s3));
    metalsmith.use(cloudfront(config.cloudfront))
}

metalsmith.build(function (error, files) {
    if (error) {
        throw error;
    }

    console.log('Build complete');
});

        

The above script is placed in a file named build.js and invoked via npm:

npm i && MODE=production node build.js
	

Summary

I now have:

  • A CloudFormation template that created the infrastructure. The stack is managed via aws-cloudformation-simple-cli.
  • A Metalsmith script that generates the static pages
  • The same Metalsmith script that deployes the website and invalidates the CloudFront cache

I did all this in a a couple of hours, from creating the infrastructure to deploying the site. Once I was done, not only had I deployed my website but I also had very simple and fast way of deploying updates and even testing locally before going to the live site.

How much does it cost? I don't have that much traffic but my monthly bill for this website runs at about $0.56 out of which $0.50 is for the Route53 hosted zone. I'd say that I hit my capex/opex goals.

Icons made by Madebyoliver