Octopus: Build Once, Deploy Many

The Problem

I had to setup a production environment for a client who had a development environment, a QA CM, two QA CD’s, a production CM, and 2 production CD’s. These machines are behind all sorts of firewalls and on different subnets. The normal robocopy procedure wouldn’t have been easy. On top of that, the build in Jenkins takes about 10 minutes to complete. Simple math: 10 minutes x 7 targets = 70 minutes.

I needed to solve 2 problems: first, deploying code with the network configuration. Second, cutting that deployment time down. This is where Octopus shined.

What is Octopus?

Octopus is a deployment tool that allows for a “build once, deploy many” style of deployments. It allows you to use your CI tool of choice (i.e. TeamCity or Jenkins) to build your code, and it will then deploy that code on as many servers as you want.

How it Works

Octopus has 2 main parts: the Octopus server itself, and its tentacles. Tentacles are listening applications that you install on the deployment target. They listen on specific ports, which are easy to whitelist on a firewall across different networks. Think of Octopus as the conductor, and the tentacles as the orchestra.

The Process

Deployments

The deployment procedure becomes the following:

  1. Jenkins builds the code and packages it up into a zip format for Octopus
    • Note: an alternative (and preferred) method is to have the CI server create NuGet packages. TeamCity has a great integration for that, but as of my this post, my experience is that Jenkins and NuGet were difficult to work with.
  2. Jenkins pushes the code package to Octopus' package repository
  3. Octopus pushes this code package to each tentacle that is configured to build this code
  4. The tentacle performs any config transforms, runs any custom scripts (PowerShell)
  5. The tentacle deploys the code

Source Control

This was the hardest part actually, and it didn’t have to be. When you’re not working with a release management tool such as Octopus, you get into a certain mindset. I’ve used GitFlow in the past and it’s a great way of handling releases. I’d have separate, identical Jenkins jobs setup to build the code on different branches. The jobs would also push the code to where it needed to go (via robocopy or web deploy). Once a release manager is in the picture, things change a bit. I mentioned earlier the concept of “build once, deploy many.” No more separate Jenkins jobs. I needed to get things down to a “build once” philosophy. In the end, the same exact code is going to get pushed to different environments. How do we do this? This was a hard question for me to solve. I couldn’t find too many blog posts out there or instructions.

Since the team was new to Octopus, we decided to keep things simple and build only off of master. We’d make feature branches off and submit pull requests to be reviewed. Once approved, the code would enter master. Octopus would handle the individual releases and rollbacks effortlessly, so we thought this would be sufficient. Once we got closer and closer to the production deployment, we realized that this wouldn’t be sustainable. We needed to have a separate unstable channel, but we had a problem. I thought we’d need to have a separate Jenkins job: one for unstable and one for stable. This would violate the “build once” principle, and we’d have different code on different environments.

The Jenkins Conundrum

I reached out to the Sitecore Community Slack team to see if anybody’s encountered this before. Kamruz Jaman answered my distress call and shared with me his experiences with Octopus and TeamCity. He gave me a lot of helpful information, and helped me realize my problem. I had forgotten that a Jenkins job could build multiple Git branches! TeamCity handles different build configs a lot more cleanly (my opinion) and it was probably second nature to him, but it blew my mind.

We added two new branches to our workflow: “dev” and “release”. I altered Jenkins' Git config to look for the following regex as a branch specifier:

:^(origin\/master$|origin\/dev$|origin\/release-\d{0,3}\.\d{0,3}\.\d{0,3}$).*

Using GitVersion, I was able to setup different channels in Octopus, so it was able to look at the version name and deploy the package to different environments. Packages with a version “unstable” went to dev, “beta” went to QA, and release versions went to QA + Release.