Разработка, ориентированная на релиз: создание минималистического конвейера релизов с помощью Github Actions

Разработка, ориентированная на релиз: создание минималистического конвейера релизов с помощью Github Actions

28 марта 2023 г.

Вы когда-нибудь думали о том, чтобы начать новый проект с конвейером релизов? Я говорю об автоматизации, которая перенесет ваше приложение в реальную среду. Я знаю, звучит немного странно начинать проект с материалов CI/CD. Это как отпустить то, чего еще нет. Это как писать тест для кода, который даже не написан. Но подождите, это хорошо известная техника под названием «Разработка через тестирование»!

Так что я говорю о чем-то подобном здесь. Прежде чем писать какой-либо фрагмент кода, подготовьте свой будущий сервис к реальному миру. Каждый должен начать создавать свое приложение с «ядра». Но, пожалуйста, убедитесь, что ваше ядро ​​==deployable==. Ваши клиенты/покупатели/кто бы ни хотел, чтобы ваш сервис был доступен не только с вашего локального компьютера.

Я не нашел в Интернете ни одного упоминания о «разработке, ориентированной на релиз» (натыкаю здесь, я первый). Кто-то (читай: я) должен подготовить большую теоретическую статью, подробно описывающую, что такое RDD, и как это связано с методологиями Continuous Deployment и Agile. А теперь давайте повеселимся. Я собираюсь показать вам, как начать работу с RDD (мне уже нравится эта аббревиатура!) с очень простой SpringBoot, Github Actions и DigitalOcean.

Покажите мне код

Мы создадим минималистичный конвейер выпуска на основе правильно выполненных действий GitHub. При отправке основной ветки он создаст и протестирует ваш модуль Gradle, пометит его новой версией и развернет в DigitalOcean.

Мы строим что-то вроде этого:

Release Pipeline

Ключевые моменты:

  1. Git — это источник достоверной информации о выпуске. Прочтите Шаг 4 для получения дополнительной информации.
  2. Сохранить версию выпуска как тег Git.
  3. Нет подключаемого модуля выпуска Gradle. Только подключаемый модуль, который получает последний тег.
  4. Github Actions как платформа CI/CD.

:::подсказка Разработка программного обеспечения должна быть итеративной, эта статья не исключение. Считайте это руководством. Каждый шаг — это отдельная фиксация в репо. И, конечно же, не стесняйтесь экспериментировать на любом этапе. Или ознакомьтесь с репозиторием проекта на GitHub.

:::

Шаг 1. Скелет

Мы начнем с очень простого приложения, созданного с помощью Spring Initializr. Добавьте зависимость Spring Web (мы собираемся быть серьезными), прежде чем нажимать кнопку Создать.

Ваш проект должен выглядеть примерно так:

Initial project structure

Перейдите к settings.gradle и измените rootProject.namee на что-то лучше, чем demo. Не уверен, что ==Deployinator== (так я назвал свой) лучше, но Dr. Дофеншмиртц был бы в восторге.

when you are very bad at naming

Шаг 2. Версия проекта

Далее мы хотим убедиться, что наше Java-приложение знает версию… самого себя. Мы должны передать версию проекта Gradle (в настоящее время 0.0.1-SNAPSHOT) внутри нашего приложения.

Чтобы это произошло, нам нужно создать build.properties в разделе src/main/resources и добавить в него следующую строку:

info.build.version = ${version}

и этот фрагмент кода для build.gradle:

processResources {
    filesMatching("build.properties") {
        expand(project.properties)
    }
}

==Подождите?==

Зачем создавать новый файл свойств, если уже есть пустой application.properties?

Позволь мне объяснить. Во-первых, чтобы все было раздельно. Используйте application.properties — для любой конфигурации, связанной с бизнесом или приложением. Используйте build.properties — для любых материалов, связанных со сборкой/Gradle.

Во-вторых, расширение свойств Gradle конфликтует с расширением Spring. Если вы фильтруете application.properties с помощью Gradle, вы должны использовать экранированный заполнитель /${} для любых инъекций Spring, потому что классический ${} один используется Gradle. Неэффективно везде использовать нестандартные плейсхолдеры ради одного свойства из Gradle.

:::подсказка Существуют и другие варианты передачи версии проекта Gradle в приложение, например добавление ее в манифест или с помощью buildInfo . Но мне больше нравится упомянутый выше, потому что это самый прозрачный подход. Кроме того, он работает без дополнительной магии, когда вы выполняете :bootRun или запускаете класс Main из IDE.

:::

Не забудьте добавить новый файл build.properties в качестве источника свойств в Spring:

import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@PropertySource("classpath:build.properties")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Коммит с изменениями для шага здесь

Шаг 3. Контроллер версий

Давайте познакомимся с Контроллером версий.

Он распечатает текущую версию приложения:

@RestController
public class VersionController {

   private static final Logger logger = LoggerFactory.getLogger(VersionController.class);

   @Value("${info.build.version}")
   private String version;

   @PostConstruct
   public void printVersion() {
       // check version in logs
       logger.info("You deployed me well! My version is {}", version);
   }

   @GetMapping("/")
   public String helloWorld() {
       return version;
   }
}

Поскольку мы закончили с нашим приложением (да, это так), давайте удостоверимся, что у нас все работает

> ./gradlew :bootRun

и найдите следующую строку в выходных журналах:

Перейдите в терминал и проверьте HTTP:

> curl localhost:8080/version 
> 0.0.1-SNAPSHOT

Шаг 4. Управление версиями

Пришло время поговорить об управлении версиями. Есть несколько подходов (конечно) к хранению информации о версии:

  1. Храните его в инструменте сборки (Gradle, Maven и т. д.) как свойство.

Как project.version = 0.0.1-SNAPSHOT внутри build.gradle

Я помню те времена, когда почти в каждом проекте использовался Maven Release Plugin. Эти блестящие коммиты «Подготовить выпуск ..» и «Подготовить следующую итерацию разработки…» с изменением версии. Такой… бардак.

Я считаю, что ответственность ==release== должна быть снята с инструментов сборки. Они должны знать, как тестировать/собирать/создавать ресурсы и артефакты. Они не должны знать о выпуске, это часть, которую в настоящее время можно легко делегировать платформам CI/CD. Принцип единой ответственности. Так что, если инструмент сборки не выходит, нет смысла хранить там версии и использовать их как источник правды.

2. Хранить версии в Git.

Git — это система контроля версий. По крайней мере, судя по описанию, хранить там версию приложения — здравая идея. ==Store== — чтобы иметь тег, который, например, однозначно идентифицирует выпуск. Тег с именем 0.0.1-SNAPSHOT или v12-my-first-release. Это более гибко, вы можете переназначить тег другому коммиту и легко добавлять/удалять функции из предстоящего релиза. Вы можете просто отслеживать историю выпусков, заглянув в журнал Git. При таком подходе инструмент сборки является просто последователем — берет информацию из Git и выполняет свою работу.

Можно привести доводы в пользу другого управления версиями. Если вы еще не обеспокоены, просто попробуйте новый проект. В этом руководстве мы стараемся сделать вещи простыми и гибкими, поэтому лучшим выбором будет Git.

:::информация Народная мудрость: Если у вас есть проблема и вы хотите решить ее с помощью Gradle, то теперь у вас есть 2 проблемы.

:::

Давайте сообщим Gradle о том, что происходит в репо. Простой gradle-git-plugin решает нашу проблему. Это еще более круто, чем я изначально думал, потому что он не основан на JGit, он просто читает папку .git. Это означает меньше настроек и более предсказуемые результаты.

Изменения в build.gradle:

plugins {
  id 'java'
  id 'org.springframework.boot' version '3.0.4'
  id 'io.spring.dependency-management' version '1.1.0'
  id 'com.palantir.git-version' version '2.0.0'
}

group = 'com.example'
version = gitVersion()
sourceCompatibility = '17'

Посмотрим, что у нас есть для версии проекта в логах.

Хорошо!

После фиксации всех изменений и добавления тега v1-manualReleaseHere в логах видим:

Хорошо, теперь у нас есть ==поток версий== из Git -> Грейдл-> Java-приложение

Шаг 5. Докер

Современное приложение трудно представить без Docker, поэтому давайте быстро поместим в контейнер то, что у нас есть.

Самый простой Dockerfile может выглядеть так:

FROM eclipse-temurin:17

COPY build/libs/deployinator-*.jar /app/deployinator.jar

ENTRYPOINT java -jar /app/deployinator.jar

Создайте и отправьте, чтобы убедиться, что все в порядке:

> docker build -t <your_name_here>/deployinator .
> docker tag  <your_name_here>/deployinator <your_name_here>/deployinator:latest
> docker push <your_name_here>/deployinator:latest

Шаг 6. Действия на Github

Пришло время собрать все в одном месте. У нас будет один конвейер в .github/workflows/createRelease.yml. Хотя вы можете найти весь рабочий конвейер здесь, давайте сосредоточимся на наиболее важных частях.

В первую очередь объявите версию текущего релиза. Нам нужна env var, чтобы иметь к ней доступ позже:

- name: Create Release Version
  run: echo "RELEASE_VERSION=v${{github.run_number}}-${GITHUB_SHA::7}" >> $GITHUB_ENV

Версия имеет вид v12-123abcd, где 12 — номер выполнения конвейера (задания). Последние 7 – короткий хэш коммита

:::подсказка SemVer (такие версии, как 1.2.3) является стандартом де-факто в отрасли. Здесь я предлагаю использовать run_number задания по нескольким причинам. Во-первых, вам вообще не нужно извлекать предыдущие теги (когда у вас тысячи релизов, это может быть громоздко). Второе — простота. Вам не нужна логика для увеличения номера последней версии (например, с 1.2.3 до 1.2.4), потому что run_number монотонно увеличивается. бесплатно (без смс и регистрации).

:::

Наличие короткого ша в номере версии — очень важная вещь. Это часто случается, когда вам нужно быстро сопоставить свой выпуск с соответствующим коммитом Git, чтобы понять, что именно развернуто. Когда вы открываете облачную консоль и видите, что развернута версия 1.2.10, довольно сложно догадаться, установлено ли ваше полуночное исправление или нет. Вы должны взять эту версию и обратиться к системе отслеживания ошибок или еще куда-нибудь, чтобы получить представление о том, что означает 1.2.10. Но когда у вас есть хэш в версии, вы можете просто взять его и проверить, что вам нужно, даже локально.

Далее создается файл Jar. Под капотом плагин Gradle Git будет извлекать текущую версию из тега и использовать ее для именования jar.

- name: Build
   uses: gradle/gradle-build-action@v2
        with:
          arguments: clean build

Затем образ докера. Обязательно добавьте необходимые секреты (DOCKERHUB_USERNAME и DOCKERHUB_TOKEN) на GitHub, чтобы этот шаг работал:

- name: Build & Push Docker Image
  uses: docker/build-push-action@v4
  with:
    context: .
    tags: |
      ${{ secrets.DOCKERHUB_USERNAME }}/deployinator:latest
      ${{ secrets.DOCKERHUB_USERNAME }}/deployinator:${{env.RELEASE_VERSION}}
    push: true

Мы помечаем изображение как -==latest==, так и версией выпуска. Но никогда не полагайтесь и не развертывайте ==latest==, это просто для удобства. Вы всегда должны точно знать, что развернуто и с какой версией приложения работают ваши клиенты.

Теперь, когда мы знаем, что наш артефакт протестирован, собран и полностью упакован, мы можем объявить выпуск успешным, отправив тег в репозиторий Git. Да, развертывание по-прежнему может завершиться ошибкой. Но если он не работает из-за неработающего релиза — это нормально с точки зрения релиза (очевидно, что это не с точки зрения здравого смысла). У нас просто «плохой» релиз. Но это все же релиз, верно?

И последний шаг — развертывание. Мы собираемся развернуть ==Deployinator== (в моем университете специальностью была тавтология) на DigitalOcean App Platform. Одним словом, платформа приложений — это решение, которое может размещать образы Docker с минимальной конфигурацией и заданной инфраструктурой. Каждое приложение может иметь один или несколько компонентов, которые являются строительными блоками вашего сервиса. В нашем случае одного приложения и одного компонента более чем достаточно.

:::информация Согласно официальной документации для действия DigitalOcean GitHub, оно работает только при обновлении приложение. Поэтому для самого первого запуска вам нужно создать приложение вручную. Пожалуйста, сделайте это и поместите свой токен DigitalOcean вместе с именем приложения в секреты GitHub. заранее (см. имена переменных ниже).

:::

- name: Deploy to DigitalOcean
  if: ${{ env.DEPLOY_TO_DIGITAL_OCEAN == 'true' }}
  uses: digitalocean/app_action@main
  with:
    app_name: ${{ secrets.DIGITALOCEAN_APP_NAME }}
    token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
    images: '[
      {
        "name": "deployinator-component",
        "image":{
          "registry": "${{ secrets.DOCKERHUB_USERNAME }}",
          "registry_type": "DOCKER_HUB",
          "repository": "deployinator",
          "tag": "${{env.RELEASE_VERSION}}"
        }
      }
    ]'

Теперь пришло время сделать последнюю проверку. Перейдите в приложение DigitalOcean и нажмите кнопку ==Live App==

DigitalOcean's App page

== Та-дам! Версия не отображается!==

А, это нормально. Это потому, что наш VersionContoller находится по пути /version. Поэтому добавьте /version к URL-адресу и увидите что-то вроде этого:

Now we can see application version

Заключение

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

15 минут ежедневного развертывания могут легко превратиться в 5+ часов в месяц! Вы хотите потратить часы на ручное развертывание своего приложения или хотите потратить это время на что-то более интересное, например, пялиться в стену? Хорошо, может быть, это не так весело, но вы поняли идею.

==Так сэкономьте время на разработку с самого начала.==


Оригинал