View on GitHub

blog

О программировании и не только

пайплайны и хуки в 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, предоставить команде разрабов привычный интерфейс репо в браузере.

вернуться обратно в блог