Всем привет! Меня зовут Андрей, и я работаю DevOps инженером в компании Exness в команде разработки. Моя основная деятельность связана со сборкой, деплоем и поддержкой приложений в docker под операционной системой Linux (далее — ОС). Не так давно у меня появилась задача с теми же активностями, но в качестве целевой ОС проекта стала Windows Server и набор проектов на C++. Для меня это было первое плотное взаимодействие c docker контейнерами под ОС Windows и в целом с приложениями на C++. Благодаря этому я получил интересный опыт и узнал о некоторых тонкостях контейнеризации приложений в ОС Windows.
В этой статье хочу рассказать, с какими трудностями мне пришлось столкнуться, каким образом их удалось решить. Надеюсь, это окажется полезным для решения ваших текущих и будущих задач. Приятного чтения!
Почему контейнеры?
В компании есть существующая инфраструктура оркестратора контейнеров Hashicorp Nomad и связанных компонентов — Consul и Vault. Поэтому контейнеризация приложений была выбрана как унифицированный метод доставки готового решения. Так как в инфраструктуре проекта имеются docker-хосты с версиями ОС Windows Server Core 1803 и 1809, то необходимо собирать отдельно версии docker-образов для 1803 и 1809. В версии 1803 важно помнить о том, что номер ревизии сборочного docker-хоста должен совпадать с номером ревизии базового docker-образа и хоста, где контейнер из этого образа будет запущен. Версия 1809 лишена такого недостатка. Подробнее можно прочитать здесь.
Почему multi-stage?
У инженеров команд разработки доступ к сборочным хостам отсутствует или сильно ограничен, нет возможности оперативно управлять набором компонентов для сборки приложения на этих хостах, например, установить дополнительный toolset или workload для Visual Studio. Поэтому мы приняли решение — все необходимые для сборки приложения компоненты установить в сборочный docker-образ. При необходимости можно достаточно быстро изменить только dockerfile и запустить пайплайн создания этого образа.
От теории к делу
В идеальной docker multi-stage сборке образа подготовка окружения для сборки приложения происходит в том же dockerfile скрипте, что и сборка самого приложения. Но в нашем случае было добавлено промежуточное звено, а именно, шаг предварительного создания docker-образа со всем необходимым для сборки приложения. Так сделано, потому что хотелось использовать возможность docker cache, чтобы сократить время установки всех зависимостей.
Давайте разберем основные моменты dockerfile скрипта для формирования этого образа.
Для создания образов разных версий ОС в dockerfile можно определить аргумент, через который при сборке передаётся номер версии, и он же тэг базового образа.
Полный список тэгов образов Microsoft Windows Server можно найти здесь.
ARG WINDOWS_OS_VERSION=1809
FROM mcr.microsoft.com/windows/servercore:$WINDOWS_OS_VERSION
По умолчанию команды в инструкции RUN
внутри dockerfile в ОС Windows выполняются в консоли cmd.exe. Для удобства написания скриптов и расширения функционала используемых команд переопределим консоль исполнения команд на Powershell через инструкцию SHELL
.
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"]
Следующим шагом устанавливаем пакетный менеджер chocolatey и необходимые пакеты:
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'
Чтобы установить пакеты, используя chocolatey, можно просто передать их списком или же установить по одному в том случае, если необходимо передать уникальные параметры для каждого пакета. В нашей ситуации мы использовали манифест файл в формате XML, в котором указан список необходимых пакетов и их параметров. Его содержимое выглядит так:
<?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>
Далее мы устанавливаем среду сборки приложения, а именно, MS Build Tools 2019 — это облегченная версия Visual Studio 2019, которая содержит в себе минимально необходимый набор компонентов для компиляции кода.
Для полноценной работы с нашим C++ проектом нам потребуются дополнительные компоненты, а именно:
- Workload C++ tools
- Toolset v141
- Windows 10 SDK (10.0.17134.0)
Установить расширенный набор инструментов в автоматическом режиме можно при помощи файла конфигурации в формате JSON. Содержимое файла конфигурации:
Полный список доступных компонентов можно найти на сайте документации 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"
]
}
В dockerfile выполняется скрипт установки, и для удобства добавляется путь к исполняемым файлам build tools в переменную окружения PATH
. Также желательно удалить ненужные файлы и директории, чтобы уменьшить размер образа.
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 Studio\Installer' ;\
$env:PATH = 'C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin;' + $env:PATH; \
[Environment]::SetEnvironmentVariable('PATH', $env:PATH, \
[EnvironmentVariableTarget]::Machine)
На этом этапе наш образ для компиляции C++ приложения готов, и можно приступать непосредственно к созданию docker multi-stage сборке приложения.
Multi-stage в действии
В качестве сборочного образа будем использовать созданный образ со всем инструментарием на борту. Как и в предыдущем dockerfile скрипте, добавим возможность динамически указывать номер версии/ тэга образа для удобства переиспользования кода. Важно добавить метку as builder
к сборочному образу в инструкции FROM
.
ARG WINDOWS_OS_VERSION=1809
FROM buildtools:$WINDOWS_OS_VERSION as builder
Настал черед сборки приложения. Здесь все достаточно просто: скопировать исходный код и все, что с ним связано, и запустить процесс компиляции.
COPY myapp .
RUN nuget restore myapp.sln ;\
msbuild myapp.sln /t:myapp /p:Configuration=Release
Завершающий этап создания конечного образа — указание базового образа приложения, где будут располагаться все артефакты компиляции и файлы конфигурации. Для копирования скомпилированных файлов с промежуточного сборочного образа надо указать параметр --from=builder
в инструкции COPY
.
FROM mcr.microsoft.com/windows/servercore:$WINDOWS_OS_VERSION
COPY --from=builder C:/x64/Release/myapp/ ./
COPY ./configs ./
Теперь остается добавить необходимые зависимости для работы нашего приложения и указать команду запуска через инструкции ENTRYPOINT
или CMD
.
Заключение
В этой статье я рассказал, как создать полноценную среду компиляции C++ приложений внутри контейнера под Windows и о том, как использовать возможности docker multi-stage сборок для создания полноценных образов нашего приложения.