пайплайны и хуки в git
Представим идеальную ситуацию: я не только девопс, но и проектный менеджер, выбираю не только архитектуру проекта, но и инструменты, и команду разрабов, т.е. полный контроль над проектом. В жизни такое вряд ли встретишь, но нам, для примера, это нужно чтобы сравнить какие есть варианты.
Первый, классический пайплайн - это утилита make
. Обычно она используется для сборки программ из исходного кода. На самом деле make
хорошо подходит для решения сразу нескольких задач. Первая задача - отслеживание зависимостей одних файлов от других, например, при изменении сервиса пересобрать только соответствующий контейнер. Вторая задача, легко реализуемая через make
- собрать в один файл много команд или скриптов, удобно их организовать. Как правило, сборка образа, его загрузка в репо, удаление временных файлов, и прочее, делается несколькими рутинными командами. Точно также можно поместить в Makefile команды запуска сервисов, тестирование приложения локально. Если эти этапы прошли успешно, можно выполнить коммит кода в репо проекта, сделать деплой в dev или stage environment. В github actions это называется jobs и steps, в make
такая группа команд называется целью, она указывается параметром при вызове. Итого, несмотря на разную терминологию, по сути можно создать полноценный пайплайн для современного проекта с контейнеризованными сервисами.
Возьмём для примера проект с прокси сервером traefik и бэкендом на golang из репозитария awesome-pods. Этот репо задуман как форк замечательного проекта awesome-compose, в котором собраны конфиги docker compose
для 41 самых популярных сервисов. Я же пытаюсь сделать что-то похожее для манифестов podman
. Приглашаю к сотрудничеству начинающих девопс - сможете поучаствовать в открытом проекте, заработать почётные гитхаб бейджи, резко улучшить своё резюме. Подробности в CONTRIBUTING.md.
Мой проект в интересном положении - сделаны манифесты для нескольких сервисов, опробованы описанные выше подходы для миграции конфигов compose.yaml
в манифесты kube.yaml
. Но захотелось большего: а почему бы не сделать сразу пайплайны для тестирования, коммита в апстрим, деплоя и проч. Зайдём в каталог traefik-golang
и создадим пару мейк-файлов. Для начала сделаем всё это локально, начнём с make_compose
:
... $ cat make_compose
.PHONY: help build up down stop restart logs test ps stats shell
help:
@echo "make_compose avalable targets:\n"
@make -pRrq 2>/dev/null|grep -v ^Makefile|grep -P '^\w+:'|sort
build:
docker compose build
up:
docker compose up -d
down:
docker compose down -v
stop:
docker compose stop
restart:
docker compose stop
docker compose up -d
logs:
docker compose logs
test:
curl localhost:8080
ps:
docker compose ps -a
stats:
docker compose stats --no-stream
shell:
docker exec -ti traefik /bin/sh
Этот файл уже в истории, равно как и сооветсвующий README.md, привожу его для примера. Т.к. я делаю конфиги сразу для 2 платформ - docker и podman, для включения соответствующего Makefile’а надо сделать линк на него: ln -s make_compose Makefile
. Отлично, основную идею обсудили, идём дальше. В docker’е есть замечательная опция context, позволяющая работать с любыми серверами, где настроен доступ. В нашем случае список контекстов выглядит так:
... $ docker context ls
NAME DESCRIPTION DOCKER ENDPOINT ERROR
deb12fri vm on https://cloud.ru ssh://deb12fri
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
desktop-linux Docker Desktop unix:///home/ophil/.docker/desktop/docker.sock
fc42dev * libvirt vm fc42dev ssh://dev@fc42dev
localhost localhost for make_compose pipeline unix:///var/run/docker.sock
Здесь я использовал простейший хак - сделал копию дефолтового контекста с именем localhost
. Теперь мы можем сделать наш пайплайн способным на удалённый деплой, достаточно прописать в /etc/hosts имя и адрес нашего dev сервера. Вот новая версия make_compose
:
... $ nl make_compose
1 .PHONY: help build up down clean commit stop restart logs test ps stats shell
2 ifndef DKR_CONTEXT
3 DKR_CONTEXT = localhost
4 endif
5 help:
6 @echo "make sure you in proper docker context: ${DKR_CONTEXT}"
7 docker context use ${DKR_CONTEXT}
8 docker context ls
9 @echo "make_compose avalable targets:"
10 @make -pRrq 2>/dev/null|grep -v ^Makefile|grep -P '^\w+:'|sort
11 build: main.go make_compose
12 docker compose build
13 up: build down
14 docker compose up -d
15 clean:
16 docker system prune -f
17 commit: test
18 git branch -v
19 @read -p "Enter commit message:" mesg;\
20 git commit -am "$$mesg"
21 git push
22 down:
23 docker compose down -v
24 stop:
25 docker compose stop
26 restart:
27 docker compose stop
28 docker compose up -d
29 logs:
30 docker compose logs
31 test:
32 curl ${DKR_CONTEXT}:8080
33 ps:
34 docker compose ps -a
35 stats:
36 docker compose stats --no-stream
37 shell:
38 docker exec -ti traefik /bin/sh
Поясню немного подробнее. Самая первая строка : стандартное отъявление списка целей. Строки 2-4 задают дефолтовое значение переменной, если оно не задано в текущем env
. В хелп, строки 5-10, добавлено предупреждение о текущем контексте, он задаётся в глобальной переменной, напр. export DKR_CONTEXT=localhost
для локального контекста. Также добавлена цель commit в репо, строки 17-21, после выполнения цели test. Test, строки 31-32, в свою очередь, выполняется для текущего контекста, см. хак #1, имя контекста должно совпадать с именем хоста нашего dev-сервера. Добавлена также цель clean
: очистка старых образов с локальном репо, и зависимости в цель up
: убедиться, что образ пересобран и старые контейнеры остановлены.
Отлично, пайплайн для докера работает. Пробуем сделать то же самое для подмана. Здесь нас ждёт сюрприз, попробую рассказать в стиле прямого репортажа. Первоначально наш пайплайн для podman
а выглядел вот так:
... $ nl make_pods
1 .PHONY: help up clean down logs test ps stats shell
2 help:
3 @echo -e "make_pods avalable targets:\n"
4 @make -pRrq 2>/dev/null|grep -v ^Makefile|grep -P '^\w+:'|sort
5 back: main.go make_pods
6 -buildah rm backend
7 buildah from --name backend scratch
8 CGO_ENABLED='0' go build -v -ldflags "-w -s" -o back main.go
9 buildah copy backend back /usr/local/bin/backend
10 buildah config --entrypoint '["/usr/local/bin/backend"]' backend
11 buildah commit backend backend:latest
12 clean:
13 podman system prune -f
14 down:
15 envsubst < kube.yaml | podman kube down -
16 up: down
17 envsubst < kube.yaml | podman kube play -
18 logs:
19 podman pod logs go-app
20 test:
21 curl localhost:8080
22 ps:
23 podman ps -ap
24 stats:
25 podman stats --no-stream
26 shell:
27 podman exec -ti go-app-traefik /bin/sh
В строке 5 определяются зависимости: target back соберёт исполняемый файл только в том случае, если код main.go или сам make_pods новее уже собранного бинарника.
Строка 6 удаляет 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
:
$ podman system connection add --identity ~/.ssh/id_ed25519 --port 22 fc41dev dev@fc41dev
$ podman system connection list
Name URI Identity Default
fc42dev ssh://dev@fc42dev:22/run/user/1001/podman/podman.sock /home/ophil/.ssh/id_ed25519 true
proba ssh://[email protected]:37973/run/user/1000/podman/podman.sock /home/ophil/.ssh/proba false
proba-root ssh://[email protected]:37973/run/podman/podman.sock /home/ophil/.ssh/proba false
Первым в списке стоит настроенная в прошлой статье ВМ. Пока искал правильные опции для создания коннекшена (aka контекст в докере), столкнулся с подсказкой от подмана, мол, создайте сначала машину, а именно:
$ podman system df
Cannot connect to Podman. Please verify your connection to the Linux system using
"podman system connection list", or try "podman machine init" and "podman machine start"
to manage a new Linux VM
Выполнил эти рекомендации, подман выкачал, настроил и добавил 2 новых коннекшена для новой ВМ. Какой же меня ждал сюрприз, когда я стал смотреть что же это за machine. Во-первых, в моём HOME появились новые файлы и каталоги:
$ ll ~/.ssh/proba*
-rw------- 1 ophil ophil 399 Apr 25 16:59 /home/ophil/.ssh/proba
-rw-r--r-- 1 ophil ophil 94 Apr 25 16:59 /home/ophil/.ssh/proba.pub
$ tree /home/ophil/.config/containers/podman/
/home/ophil/.config/containers/podman/
└── machine
└── qemu
├── proba.ign
├── proba.json
└── proba.lock
3 directories, 3 files
$ tree /home/ophil/.local/share/containers/podman
/home/ophil/.local/share/containers/podman
└── machine
└── qemu
├── cache
│ └── fedora-coreos-42.20250410.2.1-qemu.x86_64.qcow2.xz
├── podman.sock
└── proba_fedora-coreos-42.20250410.2.1-qemu.x86_64.qcow2
4 directories, 3 files
Во-вторых, это полноценная ВМ fedora coreos
:
$ podman machine ssh proba head /etc/os-release
NAME="Fedora Linux"
VERSION="42.20250410.2.1 (CoreOS)"
RELEASE_TYPE=stable
ID=fedora
VERSION_ID=42
VERSION_CODENAME=""
PLATFORM_ID="platform:f42"
PRETTY_NAME="Fedora CoreOS 42.20250410.2.1"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
$ podman machine ssh proba sudo dmesg|head -2
[ 0.000000] Linux version 6.14.0-63.fc42.x86_64 (mockbuild@d5701c6d040c430c8283c8c9847dc93f) (gcc (GCC) 15.0.1 20250228 (Red Hat 15.0.1-0), GNU ld version 2.44-3.fc42) #1 SMP PREEMPT_DYNAMIC Mon Mar 24 19:53:37 UTC 2025
[ 0.000000] Command line: BOOT_IMAGE=(hd0,gpt3)/boot/ostree/fedora-coreos-5d67a1bf86573c8e67b45ca2e1c1bde3a618c688ea15e69053c8b88695b1a8e4/vmlinuz-6.14.0-63.fc42.x86_64 rw mitigations=auto,nosmt ostree=/ostree/boot.1/fedora-coreos/5d67a1bf86573c8e67b45ca2e1c1bde3a618c688ea15e69053c8b88695b1a8e4/0 ignition.platform.id=qemu console=tty0 console=ttyS0,115200n8 root=UUID=297242c0-0f78-47fe-9161-0ac28761bdc0 rw rootflags=prjquota boot=UUID=553bd753-07f8-4dd3-b3bd-5c0605bdae3f
Конечно, приятно, что моё мнение совпало с мнением авторов подмана, точнее, со стратегией RedHat - fedora coreos
наиболее подходящая система для контейнерных приложений. С другой стороны, ВМ в подмане крутится полностью внутри пространства пользователя, у меня уже настроена почти такая же для удалённой работы всей команды разрабов. Решено, останавливаем новую виртуалку и правим мейкфайл для подмана по образцу компоуза, делаем пайплайн для деплоя и локально, и на удалённый дев. сервер.
Но прежде нам понадобится ещё один хак #2. Если в случае докера переключение контекста можно было сделать любой переменной, то для подмана между локальным, через сокет, соединением, и удалённым, через uri:ssh, имя переменной фиксировано CONTAINER_HOST. Вот как выглядит пайплайн make_pods.v1, настроенный и для локальной сборки, и для деплоя в наш дев. сервер:
... $ nl make_pods.v1
1 .PHONY: help up back commit test clean down logs ps stats shell
2 vshell = /bin/sh -c
3 vhost = localhost
4 workdir = ${HOME}/src/traefik-golang
5 ifdef CONTAINER_HOST
6 vshell = ssh dev@fc42dev
7 vhost = fc42dev
8 workdir = /home/dev/src/traefik-golang
9 endif
10 help:
11 @echo "make sure you are in proper podman system connection : ${CONTAINER_HOST}\n"
12 podman system connection list
13 @echo "make_pods avalable targets:\n"
14 @make -pRrq 2>/dev/null|grep -v ^Makefile|grep -P '^[\w-]+:'|sort
15 @echo "make vars: ${vshell} ${vhost} ${workdir}"
16 back: main.go make_pods
17 podman build . -t backend
18 down:
19 ${vshell} 'cd ${workdir} ; envsubst < kube.yaml | podman kube down -'
20 up: down
21 ${vshell} 'cd ${workdir} ; envsubst < kube.yaml | podman kube play -'
22 test:
23 curl ${vhost}:8080
24 clean:
25 -rm back
26 podman system prune -f
27 commit: test
28 git branch -v
29 git add .
30 @read -p "Enter commit message:" mesg;\
31 git commit -m "$$mesg"
32 git push
33 logs:
34 podman pod logs go-app
35 ps:
36 podman ps -ap
37 stats:
38 podman stats --no-stream
39 shell:
40 podman exec -ti go-app-traefik /bin/sh
По большей части цели мейкфайла остались теми же, но для удалённого деплоя настраиваем переменную 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, и общепризнанные стандарты.
Для дальнейшего рассказа хочу обсудить принципиальный вопрос о контейнерах. Основное их преимущество - новый способ доставки приложений в облака, решение проблем с зависимостями, версиями библиотек, фреймворков и проч. Сборка контейнеров в контейнерах - это побочный эффект облачных сервисов - гитхаба, гитлаба, других похожих, с одной стороны, и ограничения докера с другой. Он не умеет, в отличие от подмана, точнее, от его сопутствующей утилиты buildah
, выполнять билд и создавать образ, используя локальное окружение.
Основная проблема сборки образа внутри контейнера - неэффективное использование кэша. Да, появились возможности как-то сохранять объемные загрузки внешних библиотек, модулей: это опции --mount=type=cache
для некоторых языков. Но, во-первых, эти возможности используются далеко не всегда. Во-вторых, опции для кэширования отличаются в podman и buildah, см. podman-build(1), придётся делать отдельный Containerfile. В-третьих, эффект от такого кэширования минимален. Предлагаю замерить время сборки, сделав ещё одну, 3-ю версию пайплайна. Сначала соберём команды для buildah в отдельный файл:
... $ nl buildah.cmd
1 cd $HOME/src/traefik-golang
2 buildah rm backend
3 buildah from --name backend scratch
4 CGO_ENABLED='0' go build -v -ldflags "-w -s" -o back main.go
5 buildah copy backend back /usr/local/bin/backend
6 buildah config --entrypoint '["/usr/local/bin/backend"]' backend
7 buildah commit backend backend:latest
и поправим пару строк в пайплайне:
... $ diff make_pods.v{1,2}
16,17c16,17
< back: main.go make_pods.v1
< podman build . -t backend
---
> back: main.go make_pods.v2 buildah.cmd
> ${vshell} 'cd ${workdir} ; . ./buildah.cmd'
Уточню условия нашего эксперимента - мы настроили одинаковую среду разработки с помощью утилиты mise
, см. предыдущую статью, и на нашем дев. сервере, и у каждого из разрабов команды. Репозитарий git использует этот же дев. сервер, доступ к репо и серверу по ключу, парольный доступ закрыт. Пайплайны настроены как для локальной сборки, так и на дев. сервере. Перед запуском 3-й версии пайплайна на дев. сервере нужно сделать коммит изменений в репо - buildah
не знает о коннекшенах, работает с кодом в текущем каталоге, см. строку 1: после логина на сервер сначала переключается в корень проекта. Преварительно выкачиваем образ компилятора go для сборки в контейнере - это вполне честно, мы же выкачали и настроили компилятор golang заранее. Замеряем:
rm Makefile ;ln -s make_pods.v1 Makefile
time make back
...
real 0m15.328s user 0m28.341s sys 0m6.315s
rm Makefile ;ln -s make_pods.v2 Makefile
time make back
...
real 0m1.301s user 0m0.754s sys 0m0.917s
rm Makefile ;ln -s make_compose Makefile
time make build
...
real 0m13.429s user 0m0.177s sys 0m0.142s
Мы получили 10+ кратный выигрыш по времени сборки образа для подмана. Абсолютные времена не важны, так же не влияет, запускали мы сборку локально или на дев. сервере, мы сравниваем только билд в контейнере и в настроенном локальном окружении. Третье время сборка в докере, он умеет собирать только в контейнере, для него кэширование в Containerfile было настроено:
$ grep ^RUN Containerfile
RUN --mount=type=cache,target=/go/pkg/mod CGO_ENABLED=0 go build -v -ldflags "-w -s" -o backend main.go
но оно не сильно помогло. Конечно, наш проект игрушечный, golang кэширует лучше других языков, но в целом вывод понятен - сборка в контейнере далеко не оптимальный вариант если есть возможность настроить дев. сервер для работы команды. Ещё замечание - конечно, образы, собираемые buildah
совместимы с докером, их можно использовать в конфигах compose.yaml. Но для этого надо настроить репозитарий образов и сначала загрузить образ в него. Локальные репозитарии отличаются, докер использует общий репо для всех пользователей, т.н. Docker Root Dir: /var/lib/docker
, в подмане всё хранится в домашнем каталоге пользователя, graphRoot: /home/$USER/.local/share/containers/storage
Как я предположил в самом начале, мы попробовали вариант с гипотетической идеальной командой разрабов, работающей в Линукс, умеющей в make. А как быть обычному девопсу с разношерстой командой, где кто-то сидит под виндой, а кто-то ни за что не откажется от привычного Макбука с M4 процем?? Есть вариант и для этого случая. Пусть они пишут код и тестируют его как им нравится, а в нашем репо на дев. сервере мы сделаем гит хук, см. githooks(5), выполняющий наши цели сборки и старта приложения при коммите в репо:
... $ cat .git/hooks/update
#!/usr/bin/env bash
# local git build
LOGFILE=$(date +%y%m%d_%H%M).log
(cd .. && make back && make up && make test && make clean)|tee -a logs/$LOGFILE
Надеюсь, мне удалось показать, что пайплайны можно делать на основе древней забытой утилиты make
. В следующей статье разберём как можно добавить в наш скромный дев. сервер нечно похожее на монстров гит-сервисов, гитлаба и гитхаба, создавать пайплайны, совместимые с github Actions, предоставить команде разрабов привычный интерфейс репо в браузере.