golang

Докеризация сборки проекта на всех уровнях

  • вторник, 23 июля 2024 г. в 00:00:06
https://habr.com/ru/companies/ispring/articles/830566/

Всем привет, на связи Вадим Макеров, бэкенд-разработчик iSpring. Успешная воспроизводимая сборка проекта является критическим фактором в поддержке и развитии проекта. При большом количестве проектов и технологических стеков гарантировать воспроизводимость сборки — «собралось однажды, соберется всегда» — сложнее.

О том, как реализовать идемпотентность сборки, я рассказывал в рамках митапа в офисе iSpring в 2023 году. Эта статья — текстовая версия моего доклада.

Моделируем

Предположим, что имеем систему, состоящую из множества проектов. Все проекты используют такой набор инструментов для сборки:

Этих инструментов достаточно для того, чтобы собрать любой проект. Количество проектов в системе увеличивается и благодаря изолированности команд проекты разрабатываются параллельно.

Здесь возникает первая проблема

Проблема №1 — Доработка старых проектов

По требованию отдела ИБ нужно доработать несколько проектов, которые уже давно не трогали.

Алгоритм внесения изменений:

  • Клонировать репозиторий проекта

  • Собрать проект локально (необходимо для выгрузки библиотек проекта)

  • Внести изменения

  • Скомпилировать, прогнать тесты, запустить линтер

  • Закоммитить изменения

После начала работ над первым проектом пошли первые неприятности:

Отсутствие специфических инструментов

Некоторые проекты, помимо общего набора инструментов, могут требовать специфические, которые не установлены у большинства разработчиков.

Новый линтер — старый проект

После внесения доработок запуск линтера выдал следующее

Линтер обнаружил замечания, которые раньше не замечал. Мы уверены, что раньше их не было, т.к. линтер запускается в CI/CD перед релизом изменений в проекта.

Такое поведение вызвано различием версий линтеров при разработке проекта и обновлении проекта у разработчика локально.

Замечания корректные и по-хорошему их нужно править, но без обновления инструментария — мы хотим, чтобы сборка всегда выдавала один и тот же результат, без фантомных замечаний линтера.

В итоге

Имеем сложности сборки проекта и замечания линтера, которые нужно править.

Проблема №2 — Обновление инструмента

Если быть точным — обратно несовместимое обновление инструмента в проекте.

Приведу пример реальной ситуации с обновлением инструментов:

Обновляется кодогенератор с версии v1  на v2, версии между собой несовместимы.

Команда A и B имеют локально установленные версии v1 инструмента.

В рамках доработок проекта команда A решает использовать инструмент v2.

Команда A ставит себе инструмент локально и дорабатывает проект под использование версии v2.

В виду производственной необходимости команде B необходимо доработать проект вместо команды A.

Команда B не способна собрать проект без указаний команды A об обновлении инструмента.

В итоге

Команда B не способна собрать проект без обращения к команде A или самостоятельных поисков и изучении нужной версии инструмента.

Ситуация осложняется при большом количестве команд, дорабатывающих разные проекты.

Проблема №3 — Отсутствие инструмента локально

Этого тезиса я касался в секции доработки старых проектов, хочу отдельно разобрать эту проблему.

Отсутствие заранее подготовленного инструмента локально вынуждает разработчика идти по шагам:

  • Искать в документации к проекту версию инструмента и как её установить. Но вряд ли это будет там описано 

  • Отвлекать других разработчиков

  • Искать в интернете нужный инструмент и подбирать версию

В итоге

  • Разработчик дольше вливается в разработку

  • Нет гарантий, что разработчик установит нужную версию инструмента

Проблематика

Вышеописанные примеры являются следствием системных недоработок

Версии инструментов не зафиксированы

  • Неизвестно какая версия используется в проекте

    • Усложняется первичная настройка

  • При обновлении инструментов нужно как-то уведомить другие команды/отделы

Локальное влияние

  • Локальный сетап непредсказуемо влияет на сборку

  • Появляются сайд-эффекты

Решение

Одним из решений может быть контейнеризация сборки

Под контейнеризацией я подразумеваю использование Docker и Docker-образов с необходимыми инструментами

Контейнеризация сборки - не единственный способ решения описанных проблем.
Решений существует множество, таких как Nix-shell, к примеру. Нам хотелось идти в контейнеризацию и изолированность инструментов, поэтому мы выбрали контейнеры и Docker.

Основные плюсы, которые привносит докеризация сборки:

Портативность

Docker-образы легко переносятся между машинами разработчиков, распространяются через registry docker.hub или другие registry.

К тому же, образы легко переносятся в локальное registry организации при необходимости изоляции CI/CD от внешних факторов

Консистентное окружение

Инструмент запускается в настроенном под него окружении, не требуя настройки локального окружения(переменные окружения, пути к исполняемым утилитам) и не конфликтуя с локальными утилитами разработчика.

Изоляция

Использование утилит изолированно в контейнере, что дает дополнительную безопасность локального окружения разработчика и CI/CD.

Запускаемые инструменты изолированы и не могут влиять на хост‑машину разработчика.

(Заметка на полях: у зловредных инструментов есть множество способов выбраться, но инструменту запущенному в докер контейнера навредить хост‑машине сложнее — чем будучи запущенным на хосте напрямую)

Версионирование

Docker‑образы идеально версионируются, в качестве тэга позволяя выставить любую строку. Можно использовать semver, день выпуска версии инструмента или просто хэш коммита из GIT.

Выбор инструмента

В CI/CD мы уже используем сборку в контейнерах посредством макро-образа со всеми утилитами.

Данное решение не подходит для локальной сборки и усложняет независимое обновление инструментов.

Таким образом, имеем следующее требование:

Сборка проекта должна проходить одинаково — локально и в CI/CD.

Makefile + Docker

Можно описать сборку в 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

Dev containers расширение для VS Code (JetBrains также поддержали в своих продуктах) позволяющее запустить IDE внутри контейнера с заранее подготовленным окружением для разработки.

Минусом такого подхода является монолитный образ для IDE и невозможность оперировать такими контейнерами на CI/CD.

DevConainer больше похож на описание окружения, а не утилиту сборки. Из-за чего с ним возникают неудобство в оперировании путями кэша, экспорта кэша в CI/CD и так далее.

Earthly.dev

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

(да-да, написали свой велосипед)

Выделяющими качествами BrewKit являются:

  • Сборка в контейнерах

    • Команды не выполняются локально

  • Исходники копируются в стадию сборки и результаты стадии явно экспортируются или используются дальше

    • Неправильное использование утилит не позволит удалить или испортить локальные файлы

    • Если зависимые файлы для стадии не изменились — стадия будет пропущена

  • В качестве движка сборки используется BuildKit

  • Описание сборки в Jsonnet — мощное расширение над классическим JSON

Архитектура

BrewKit обращается к docker-демону с конкретными командами сборки в рамках определенных образов.

Демо работы инструмента

https://asciinema.org/a/q09d6OZyAiGNz1QEFyrPLxPTi

Выводы

С контейнеризацией сборки теперь:

  • Проще контроль за используемыми инструментами и их версиями

  • Упрощенное обновление инструментов

  • Улучшение Developer Experience

    • Разработчик быстрее вливается в разработку проекта

Дополнение к выводам спустя 1 год

С данной темой я выступал на митапе год назад и хочу поделится тем, что изменилось в проекте за это время:

Brewkit → OpenSource

BrewKit выложен в opensource под лицензией MIT. Как и обещалось на митапе.
Теперь BrewKit можно опробовать у себя. В REDME.md лежит пример быстрого старта, а в docs/ лежит больше деталей о его внутренней реализации.

Обновления упростились

Недавно вышел go 1.22 и наше обновление на него было простым и быстрым.
Раньше у нас обновление проекта на новую версию Go, линтера и кодогенераторов занимал часа 4 на проект. С введением контейнеризации сборки — обновление каждого проекта занимает полчаса(в реальности даже меньше).

Подготовка отдельных образов инструментов, вместо одного макрообраза со всеми интрументами — сократило время введения новых инструментов до пары минут, вместо часа и сложной поддержки макрообраза как раньше.