Ensuring backwards compatibility in deployments by leveraging git tags
Overview
I recently came across a blog post on injecting variables into Golang at build time and that gave me the idea for this blog post. Automating versioning and ensuring backwards compatibility.
GitHub repository and sample application
You can find the code for this application here, on my GitHub
Git tags and versioning
This blog post assumes some basic understanding of the git version control system. For anyone new to it you can probably get by with thinking that commits
are incremental changes to the software and tags
are user-created labels that reference a specific commit
.
Versioning using git tags is a pretty common practice. The way I like to have my automation setup is the main branch of a repository is wired to auto-deploy any changes to a staging/QA environment for testing, while any tags that meet certain criteria are wired to auto-deploy to a production environment. For example tags could be in vMajor.Minor.Revision
or release/vMajor.Minor.Revision
format.
For this blog post we’re using the format of vMajor.Minor.Revision
.
Injecting the version during the build
The blog post I linked from Alex Ellis does a good job explaining injecting variables at build time so I don’t go too deep into that.
The basic gist of what we’re doing is creating a string variable named APIVersion
1 and then injecting the value we want that to be in when we build the application, in this case we want it to be v#
where #
is the major version number of our application.
To build our application we use go build -ldflags "-X main.APIVersion=[versionhere]"
, so for example for v1
we would use go build -ldflags "-X main.APIVersion=v1"
.
Ensuring backwards compatibility
With a properly versioned API it’s almost trivial to maintain and ensure backwards compatibility, but it does take a little thought during the initial setup to make everything smooth and easily maintainable.
For this blog post we’re using the format of http://domain.com/[apiVersion]/...
, where apiVersion
is the vMajor
version from our git tags.
In the sample API I wrote for this blog post you can see I’ve created several git tags, the first being v1.0.0
and it references the initial commit to the repo. We’ve created the initial git tag and from here on out we can’t make any changes that would break applications relying on the format and structure of the API URLs and JSON response.
However, we do have some wiggle room here. Since we’re returning JSON we can add new fields to the JSON output without breaking backwards compatibility, the next git tag v1.0.1
does this and adds a Description field in the JSON response.
We can also add new API endpoints because any application created prior to the new additions will still work as intended, it just wont have the new functionality. This keeps backwards compatibility while adding new features. The next git tag of v1.0.2
does this by allowing us to append /itemNumber
to the end of the /items
endpoint to return a specific item.
If you look at the commit referenced with tag v1.0.2
you’ll notice that we do slightly change a legacy endpoint from /items
to /items/
, this is okay because /items
now automatically redirects to /items/
. Even if a legacy application is using the URL without the slash it will still work as intended, so backwards compatibility is still ensured.
Making a breaking change
The next versioning tag in the sample API makes a breaking change, we’re altering the structure of the JSON response for the /items/
endpoint, returning [{item1},{item2},...]
instead of {"Items": [{item1},{item2},...]}
.
Since this is a breaking change any application that relies on the previous structure will stop working, to prevent this we cannot deploy the new API version to the previous endpoint, and this is where versioning comes in handy.
The previous versions of the API were tagged with v1.0.x
and use a url format of /v1/...
, with our current setup to maintain backwards compatibility all we have to do is bump the major version number of our git tags, so our new version becomes v2.0.0
instead of v1.0.3
Automating the build process
Given the vast amount of tools for automation and CI/CD setups I’m not going to go in depth into any particular software but instead speak mostly in generic process outline here.
The first step to automating is getting our latest git tag, cutting it down from vMajor.Minor.Revision
to vMajor
, and injecting that into our build process. The simplest way to do this is something along the lines of this: git describe --abbrev=0 --tags | cut -d'.' -f1
.
Now that we have our latest git tag we save it as a variable in our automation software of choice and inject it into the build with go build -ldfags "-X main.APIVersion=${gitTagVersion}"
.
From here we have our API versioned, built to use /[apiVersion]/...
for our endpoint URLs, and now it can be deployed.
Conclusion
We now have an RESTful API with automated versioning based on tags in our code revisioning system, and build process that ensures backwards compatibility.
There are a couple of caveats though, for example once you break compatibility and bump the vMajor
version of a git tag you can’t edit a previous version on the main branch of the repository. So for example once we went from v1.0.3
to v2.0.0
we couldn’t go back and create a v1.0.4
. To do this we would need to create a new branch off the commit that v1.0.3
references, make our changes, and create a tag there for v1.0.4
. This isn’t a huge issue since once the major version is bumped up there should be minimal work put into the legacy version, but it is something to be aware of.
Another caveat is you can’t run multiple versions of the API from the same address and port, this is because only one service can listen on a port at a time. To get around this your CI/CD pipeline should include a loadbalancer that passes traffic based on the /vMajor/
portion of the URL. For local testing you can setup a very lightweight nginx service, run the versions on different ports, and have nginx load balance from localhost:80 based on the request URL and send it to the ports your services are listening on.