While it’s true that not having to compile your code on every change certainly lends itself to being able to test and see the benefits of changes sooner, there is one drawback — there are more variables affecting the behaviour of the code you write.
leftpad is a good example of this).
There are ways that mitigate the issue, such as explicitly setting your dependency versions (pinning) and using a
package-lock.json file to ensure that specific versions are pulled in, but there are still some unknowns not covered.
But what happens if the dependencies are installed in a different environment? It’s not uncommon for a developer or even CI machines to differ from production so how can we be 100% certain that the behaviours being tested will be the same in live?
What happens if the installation of the dependencies fails on the deployment to live? Production environments may have stricter firewall rules which block outgoing connections to repositories where dependencies live, meaning downtime during the deployment needs to be retried and fixed.
The easiest solution to these unknowns is to build, test and deploy one version of the code instead of rebuilding it every time.
Build, Test and Deploy the same code
One of the benefits a compiled language such as Java has is that you can produce binaries (an artifact) which doesn’t change after they’re built. This means the same binary that is compiled as part of the developer’s workflow can then be tested and deployed with a high level of certainty of it’s behaviour.
This artifact approach can also be achieved with scripting languages but requires a little more effort to set up as it’s not something you’ll get out of the box for free.
Depending on the architecture of your project the quickest way to introduce an artifact-based approach would be using containerisation like Docker to build an image for a service that can be deployed into the different CI environments for testing, before being promoted into production once those tests pass.
When your architecture doesn’t facilitate containerisation
If your architecture doesn’t allow for containerisation then you can look to introduce your own artifact repository such as Nexus and build up a bundle of your code and it’s dependencies so that those don’t get rebuilt every time you deploy them.
Additionally, you can use Nexus as the repository for your dependencies, giving you greater control over the versions of different packages used to build your app and preventing unexpected updates to those packages from introducing new behaviours.
Running your own artifact repository doesn’t solve issues with differences in environments, which is a benefit of the containerisation approach, but you can get closer to homogenous environments by using configuration management tools to configure production and running the same tooling against the developer and CI environments.
Enabling artifact-based deployments
In order to build an artifact that will run in all environments that make up the code’s path to live, there needs to be a mapping of all the possible configuration values that might be used in the running service that change between environments.
These configuration values may be database connection strings, external service credentials or something as simple as a prefix used when logging to distinguish the environment being run. Once you have a map of these values you should look to replace these with environment variables.
Cloud container platforms such as AWS can provide configuration values using environment variables and Docker Compose has similar functionality so these can be passed in during local development.
If you use a tool such as Kubernetes you can take this approach one step further and use Helm charts to define your configuration and run these against both your local Kubernetes cluster and the production one.
An example of an artifact-based workflow
For this example we’ll be using Docker to create a container for a simple service used in a microservice architecture running in the cloud.
Once a developer has made the changes and is happy that everything is running as intended via low level, quick feedback checks, they run the build script which produces the Docker image.
They can then use the local image to build the system (or part of it) to carry out integration and system tests before uploading the image to a central repository and raising the pull requests.
This Docker image is tagged with the feature branch name and is pushed to the Docker image repository so it can be pulled down by other developers and automated tooling such as a CI server.
The same image uploaded to the repository is then pulled down to run unit, integration & system tests and any other checks that make up the CI pipeline.
Once these checks have passed and the developer’s code has been accepted to be merged into the trunk branch then the image used for the feature branch testing is tagged as the main branch’s image.
Bugs caught as this stage would be at a higher level (most likely system level) as the lower level checks would have already been carried out before the image was pushed.
If you do encounter bugs running those lower level checks then it may be that the tests aren’t set up correctly or the developer didn’t create the image correctly — both things that are better to catch at this stage than during deployment.
With the image then being promoted to main branch the next job is to promote it to production. If you’re practising trunk-based development then the tagging should be seen as the event to trigger this deployment.
Building artifacts like Docker images enables deployment patterns like blue/green where you run both the as-is and to-be production systems side by side and verify the to-be system before switching all your users over to it.
An artifact based deployment approach gives you a high level of certainty that the changes you make to your product/system are going to work and allows for a better understanding of the underlying factors of what causes a system to misbehave.
It may require some changes to the way the team works but by using artifacts in your development workflow you’ll prevent a wide range of possible bugs that could cost developer time and you’ll free up headroom for your developers.