Проблема

Традиционно, для реализации 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 состоит из нескольких основных компонентов:

  1. .NET проект, в котором определены сценарии сборки. Это обычное консольное приложение со ссылкой на NuGet-пакет Nuke Build.

  2. Цели (targets), определяющие последовательность задач, которые должны быть выполнены для достижения конкретной цели сборки. Например, цель может быть связана со сборкой решения или развертыванием приложения. Цели позволяют разработчикам организовать задачи в логические группы и определить порядок их выполнения (сценарии).

  3. Задачи (tasks), представляющие собой отдельные действия, которые могут быть выполнены в рамках сценария сборки. Например, задача может компилировать код, запускать тесты, создавать пакеты или развертывать приложение. Nuke Build предоставляет широкий набор встроенных задач, а также возможность создания пользовательских задач.

  4. Параметры (parameters), позволяющие передавать значения в сценарии сборки из командной строки или других источников. Они могут использоваться для настройки поведения сценария сборки в зависимости от конкретных требований проекта или окружения.

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

Внедрение

Давайте рассмотрим пример внедрения подхода сборки через код с использованием Docker'а на Nuke build. Сборка решений в Docker обеспечивает максимальную воспроизводимость процесса, убирая зависимость от окружения.

Был определён единый сценарий сборки для всех сервисов компании, который включает в себя следующие цели:

  • Сборка Docker-файлов для каждой единицы развёртывания:

    • Сборка проектов.

    • Запуск unit-тестов.

    • Формирование артефактов сборки (NuGet-файлы).

    • Формирование итогового Docker-образа для дальнейшего развёртывания.

  • Запуск интеграционных тестов (если это применимо для решения).

  • Публикация NuGet-артефактов.

  • Публикация Docker-образов.

  • Создание релиза по итогам сборки в Octopus Deploy.

Визуализация сценария сборки
Визуализация сценария сборки

Пример реализации

Для реализации такого сценария, был разработан общий проект, распространяемый как NuGet-пакет, в котором заранее определены все необходимые цели, задачи и их связи. Я подготовил пример такого проекта, который можно найти здесь.

Опустим в данной статье сам процесс создания проекта Nuke Build, т.к. это очень просто и хорошо описано в документации или на хабре.

Наш основной сценарий сборки содержит набор из взаимосвязанных компонентов:

  • IBaseBuild

    • Базовый компонент, определяющий основные параметры сборки, от которого унаследованы все остальные компоненты.

  • IDockerBuild

    • Цели для взаимодействия с Docker:

      • Сборка Docker-файлов. Один Docker-файл на единицу развёртывания в рамках решения.

      • Публикация полученных Docker-образов в репозиторий.

  • IIntegrationTestsBuild

    • Запуск интеграционных тестов, если задано значение параметра ExecuteIntegrationTests как true.

  • INuGetBuild

    • Публикация NuGet-артефактов в репозиторий.

  • IReleaseBuild

    • Создание релиза в 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 решения.

Дополнительные материалы

P.S. Если хочется погрузиться в данную тему еще больше, могу порекомендовать грядущий доклад Анатолия Кулакова на DotNext 2023.

Комментарии (12)


  1. lair
    11.09.2023 12:31
    +2

    Сборка и тесты выполняются на том же окружении, что и продуктивное.

    Каким образом это следствие перехода на Nuke (или любой другой способ описания билда)?

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

    Как это сочетается с

    Разработчики теперь сами настраивают процесс сборки так, как нужно им

    ?

    Не подумайте, я люблю описание (много чего) в коде, я просто не понимаю, как конкретно Nuke достигает описанных преимуществ.


    1. alex_ozr Автор
      11.09.2023 12:31

      Билд идёт в Docker'е, что сразу приближает нас к тому, как это потом в том же Docker'e будет развёрнуто. Раньше была ситуация, что билд шёл на Windows, а потом деплоили в Linux.

      Разработчики теперь контролируют (пишут) Dockerfile'ы для всех точек деплоя (а не пользуются каким-то универсальным и перегруженным вариантов от DevOps-коллег), настраивают версионирование и другие фишки. Раньше это всё делали DevOps и не всегда прозрачно. Ну и code-ревью никто не отменял - раньше оно было невозможно.

      В то же самое время, наша общая библиотека поставляет нам обобщённый конвейер сборки (там где картинка в статье) - разработчикам остаётся разобраться с Dockerfile'ми и переопределить то, что им хочется. Опять же пример такого представлен в последнем примере кода - вся магия скрыта за Nuget-пакетом.


      1. lair
        11.09.2023 12:31
        +2

        Билд идёт в Docker'е

        Для того, чтобы билд шел в докере, нужно, чтобы это поддерживал билд-сервер. Nuke, как и любой другой скрипт, для этого лишь сильно вспомогательный инструмент.

        Разработчики теперь контролируют (пишут) Dockerfile'ы для всех точек деплоя

        Так билда или деплоя?

        Ну и code-ревью никто не отменял - раньше оно было невозможно.

        Что вам раньше запрещало класть dockerfile в версионник?

        В то же самое время, наша общая библиотека поставляет нам обобщённый конвейер сборки

        В этот момент мы и возвращаемся к вопросу "все собирается одинаково" vs "каждый настраивает, как хочет". Как человек, который поддерживает аналогичный обобщенный компонент для деплоя, я много плохих слов могу тут сказать.


        1. alex_ozr Автор
          11.09.2023 12:31
          +1

          Для того, чтобы билд шел в докере, нужно, чтобы это поддерживал билд-сервер. Nuke, как и любой другой скрипт, для этого лишь сильно вспомогательный инструмент.

          Бесспорно

          Так билда или деплоя?

          В рамках билда решения (Solution) мы можем получить N сервисов для деплоя в Docker (монолит, что поделать) - для этого мы и пишем эти N Dockerfile'ов :)

          Что вам раньше запрещало класть dockerfile в версионник?

          Отличное замечание! В компании много сервисов, относительно много legacy, настроенного много лет назад. На Linux/Docker мы только недавно перевели все сервисы и теперь у нас есть более удобный и единообразный способ настройки билда.

          В этот момент мы и возвращаемся к вопросу "все собирается одинаково" vs "каждый настраивает, как хочет". Как человек, который поддерживает аналогичный обобщенный компонент для деплоя, я много плохих слов могу тут сказать.

          Так отлично же! Был бы рад узнать Вашу историю и проблемы, с которыми столкнулись :)

          Наши проблемы:

          • Более чёткий ошибок сборки (dotnet build) в Docker'е с агентом сборки в TeamCity - работаем над этим.

          • Наш вариант версионирования GitFlow - решили, просто нужно было скурпулёзно перенести всю эту древнюю логику вычисления версий.

          А насчёт "все собирается одинаково" vs "каждый настраивает, как хочет" - наш конвеер довольно простой (сборка - отправка артефактов - интеграционные тесты - создание релиза) и все сервисы хорошо на него легли. Конвеер общий для всех, но поддающийся конфигурированию там, где это нужно.

          Ну и, в конце концов, раньше все эти многочисленные скрипты и шаги сборки писали DevOps'ы - вряд ли у них был процесс ревью кода, да и кто знает, под каким SDK и с какими подключёнными библиотеками это всё происходило.


          1. lair
            11.09.2023 12:31
            +3

            В рамках билда решения (Solution) мы можем получить N сервисов для деплоя в Docker

            Тогда Nuke, как и любая другая билд-система, никак на это не влияет.

            На Linux/Docker мы только недавно перевели все сервисы

            Ну то есть ваш профит - он от того, что вы сервисы перевели на докер, а не от того, что вы Nuke взяли.

            Был бы рад узнать Вашу историю и проблемы, с которыми столкнулись

            Основная проблема очень простая: есть общий компонент для деплоя, он использутся в 15 сервисах, внезапно одному из них нужна функциональность, которой в этом компоненте нет. Дальше веселье.

            Ну и, в конце концов, раньше все эти многочисленные скрипты и шаги сборки писали DevOps'ы - вряд ли у них был процесс ревью кода

            В моем скромном понимании весь смысл Dev в DevOps как раз в том, что они пользуются теми же практиками с ревью и версионированием. А если еще точнее, то это не кто-то отдельный от вас должен быть...


            1. alex_ozr Автор
              11.09.2023 12:31
              +1

              Тогда Nuke, как и любая другая билд-система, никак на это не влияет.

              Она помогает более детально, при необходимости, отнестись к сборке решений и пройти ревью.

              Ну то есть ваш профит - он от того, что вы сервисы перевели на докер, а не от того, что вы Nuke взяли.

              Docker нам помог, безусловно. Но и общий конвеер тоже - раньше, чтобы добавить проекту возможность запуска интеграционных тестов с поднятием БД, скажем, в Docker, нужно было идти к DevOps и просить добавить новый шаг сборки в TeamCity. То же самое и при создании новых сервисов - мы уменьшили TTM.

              Основная проблема очень простая: есть общий компонент для деплоя, он использутся в 15 сервисах, внезапно одному из них нужна функциональность, которой в этом компоненте нет. Дальше веселье.

              Это применимо к абсолютно любой общей библиотеке, да :)

              Что ж поделать, жизнь такая. Возможно, у вас очень изощрённые требования.

              В моем скромном понимании весь смысл Dev в DevOps как раз в том, что они пользуются теми же практиками с ревью и версионированием. А если еще точнее, то это не кто-то отдельный от вас должен быть...

              Не уверен, что в TeamCity это есть. Ну кроме чужеродного нам Kotlin DSL.


              1. lair
                11.09.2023 12:31
                +2

                Она помогает более детально, при необходимости, отнестись к сборке решений и пройти ревью.

                Для докерфайлов, используемых для деплоя? Нет, не помогает. Докерфайлы как до Nuke должны были лежать в репо, так и после. Ничего не поменялось (не должно было поменяться).

                Но и общий конвеер тоже - раньше, чтобы добавить проекту возможность запуска интеграционных тестов с поднятием БД, скажем, в Docker, нужно было идти к DevOps и просить добавить новый шаг сборки в TeamCity.

                Вот снова. Вам не общий конвеер помог. Вам помогло то, что ваши девопсы дали вам права управлять сборкой из вашего кода. Общий это конвеер или десять разных, на nuke или на PS - уже не важно.

                Это применимо к абсолютно любой общей библиотеке, да

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

                Не уверен, что в TeamCity это есть.

                Так при чем тут то, что есть или нет в TeamCity? DevOps могут пользоваться всеми теми же инструментами, что и вы. Если вы смогли управлять билдом из Nuke, то и они могут.


              1. papazogl0
                11.09.2023 12:31

                Не уверен, что в TeamCity это есть. Ну кроме чужеродного нам Kotlin DSL.

                А что мешает хранить код buildStep-ов в git и обновлять его по тригеру?


      1. papazogl0
        11.09.2023 12:31

        Разработчики теперь контролируют (пишут) Dockerfile'ы для всех точек деплоя (а не пользуются каким-то универсальным и перегруженным вариантов от DevOps-коллег), настраивают версионирование и другие фишки

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

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


        1. alex_ozr Автор
          11.09.2023 12:31

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

          Конвенции всего две и разработчику достаточно выбрать нужную. Более того, идём к тому, чтобы осталась только одна конвенция. Все новые сервисы - только SemVer.

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

          Да, получился унифицированный вариант старого пайплайна без вороха локальных решений, которые были раньше.


          1. papazogl0
            11.09.2023 12:31

            Т.е. у разработки в целом ничего не изменилось, был "один путь", "один путь" и остался. В чем профит тогда?


            1. alex_ozr Автор
              11.09.2023 12:31

              Профит описан в статье и в первой половине доклада https://www.youtube.com/watch?v=yaQsQvPwlvg :)

              Теперь мы, как разработчики, сами контролируем происходящее и имеем Dockerfile для настройки, под какое окружение нужно собирать решение, какие библиотеки подключать и др. А не использовать единый сферический базовый образ от DevOps.