Automated deployments of this site with Brigade

With the site up and running, I wanted to make my life easier by setting up an automated deployment pipeline powered by Brigade. Brigade is ideal for an event-driven workflow (such as on push) which will form the basis of my continuous deployment for my projects. In this post, I will cover how to create a brigade project and configure it to deploy this website automatically.

Background

Before getting into the setup, let’s cover how this website is built and deployed today, as well as a recap of my Brigade setup.

Building this website

This website is built using Jekyll, which is essentially the process of running jekyll build to convert the source files (found on evdev.me GitHub repository) into static assets that can be deployed into any web hosting provider. You can find more details on how this website is built on my earlier post Making this website with Jekyll.

Hosting and Deployment

I use Google’s Firebase Hosting for this website as it offers secure (HTTPS) web-hosting, caches your content globally for faster access and most importantly, it comes with a free tier that’s more than enough for me. Although this post uses Firebase CLI for deployment, using an alternative deployment hosting solution, such as uploading files over FTP/SCP should be fairly similar. It’s worth noting that this type of website can easily be hosted on GitHub pages, but I chose to set up the deployment and hosting myself as a learning exercise.

Brigade

Brigade is an event-driven scripting framework designed to work with containers and is well suited to continuous integration or to initiate continuous deployment. This post uses the Brigade instance configured with GitHub integration as set up in my earlier post Setting up a Kubernetes Cluster for Development and Production.

Continuous integration workflow

For this project I decided to use a simple workflow of building and deploying my website upon every push to some predetermined branches:

Brigade lets you define more complex workflows such as pull request validation by running test suites or other tools. However, these are unnecessary for this type of project and I only really need Brigade to trigger my deployment pipeline.

Setting up Brigade Project

Brigade provides the CLI tool brig which makes it very easy to create a project:

1. Create the project

$ brig project create
? VCS or no-VCS project? VCS
? Project Name eyjohn/evdev.me
? Full repository name github.com/eyjohn/evdev.me
? Clone URL (https://github.com/your/repo.git) https://github.com/eyjohn/evdev.me.git
? Add secrets? Yes --- SEE BELOW ---
...
? Where should the project's shared secret come from? Specify my own
? Shared Secret **********
? Configure GitHub Access? No
? Configure advanced options No
Project ID: brigade-2809670f5c85efb78f808c7446e312340c1893136c869db51aa557

You can now verify that your project has been created by running:

$ brig project list
NAME                  ID                                                              REPO                            
eyjohn/evdev.me       brigade-2809670f5c85efb78f808c7446e312340c1893136c869db51aa557  github.com/eyjohn/evdev.me  

Or alternatively by launching the “Kashti” dashboard provided by Brigade:

$ brig dashboard
2019/11/05 22:32:41 Connecting to kashti at http://localhost:8081...
2019/11/05 22:32:42 Connected! When you are finished with this session, enter CTRL+C.

Screenshot of fresh project in Kashti

2. Configure secrets

You can define secrets that would be available to your brigade.js script, allowing you to store sensitive credentials without having to place them in the repo or the containers.

In this case, I plan to store my Firebase authentication credentials, but these could likewise be used to store an SSH key or FTP credentials.

The easiest way to do this is at the time of the creation of the project.

3. Enable GitHub app access

To allow the GitHub app used by Brigade to access the repository, it will need to be configured in GitHub Apps -> Edit -> Install App.

Screenshot of GitHub app installation for project

Creating jobs

With the project set up, let’s start adding some jobs.

1. Creating a test job

First, let’s create a simple job to test that simply prints the branch:

brigade.js

const { events, Job } = require("brigadier");
events.on("push", (event, project) => {
  const branch = event.revision.ref;
  const job = new Job("test-job", "alpine", [`echo ${branch}`]);
  job.run();
});

After pushing we can verify that it’s working by using the Kashti dashboard:

Screenshot of test job build in Kashti

2. Creating a build job

To build this website, Brigade will need to pull the source code, run the jekyll build command and finally output the built website for the deployment step.

function createBuildJob(event, project) {
  var buildJob = new Job("build-job");

  buildJob.image = "jekyll/jekyll";

  buildJob.tasks = [
    "cd /src",
    "jekyll build",
  ];

  buildJob.streamLogs = true;
  return buildJob;
}

To export the build artefacts to the deployment job, the build artefacts will need to be copied to a job storage which is shared across jobs and can be configured using:

  buildJob.tasks = [
    "cd /src",
    "jekyll build",
    "cp -r firebase.json _site /build" // firebase.json required for deployment
  ];
  buildJob.storage.enabled = true;
  buildJob.storage.path = '/build';

Since my Jekyll configuration requires some modules to be installed, it would be great to not have to install them every time I run the build and re-use them from the previous builds. This can be configured using the job cache which is shared between builds for the same job and can be configured using:

  buildJob.cache.size = '100Mi';
  buildJob.cache.enabled = true;
  buildJob.cache.path = '/usr/local/bundle'; // Jekyll image cache location

3. Creating a deployment job

To deploy this website to firebase, I’ll use an image with the firebase CLI tool pre-installed. Since I plan on having two different deployments: staging and production, I will make the deployment destination a configurable parameter. I will be using project secrets to store the credentials for my deployment destinations. This job will need to pull in the build artefacts from earlier and deploy them.

function createDeployJob(event, project, staging) {
    const firebaseProject = staging
    ? project.secrets.FIREBASE_PROJECT_STAGING
    : project.secrets.FIREBASE_PROJECT_PRODUCTION;
  var deployJob = new Job("deploy", "andreysenov/firebase-tools", [
    'cd /build',
    `firebase deploy --project ${firebaseProject} --token ${project.secrets.FIREBASE_TOKEN}`
  ]);
  deployJob.storage.enabled = true;
  deployJob.storage.path = '/build';
  deployJob.streamLogs = true;
  return deployJob;
}

4. Putting it all together

Finally, let’s hook up these jobs to the push event. For my workflow, I will need to detect the branch of the push to determine whether the build should be deployed to the production or staging destination.

async function runBuildAndDeploy(event, project) {
  const buildJob = createBuildJob(event, project);

  var staging;
  if (event.revision.ref == "refs/heads/master") {
    staging = false;
  } else if (event.revision.ref == "refs/heads/staging") {
    staging = true;
  } else {
    return; // Nothing to do for other branches
  }

  const deployJob = createDeployJob(event, project, staging);

  await buildJob.run();
  await deployJob.run();
}
events.on("push", runBuildAndDeploy);

5. Testing the pipeline

By pushing to the staging branch I can now test that both jobs are working.

Screenshot of pipeline in Kashti

The final version of my brigade.js can be found on the GitHub repository of this website eyjohn/evdev.me.

Conclusion

Using Brigade was a big change to what I’m normally used to with other continuous integration tools such as Jenkins, which normally requires me to spend more time configuring my Jenkins instance and development environment on any nodes. Brigade, on the other hand, has very few configuration options and instead relies on users to express their jobs in a single brigade.js configuration file, backed by a deployment environment executed only through containers.

This required a huge mindset shift that emphasises the use of containers to execute the build, test and deployment process. This eliminates the need for a large or complex development or testing environment and encourages the use of isolated containers for each job. Furthermore, this makes it easier to run tools that might not be compatible with each other or to use new tools as everything is isolated.

After getting to grips with the Brigade fundamentals (jobs, storage/cache, secrets) I found it very intuitive to express the different jobs required for my project. When combined with GitHub integration, it can be used to create a range of workflows without much difficulty. Finally, with all the resources of my Kubernetes cluster, Brigade can start the containers whenever or wherever it needs. This certainly seems like the next step in the evolution of continuous integration and I’m looking forward to using it again for my future projects!


comments powered by Disqus