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 themaster
branch.Without the
--fail
flagcurl
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.