Калина Алексей блог программиста

Автоматизируем сборку .NET Core приложения с Docker

Несмотря на то, что число разговоров о Docker стало несколько меньше нежели пару лет назад, я считаю, что в блоге любого уважающего себя программиста-блоггера должен быть пост об этой технологии. Что важно, тезис об уменьшении обсуждения Docker возник не из-за того, что эта идея не прижилась или мало кого интересует. Просто все, кто хотел, уже погрузились в нее и используют в своих проектах. Сегодня мы будем использовать Docker для автоматизации процесса сборки и развертывания кода, написанного на еще одной модной технологии .NET Core.

Знакомимся с Docker

Я не буду углубляться в то, как именно работает докер, по крайней мере сегодня. Однако, я постараюсь объяснить для чего он нам нужен. Для тех, кто знаком с этой технологией, ничего нового здесь я не расскажу.

Идея

В основе Docker лежит понятие контейнеризации. Этим словом описывается процесс создания и использования контейнеров - изолированных исполняемых экземпляров со своим программным обеспечением. Контейнеры - легковесная альтернатива виртуальным машинам. Благодаря им вы можете:

  1. Запускать приложение в изолированных процессах и в большом количестве.
  2. Собрать свой контейнер со всеми необходимыми зависимостями и с легкостью демонстрировать работу приложения на любой системе (все, что теперь вам нужно - это Docker).
  3. Хранить в частном либо общедоступном хранилище развертываемые экземпляры вашего приложения разных версий.

У Docker есть и много других плюсов, но мы пока остановимся на этом. Разберемся, из чего он состоит и как им пользоваться.

Архитектура

Как уже было упомянуто, одно из ключевых понятий в докере - это контейнеры. В контейнере содержится все, что нужно для работы приложения. Контейнеры создаются из образов. Образ - основа для контейнеров, шаблон с необходимым программным обеспечением. Образы можно хранить в реестре. Существуют публичные реестры, например, DockerHub. Также вы можете хранить ваши образы, в своем частном реестре. Напишем пару первых команд в Docker.

Скачивание образа. Команда pull находит и скачивает запрашиваемый образ с публичного реестра (если не указан другой)

docker pull hello-world

Запуск контейнера. С помощью команды run запустим новый контейнер с этим образом.

docker run hello-world

Dockerfile

Для контроля и повторного воспроизведения процесса построения образов можно писать докерфайлы. В том числе с помощью такого файла мы опишем процесс сборки нашего приложения. Рассмотрим несколько директив, определяемых в Dockerfile:

Dockerfile также поддерживает много других инструкций и предоставляет достаточно тонкую настройку сборки образа.

.NET Core

Я уже упоминал об этой технологии в посте о телеграм-боте. Напомню, что это ветвь .NET Framework, направленная на создание кросс-платформенных приложений. Мы создадим простейшую программу, которую запустим в Linux-контейнере.

Наше решение будет состоять из библиотечного и консольного проектов. Создадим библиотеку DockerTest.Library с одним классом PhraseGenerator. В этом классе используется код, сторонней зависимости Lorem.NET. Мы воспользуемся методом этой библиотеки, возвращающим “произвольную” строку латинского содержания. Класс PhraseGenerator содержит один метод Generate, который конкатенирует латинский текст с именем, переданным в параметре.

public static class PhraseGenerator
{
    public static string Generate(string name)
    {
        return $"{ LoremNET.Lorem.Words(3) }, { name }";
    }
}

Также создадим консольное приложение DockerTest.App, которое в бесконечном цикле запрашивает имя у пользователя и отвечает ему, используя метод только что созданной нами библиотеки.

static void Main()
{
    while (true)
    {
        Console.WriteLine("Type your name:");
        var name = Console.ReadLine();
        Console.WriteLine(PhraseGenerator.Generate(name));
    }
}

Именно для этого примитивного примера мы будем создавать билд-процесс. А сейчас подробнее о том, что это такое.

Билд-процесс

Под этим понятием я имею ввиду последовательность операций, выполняющихся автоматически и создающих из исходного кода некоторый продукт. В роли продукта, например, может быть инсталятор, опубликованная в Nuget библиотека или, как в нашем случае, новый образ с приложением.

Билд-процессы - неотъемлемая часть понятий Continious Integration и Continious Delivery. Как правило, они реализуются с помощью билд-систем, таких как MsBuild для .Net или Maven для Java (на самом деле, их огромное число для самых разных языков). Рассмотрим типичные шаги для большинства билд-процессов:

  1. Очистка директории с бинарными файлами от результатов предыдущей сборки.
  2. Проставление актуальных версий в файлах проектов.
  3. Сборка проектов.
  4. Создание продукта.
  5. Публикация продукта.

Мы будем производить сборку в докере, создавая новый образ, поэтому первый шаг нас не интересует. Кроме того, мы будем использовать многоэтапную сборку. Еt смысл в разделении билд-процесса на два образа. Процесс сборки проходит в тяжелом образе, содержащем весь необходимый инструментарий для этого, в то время как для результирующего образа, за основу берется легкий, который может только запускать наше приложение.

Реализация

Итак, у нас есть консольное .NET Core приложение, использующее нашу библиотеку. Цель: описать билд-процесс, который создает образ с этим приложением и публикует в Nuget библиотеку, предварительно установив для них версию, переданную в параметре.

Dockerfile

Начнем описание процесса сборки с создания докерфайла:

FROM microsoft/dotnet:2.0.0-sdk-stretch
ARG VERS
COPY . /src
WORKDIR src
RUN ./docker_build.sh $VERS

В роли базового образа используем dotnet-sdk версии 2.0.0, который содержит все необходимое для сборки .NET Core приложения. Ключевое слово ARG определяет имя для внешнего параметра. Скопировав весь исходный код приложения в папку src в образе, мы запустим скрипт docker_build.sh с тем же параметром, который был передан в докерфайл.

Установка версий

Файл с расширением sh является bash скриптом. Для незнакомых с Linux людей поясню: Bash - командная оболочка UNIX-систем. Он реагирует на команды пользователя в интерактивном режиме, либо построчно обрабатывает команды скрипта. В файле build.sh, в первую очередь, нам необходимо сменить версии файлов проектов на те, которые были переданы в скрипт через параметр:

sed -i "s@<VersionPrefix>.*</VersionPrefix>@<VersionPrefix>$1</VersionPrefix>@" DockerTest.App/DockerTest.App.csproj
sed -i "s@<VersionPrefix>.*</VersionPrefix>@<VersionPrefix>$1</VersionPrefix>@" DockerTest.Library/DockerTest.Library.csproj

Утилита sed заменяет все вхождения подстроки, подходящей под шаблон, на другое строковое значение. Символом $1 обозначается первый параметр, переданный в скрипт, то есть, в нашем случае, версия. Ключ i обеспечивает проведение замены непосредственно в файле.

Сборка проекта

Следующим шагом нужно скачать все зависимости (у наc это Lorem.NET) и собрать проекты. Для этого воспользуемся утилитой dotnet, которая входит в SDK, и, следовательно, содержится в этом образе. Пропишем в скрипт следующие строки:

dotnet restore DockerTest.sln
cd DockerTest.App
dotnet publish -c Release

Публикация библиотеки

Теперь нам необходимо заняться созданием и публикацией продукта. Сначала проделаем это с нашей библиотекой. Чтобы собрать nuget-пакет, нужно также воспользоваться утилитой dotnet и вызвать команду pack (все операции выполняем в конфигурации Release). В результате в папке bin/Release окажется nupkg файл, который мы и опубликуем в Nuget командой nuget push. Теперь библиотека DockerTest.Library в публичном доступе, и вы можете использовать ее в своем коде!

dotnet pack ../DockerTest.Library/DockerTest.Library.csproj -c Release
PACKAGE="$(find ../DockerTest.Library/bin/Release/ -name '*.nupkg')"
dotnet nuget push $PACKAGE -s https://www.nuget.org/api/v2/package

Сборка и публикация образа

Самое время заняться образом с нашим приложением и вспомнить о многоэтапной сборке. Чтобы создать новый образ, в котором не будет ничего лишнего кроме нашей программы, нужно доработать Dockerfile. За основу второго образа возьмем легкий dotnet-runtime и поместим в него содержимое папки publish из предыдущего образа. Также обозначим точку входа в контейнер, который будет создан из этого образа.

FROM microsoft/dotnet:2.0.0-sdk-stretch
ARG VERS
COPY . /src
WORKDIR src
RUN ./docker_build.sh $VERS

FROM microsoft/dotnet:2.0.0-runtime-stretch
COPY --from=0 /src/DockerTest.App/bin/Release/netcoreapp2.0/publish /app
WORKDIR /app
ENTRYPOINT ["dotnet", "DockerTest.App.dll"]

Демонстация

У нас почти все готово. Осталось создать еще один скрипт, который будет вызываться на билд-машине, ведь мы максимально автоматизируем процесс. Первым делом в этом скрипте мы соберем наш docker-образ с тегом docker-test. После этого выполним последний пункт плана, то есть опубликуем образ на DockerHub. Код скрипта:

docker build --build-arg VERS=$1 . -t "docker-test:$1"
docker tag "docker-test:$1" "alexeykalina/docker-test:$1"
docker push "alexeykalina/docker-test:$1"

После выполнения этого скрипта образ с приложением содержится в публичном доступе. Теперь вы можете насладиться работой программы с любого компьютера, на котором установлен Docker, не устанавливая при этом никакие .NET зависимости.

docker

Заключение

Сегодня мы очень поверхностно познакомились с Docker и сборкой образов с помощью Dockerfile. Тем не менее, этих знаний нам хватило, чтобы создать автоматизированный билд-процесс для .NET Core приложения.