Автоматизация глазами разработчика: 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 — наш новый флоу
Если все было настроено верно, то при открытии 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 секунд вместе с подключением к серверу. Неплохо, не правда ли?
Заключение
Сегодня мы поговорили про варианты автоматизации, когда нужно дешево и быстро реализовать не самые сложные сценарии по запуску интеграционных тестов, обновлению стенда, созданию бэкапов. Безусловно, эти скрипты и сценарии можно и нужно улучшать, но я надеюсь, что хабровчане смогут использовать этот материал как небольшой пример, с которого можно начать автоматизацию рутины.