Конвейер DevOps, часть 3: пайплайны и хуки в Git
В этой серии статей Олег Филон, ментор Эйч Навыки, рассказывает, как прийти к крутому CI/CD пайплайну. Сегодня разбираемся, как работать с пайплайнами и хуками в Git.
2К открытий6К показов

Я — Олег Филон, ментор Эйч Навыки и Senior DevOps Engineer. В этой статье расскажу, как организовать CI/CD пайплайн для контейнеризованного проекта с использованием утилиты make, сравню подходы для Docker и Podman, а также поделюсь хаком с использованием Git bare репозитория для автоматизации деплоя.
Первые две части лежат здесь: рабочее место/облако и Fedora Core/mise.
Начало проекта и утилита make
Представим идеальную ситуацию: я не только девопс, но и проектный менеджер, выбираю архитектуру проекта, и инструменты, и команду разработчиков, то есть полностью контролирую проект. В жизни такое вряд ли встретишь, но нам это нужно для примера, чтобы рассмотреть разные варианты.
Первый — классический пайплайн — это утилита make
. Обычно она используется для сборки программ из исходного кода. На самом деле make
хорошо подходит для решения сразу нескольких задач.
- Первая задача — отслеживание зависимостей одних файлов от других, например, при изменении сервиса пересобрать только соответствующий контейнер.
- Вторая задача, легко реализуемая через
make
— сборка в один файл много команд или скриптов, чтобы удобно их организовать. Как правило, сборка образа, его загрузка в репо, удаление временных файлов и прочее делается несколькими рутинными командами. Точно так же можно поместить в Makefile команды запуска сервисов и тестирование приложения локально. - Если эти этапы прошли успешно, можно выполнить коммит кода в репо проекта, сделать деплой в dev или stage environment. В github actions это называется jobs и steps. В
make
такая группа команд называется целью, она указывается параметром при вызове.
Так, несмотря на разную терминологию, по сути можно создать полноценный пайплайн для современного проекта с контейнеризованными сервисами.
Разбираемся на практике
Возьмём для примера проект с прокси сервером traefik и бэкендом на golang из репозитария awesome-pods. Этот репо задуман как форк замечательного проекта awesome-compose, в котором собраны конфиги docker compose
для 41 самого популярного сервиса. Я же пытаюсь сделать что-то похожее для манифестов podman
. Приглашаю к сотрудничеству начинающих девопс — сможете поучаствовать в открытом проекте, заработать почётные гитхаб-бейджи и улучшить своё резюме. Подробнее — здесь.
Мой проект в интересном положении: сделаны манифесты для нескольких сервисов, опробованы описанные выше подходы для миграции конфигов compose.yaml
в манифесты kube.yaml
. Но захотелось большего: а почему бы не сделать сразу пайплайны для тестирования, коммита в апстрим, деплоя и прочее. Зайдём в каталог traefik-golang и создадим пару мейк-файлов. Для начала сделаем всё это локально, начнём с make_compose
:
Этот файл уже в истории, равно как и соответствующий README.md, привожу его для примера. Так как я делаю конфиги сразу для двух платформ — docker и podman, для включения соответствующего Makefile’а нужно сделать линк на него: ln -s make_compose Makefile
.
Отлично, основную идею обсудили, идём дальше. В docker’е есть замечательная опция context
, позволяющая работать с любыми серверами, где настроен доступ. В нашем случае список контекстов выглядит так:
Здесь я использовал простейший хак — сделал копию дефолтного контекста с именем localhost
. Теперь мы можем сделать наш пайплайн способным на удалённый деплой. Достаточно прописать в /etc/hosts имя и адрес нашего dev сервера. Вот новая версия make_compose
:
Поясню немного подробнее.
- Самая первая строка — стандартное объявление списка целей.
- Строки 2-4 задают дефолтное значение переменной, если оно не задано в текущем
env
. - В хелп — строки 5-10 — добавлено предупреждение о текущем контексте, он задаётся в глобальной переменной, например,
export DKR_CONTEXT=localhost
для локального контекста. - Также добавлена цель commit в репо — строки 17-21 — после выполнения цели test.
- Test — строки 31-32 — в свою очередь, выполняется для текущего контекста, см. хак #1. Имя контекста должно совпадать с именем хоста нашего dev-сервера.
- Добавлена также цель
clean
: очистка старых образов с локальном репо,и зависимости в цельup
. Здесь убеждаемся, что образ пересобран и старые контейнеры остановлены.
Отлично, пайплайн для докера работает. Пробуем сделать то же самое для подмана. Здесь нас ждёт сюрприз, попробую рассказать в стиле прямого репортажа. Первоначально наш пайплайн для podman
выглядел вот так:
В строке 5 определяются зависимости: target back
соберёт исполняемый файл только в том случае, если код main.go или сам make_pods новее уже собранного бинарника.
Строка 6 удаляет backend
контейнер с едва заметным знаком минус -, чтобы игнорировать ошибку, если контейнер с именем backend не существует.
Строки 7–10 создают контейнер с именем backend
из пустого (scratch) контейнера — команды buildah
следуют обычным командам Dockerfile
, но в нижнем регистре: FROM -> from, COPY -> copy, RUN -> run, ENTRYPOINT -> config –entrypoint
и т. д. Здесь вы видите основное отличие от традиционного docker buildx подхода — вы работаете в двух контекстах одновременно: в локальном контексте, используя установленный компилятор go, и в контексте контейнера, копируя файлы в/из контейнера, запуская команды внутри контейнера и т. д. Другая новая возможность buildah
— вы можете собирать образ шаг за шагом, то есть отлаживать процесс сборки.
Строка 8 компилирует main.go в исполняемый файл back с соответствующими флагами.
Строка 11 создаёт из контейнера новый образ (image) с тегом backend:latest
.
Цель up — строка 16 — зависит от цели down
— строка 14, — то есть она сначала останавливает pod и удаляет контейнеры, если они всё ещё запущены, затем запускает новый под.
Цель down в строке 15 подставляет глобальную переменную $XDG_RUNTIME_DIR
из env пользователя в kube.yaml
, используемый далее в podman kube
командах, принимая новый манифест со стандартного ввода. Это также специфика podman — он работает полностью в пространстве пользователя, контейнеры взаимодействуют через собственный podman.sock. Таким образом, делаем пайплайн независимым от UID.
В подмане есть фунциональность наподобие docker context
, под другим именем, в подкоманде system connection
:
Первым в списке стоит настроенная в прошлой статье ВМ. Пока искал правильные опции для создания коннекшена (aka контекст в докере), столкнулся с подсказкой от подмана — «создайте сначала машину», а именно:
Выполнил эти рекомендации, подман выкачал, настроил и добавил два новых коннекшена для новой ВМ. Какой же меня ждал сюрприз, когда я стал смотреть, что же это за machine. Во-первых, в моём HOME появились новые файлы и каталоги:
Во-вторых, это полноценная ВМ fedora coreos
:
Конечно, приятно, что моё мнение совпало с мнением авторов подмана, точнее, со стратегией RedHat — fedora coreos
наиболее подходящая система для контейнерных приложений. С другой стороны, ВМ в подмане крутится полностью внутри пространства пользователя. У меня уже настроена почти такая же для удалённой работы всей команды разрабов. Решено: останавливаем новую виртуалку и правим мейкфайл для подмана по образцу компоуза, делаем пайплайн для деплоя и локально, и на удалённый дев-сервер.
Но прежде нам понадобится ещё один хак #2. Если в случае докера переключение контекста можно было сделать любой переменной, то для подмана между локальным соединением через сокет и удалённым, через uri:ssh
, имя переменной фиксировано CONTAINER_HOST. Вот как выглядит пайплайн make_pods.v1, настроенный и для локальной сборки, и для деплоя в наш дев-сервер:
По большей части цели мейкфайла остались теми же, но для удалённого деплоя настраиваем переменную export CONTAINER_HOST=ssh://dev@fc42dev:22/run/user/1001/podman/podman.sock
— берём её из коннекшена, она служит переключателем между локальным и удалённым контекстом. Для локального контекста эту переменную надо удалить: unset CONTAINER_HOST
. Команды в строках 19, 21 и 23 — это обычные команды шелла, они также меняются на локальное либо удалённое исполнение, переопределяются на основе этой же переменной CONTAINER_HOST
.
Как заметил внимательный читатель, в цели back исчезла сборка контейнера утилитой buildah. Как и для docker compose
, используется возможность самого подмана создавать образы на основе Containerfile, он же Dockerfile, эти названия синонимичны. Это намёк: пора отвыкать от слова докер, контейнеры уже давно стали основой облачных вычислений, для них созданы сотни приложений, например, CNCF и общепризнанные стандарты.
Принципиальный вопрос о контейнерах
Основное их преимущество — новый способ доставки приложений в облака, решение проблем с зависимостями, версиями библиотек, фреймворков и проч. Сборка контейнеров в контейнерах — побочный эффект облачных сервисов Github, Gitlab и других, с одной стороны, и ограничения Docker — с другой. Он не умеет, в отличие от подмана, точнее, от его сопутствующей утилиты buildah
, выполнять билд и создавать образ, используя локальное окружение.
Основная проблема сборки образа внутри контейнера — неэффективное использование кэша. Да, появились возможности как-то сохранять объемные загрузки внешних библиотек, модулей: это опции --mount=type=cache
для некоторых языков. Но, во-первых, эти возможности используются далеко не всегда. Во-вторых, опции для кэширования отличаются в podman и buildah, см. podman-build(1), придётся делать отдельный Containerfile. В-третьих, эффект от такого кэширования минимален. Предлагаю замерить время сборки, сделав ещё одну, третью версию пайплайна. Сначала соберём команды для buildah в отдельный файл:
и поправим пару строк в пайплайне:
Уточню условия нашего эксперимента — мы настроили одинаковую среду разработки с помощью утилиты mise (предыдущая статья) на нашем дев-сервере и у каждого из разрабов команды. Репозитарий git использует этот же дев-сервер, доступ к репо и серверу по ключу, парольный доступ закрыт. Пайплайны настроены как для локальной сборки, так и на дев-сервере. Перед запуском 3-й версии пайплайна на дев-сервере нужно сделать коммит изменений в репо — buildah
не знает о коннекшенах, работает с кодом в текущем каталоге (строка 1): после логина на сервер переключается в корень проекта. Предварительно выкачиваем образ компилятора go для сборки в контейнере — это вполне честно, мы же выкачали и настроили компилятор golang заранее. Замеряем:
Мы получили 10+-кратный выигрыш по времени сборки образа для подмана. Абсолютные времена не важны, также не влияет, запускали мы сборку локально или на дев-сервере — мы сравниваем только билд в контейнере и в настроенном локальном окружении. Третье измеренное время — сборка в Docker. Он умеет собирать только в контейнере, для него настроили кэширование в Containerfile:
Но оно не сильно помогло. Конечно, наш проект игрушечный, golang кэширует лучше других языков, но в целом вывод понятен: сборка в контейнере далеко не оптимальный вариант, если есть возможность настроить дев-сервер для работы команды.
Ещё замечание: конечно, образы, собираемые buildah
, совместимы с Docker, их можно использовать в конфигах compose.yaml. Но для этого надо настроить репозиторий образов и сначала загрузить образ в него. Локальные репозитории отличаются: Docker использует общий репо для всех пользователей — Docker Root Dir: /var/lib/docker
, а в подмане всё хранится в домашнем каталоге пользователя — graphRoot: /home/$USER/.local/share/containers/storage.
Как я предположил в самом начале, мы попробовали вариант с гипотетической идеальной командой разрабов, работающей в Линукс и умеющей в make. А как быть обычному девопсу с разношерстой командой, где кто-то сидит на Винде, а кто-то ни за что не откажется от привычного Макбука на M4? Есть вариант и для этого случая. Пусть они пишут код и тестируют его как им нравится, а в нашем репо на дев-сервере мы сделаем хак #3, а именно git hook и bare репозиторий — githooks(5), выполняющий наши цели сборки и старта приложения при коммите в репо.
Для этого на пару минут придётся стать безжалостным хакером, удаляющим лишнее и открывающим скрытые возможности гита. Выполняем следующие шаги:
- Заходим под юзером dev на сервер, создадим пустой каталог, например,
mkdir -pv ~/bare/t0
. Это станет новым GIT_DIR, зайдём в него и выполнимcd ~/bare/t0;git init --bare
. - Видим, что файлы, обычно спрятанные в каталоге .git, лежат прямо в корне. Сделаем дополнительно каталог для логов
mkdir logs
. Переходим в каталог hooks и создаём файл, где укажем команды выполнения при каждом изменении в репо.
Закомментированные строки 3, 8, 9 полезны при отладке пайплайна. Строки 4 и 5 задают, что есть, собственно, репозиторий, переменная GIT_DIR и переменная WORK_TREE (куда будут записываться файлы проекта). В цикле от строки 6 до 14 читаются и обрабатываются три переменные, с которыми гит вызывает этот хук. Строка 11 принимает все изменения в репо и обновляет WORK_TREE — всё то, что гит обычно делает в общем каталоге, как видим, в bare репо они разные. Далее, в 12 строим имя лога и строка 13 — собственно, пайплайн.
- Идём в каталог, где расположен репо проекта. Без страха и сожаления удаляем старый и создаём новый под тем же именем:
cd ~/src;rm -rf traefik-golang;mkdir traefik-golang.
- Завершаем сессию на дев-сервере, возвращаемся на рабочий комп и заходим в репо проекта. Конечно, репо цел, клоны репо не так просто уничтожить, пока есть хотя бы одна копия. Теперь смотрим старые настройки
git remote -v
и удаляем ихgit remote remove fc42dev
в моём случае. Создаём новый remote, указывая новый гит bare репо:git remote add bare.t0 dev@fc42dev:~/bare/t0
. Это также нужно сделать всем разрабам в их локальных копиях. - Проверяем результат. Возможно, нужно сделать новый комит и push в новый remote. Стоит посмотреть подробнее, как изменился репо проекта на сервере: проверить логи в
~/bare/t0/logs
, сравнить конфиги обычного репо проекта и на сервере, проверить, какие команды перестали работать в серверном репо. Например, в WORK_TREE не работают команды гит status; branch; commit; log. То есть наш хак #3 с git --bare не только позволил делать деплой на сервере, но также защитил репо от локальных изменений, а серверный репо всегда в чистоте и порядке. Можно редактировать код, но закомитить его только через обычный репо. Изменения на сервере удалятся после любого коммита.
Надеюсь, мне удалось показать, что пайплайны можно делать на основе древней забытой утилиты make. В следующей статье разберём, как можно добавить в наш скромный дев-сервер нечто похожее на монстров гит-сервисов, Gitlab и Github, создавать пайплайны, совместимые с github Actions, предоставить команде разрабов привычный интерфейс репо в браузере.
2К открытий6К показов