When you use containers for your application, one of the things you need to think about is how to move (aka promote) the container images you generate across different environments.
In this series, I will explore different ways to do so... with the help of Azure DevOps
In this article, the first of the series, we will explore the simplest and most used way (at least in my experience) to promote a Container image across different environments.
We will use a non-environment-specific registry as a base repo, and we will push the image to this registry during the build phase. Then, we will take the image and push it to some environment-specific registries in the release phases as we move forward.
As step "zero", we want to create our container image with all we need for our application to function properly. I will not focus on the image creation at this time.
There are two ways to "install" your code onto a container image: a multi-step Dockerfile which compiles your code AND create the image, or compiling the application outside the container and then simply install it into the image.
While the first approach ensures (in theory) even more immutability, the build process is often way less verbose and more difficult to tune.
I personally prefer using the CI capabilities of Azure DevOps to build the application and then create the image with the result. Let me know if you are interested in this approach so I can write another post about it.
Whatever your approach is, you will at the end have your container image created.
As I've mentioned before, we need to push it to an environment-neutral container registry. Even tho we could do this in the release process as well, I normally consider this as part of the build, simply because you want your build process to give you something usable. In our case, usable means having the image ready to be processed, therefore we'd need to have it in the registry.
As you can see, I simply create the image and then I push it to the registry.
Actually Azure DevOps has a task that can combine both operation, called buildAndPush, but for clarity I prefer keeping the two commands separated.
Ok, now we have our container image, and we have pushed it to the general container registry.
The temptation here would be to pull the image from that very registry and ship it to our server. NO, we won't do that. Instead, we are going to create a Release Pipeline which will use the container registry as input.
To do so, simply click on the "Add an artifact" button (after creating a new release pipeline), and select "Azure Container Registry" (or Docker Hub, if you want to use that). Just few parameters to set, and you're ready to go.
Also, be sure to enable the Continuous Delivery flag so this release pipeline can run every time a new container image is pushed to the registry.
Now, let's talk about the next steps. Let's say you have three different environments: Dev, Test, and Prod. First thing to do is to create three Stages in the Pipeline.
Then let's edit the Dev stage. We need to:
- Pull the image from the generic registry
- Change it's name so it can be pushed to the Dev registry
- Push it to the Dev registry
To pull the image from the generic container registry we need to do a little trick. The pull command, in fact, is not directly embedded in the v2 of the Docker task.
So we need to manually type pull as command name, and insert the registry name and the image name in the arguments box.
I have defined variables for the names so I can reuse them across the different environments.
Next we need to tag the image differently, to change the name of the registry. This is because if you have to push an image to a certain registry, you need the image full name as "registryName/ImageName", where registryName is the full qualified domain in case of anything different from Docker Hub (for Azure Container Registry, it would be something like myregistryname.azurecr.io
Again, the tag command is not embedded in the v2 of the task so we need to use the arguments box. The use of variables is optional but I recommend it, it just makes everything easier to automate and templatize.
Last step, we need to push the image to the new registry.
This time, the push command is fully supported so no need for the custom arguments!
And we are done for Dev!
Now we can replicate the same process to the other environments, just changing the source and target registries.
Ideally when pushing to the environment-specific registry you should have a mechanism to notify your target host service (App Service, AKS, Container Instances, etc) of the new image so the deployment can be executed.
And of course you probably want to set some Release Gates or Approvals for deploying to Test and Prod.
This process is probably the most used, however it is not my favorite.
First of all, you need more registries than environments.
Second, and probably more important, it doesn't ensure 1:1 mapping between build and release. And this means that the image you build and the one you deploy might not be the same.
If someone does any change to the images in the generic registry, then you will in fact end up having deployed a different version than the one your CI pipeline generated.
Another issue here is that you cannot directly reference the Build number, or any other parameter that comes from the Build, because you CI and your CD pipelines are not directly related. This may not be a problem for everyone, but I like to version my images using the BuildId. So this won't work for me.
This is why in the next article of the series we will explore another approach, using the Build Artifact capabilities of Azure DevOps to get a fully immutable image to be promoted across environments.