3 minutes, 54 seconds
At work we’ve written some monitoring and alerting software. We’ve also automated releases and we wanted to take it to the next level so that the instant there was a new release, we’d deployed to production. This automated releasing of software upon a new release is called Continuous Deployment, or “CD” for short. This post documents the simple yet effective approach we took to achieve CD using just 20 lines of bash.
The 20 lines of bash
Super impatient? I got you. This code will mean a lot more if you read the post below, but I know a lot of engineers want more copypasta and less preachy blog posts:
#!/usr/bin/env bash
# Checks for local version (current) and then remote version on GH (latest)
# and if they're not the same, run update script
#
# uses lasttversion: https://github.com/dvershinin/lastversion
current=$(cd /root/cht-monitoring;/usr/bin/git describe --tags)
latest=$(/usr/local/bin/lastversion https://github.com/medic/cht-watchdog)
update(){
cd /root/cht-monitoring
git fetch
git -c advice.detachedHead=false checkout "$latest"
/root/down-up.sh
}
announce(){
/usr/bin/curl -sX POST --data-urlencode "payload={\"channel\": \"#channel-name-here\", \"username\": \"upgrade-bot\", \"text\": \"Watchdog has been updated from "$current" to "$latest". Check it out at https://WATCHDOG-URL-HERE\", \"icon_emoji\": \":dog:\"}" https://hooks.slack.com/services/-SECRET-TOKEN-HERE-
}
if [ ! "$current" = "$latest" ];then
$(update)
$(announce)
echo "New version found, upgraded from $current to $latest"
else
echo "No new version found, staying on $current."
fi
Why use CD?
There’s been a lot written about CD, like this quote:
Engineering teams can push code changes to the main branch and quickly see it being used in production, often within minutes. This approach to software development [allows] continuous value delivery to end users.
– rando GitHub blog post
I’ll add to this and say, as an engineer, it’s also extremely gratifying to have a pull request get merged and then minutes later see a public announcement in Slack that a new release has just automatically gone live. There’s no waiting even to do the release manually yourself, it’s just done for you, every time, all the time! Often when engineers are bogged down by a lengthy release process and slow customer adoption where it can take weeks or months (or years?!) to see their code go live, CD is the antidote to the poison of slowness.
Tools Used
The script uses just one custom binary, but otherwise leans on some pretty bog standard tools:
git
– Since our app is in a local checked out git repository, we can use trivially find out what version is running locally withgit describe --tags
curl
– We usecurl
to do thePOST
to slack using their webhook API. It’s dead simple and requires just a bearer tokenlastversion
– I found this when searching for an easy and reliable way to resolve what the latest release is for our product. It’s on GitHub and it just made my day! It solved the exact problem I had perfectly and it really Does One Thing Well (well, admittedly also downloads)down-up.sh
– this is the cheating part of this solution ;) This is an external bash script that makes it easy tokeep track of which docker compose files we’re using. Every time we create a new compose file, we add it to thecompose down
andcompose up
calls in the script. This ensures we don’t exclude one by accident. It’s just two lines which are something like:docker compose -f docker-compose.yml -f ../docker-compose-extra.yml down
docker compose -f docker-compose.yml -f ../docker-compose-extra.yml up --remove-orphans -d
- Inside the repository itself, we’ve hooked up Semantic Release which automatically cuts a new release based on Semantic Versioning (aka “SemVer”)
New release process
With all the tools in place, here’s how we cut a new release, assuming we’re on version 1.10.0
:
- An engineer will open a pull request (PR) with some code changes.
- The PR will be reviewed by another engineer and any feedback will be addressed.
- Once approved, the PR is merged to
main
with an appropriate commit message. - A release is automatically created: A
fix
commit will release version1.10.1
. Afeat
(feature) commit will release version1.11.0
. A commit citingBREAKING CHANGE
will release version2.0.0
- Every 5 minutes a cronjob runs on the production server to compare the current local version verses the latest version on GitHub
- If a new version is found,
git
is run to check out the latest code - The
down-up.sh
script is called to restart the docker services. We even support updates to compose files citing a newer tag of a docker image in which case docker downloads new releases of upstream. Easy-peasy! - A
curl
command is run to make aPOST
to the Slack API so an announcement is made in our team’s public channel:
Wrap up
You may become overwhelmed in the face of needing to store SSH private keys as a GitHub secret or having a more complex CD pipeline with a custom GitHub Runner. Don’t be overwhelmed! Sometimes the best solution is just 20 lines of bash you copied off a rando blog post (that’s me!) which you tweaked to work for your deployment. You can always get more complex at a later date as needed, but getting CD up and running today can be incredibly empowering for the engineers working on your code!