Enhancing Docker build pipelines based on external tags
Introduction
In my last blog post I’ve described how you would use process isolation in your .NET project.
However, it caused a new problem: you would need to manage multiple Docker images for your own images, e.g.:
- myregistry.com/mssql/windows:ltsc2016
- myregistry.com/mssql/windows:ltsc2019
- myregistry.com/mssql/windows:1903
- myregistry.com/mssql/windows:1909
- myregistry.com/mssql/windows:2004
- myregistry.com/mssql/windows:2009
So, instead of having a single image, you would need to have six times as much now. This post will give you some tools and hints on how to solve this problem.
⚠️ This post will talk about Microsoft and .NET, however the solution is platform and language agnostic. It can be used whether you are using .NET, Python, Linux, FreeBSD, Java etcetera. It does not matter. As long as you want to query a Docker registry API to base your images on.
Retrieving the available Docker tags
First, you would need to know what tags are available so you can populate your list. You could (and this is still a very valid solution!) have this array of tags somewhere locally or hardcoded in your build script. This will depend on your problem and your infrastructure. However, I assume you don’t have anything in place at all yet.
For this very reason (it might be overkill, but I like nice things 😊), I’ve developed an API (which is publicly available on https://api.gerwim.com/dockertags
) which you can query to list the available tags for a specific image.
For now, it supports both the Docker Hub and the Microsoft Container Registry. You can also run this API yourself. It’s available as a Docker image and source code is on GitHub.
So, without further ado, here’s an example:
curl 'https://api.gerwim.com/dockertags/v1/tags' \
--header 'registry: mcr.microsoft.com' \
--header 'imageName: dotnet/framework/aspnet' \
--header 'searchRegex: ^4\.8-windowsservercore-(\d{4}|ltsc\d{4})'
will return:
{
"name": "dotnet/framework/aspnet",
"tags": [
"4.8-windowsservercore-1803",
"4.8-windowsservercore-1903",
"4.8-windowsservercore-1909",
"4.8-windowsservercore-2004",
"4.8-windowsservercore-2009",
"4.8-windowsservercore-ltsc2016",
"4.8-windowsservercore-ltsc2019"
]
}
The curl
command is just a basic HTTP GET. The Powershell equivalent would be:
Invoke-RestMethod -Uri "https://api.gerwim.com/dockertags/v1/tags" `
-Headers @{ `
"registry" = "mcr.microsoft.com"; `
"imageName" = "dotnet/framework/aspnet"; `
"searchRegex" = "^4\.8-windowsservercore-(\d{4}|ltsc\d{4})" `
}
- It does a HTTP GET to
api.gerwim.com
- Sets three headers:
registry
,imageName
andsearchRegex
registry
: can be (as of writing) be hub.docker.com
or mcr.microsoft.com
imageName
: is the image name on the registry
searchRegex
: is optional (defaults to (.*)
) but will filter the tags based on the regex
Building an ASP.NET image based on Microsoft’s tags
Now, we have the list of available tags we can base our custom image on. Since Microsoft will maintain their list and deprecate older (no longer supported) Windows versions, so we do not have to worry about that.
Assuming you have a custom image, based on 4.8-windowsservercore
, your FROM
in your dockerfile
would look something like this:
FROM mcr.microsoft.com/dotnet/framework/aspnet/4.8-windowsservercore:ltsc2019
We will change this to (if you read my last blog post, this might seem familiar to you 😉):
ARG WINDOWS_VERSION
FROM mcr.microsoft.com/dotnet/framework/aspnet/4.8-windowsservercore:$WINDOWS_VERSION
This means the Docker build command accepts an additional parameter, WINDOWS_VERSION
. We can use this argument in our build script to define the tag.
To build and push the Docker images to your registry, we can use the following Powershell script (e.g. build.ps1
):
param ([Parameter(Mandatory)]$imageName)
# Retrieve tags from the public API
$response = Invoke-RestMethod -Uri "https://api.gerwim.com/dockertags/v1/tags"-Headers @{"registry" = "mcr.microsoft.com"; "imageName" = "dotnet/framework/aspnet"; "searchRegex" = "^4\.8-windowsservercore-(\d{4}|ltsc\d{4})"}
foreach ($tag in $response.tags) {
$split = $tag.Split('-'); # split the response (e.g. 4.8-windowsservercore-2009)
$version = $split[$split.Count - 1]; # get the last string from the array (e.g. 2009)
Write-Host [$version] Building image
$imageNameWithVersion = $imageName.Replace("WINDOWS_VERSION", $version); # replace WINDOWS_VERSION by our version, e.g. 2009
Start-Process -FilePath docker -ArgumentList "build --pull -t $imageNameWithVersion --build-arg WINDOWS_VERSION=$version ." -Wait -NoNewWindow
# Push image
Start-Process -FilePath docker -ArgumentList "push $imageNameWithVersion" -Wait -NoNewWindow
}
To run the above script:
powershell -File build.ps1 -imageName "myregistry.com/aspnet-image/windows/WINDOWS_VERSION:latest"
which will build eight images (as of writing):
- myregistry.com/aspnet-image/windows/1803:latest
- myregistry.com/aspnet-image/windows/1903:latest
- myregistry.com/aspnet-image/windows/1909:latest
- myregistry.com/aspnet-image/windows/2004:latest
- myregistry.com/aspnet-image/windows/2009:latest
- myregistry.com/aspnet-image/windows/ltsc2016:latest
- myregistry.com/aspnet-image/windows/ltsc2019:latest
Real life example using GitLab CI
A real life example using GitLab CI (which is amazing!) is quite easy, but it will depend on your configuration. In my case:
- I have a virtual machine running Windows Server Core 20H2.
- Has Docker along with Hyper-V (nested virtualization) installed (which is required to build older versions than 20H2 (which is Windows release ID
2009
). For more information, see the compatibility matrix).
My .gitlab-ci.yml
file:
stages:
- prepare
- build
# generate a dynamic config file
generate-config:
stage: prepare
script: powershell -File createconfig.ps1 -imageName "%CI_REGISTRY_IMAGE%/windows/WINDOWS_VERSION:%CI_COMMIT_REF_SLUG%"
artifacts:
paths:
- generated-config.yml
# build the child pipeline (this part will create the actual Docker images)
build-images:
stage: build
trigger:
include:
- artifact: generated-config.yml
job: generate-config
My createconfig.ps1
file (which is a customized version of the build.ps1
script above):
param ([Parameter(Mandatory)]$imageName)
$response = Invoke-RestMethod -Uri "https://api.gerwim.com/dockertags/v1/tags"-Headers @{"registry" = "mcr.microsoft.com"; "imageName" = "dotnet/framework/aspnet"; "searchRegex" = "^4\.8-windowsservercore-(\d{4}|ltsc\d{4})"}
$configTemplate = @"
build-WINDOWS_VERSION:
stage: build
before_script:
- docker login -u "%CI_REGISTRY_USER%" -p "%CI_REGISTRY_PASSWORD%" %CI_REGISTRY%
script:
- BUILD_COMMAND
- PUSH_COMMAND
"@
foreach ($tag in $response.tags) {
$split = $tag.Split('-'); # split the response (e.g. 4.8-windowsservercore-2009)
$version = $split[$split.Count - 1]; # get the last string from the array (e.g. 2009)
Write-Host [$version] Generating config
# Start replacing magic strings
$imageNameWithVersion = $imageName.Replace("WINDOWS_VERSION", $version);
$template = $configTemplate.Replace("WINDOWS_VERSION", $version);
$template = $template.Replace("BUILD_COMMAND", "docker build --pull -t $imageNameWithVersion --build-arg WINDOWS_VERSION=$version .");
$template = $template.Replace("PUSH_COMMAND", "docker push $imageNameWithVersion");
# Append the template variable (with the replaced strings) to a local file
Add-Content -Path generated-config.yml -Value $template
}
This will result in a pipeline like:
The most important part is the magic string WINDOWS_VERSION
in the imageName
parameter. This string, WINDOWS_VERSION
, is replaced in the Powershell file and passed along as a Docker build argument to fetch and use the correct base image.
Of course, if you use a different CI solution (or don’t use Windows at all), you will need to use a different solution, but the gist is:
- Get the available Docker tags using a HTTP GET
- Build each image using the correct base version, by using an argument in the
FROM
line, passed by--build-arg
. E.g.:
Dockerfile:
ARG WINDOWS_VERSION
FROM mcr.microsoft.com/dotnet/framework/aspnet/4.8-windowsservercore:$WINDOWS_VERSION
Build command:
docker build -t myimage --build-arg WINDOWS_VERSION=ltsc2019 .
What about non Microsoft or .NET?
In the example above the argument WINDOWS_VERSION
is used, but it could be anything and has nothing to do with Microsoft or Windows itself. It’s called WINDOWS_VERSION
for clarity.
This solution could be used to build your custom image based on anything. E.g. build against all available Ubuntu LTS tags:
curl 'https://api.gerwim.com/dockertags/v1/tags' \
--header 'registry: hub.docker.com' \
--header 'imageName: ubuntu' \
--header 'searchRegex: (^1\d*[68]\.04$)|(^2\d*[02468]\.04$)'
returns:
{
"name": "ubuntu",
"tags": [
"16.04",
"18.04",
"20.04"
]
}
which would be used in your build script.
Conclusion
Hopefully you gained insight on how to build your Docker images based on external tags so you can further enhance your build pipeline(s) 🎉.
Have any questions or have feedback? Let me know on Twitter!