Within the realms of software development, one of the things I try and push the most to anyone I talk to is the accurate and unwavering adherence to phenomenally good version control.
In this post, I want to show you how I achieved git tag version binding into the application scope, and provide you with a working example repository.
To take just a tiny part from the 12FactorApp:
Any change must create a new release.
12FactorApp – Section V, Build Release Run – https://12factor.net/build-release-run
This to me is such a simple and important concept to follow, and crucial for the accurate, knowledgeable and declarative release of project elements.
If you have a book, and you tear out a page, it’s no longer the same book – add a chapter – it's no longer the same book. The same is true of our code. Even a typo – new release; it is not the same as it was before.
As someone involved in the release and support of production applications, I want to know at all times what version of anything is running; so should an error occur I can pin it back to an absolute position in time within that codebase history – without this, you are stabbing in the dark and have no real way of knowing if what you are looking at is what broke or not.
When applications are containerised, they are bundled into Docker containers (other containerisation platforms are available); but your dependencies, assets and codebase are all bundled together into a thing and released.
We can then tie our pipelining into this and add build our images based on git tags, and we have a solid link from code to image to version control to tag… nice.
It is also of course nice and easy to put a version number in your config somewhere, or an environment/config variable that the application reads and can be queried by the application to declare its version.
What happens when you forget to increment it on change?
What happens if you mismatch the version in config to the tag?
What happens if you completely typo the tag in either – that means a new build right!?
There is now a new problem occurring that our inner image version is not linked to the version control we are defining at our git tag level – how can we tie these together a little better?
How can we get the version into the application scope and the image scope from the git commit scope?
A Solution
I’ve created a simple example repository here:
https://gitlab.com/danstreeter/docker-image-tag-versioning/
which targets this problem within a simple GitLab CI Pipeline and I think provides quite an easy-to-use solution that can be transposed to similar technologies offering similar features.
Docker Build Args
Within it, I pass docker --build-args to the build process based on the pipeline provided $CI_COMMIT_TAG variables. This variable is set to the tag that was pushed.
This gets the tag into the build scope, which can then be used to set docker image metadata as follows:
# Declare and default our image version to 'invalid' for checking later
ARG IMAGE_VERSION=invalid
# Set the image version to the docker metadata
LABEL IMAGE_VERSION=${IMAGE_VERSION}
Then we can get this into the application/container scope by passing it through:
# Write the version to a file which is read by entry point
RUN echo $IMAGE_VERSION > /IMAGE_VERSION
Why not use ENV?
This is a great question, and something I was doing prior to thinking about it a little. Using ENV would expose overriding at runtime, anyone would be able to state:
docker run -e IMAGE_VERSION=1.5.3 ...
And trick any image into thinking that it was running that version, negating all our work to try and fix it throughout the different scopes. Writing it to a file means that someone is going to have to go about some real trickery that will stand out like a sore thumb to any reviewer of those changes.
Pickup in entrypoint.sh
Within the entry point script I then collect the IMAGE_VERSION, echo it out for logging and also check that it was not the default (invalid) to ensure that only images with a valid version from build time can be run; a tiny little more protection against running imperfect images:
IMAGE_VERSION=$(cat /IMAGE_VERSION)
echo "Image Version [$IMAGE_VERSION]"
if [[ $IMAGE_VERSION == "invalid" ]]
then
echo "The version for this image has not been defined at built time, unable to guarantee results without knowing which version you are running. Exiting"
exit 1
fi
Application Scope
Now that the image version defined by the git tag is set to the image metadata, and within the application scope, we can now collect it and use it any way we desire within our application by reading that file.
There is no longer any need to define that version within code as it's being pushed in externally.
I won’t go into specific details on how specific applications can achieve this as there will be a thousand different ways, and there is also a need to handle ‘between tag’ versioning – where developers may be working day to day and a tag just doesn’t exist, how this is handled is not covered here as is meant for more release management than a day to day development version tracking approach.
Closing thoughts
There are of course; with all things within the bubble of software where this approach may not be suitable, may not work or just isn’t a desirable outcome. Where releases might need to depend on latest for example (not that I advocate for this), or releases need to be based on the commit hash (but this approach could work for that) or other things I have not thought about whilst writing this post. It was written to solve a specific problem I have encountered; this problem may not be the same as yours, but what I’ve got here might help you achieve the solution for your problem!
But that’s what makes the software industry so great, not only are there many ways to skin the cat; there are many cats and sometimes those cats are sheep and sometimes frogs! No two problems are always the same.
Oh, and for anyone screaming ‘Hold on, this isn’t irrefutable!!!’ – well no, there are always ways around everything, but to do so in this case I think will be a pretty determined act, and easy to pick out at the code review / merge request point. I’m trying to avoid common pitfalls in this approach, providing a simple way that once done, shouldn’t need to be touched again.
Image Attribution: Cargo Container Image by Erwan Hesry
Originally posted on Dan Streeters personal blog 10th June 2022