Deploying a DocPad Site to Azure Using CircleCI

When I wanted to replace my local TeamCity based deployment setup for my blog with a hosted one, I chose CircleCI based on its good support for private repositories in BitBucket and an abundant free offering (at least for my needs). The migration process went surprisingly smooth, even though I had to change the technique I used for deploying to Azure since there's no Web Deploy on Linux.

The Build Steps

The initial setup was really quick. Once I signed up with my BitBucket identity, I simply chose the correct repository and the language it used to get the initial .circleci/config.yml file with all the dependency handling already preconfigured. After replacing the default build step with my own ones, I was ready for my first build:

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:9.5

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run:
          name: Install Dependencies
          command: npm install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      - run:
          name: Generate Site
          command: grunt generate

      - run:
          name: Run Tests
          command: grunt test

Although the build failed, the error message made it very clear what went wrong:

#!/bin/bash -eo pipefail
grunt generate

/bin/bash: grunt: command not found
Exited with code

My build steps assumed that Grunt was installed globally. To avoid this issue altogether, I wrapped the Grunt calls into NPM scripts in package.json:

"scripts": {
  "start": "grunt generate",
  "test": "grunt test"
}

I updated the build configuration to invoke the scripts instead of Grunt directly:

- run:
    name: Generate Site
    command: npm start

- run:
    name: Run Tests
    command: npm test

The build now passed as expected. It was time to move on to deployment.

Generating Artifacts

Using a ZIP file seemed the best strategy for deployment to Azure App Service. Creating a ZIP file would be easy and the approach would still ensure that any stale files would automatically be deleted if they weren't a part of the latest deploy any more. Having this functionality was one of the reasons I was using Web Deploy previously.

To make troubleshooting easier I started by creating the required ZIP file and exposing it as a build artifact so that I could verify it before I started trying to upload it.

I first created a Bash script to create the archive:

#!/bin/bash
cd ~/repo/out
zip -r ~/repo/out.zip ./*

From the build configuration, I then invoked the script and stored the resulting file as an artifact:

- run:
    name: Create ZIP Archive
    command: ./.scripts/archive.sh

- store_artifacts:
    path: ~/repo/out.zip

Again, the first attempt resulted in an error:

#!/bin/bash -eo pipefail
./.scripts/archive.sh

/bin/bash: ./.scripts/archive.sh: Permission denied
Exited with code 1

As I couldn't quickly figure out how to mark the script as executable after checking it out from the repository, I changed the build step to avoid the requirement:

- run:
    name: Create ZIP Archive
    command: bash ./.scripts/archive.sh

It worked now and I was ready to upload the archive to Azure.

Deploying a ZIP Archive to Azure

The deployment step could hardly be any simpler. I only needed to post the ZIP archive to the specified URL and of course authenticate the request correctly. This is what I came up with after several failed attempts:

- deploy:
    command: |
      if [ "${CIRCLE_BRANCH}" == "master" ]; then
        curl --fail -X POST --data-binary @out.zip https://${DEPLOY_USERNAME}:${DEPLOY_PASSWORD}@${DEPLOY_APPNAME}.scm.azurewebsites.net/api/zipdeploy
      fi

Let's analyze it a bit, as this might help you to avoid some of the difficulties I went through:

  • CIRCLE_BRANCH is a built-in environment variable provided by CircleCI containing the name of the branch being built. By default, CircleCI builds all the branches. I don't mind that as it helps me detect any issues early, but I only want the site to be deployed when building from the master branch.

  • Without the --fail flag curl exits with status 0 even if something goes wrong with the request, e.g. authentication fails. That's not good because CircleCI can't detect the failure and will mark the build as successful even if the site wasn't deployed.

  • For some reason curl couldn't find the file for upload if I specified it relatively to the home directory (~/repo/out.zip). This seemed a safer approach as it would work no matter the current directory. Since I couldn't make it work, I now rely on ~/repo being the current directory. That's not really an issue since NPM commands wouldn't work either if that wasn't the case.

  • To avoid having deployment credentials committed to Git, I use environment variables in their place. In CircleCI you can set the values for your own build environment variables in the project configuration.

I'm really happy with the final result. Thanks to CircleCI, I'm now one step closer to having less local infrastructure.

Copyright
Creative Commons License