Докеризация сборки проекта на всех уровнях
- вторник, 23 июля 2024 г. в 00:00:06
Всем привет, на связи Вадим Макеров, бэкенд-разработчик iSpring. Успешная воспроизводимая сборка проекта является критическим фактором в поддержке и развитии проекта. При большом количестве проектов и технологических стеков гарантировать воспроизводимость сборки — «собралось однажды, соберется всегда» — сложнее.
О том, как реализовать идемпотентность сборки, я рассказывал в рамках митапа в офисе iSpring в 2023 году. Эта статья — текстовая версия моего доклада.
Предположим, что имеем систему, состоящую из множества проектов. Все проекты используют такой набор инструментов для сборки:
Этих инструментов достаточно для того, чтобы собрать любой проект. Количество проектов в системе увеличивается и благодаря изолированности команд проекты разрабатываются параллельно.
Здесь возникает первая проблема
По требованию отдела ИБ нужно доработать несколько проектов, которые уже давно не трогали.
Алгоритм внесения изменений:
Клонировать репозиторий проекта
Собрать проект локально (необходимо для выгрузки библиотек проекта)
Внести изменения
Скомпилировать, прогнать тесты, запустить линтер
Закоммитить изменения
После начала работ над первым проектом пошли первые неприятности:
Некоторые проекты, помимо общего набора инструментов, могут требовать специфические, которые не установлены у большинства разработчиков.
После внесения доработок запуск линтера выдал следующее
Линтер обнаружил замечания, которые раньше не замечал. Мы уверены, что раньше их не было, т.к. линтер запускается в CI/CD перед релизом изменений в проекта.
Такое поведение вызвано различием версий линтеров при разработке проекта и обновлении проекта у разработчика локально.
Замечания корректные и по-хорошему их нужно править, но без обновления инструментария — мы хотим, чтобы сборка всегда выдавала один и тот же результат, без фантомных замечаний линтера.
Имеем сложности сборки проекта и замечания линтера, которые нужно править.
Если быть точным — обратно несовместимое обновление инструмента в проекте.
Приведу пример реальной ситуации с обновлением инструментов:
Обновляется кодогенератор с версии v1 на v2, версии между собой несовместимы.
Команда A и B имеют локально установленные версии v1 инструмента.
В рамках доработок проекта команда A решает использовать инструмент v2.
Команда A ставит себе инструмент локально и дорабатывает проект под использование версии v2.
В виду производственной необходимости команде B необходимо доработать проект вместо команды A.
Команда B не способна собрать проект без указаний команды A об обновлении инструмента.
Команда B не способна собрать проект без обращения к команде A или самостоятельных поисков и изучении нужной версии инструмента.
Ситуация осложняется при большом количестве команд, дорабатывающих разные проекты.
Этого тезиса я касался в секции доработки старых проектов, хочу отдельно разобрать эту проблему.
Отсутствие заранее подготовленного инструмента локально вынуждает разработчика идти по шагам:
Искать в документации к проекту версию инструмента и как её установить. Но вряд ли это будет там описано
Отвлекать других разработчиков
Искать в интернете нужный инструмент и подбирать версию
Разработчик дольше вливается в разработку
Нет гарантий, что разработчик установит нужную версию инструмента
Вышеописанные примеры являются следствием системных недоработок
Неизвестно какая версия используется в проекте
Усложняется первичная настройка
При обновлении инструментов нужно как-то уведомить другие команды/отделы
Локальный сетап непредсказуемо влияет на сборку
Появляются сайд-эффекты
Одним из решений может быть контейнеризация сборки
Под контейнеризацией я подразумеваю использование Docker и Docker-образов с необходимыми инструментами
Контейнеризация сборки - не единственный способ решения описанных проблем.
Решений существует множество, таких как Nix-shell, к примеру. Нам хотелось идти в контейнеризацию и изолированность инструментов, поэтому мы выбрали контейнеры и Docker.
Основные плюсы, которые привносит докеризация сборки:
Docker-образы легко переносятся между машинами разработчиков, распространяются через registry docker.hub или другие registry.
К тому же, образы легко переносятся в локальное registry организации при необходимости изоляции CI/CD от внешних факторов
Инструмент запускается в настроенном под него окружении, не требуя настройки локального окружения(переменные окружения, пути к исполняемым утилитам) и не конфликтуя с локальными утилитами разработчика.
Использование утилит изолированно в контейнере, что дает дополнительную безопасность локального окружения разработчика и CI/CD.
Запускаемые инструменты изолированы и не могут влиять на хост‑машину разработчика.
(Заметка на полях: у зловредных инструментов есть множество способов выбраться, но инструменту запущенному в докер контейнера навредить хост‑машине сложнее — чем будучи запущенным на хосте напрямую)
Docker‑образы идеально версионируются, в качестве тэга позволяя выставить любую строку. Можно использовать semver, день выпуска версии инструмента или просто хэш коммита из GIT.
В CI/CD мы уже используем сборку в контейнерах посредством макро-образа со всеми утилитами.
Данное решение не подходит для локальной сборки и усложняет независимое обновление инструментов.
Таким образом, имеем следующее требование:
Сборка проекта должна проходить одинаково — локально и в CI/CD.
Можно описать сборку в Makefile, где использовать контейнеры определенных образов для сборки
Пример:
all: build test check
.PHONY: build
build:
@docker run --rm -it \
-w ${PWD} \
-v ${PWD}:${PWD} \
-e GOCACHE=/app/cache/go-build \
-v /app/cache/go-build \
golang:1.22 \
build -v ./cmd/app -o ./bin/app
.PHONY: test
test:
@docker run --rm -it \
-w ${PWD} \
-v ${PWD}:${PWD} \
-e GOCACHE=/app/cache/go-build \
-v /app/cache/go-build \
golang:1.22 \
test ./...
.PHONY: check
check:
@docker run --rm -it \
-w ${PWD} \
-v ${PWD}:${PWD} \
-e GOCACHE=/app/cache/go-build \
-v /app/cache/go-build \
golangci/golangci-lint:v1.56 \
golangci-lint run
Плюсами я бы выделил:
Простота — такой Makefile написать достаточно просто
Интуитивность — в таком Makefile понятно что каждая команда делает
Минусы же:
Нет возможности переиспользовать слои от других этапов сборки, как в классическом Dockerfile
Не очень удобно оперировать docker run
, когда нужно писать более сложные команды
Dev containers расширение для VS Code (JetBrains также поддержали в своих продуктах) позволяющее запустить IDE внутри контейнера с заранее подготовленным окружением для разработки.
Минусом такого подхода является монолитный образ для IDE и невозможность оперировать такими контейнерами на CI/CD.
DevConainer больше похож на описание окружения, а не утилиту сборки. Из-за чего с ним возникают неудобство в оперировании путями кэша, экспорта кэша в CI/CD и так далее.
Earthly позволяет описывать сборку в формате Eaethfile похожему на Makefile + Dockerfile
(пример с README.md проекта)
# Earthfile
VERSION 0.8
FROM golang:1.15-alpine3.13
RUN apk --update --no-cache add git
WORKDIR /go-example
all:
BUILD +lint
BUILD +docker
build:
COPY main.go .
RUN go build -o build/go-example main.go
SAVE ARTIFACT build/go-example AS LOCAL build/go-example
lint:
RUN go get golang.org/x/lint/golint
COPY main.go .
RUN golint -set_exit_status ./...
docker:
COPY +build/go-example .
ENTRYPOINT ["/go-example/go-example"]
SAVE IMAGE go-example:latest
Плюсами Earthly я бы выделил:
Все команды выполняются в контейнерах
Использует Buildkit
Минусы же для нас оказались более значительными
Форсирование своей инфраструктуры
Используются кастомные версии Buildkit, что является еще одной точкой отказа в долговременной поддержке
Возможны Breaking-changes в инструменте
Версия v0.8.14 на момент написания статьи
Решили сделать собственный инструмент: BrewKit
(да-да, написали свой велосипед)
Выделяющими качествами BrewKit являются:
Сборка в контейнерах
Команды не выполняются локально
Исходники копируются в стадию сборки и результаты стадии явно экспортируются или используются дальше
Неправильное использование утилит не позволит удалить или испортить локальные файлы
Если зависимые файлы для стадии не изменились — стадия будет пропущена
В качестве движка сборки используется BuildKit
Описание сборки в Jsonnet — мощное расширение над классическим JSON
BrewKit обращается к docker-демону с конкретными командами сборки в рамках определенных образов.
https://asciinema.org/a/q09d6OZyAiGNz1QEFyrPLxPTi
С контейнеризацией сборки теперь:
Проще контроль за используемыми инструментами и их версиями
Упрощенное обновление инструментов
Улучшение Developer Experience
Разработчик быстрее вливается в разработку проекта
С данной темой я выступал на митапе год назад и хочу поделится тем, что изменилось в проекте за это время:
BrewKit выложен в opensource под лицензией MIT. Как и обещалось на митапе.
Теперь BrewKit можно опробовать у себя. В REDME.md лежит пример быстрого старта, а в docs/
лежит больше деталей о его внутренней реализации.
Недавно вышел go 1.22 и наше обновление на него было простым и быстрым.
Раньше у нас обновление проекта на новую версию Go, линтера и кодогенераторов занимал часа 4 на проект. С введением контейнеризации сборки — обновление каждого проекта занимает полчаса(в реальности даже меньше).
Подготовка отдельных образов инструментов, вместо одного макрообраза со всеми интрументами — сократило время введения новых инструментов до пары минут, вместо часа и сложной поддержки макрообраза как раньше.