DevOps for Brownfields - The code

It's easy to decide you want to get away from long, painful, weekend deployments, but it takes much more than just a decision. For legacy code (whatever your definition of legacy might be), there are likely some things that prevent you from deploying fast and frequently.

In my previous Brownfields posts I talked about DevOps as a Culture and changing the dev process. In this post, I'll talk about some practical changes you can make to your codebase and how you work with it. These changes will make it easier to deploy regularly and without fuss.

One codebase

An application should only have a single codebase, and only one branch that you deploy to production from. There are two common practices that get in the way of this:

  1. Separate copies of code for different scenarios
  2. Long-lived branches

Code for different scenarios

This is a surprisingly common practice. I've worked with organisations in the past that had different, almost-the-same codebases for different flavours of the application. They could be separated based on country, set of features, or individual customers.

The common thread is that at some point there was a diversion based on two seemly unreconcilable differences. Rather than a larger effort to refactor, two versions emerged. The result is that it's incredibly difficult to apply changes and bug fixes to all versions consistently and safely.

Long-lived branches

It's also common to separate versions of code into branches. This can be a way of implementing the previous practice (code for different scenarios) that gives the feeling that it's the same codebase. It can make changes a little easier to propagate across versions, provided the versions are similar.

It's also common to have separate long-lived branches for different environments. The team writes code on a dev branch which then gets merged into staging for a test deployment, then to production for production deployment.

In my opinion, this is a mistake. Each time you need to deploy, you're merging and ending up with a brand new codebase. That means when you deploy, it's the first time you've ever deployed that code.

If a successful deployment relies on a clean merge, that should scare you!

So how do you get to a single codebase? Well, it's likely to require (sometimes significant) refactoring.

Identify and isolate variations

There will invariably be variations between deployments of your application. Whether they're environmental (test vs production) or otherwise (e.g. customer configuration).

It's important to find these differences and isolate them. Rather than if statements scattered throughout your code, properly independent classes or assemblies make it much easier and safer to make changes to your software without unexpected side-effects.

Dependency injection is great for this, but of course you don't want to have to change the code that resolves your dependencies each time you compile for a different scenario. You need to be able to change your configuration without recompiling.

Externalise configuration

To avoid having to recompile for each different deployment scenario, it's important that your configuration is outside the compiled application.

That means you can deploy the same binaries and compiled code, but change the configuration based on the environment you're in, or the customer you're deploying to.

It's really up to you where you keep your configuration. .NET developers tend to keep config settings in .config files, but environment variables or even external databases are reasonable alternatives. As long as you don't have to recompile to deploy to a new location!

Get your versioning right

Finally, it's extremely important that you can easily distinguish between different compiled versions of your software. The usual way to do this is with versioning.

Because there's only a single codebase, and we only deploy from the master branch, we only need one version number. At least for production candidates.

Following Semantic Versioning is an awesome way to manage your versions, but if you can't do that, you just need to follow two rules:

  1. Each build has a new version number
  2. New code has a higher version number

These two rules mean that given any two builds, you can easily determine whether they're the same, and if not, which is newer. Remember, we only have one branch, so we're comparing like with like!

Summary

There are changes you can make to your code and the way you work with it that will make deployments easier.

  1. Get to one codebase
  2. Identify and isolate variations
  3. Externalise your configuration
  4. Get your versioning right

Once deployments are easier, you can feel confident doing them more often, which will ultimately lead to a better DevOps story!

Damian Brady

I'm an Australian developer, speaker, and author specialising in DevOps, MLOps, developer process, and software architecture. I love Azure DevOps, GitHub Actions, and reducing process waste.

--