Автоматизация глазами разработчика: Github Actions для стартапа

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

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

Логотип GitHub ActionsЛоготип GitHub Actions

Привет, хабровчане!

Однажды нам надоело сливать ветки с репозитория на виртуальную машину тестового сервера и прописывать команды создания бэкапов для всех баз данных. Также очень не хотелось из-за маленького хотфикса поднимать сервисы в Docker, поднимать сам Docker, пересобирать контейнер и запускать тесты. Мы провели исследование не только в чертогах разума, но даже на второй странице результатов поисковиков. И мы пришли к решению, которое нас более чем устроило как по цене, так и по возможностям — Github Actions.

Итак, в нашем распоряжении решении имеется микросервисная архитектура (ну куда же без неё в наше непростое время), состоящая из 10 контейнеров, среди которых есть базы данных на PostgreSQL, сервисы бэкенда на .NET Core, сервис Nginx, небольшой Telegram-бот для быстрого сбора данных о возможных проблемах в решении, а также один контейнер со статической страничкой, который до релиза мы хотим вынести за пределы не только архитектуры, но и виртуальной машины в целом. Естественно, все это работает внутри системы Docker. Также имеется проект с тестами, которыми мы проверяем работоспособность всех основных модулей решения.

Запуск автотестов

Начнем с создания небольшого флоу для запуска наших тестов на гитхабе. Мы нашли множество примеров реализации запуска Unit-тестов для .NET, но одними Unit-тестами сыт не будешь: большая часть нашего решения покрыта интеграционными тестами Поэтому мы реализовали сборку наших основных сервисов и запуск тестов прямо на Github.

Приводим пример такого флоу с комментариями:

# Имя флоу
name: .NET Core

# Когда действие запустится (триггеры)
on:
  push:
    # при push в master
    branches: [ master ]

  pull_request:
    # при создании pull request на master
    branches: [ master ]

# Что будем делать (экшены)
jobs:
  # Имя действия, придумываем сами
  integration-tests:
    # На какой ОС будет работать виртуальная машина
    # Можно выбрать Ubuntu, Windows Server или macOS
    runs-on: ubuntu-latest
    # Шаги действия
    steps:
      # Шаг 1: собираем сервисы в режиме тестирования
      - uses: actions/checkout@v2
      - name: Build the stack
        run: docker-compose -f docker-compose.prod.yml -f docker-compose.test.yml up -d --build
 
      # Шаг 2: собираем проект с тестами
      - name: Build tests
        run: dotnet build GOT.Tests

      # Шаг 3: запускаем тесты с небольшой детализацией
      - name: Run tests
        run: dotnet test LOT.Tests --verbosity normal

После сохранения файла в репозитории по пути RepositoryName/.github/workflows/FileName.yml мы можем перейти на вкладку «Actions» и обнаружим там наш новый флоу:

.NET Core - наш новый флоу.NET Core — наш новый флоу

Если все было настроено верно, то при открытии pull request на master или при коммите на master (за последнее имеется отдельный котел в известном месте) Github запустит флоу, который пойдет по шагам и сначала соберет и запустит ваши контейнеры, а затем соберет и запустит проект с тестами, попутно выводя всю информацию из консоли своей виртуальной машины прямо в вашем браузере. При желании можно прикрутить бота Github, который будет сообщать о проблемах с тестами, заведет новые ишью, если тест не был пройден, а также сообщит вам в Slack/Telegram о проблемах. Все ограничено лишь фантазией и сроками. Тесты — это хорошо, но как насчет автоматизации доставки?

Автоматизация доставки

Но и тут у Github Actions есть все, что необходимо, но для полного понимания концепции стоит провести небольшой экскурс по возможностям Github:

  • Github Packages — используется для хранения сборок. В нашем случае мы используем его как внешний Docker registry. Подробности можно посмотреть тут.

  • Secrets — «секреты» на уровне репозитория или организации. С их помощью можно доверить Github такие ценные вещи, как логины, пароли, токены. Преимущество секретов в том, что даже в случае утраты доступа к аккаунту никто не получит доступа к секретам: Github выведет только их название, сами значения после нажатия «Save» останутся секретом для всех. Больше о секретах.

  • Github Actions от сторонних разработчиков. Маркетплейс различных экшенов растет очень быстро, с каждым месяцем появляются сотни различных интеграций и возможностей для разработчиков: Jira, Azure, Telegram, Slack и так далее. На момент написания статьи в маркетплейсе было более 9000 экшенов.

Будем использовать все эти возможности, чтобы реализовать полноценный CI для обновления тестового стенда.

Подбираем подходящие экшены

Нам понадобятся экшены для логина в Github Packages, для сборки образов микросервисов, для публикации образов в Github Packages. Также нам пригодится экшен, умеющий устанавливать SSH-соединение к нашей виртуальной машине и выполнять произвольные команды.

После небольшого поиска по маркетплейсу и изучения документации был подобран следующий список экшенов:

  • docker/setup-qemu-action@v1 — надстройка для виртуализации

  • docker/setup-buildx-action@v1 — модуль Docker для сборки образов

  • docker/login-action@v1 — экшен для логина в Docker Registry (в нашем случае — Github Packages)

  • docker/build-push-action@v2 — экшен сборки и публикации образа

  • appleboy/ssh-action@master — экшен для инициализации подключения по SSH и выполнения скрипта 

Настраиваем секреты

Один из немногих случаев, когда можно сделать скрин из приватного production-репозитория.

«Секреты» репозитория

Небольшие пояснения:

REGISTRY_TOKER — токен для авторизации в Github Packages. Его можно получить тут.

SERVER_HOST — IP-адрес сервера в виде »192.168.100.100».

SERVER_KEY — PEM-ключ для подключения к серверу. Обычно выдается провайдером вашей виртуальной машины.

SERVER_PORT — порт сервера для SSH-соединения. По-умолчанию это 22.

SERVER_USERNAME — имя пользователя, под которым авторизовываемся на виртуальной машине.

Как было описано выше про секреты, у нас нет возможности посмотреть, чему равен тот или иной секрет: Github даже не дает нам кнопки для просмотра этих значений.

Самое время готовить стенд: напишем docker-compose и залогинимся в Github Packages. После сборки и публикации образов в Github Packages нам необходимо подключиться к удаленной машине, выгрузить и запустить новую сборку. Для этого будем использовать практически тот же docker-compose файл, что и для локальной сборки и запуска, но немного отредактируем его. Во-первых, в качестве «image» будем использовать ссылку на Github Packages, где у нас будет опубликована сборка. Во-вторых, уберем из docker-compose-файла все параметры, которые касаются именно сборки: тут они нам попросту не нужны, так как сервер отныне не отвечает за сборку.

По итогу у нас получился примерно такой файл (некоторые параметры исправлены на «template»):

version: "3.4"
services:
  template-server:
    image: ghcr.io/template-inc/prod-template-server:latest
    environment:
      DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}"
      DOCKER: "true"
      EMAIL_SERVER: "http://template-email-server"
    restart: always
    links:
      - template-db
    networks:
      template-network: 

  template-db:
    image: postgres:11
    volumes:
      - db-volume:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    restart: always
    networks:
      template-network:

  template-nginx:
    image: ghcr.io/template-inc/prod-template-nginx:latest
    restart: always
    networks:
      template-network:
    depends_on:
      - template-server
    ports:
      - "90:80" 

networks:
  template-network:

volumes:
  db-volume:

Маленькое примечание: ссылка на image не должна содержать заглавных букв. Даже если имя репозитория или логин содержат заглавные буквы, в ссылке приведите их к lower-case формату. Иначе возникнут проблемы.

Опубликуем этот файл прямо в стартовом каталоге виртуальной машины, чтобы не усложнять скрипт запуска. Для примера назовем его «docker-compose.prod-ci.yml». Также, раз мы уже на машине, сразу залогинимся в Github Packages:

«docker login ghcr.io --username yourGithubUsername --password yourGithubToken»

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

Пишем workflow

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

name: 'build and deploy test server'
on:
  release:
    types: [published]
  workflow_dispatch:
jobs:
  build:
    name: 'Build & Publish'
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout repository"
        uses: actions/checkout@v2
        
      - name: "Set up QEMU"
        uses: docker/setup-qemu-action@v1
        
      - name: "Set up Docker Buildx"
        uses: docker/setup-buildx-action@v1

      - name: "Login to GitHub Registry"
        uses: docker/login-action@v1 
        with:
          registry: ghcr.io
          username: "yourGithubUsername"
          password: ${{ secrets.REGISTRY_TOKEN }}
          
      - name: "Build&Deploy template-server"
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/prod-template-server:${{ github.event.release.tag_name }}
            ghcr.io/${{ github.repository_owner }}/prod-template-server:latest
          secrets: |
            "ASPNETCORE_ENVIRONMENT=Release"
          build-args: |
            build_mode=Release
            
      - name: "Build&Deploy template-nginx"
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/prod-template-nginx:${{ github.event.release.tag_name }}
            ghcr.io/${{ github.repository_owner }}/prod-template-nginx:latest
          build-args: |
            build_mode=Release

      - name: "Run deploy on server"
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SERVER_KEY }}
          port: ${{ secrets.SERVER_PORT }}
          script: |
            sudo docker-compose -f docker-compose.prod-ci.yml -p prod pull
            sudo docker-compose -f docker-compose.prod-ci.yml -p prod up -d

Сохраним этот файл в репозитории по пути .github/workflows/test-server-ci.yml.

На этом настройка закончена. Переходим на вкладку «Actions» и видим новый экшен «build and deploy». Триггерами этого экшена выступают два действия: публикация нового релиза и ручной запуск.

Ручной запуск экшена можно осуществить прямо со страницы экшена. Для этого нажимаем «Run workflow», выбираем ветвь, с которой будет осуществляться сборка, и запускаем.

Запуск наших экшеновЗапуск наших экшенов

Для срабатывания триггера нового релиза зайдем в раздел «Releases» репозитория и создадим новый релиз, нажав «Draft a new release»:

Кнопка создания нового релизаКнопка создания нового релиза

Заполним основную информацию про новый релиз:

Форма описания нового релизаФорма описания нового релиза

 Сразу после нажатия «Publish release» сработает триггер и начнет выполнять все те шаги, которые мы описали: виртуальная машина Github«а клонирует себе репозиторий, залогинится в Github Packages, установит необходимые для Docker инструменты, соберет образы, опубликует их, затем подключится к нашей виртуальной машине и выполнит скрипт, который подключится к Github Packages, стянет последние сборки сервисов и развернет их. А за всем процессом можно зорко следить прямо в консоли запуска на Github. Разве это не прекрасно?

Бэкапы баз данных 

Какие шансы, что вы не потеряете свои данные в случае, если совершаете по 2–3 обновления в неделю, многие из которых мигрируют базу?

Спойлер: шансов немногоСпойлер: шансов немного

Поэтому практически сразу встал вопрос о том, как автоматизировать создание бэкапов баз данных. Тем более, когда их несколько, а варианты с готовыми системами управления либо дорогие, либо очень дорогие. И вновь на помощь приходит Github Actions!

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

Начнем с разметки каталогов на сервере: в корне создадим каталог db_backup, внутри него — template-db и template-identity-db

В каталоге db_backup создадим маленький скрипт:

#db username
dbUsername=""

# container additionals
containerPrefix="prod_"
containerPostfix="_1"

#container names
templateDb="template-db"
templateIdentityDb="template-identity-db"

#create template-db backup
cd $templateDb
docker exec -t $containerPrefix$templateDb$containerPostfix pg_dumpall -c -U $dbUsername > dump_$(date +%Y-%m-%d_%H_%M_%S).sql
echo "Backup of" $templateDb "created success"
cd ../

#create template-identity-db backup
cd $templateIdentityDb 
docker exec -t $containerPrefix$templateIdentityDb$containerPostfix pg_dumpall -c -U $dbUsername > dump_$(date +%Y-%m-%d_%H_%M_%S).sql
echo "Backup of" $templateIdentityDb "created success"
cd ../

echo "All database backups created success!"

Это максимум, который удалось реализовать за 15 минут, загуглив общий синтаксис sh-скриптов. Но этого нам хватит. Сохраним файл под именем create_backups.sh.

Теперь переходим к репозиторию и создадим новый workflow по аналогии выше:

name: 'Create prod database backups'
on:
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
  
  # Run action every day
  schedule:
  - cron: "0 0 * * *"

jobs:
  build:
    name: 'Build & Publish'
    runs-on: ubuntu-latest
    steps:
      - name: "Run backup script on server"
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SERVER_KEY }}
          port: ${{ secrets.SERVER_PORT }}
          script: |
            cd db_backup/
            sudo bash create_backups.sh

К слову, тут появился новый для этого материала тип триггера — schedule. Данный вид триггеров запускается по расписанию, а вот время запуска описано в формате «cron» (подробнее): многим ненавидимый, но в то же время прекрасный формат, позволяющий прописывать правило времени.

Итак, что мы имеем по итогу: раз в сутки (в полночь по UTC) на Github срабатывает триггер и запускает экшен, который, в свою очередь, устанавливает соединение с нашим сервером и выполняет скрипт запуска нашего бэкап-скрипта. Бэкап-скрипт пробегается по контейнерам с базами данных и создает бэкапы в соответствующих каталогах. Также у нас есть возможность создать резервные бэкапы вручную со страницы соответствующего workflow. Все, что вам останется — придумать, куда отправлять бэкапы на долгосрочное хранение.

К слову, автоматизированное создание бэкапов из 5 контейнеров занимает у нас 13–15 секунд вместе с подключением к серверу. Неплохо, не правда ли?

Заключение

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

© Habrahabr.ru