In part 5 of our blog series, we’ll examine artifact reuse, a key aspect of both twelve-factor DevOps and the twelve-factor app.
Factor 5: Reuse
Put simply, the notion of reuse is that you should not rebuild an artifact between environments, or even jobs, when possible. Artifacts must be immutable; they must not change between deployments, and they should not be changed once deployed. In many ways, this point parallels Unify, but at a higher level.
If you are using container images, and you probably are, you must avoid building different images for each environment. A common anti-pattern is to build the image for the development environment from the dev branch, and then a new image for the QA environment from the master. Depending on your base image, it’s entirely possible that the dev & QA images may have differences even if they’re only built a day apart and the application code itself did not change between merges. Beyond that, you’re building from different commits, which itself is not a good practice.
Instead, you should build the image exactly once and promote it through its lifecycle. The most acceptable way to do that is to create a semantically-versioned release (whether through SCM tags, release branches, or similar) used as the image tag. Therefore from your v2.9.4 release, you get an image with the tag “v2.9.4”. No matter where you run it, it’s the same code, compiled the same way, on the same base.
You may be tempted to reuse artifacts via promotion by retagging them. This is another anti-pattern to be avoided. Tags like ‘dev’, ‘qa’, ‘prod’, or ‘latest’, although convenient, are ambiguous, too easy to overwrite, and likely cause problems. If you had a process where you re-tagged ‘qa’ to ‘prod’ for its production deployment, but accidentally missed the tagging or, worse, re-tagged ‘dev’ to ‘prod’, you would deploy the wrong version. And it could be very difficult to track down exactly which version you released and when. Should you need to revert, you can’t go back to the previous ‘prod’ image without significant work.
Version tagging and reuse of artifacts can be a major change if you’re used to Git Flow due to the back-and-forth of branches for hotfixes, tests, and new features. It also means that you’re likely to cycle through tags more rapidly since any given release may never make it past the development or QA environment.
Similarly, you should take pains to ensure that your image builds don’t contain redundant steps. Suppose you were building two different Java applications. In both, you want a runtime user with a defined home path, and you want to import trusted certificates. Instead of performing these steps in both Dockerfiles, you’d be better off creating a common runtime image with those steps already done. Then, in your two image builds all you need to do is carry out the steps that make them different, like loading the application code itself. This reuse of the base image promotes consistency where possible among different applications.