Using docker multi-stage to build windows images

Hi all! My name is Andrey and I work as a DevOps engineer at Exness in the development team. My main activity is related to the assembly, deployment and support of applications in docker under the Linux operating system (hereinafter referred to as the OS). Not so long ago, I had a task with the same activities, but Windows Server and a set of projects in C ++ became the target OS of the project. For me, this was the first close interaction with docker containers under Windows OS and, in general, with C ++ applications. Thanks to this, I got an interesting experience and learned about some of the intricacies of containerizing applications in Windows.

Using docker multi-stage to build windows images

In this article I want to tell you what difficulties I had to face, how I managed to solve them. I hope this will be useful for your current and future needs. Enjoy reading!

Why containers?

The company has an existing infrastructure for the Hashicorp Nomad container orchestrator and related components Consul and Vault. Therefore, application containerization was chosen as a unified method for delivering a turnkey solution. Since the project infrastructure has docker hosts with Windows Server Core OS versions 1803 and 1809, it is necessary to build separately versions of docker images for 1803 and 1809. In version 1803, it is important to remember that the revision number of the build docker host must match the revision number of the base docker image and the host where the container from this image will be launched. Version 1809 does not have this disadvantage. Read more here.

Why multistage?

Engineers of development teams have no or very limited access to build hosts, there is no way to quickly manage the set of components for building an application on these hosts, for example, install additional toolset or workload for Visual Studio. Therefore, we made a decision - to install all the components necessary for building the application in the assembly docker image. If necessary, you can quickly change only the dockerfile and run the pipeline for creating this image.

From theory to business

In an ideal docker multi-stage build of an image, the environment for building the application is prepared in the same dockerfile script as the build of the application itself. But in our case, an intermediate link was added, namely, the step of pre-creating a docker image with everything necessary to build the application. This was done because I wanted to use the docker cache feature to reduce the installation time for all dependencies.

Let's take a look at the main points of the dockerfile script for building this image.

To create images of different OS versions in the dockerfile, you can define an argument through which the version number is passed during assembly, and it is also the base image tag.

A complete list of Microsoft Windows Server image tags can be found here.

ARG WINDOWS_OS_VERSION=1809
FROM mcr.microsoft.com/windows/servercore:$WINDOWS_OS_VERSION

Default commands in instructions RUN inside the dockerfile on Windows are executed in the cmd.exe console. For the convenience of writing scripts and expanding the functionality of the commands used, we will redefine the command execution console on Powershell through the instruction SHELL.

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"]

The next step is to install the chocolatey package manager and the necessary packages:

COPY chocolatey.pkg.config .
RUN Set-ExecutionPolicy Bypass -Scope Process -Force ;
    [System.Net.ServicePointManager]::SecurityProtocol = 
    [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 ;
    $env:chocolateyUseWindowsCompression = 'true' ;
    iex ((New-Object System.Net.WebClient).DownloadString( 
      'https://chocolatey.org/install.ps1')) ;
    choco install chocolatey.pkg.config -y --ignore-detected-reboot ;
    if ( @(0, 1605, 1614, 1641, 3010) -contains $LASTEXITCODE ) { 
      refreshenv; } else { exit $LASTEXITCODE; } ;
    Remove-Item 'chocolatey.pkg.config'

To install packages using chocolatey, you can simply pass them in as a list, or install one at a time if you need to pass unique options for each package. In our situation, we used a manifest file in XML format, which contains a list of required packages and their parameters. Its content looks like this:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="python" version="3.8.2"/>
  <package id="nuget.commandline" version="5.5.1"/>
  <package id="git" version="2.26.2"/>
</packages>

Next, we install the application build environment, namely, MS Build Tools 2019 is a light version of Visual Studio 2019, which contains the minimum required set of components for compiling code.
For full-fledged work with our C ++ project, we need additional components, namely:

  • Workload C++ tools
  • Toolset v141
  • Windows 10 SDK (10.0.17134.0)

You can install an advanced set of tools in automatic mode using a configuration file in JSON format. Configuration file content:

A complete list of available components can be found on the documentation site Microsoft Visual Studio.

{
  "version": "1.0",
  "components": [
    "Microsoft.Component.MSBuild",
    "Microsoft.VisualStudio.Workload.VCTools;includeRecommended",
    "Microsoft.VisualStudio.Component.VC.v141.x86.x64",
    "Microsoft.VisualStudio.Component.Windows10SDK.17134"
  ]
}

The dockerfile executes the installation script, and for convenience, the path to the build tools executable files is added to the environment variable PATH. It is also advisable to remove unnecessary files and directories in order to reduce the size of the image.

COPY buildtools.config.json .
RUN Invoke-WebRequest 'https://aka.ms/vs/16/release/vs_BuildTools.exe' 
      -OutFile '.vs_buildtools.exe' -UseBasicParsing ;
    Start-Process -FilePath '.vs_buildtools.exe' -Wait -ArgumentList 
      '--quiet --norestart --nocache --config C:buildtools.config.json' ;
    Remove-Item '.vs_buildtools.exe' ;
    Remove-Item '.buildtools.config.json' ;
    Remove-Item -Force -Recurse 
      'C:Program Files (x86)Microsoft Visual StudioInstaller' ;
    $env:PATH = 'C:Program Files (x86)Microsoft Visual Studio2019BuildToolsMSBuildCurrentBin;' + $env:PATH; 
    [Environment]::SetEnvironmentVariable('PATH', $env:PATH, 
      [EnvironmentVariableTarget]::Machine)

At this stage, our image for compiling the C ++ application is ready, and you can proceed directly to creating a docker multi-stage build of the application.

Multi-stage in action

As an assembly image, we will use the created image with all the tools on board. As in the previous dockerfile script, we will add the ability to dynamically specify the version number / tag of the image for the convenience of reusing the code. It is important to add a label as builder to the assembly image in the instructions FROM.

ARG WINDOWS_OS_VERSION=1809
FROM buildtools:$WINDOWS_OS_VERSION as builder

Now it's time to build the application. Everything is quite simple here: copy the source code and everything connected with it, and start the compilation process.

COPY myapp .
RUN nuget restore myapp.sln ;
    msbuild myapp.sln /t:myapp /p:Configuration=Release

The final step in creating the final image is to specify the base image of the application, where all compilation artifacts and configuration files will be located. To copy compiled files from an intermediate build image, you must specify the parameter --from=builder in the instructions COPY.

FROM mcr.microsoft.com/windows/servercore:$WINDOWS_OS_VERSION

COPY --from=builder C:/x64/Release/myapp/ ./
COPY ./configs ./

Now it remains to add the necessary dependencies for our application to work and specify the launch command through the instructions ENTRYPOINT or CMD.

Conclusion

In this article, I showed you how to create a full-fledged C++ application compilation environment inside a container on Windows and how to use docker multi-stage builds to create full-fledged images of our application.

Source: habr.com

Add a comment