Появилась новая встроенная поддержка контейнеров для версий .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, устанавливаем эту переменную окружения:
- ENV ASPNETCORE_URLS=http://+:5000.
Добавим новый файл 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.
Читайте также:
- AlterNats — эффективный PubSub-клиент среды .NET. Как реализовать оптимизированное программирование сокетов в .NET 6
- Контейнеры Docker и их связывание в сети
- Лёгкое пополнение баз данных в приложениях платформы .NET
Читайте нас в Telegram, VK и Дзен
Перевод статьи Nisar Ahmed: Built-In Container Support for .NET 7 — Dockerize .NET Applications without Dockerfile!