Проблема
Традиционно, для реализации CI/CD сценариев DevOps-инженеры используют различные платформы, такие как Jenkins, TeamCity, Azure DevOps и т.д. Их конфигурирование для сборки, версионирования, создания релизов решений может быть сложным и трудоемким, особенно если решение состоит из множества проектов/единиц развёртывания.
Обычно для настройки сборки решений в .NET используется большое количество разнородных скриптов, что создает ряд проблем:
Сложность поддержки: использование такого количества скриптов делает их поддержку более сложной и трудоемкой. Каждый скрипт может иметь свою синтаксическую структуру и требовать специфических знаний для его изменения или исправления ошибок.
Отсутствие стандартизации: в случае использования скриптов, каждый разработчик может использовать свой собственный подход к настройке процесса. Это приводит к отсутствию стандартов и единого подхода в организации, затрудняя совместную работу и повышает сложность обслуживания.
Непредсказуемость: нет уверенности, что процесс будет проходить одинаково на всех серверах сборки, поскольку он зависит от настроек окружения и установленных там SDK. Более того, сборка или запуск тестов могут проходить на одной ОС, а развёртывание - на другой, приводя к непредсказуемым ошибкам.
Зависимость от инструментов: для настройки CI/CD с использованием скриптов обычно требуется определенный набор инструментов, создавая зависимость от них и усложняя переносимость настроек.
Отсутствие контроля версий: нередко бывает так, что скрипты не хранятся в системе контроля версий, тем самым затрудняя отслеживание изменений и воспроизведение конфигурации для определенной версии приложения.
Отсутствие вовлеченности разработчиков: поскольку, зачастую, настройка CI/CD выполняется DevOps-инженерами, разработчики не могут вносить изменения в процесс сборки напрямую. Более того, разработчики могут не знать, как настроен весь процесс, что делает его менее прозрачным и увеличивает время реакции на изменения или проблемы.
Зависимость от внешних поставщиков: в постоянно изменяющихся условиях рынка, коробочные решения для CI/CD могут устаревать, переставать соответствовать требованиям организации или просто уходить с рынка. Всё это может приводить к необходимости перехода на другие решения и влечёт дополнительные затраты на переобучение и перенастройку процесса.
Варианты решения
Что можно сделать, чтобы решить эти проблемы?
Перейти на подход "сборка через код", являющийся практикой автоматизации процессов сборки и развёртывания приложений, используя код в рамках самого приложения вместо ручной настройки и скриптов в CI/CD платформе.
Так мы и решили сделать в компании Монополия, когда столкнулись с описанными выше проблемами. Был проведён анализ рынка и рассмотрены различные инструменты для реализации требуемого подхода в .NET.
Cake
Платформа для автоматизации сборки и развертывания приложений, написанная на C#.
Плюсы:
Гибкость: Cake позволяет настроить сложные сценарии сборки, используя собственный синтаксис.
Позволяет определять настройки сборки в виде кода, который хранится в репозитории проекта.
Поддерживает .NET Framework, .NET Core, Mono, Xamarin, Unity, и т.д.
Поддерживает различные CI/CD платформы, такие как Jenkins, TeamCity, Azure DevOps и другие.
Позволяет использовать различные инструменты, такие как MSBuild, .NET CLI, NuGet, Git, Docker и другие.
Открытый исходный код.
Минусы:
Сложный синтаксис: несмотря на C# в своей основе, Cake использует собственный синтаксис для определения настроек сборки, что может быть препятствием для новых пользователей.
Сложность настройки: настройка Cake может быть сложной и требовать специфических знаний.
Отсутствие интеграции с IDE: Cake не интегрируется с IDE, что делает его менее удобным для использования. Поддержки IntelliSense нет.
Проблемная документация: документация Cake может быть трудна для понимания и не всегда содержит достаточно информации для решения конкретных проблем.
Тем не менее многие из описанных выше минусов нивелируются использованием библиотеки Cake.Frosting, позволяющую использовать C# вместо собственного синтаксиса и имеет поддержку IDE. Более подробно можно прочитать в статье.
Fake
Платформа для автоматизации сборки и развертывания приложений, написанная на F#.
По возможностям и недостаткам похожа на Cake. Отличается от него тем, что использует функциональный подход в связке с языком F# и предоставляет более высокоуровневый DSL (Domain-Specific Language).
PSake
Платформа для автоматизации сборки и развертывания приложений, написанная на PowerShell.
По возможностям и недостаткам похожа на предыдущие, отличаясь тем, что теперь разработчикам необходимо владеть языком PowerShell. Это привносит дополнительные сложности и высокий порог входа. Также можно отметить ограниченную поддержку сообщества и документации. Зависимость от PowerShell может быть проблемой, поскольку он может быть не всегда доступен для использования по тем или иным причинам.
Kotlin DSL
Встроенный в TeamCity способ настройки сборки и развёртывания на языке Kotlin.
В отличие от Cake, имеет встроенную поддержку IDE (но только IntelliJ IDEA и Android Studio).
Не поддерживает другие CI/CD платформы и не может быть использован вне TeamCity.
Требует знания языка Kotlin... и не все .NET разработчики готовы его изучать.
Ручное конфигурирование на PowerShell
Возможно, один из самых простых способов настройки сборки и развёртывания приложений.
Множество возможностей и вариантов настройки.
Простота прототипирования всего CI/CD процесса.
Требует знания языка PowerShell.
В результате получается много скриптов, которые хоть и могут храниться в репозитории, но их отладка и поддержка могут быть сложны для новых разработчиков.
Сложнее версионировать скрипты и распространять изменения, т.к. каждое решение будет содержать их полную копию.
Nuke Build
Платформа для автоматизации сборки и развертывания приложений, написанная на C#.
Главный конкурент Cake по части гибкости конфигурирования и возможностей.
Большое количество встроенных инструментов для интеграции с множеством CI/CD платформ и сервисов.
Более дружелюбный и простой в использовании ввиду того, что для конфигурирования использует C# и имеет поддержку IDE (Rider и Visual Studio).
Для запуска требуется только .NET SDK и, необязательно, скриптовый движок (PowerShell, Bash).
Тем не менее, данный инструмент тоже имеет ряд ограничений:
Некоторый порог входа для изучения особенностей написания сценариев сборки.
Ограниченная документация.
Сборка через код на Nuke Build
Потратив некоторое время на изучение различных инструментов и прототипирование "build as code" подхода на PowerShell, было решено остановиться на Nuke Build.
Главным преимуществом Nuke стала возможность использовать знакомый синтаксис C# для настройки сборки, что сделало весь процесс более прозрачным и понятным для разработчиков. Конфигурация сборки хранится прямо в решении - любой разработчик имеет возможность узнать подробности процесса или внести изменения в него, не выходя из своей IDE.
Более того, был унифицирован весь процесс и всё, что теперь требуется для развёртывания новых сервисов - создать новый проект Nuke и подключить нашу общую библиотеку, содержащую основные сценарии.
Анатомия Nuke Build
Nuke Build состоит из нескольких основных компонентов:
.NET проект, в котором определены сценарии сборки. Это обычное консольное приложение со ссылкой на NuGet-пакет Nuke Build.
Цели (targets), определяющие последовательность задач, которые должны быть выполнены для достижения конкретной цели сборки. Например, цель может быть связана со сборкой решения или развертыванием приложения. Цели позволяют разработчикам организовать задачи в логические группы и определить порядок их выполнения (сценарии).
Задачи (tasks), представляющие собой отдельные действия, которые могут быть выполнены в рамках сценария сборки. Например, задача может компилировать код, запускать тесты, создавать пакеты или развертывать приложение. Nuke Build предоставляет широкий набор встроенных задач, а также возможность создания пользовательских задач.
Параметры (parameters), позволяющие передавать значения в сценарии сборки из командной строки или других источников. Они могут использоваться для настройки поведения сценария сборки в зависимости от конкретных требований проекта или окружения.
Таким образом, создав проект и определив в нем сценарии сборки, разработчики могут использовать их для выполнения самых различных задач.
Внедрение
Давайте рассмотрим пример внедрения подхода сборки через код с использованием Docker'а на Nuke build. Сборка решений в Docker обеспечивает максимальную воспроизводимость процесса, убирая зависимость от окружения.
Был определён единый сценарий сборки для всех сервисов компании, который включает в себя следующие цели:
Сборка Docker-файлов для каждой единицы развёртывания:
Сборка проектов.
Запуск unit-тестов.
Формирование артефактов сборки (NuGet-файлы).
Формирование итогового Docker-образа для дальнейшего развёртывания.
Запуск интеграционных тестов (если это применимо для решения).
Публикация NuGet-артефактов.
Публикация Docker-образов.
Создание релиза по итогам сборки в Octopus Deploy.
Пример реализации
Для реализации такого сценария, был разработан общий проект, распространяемый как NuGet-пакет, в котором заранее определены все необходимые цели, задачи и их связи. Я подготовил пример такого проекта, который можно найти здесь.
Опустим в данной статье сам процесс создания проекта Nuke Build, т.к. это очень просто и хорошо описано в документации или на хабре.
Наш основной сценарий сборки содержит набор из взаимосвязанных компонентов:
Базовый компонент, определяющий основные параметры сборки, от которого унаследованы все остальные компоненты.
Цели для взаимодействия с Docker:
Сборка Docker-файлов. Один Docker-файл на единицу развёртывания в рамках решения.
Публикация полученных Docker-образов в репозиторий.
Запуск интеграционных тестов, если задано значение параметра
ExecuteIntegrationTests
какtrue
.
Публикация NuGet-артефактов в репозиторий.
Создание релиза в Octopus Deploy.
Nuke Build поддерживает различные варианты версионирования из коробки, но, для сохранения совместимости с текущими процессами, были воссозданы существующие варианты семантического и GitFlow версионирования.
Для общего понимания концепции, рассмотрим в деталях реализацию цели публикации NuGet-артефактов в репозиторий:
[ParameterPrefix(nameof(NuGet))]
public interface INuGetBuild: IBaseBuild
{
[Parameter("NuGet url"), Required]
Uri Url => this.GetValue(() => Url);
[Parameter("NuGet feed name"), Required]
string FeedName => this.GetValue(() => FeedName);
[Parameter("NuGet API key"), Required, Secret]
string ApiKey => this.GetValue(() => ApiKey);
AbsolutePath NuGetArtifactsPath => ArtifactsPath / "nuget";
Target PushNuGetArtifacts => _ => _
.TryDependsOn<IIntegrationTestsBuild>(x=> x.RunIntegrationTests)
.Executes(() =>
{
var nuGetPushUrl = Url.Combine($"nuget/{FeedName}/packages");
DotNetTasks.DotNetNuGetPush(settings =>
settings
.SetTargetPath(NuGetArtifactsPath / "*.nupkg")
.SetSource(nuGetPushUrl.ToString())
.SetApiKey(ApiKey)
.EnableSkipDuplicate()
.EnableForceEnglishOutput());
var pushedArtifacts = NuGetArtifactsPath.GetFiles("*.nupkg")
.Select(x => x.Name);
Log.Information("Nuget artifacts were successfully pushed: {Artifacts}", pushedArtifacts);
});
}
ParameterPrefix - атрибут, позволяющий задать префикс для параметров, которые будут использоваться в сценарии сборки. В нашем случае это:
NuGetUrl - URL репозитория NuGet.
NuGetFeedName - имя веб-канала NuGet.
NuGetApiKey - API-ключ для доступа к репозиторию.
NuGetArtifactsPath - путь к артефактам сборки, которые будут опубликованы в репозиторий.
Отмечу, что тип AbsolutePath, поставляемый вместе с Nuke Build, позволяет работать с путями в кроссплатформенном формате.
PushNuGetArtifacts - цель, определяющая последовательность операций, которые должны быть выполнены для достижения цели публикации NuGet-артефактов.
TryDependsOn - метод, позволяющий определить зависимость цели от другой цели. В нашем случае - это успешный прогон интеграционных тестов.
DotNetTasks.DotNetNuGetPush - задача, выполняющая публикацию артефактов в репозиторий. Одна из многих встроенных задач Nuke Build.
Также хочется обратить внимание на то, что все компоненты сборки представляют собой интерфейсы с реализацией методов по умолчанию. Это официальный способ разработки распространяемых сценариев и целей сборки.
После того как были определены все необходимые компоненты, появилась возможность использовать их в проектах, просто подключив общую библиотеку и вызвав нужную конечную цель:
class Build : NukeBuild, IDefaultBuildFlow
{
public string ServiceName => "DockerTestsSample";
public ApplicationVersion Version => this.UseSemanticVersion(major: 1, minor: 0);
public bool ExecuteIntegrationTests => true;
public IReadOnlyList<DockerImageInfo> DockerImages { get; } = new[]
{
new DockerImageInfo(DockerImageName: "docker-tests-sample", DockerfileName: "Dockerfile"),
};
private Target RunBuild => _ => _
.DependsOn<IDefaultBuildFlow>(x => x.Default)
.Executes(() =>
{
});
public static int Main()
=> Execute<Build>(x => x.RunBuild);
}
В данном примере определяются:
Имя сервиса для использования в сценарии сборки.
Тип версионирования и сама версия.
Необходимость запуска интеграционных тестов.
Список Docker-образов для сборки.
Цель сборки, зависящая от цели по умолчанию, определенной в общей библиотеке.
Описание Docker-файла выходит за рамки данной статьи, но его содержимое можно посмотреть здесь.
Запуск сценария и передача параметров осуществляются в зависимости от операционной системы или предпочтений разработчика: Bash, CMD, PowerShell, утилита Nuke Build, IDE и т.д.
Итоги
Таким образом, был успешно реализован унифицированный подход сборки через код на Build Nuke, переведено множество сервисов компании на него и достигнуты следующие преимущества:
Полная предсказуемость. Сборка и тесты выполняются на том же окружении, что и продуктивное.
Разработчики теперь сами настраивают процесс сборки так, как нужно им, без лишних обращений к DevOps-инженерам и ожидания выполнения задач.
Все сервисы собираются и тестируются одинаково, что позволяет упростить их поддержку и обслуживание. Больше никакой мешанины из скриптов разной степени давности.
Уменьшение TTM (Time To Market) для новых сервисов.
Полный контроль над артефактами сборки.
Версионированием занимаются сами разработчики.
Улучшения/изменения процесса сборки распространяются через одну общую библиотеку.
Был форсирован переход на развёртывание сервисов в Docker.
Независимость от любого CI решения.
Дополнительные материалы
Доклад моего коллеги, Анатолия Кулакова: Build as Code
Доклад Романа Булдыгина - Анатомия Nuke
GitHub-респозиторий с примером реализации.
P.S. Если хочется погрузиться в данную тему еще больше, могу порекомендовать грядущий доклад Анатолия Кулакова на DotNext 2023.