Встроенная поддержка контейнеров для .NET 7  -  контейнеризация приложений .NET без Dockerfile

Появилась новая встроенная поддержка контейнеров для версий .NET 7 и новее. С совершенно новым SDK-пакетом .NET 7 образы Docker для приложения собираются теперь мгновенно, так же быстро поднимаются для него и контейнеры, а Dockerfile не нужен  —  одним файлом для сопровождения меньше.

В Microsoft анонсировали, начиная с SDK-пакета .NET 7, поддержку фреймворком создания контейнеризированных приложений в рамках инструментария публикации. Таким образом устраняется необходимость в самом дополнительном Dockerfile. Теперь сократится сопровождаемый разработчиками код Docker, значительно упростится весь рабочий процесс.

Содержание

  • Основная идея
  • Контейнеризация приложений .NET 6  —  вкратце
  • Недостатки «Dockerfile»
  • Встроенная поддержка контейнеров для .NET 7
  • Дополнительные настройки для встроенной поддержки Docker
  • Directory.Build.props
  • Для локальной разработки
  • Рабочий процесс «GitHub Actions»
  • Небольшие ограничения

Основная идея

Каков общий рабочий процесс большинства программных приложений, особенно микросервисов?

Код → отправка в репозиторий → запуск конвейера непрерывной интеграции и непрерывного развертывания. Часть этого конвейера  —  этап сборки Docker, на котором из длинного Dockerfile обычно считываются данные и генерируются образы Docker для приложения. → И, наконец, развертывание образа в службе облачных вычислений.

Теперь не нужно сопровождать Dockerfile: образ генерируется самим фреймворком .NET и отправляется в выбранный репозиторий.

Далее мы узнаем, как собираются образы Docker при помощи инструментария интерфейса командной строки .NET, изучим предоставляемые им варианты, сравним с подходом Dockerfile и интегрируем в рабочий процесс Github Actions, продемонстрировав полезность этого для ваших проектов.

Сначала быстро рассмотрим контейнеризацию приложения .NET 6 с Dockerfile.

Создадим два простых приложения: dotnet6 и dotnet7. Поместим их код в папки одного репозитория. А в конце покажем, как это интегрировать с конвейером сборки и отправляемым, например, на DockerHub образом, и прямо в GitHub напишем простой рабочий процесс GitHub Actions.

Устанавливаем оба SDK-пакета и Docker Desktop:

Создаем на GitHub репозиторий, клонируем его на компьютер для локальной разработки, при помощи Visual Code открываем папку репозитория и добавляем здесь папку dotnet6.

Вот исходный код этой реализации.

Контейнеризация приложений .NET 6  —  вкратце

В папке dotnet6 создаем простой веб-API проект HelloDocker на .NET 6, запуская команду dotnet интерфейса командной строки:

dotnet new webapi --name HelloDocker --framework net6.0

Здесь указывается на TargetFramework, то есть целевой фреймворк net6.0.

Пропустите эту часть, если уже хорошо знаете контейнеризацию приложений до версии .NET 6 включительно.

Это очень простой веб-API, которым возвращаются стандартные данные о погоде, подобный любому другому новому веб-API проекту по умолчанию на ASP.NET Core. Чтобы его контейнеризировать, до версии .NET 7 в корневой каталог проекта добавляли Dockerfile.

Совет: создав любое приложение dotnet, сразу очистить файл «launchSettings.json» и удалить все конфигурации, связанные со службами информационного сервера интернета IIS.

{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"HelloDocker": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7290;http://localhost:5033",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Так API всегда запустится на безопасном порте 7290 и HTTP-порте 5033. Внимание: только для запуска приложения на компьютере разработчика, а не в контейнере Docker.

По умолчанию, когда контейнер Docker развертывается с образом .NET, приложение запускается в http://+:80.

Чтобы переопределить его на другой номер порта в контейнере Docker, устанавливаем эту переменную окружения:

Добавим новый файл Dockerfile с таким содержимым:

FROM mcr.microsoft.com/dotnet/sdk:6.0 as build-env
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /publish

FROM mcr.microsoft.com/dotnet/aspnet:6.0 as runtime
WORKDIR /publish
COPY --from=build-env /publish .
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
ENTRYPOINT ["dotnet", "HelloDocker.dll"]

Вот что происходит дальше. Из репозитория образов Microsoft извлекается SDK-пакет dotnet6, файл HelloDocker.csproj копируется в каталог сборки, для восстановления всех зависимостей приложения запускается команда dotnet-restore, остальное содержимое копируется, и в режиме Release запускается команда dotnet publish.

После для выполнения приложения извлекается образ файла этапа выполнения aspnet:6.0, предоставляются порты вроде тех 5033 и 7290, которые мы видели в launchsettings.json. И, наконец, задается точка входа HelloDocker.dll приложения.

Внимание: этот Dockerfile автогенерируется и при создании проекта. Просто включаем Docker Support, выбираем Linux, и с помощью Visual Studio он добавляется в корневой каталог нового проекта. Интересно вам это или нет, а все-таки придется внести здесь коррективы вместе с другими изменениями структуры проекта.

Теперь создадим образ Docker, выполнив команду из папки, в которой находится файл Dockerfile:

docker build -t hellodockerfrom6 .

Так инициализируется процесс сборки Docker. По завершении образ hellodockerfrom6 отправляется в локальный экземпляр Docker.

В Docker Desktop переходим во вкладку Images («Образы»):

Отсюда из образа hellodockerfrom6 развертывается контейнер Docker, и при необходимости передаются дополнительные настройки, номера портов, переменные окружения.

Сопоставим локальный порт 5000 с портом 5000 контейнера Docker. Так любой трафик/запрос, отправленный на порт localhost 5000, перенаправляется на внутренний порт 5000 контейнера Docker, то есть в приложение hellofromdocker6:

Вот и все. Запустив контейнер, переходим в http://localhost:5000/WeatherForecast/, и в ответе видим данные о погоде:

Недостатки «Dockerfile»

Сам по себе это простой способ создания образов Docker из приложений .NET. Но по мере увеличения размера и сложности проекта на .NET файл Dockerfile становится запутанным.

Вот, например, Dockerfile одного из моих решений со ссылками на более чем 10 проектов:

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /

# Копируем «csproj» и восстанавливаем как отдельные слои
COPY ["Directory.Build.props", "/"]
COPY ["Directory.Build.targets", "/"]
COPY ["dotnet.ruleset", "/"]
COPY ["stylecop.json", "/"]
COPY ["src/Host/Host.csproj", "src/Host/"]
COPY ["src/Core/Application/Application.csproj", "src/Core/Application/"]
COPY ["src/Core/Domain/Domain.csproj", "src/Core/Domain/"]
COPY ["src/Core/Shared/Shared.csproj", "src/Core/Shared/"]
COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
COPY ["src/Migrators/Migrators.MSSQL/Migrators.MSSQL.csproj", "src/Migrators/Migrators.MSSQL/"]

RUN dotnet restore "src/Host/Host.csproj" --disable-parallel

# Копируем все остальное и выполняем сборку
COPY . .
WORKDIR "/src/Host"
RUN dotnet publish "Host.csproj" -c Release -o /app/publish

# Собираем образ файла этапа выполнения
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app

COPY --from=build /app/publish .

# Создается непривилегированный пользователь с явно заданным «UID», добавляется разрешение на доступ к папке «/app»
# Подробнее — по адресу https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

ENV ASPNETCORE_URLS=https://+:5050;http://+:5060
EXPOSE 5050
EXPOSE 5060

ENTRYPOINT ["dotnet", "Host.dll"]

Это будет очень неудобно для сопровождения. Особенно в микросервисах, где придется сопровождать один Dockerfile для каждой из служб со ссылками внутри на несколько CSPROJ.

Чтобы упростить эту ситуацию и управление ею, в Microsoft доработали поддержку контейнеров, начиная с .NET 7, где Dockerfile для фактической контейнеризации приложения уже не нужен. Однако подход Dockerfile остается рабочим.

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

Допустим, в корневой папке одного из проектов случайного решения для микросервисов имеется Dockerfile. При сборке образа Docker легко пропустить включение других файлов, которые находятся в корневом каталоге. Это весьма распространенный сценарий для Docker  —  пропускать очень важные для процесса сборки файлы вроде Directory.Packages.props.

Встроенная поддержка контейнеров для .NET 7

В Microsoft эти конкретные проблемы устранили. С новым обновлением перестали быть проблемой и пробелы в контекстах, большинство из вас с ними наверняка уже сталкивались.

Обратимся к новому подходу.

Создаем в корневом каталоге репозитория папку dotnet7, а также новый веб-API 7.0:

dotnet new webapi --name HelloDocker7 --framework net7.0

Теперь, перейдя в проект, вместо Dockerfile добавляем новый NuGet-пакет:

dotnet add package Microsoft.NET.Build.Containers

По утверждениям Microsoft, это пакет временный: в дальнейших выпусках .NET его включат в SDK-пакет. Это практически ссылка на пакет для создания образа Docker и единственное, что добавляется для получения базового образа.

Получаем доступ к образу Docker локально, запуская в корневом каталоге проекта команду dotnet publish с парой параметров:

dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:ContainerImageName=hellodocker7

В этом подходе сохраняются все настройки Dockerfile. Разберем сначала передаваемые здесь аргументы и базовые параметры:

  • –os указывается целевая ОС;
  • –arch указывается целевая архитектура;
  • -p обозначаются конкретные параметры, передаваемые в эту команду publish.

Новый образ контейнера доступен в Docker Desktop:

Упростим команду, добавив параметры прямо в файл HelloDocker7.csproj, открываем его в Visual Code и вставляем код:

<PropertyGroup>
<ContainerImageName>hellodocker7</ContainerImageName>
<PublishProfile>DefaultContainer</PublishProfile>
<ContainerImageTags>1.1.0;latest</ContainerImageTags>
</PropertyGroup>

Задав однажды, уже не нужно каждый раз добавлять это в аргументы терминала.

Теперь команда стала такой:

dotnet publish --os linux --arch x64

Сделаем ее еще проще, добавив это свойство:

<RuntimeIdentifier>linux-x64</RuntimeIdentifier>

Вот что осталось от команды:

dotnet publish

Запустив ее, обнаружим в Docker Desktop новый образ с тегом 1.1.0  —  так прямо в файле csproj указываются различные настройки для образа Docker.

Дополнительные настройки для встроенной поддержки Docker

В Dockerfile образ настраивался очень гибко, то же и с новым подходом.

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

Имеются такие:

  • ContainerBaseImage. С этим свойством мы управляем базовым образом для создания приложения dotnet, по умолчанию SDK принимаются значения mcr.microsoft.com/dotnet/aspnet для проектов ASP.NET Core.
  • ContainerImageName. Здесь меняем название образа. Если оно не указано, SDK вернется к названию самой сборки.
  • ContainerPort. Для автоматического сопоставления портов.

А так указываются переменные окружения:

<ItemGroup>
<ContainerEnvironmentVariable Include="ENABLE_REDIS" Value="Trace" />
</ItemGroup>

Вот подробная документация по другим параметрам/свойствам для настройки контейнера.

Directory.Build.props

Обычно в одном решении имеется несколько проектов на C#. Чтобы каждым из проектов, на которые ссылаются в решении, использовались все эти свойства, в таких сценариях они включаются и в корневой каталог решения Directory.Build.props.

Например, вот фрагмент кода из файла Directory.Build.props шаблонного решения для микросервисов dotnet:

<PropertyGroup>
<PublishProfile>DefaultContainer</PublishProfile>
<ContainerImageTags>1.3.0;latest</ContainerImageTags>
</PropertyGroup>

На уровне csproj указывается это:

<PropertyGroup>
<ContainerImageName>fsh-microservices.catalog</ContainerImageName>
</PropertyGroup>

Таким образом очень легко в долгую сопровождать конкретные и общие свойства.

Для локальной разработки

Это очень хорошо для локальной разработки, где приложения .NET контейнеризируются частенько. Одной dotnet publish достаточно для легкого доступа к образу Docker, и без отражения в Dockerfile многочисленных изменений и добавления новых ссылок на проекты.

Кроме того, жизнь облегчается применением задач Visual Code, похожих на этот фрагмент кода из моего файла .vscode/tasks.json:

{
"label": "publish:catalog-api",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/services/catalog/Host/Host.csproj",
"--os",
"linux",
"--arch",
"x64",
"-c",
"Release",
"-p:PublishProfile=DefaultContainer",
"-p:ContainerImageTags=latest",
"--self-contained"
],
"problemMatcher": "$msCompile"
},

Рабочий процесс «GitHub Actions»

Теперь отправим приложение .NET в репозиторий, для сборки приложения .NET 7 создадим экшен GitHub, опубликуем его со сборкой образа Docker и отправим в общедоступный репозиторий DockerHub.

Во вкладке Actions репозитория GitHub создаем рабочий процесс с новым файлом ci.yml:

Чтобы создать приложение .NET 7, сгенерировать для него образ Docker и отправить в DockerHub, напишем очень простой рабочий процесс ci экшена конвейера сборки:

name: ci
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
working-directory: ./dotnet7/HelloDocker7/
run: dotnet restore
- name: Build
working-directory: ./dotnet7/HelloDocker7/
run: dotnet build --no-restore
docker:
name: Docker Build & Push
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker Login
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Image
working-directory: ./dotnet7/HelloDocker7/
run: dotnet publish -c Release -p:ContainerImageName=${{ secrets.DOCKER_USERNAME }}/hellodockerfromdotnet7
- name: Push Image
run: docker push ${{ secrets.DOCKER_USERNAME }}/hellodockerfromdotnet7 --all-tags

Рассмотрим этот сценарий подробнее.

  • Строки 2–3: здесь указывается, что этот рабочий процесс запускается только вручную. Имеются варианты автоматического его запуска, например при каждом добавлении в репозиторий. В этом демо пока достаточно ручного запуска.
  • Строки 5–20: на основе .NET 7 в скрипте восстанавливаются зависимости HelloDocker7, выполняется попытка их собрать.
  • Строки 20–37: все, что связано с Docker.
  • Строки 28–32: попытки войти в DockerHub с именем пользователя и паролем, установленными в качестве секретных данных репозитория, скриншот прилагается ниже.
  • Строка 35: запускаем команду dotnet publish с конфигурацией Release, здесь же на основе указанного ContainerImageName генерируется образ Docker.
  • Строка 37: собранный на предыдущем этапе образ отправляется здесь в DockerHub.

Чтобы установить секретные данные, во вкладке настроек Settings репозитория GitHub выбираем Secrets и добавляем имя пользователя и пароль Docker для доступа из конвейера:

После сохраняем yml-файл и создаем в DockerHub новый общедоступный репозиторий с таким же названием, как в этом файле  —  hellodockerfromdotnet7. Так из скрипта конвейера все гарантированно отправится в этот репозиторий.

Выбираем во вкладке Actions этого нового репозитория Github рабочий процесс ci и нажимаем Run Workflow («Запустить рабочий процесс»):

Теперь конвейер готов к работе, выполняется попытка собрать приложение dotnet:

На этапе Docker Build & Push («Сборка и добавление Docker»): новая dotnet publish в действии.

Если обошлось без неожиданностей, увидите везде галочки в зеленых кружочках, то есть образ Docker отправится в DockerHub.

Небольшие ограничения

  • docker-compose для сборки образа нужен Dockerfile. Сейчас это не поддерживается. Решение  —  образ Docker отправляется на компьютер локальной разработки, затем указывается в файле docker-compose.
  • Пока поддерживаются только образы для Linux x64.
  • Нет встроенной поддержки аутентификации для внешних репозиториев образов.

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

Заключение

Мы узнали о совершенно новой встроенной поддержке контейнеров для приложений .NET, начиная с SDK-пакета .NET 7. Рассмотрели стандартный подход Dockerfile для приложений .NET 6, еще применимый для более новых SDK-пакетов .NET, обсудили недостатки Dockerfile, ознакомились с функционалом нового SDK-пакета .NET, где Dockerfile для сборки образов Docker уже не нужен.

Рассмотрели различные доступные настройки и параметры, их применение для целей локальной разработки. Наконец, сделали рабочий процесс GitHub Action для создания приложения, сборки образа Docker и отправки на DockerHub.

Вот код для этого демо на GitHub.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Nisar Ahmed: Built-In Container Support for .NET 7 — Dockerize .NET Applications without Dockerfile!

Предыдущая статьяJava 21: новый подход к созданию строк 
Следующая статьяИтераторы и генераторы в JavaScript