Комментарии 12
COPY ./scripts /tmp/scripts
RUN chmod a+x /tmp/scripts/*.sh && \
run-parts --regex '.*sh$' \
--exit-on-error \
/tmp/scripts && \
rm -rf /tmp/scripts
Но ведь при таком подходе, получается один большой слой в собранном образе, что не очень хорошо. При добавление одного нового скрипта, например на уровень 4хх, все скрипиты будут пересобраны, даже те, что выше.
Или это окей и оно магическим образом разбивается на слои и кэшируется?
Нет, всё верно, всё упаковывается в один единый большой слой. И да, никакого раздельношо кэша в таком случае не будет, образ по сути будет пересобираться целиком. В первую очередь это оптимизирует толщину образа, если слои работают с общими файлами.
Плюс, пайплайн базовых образов не так часто триггерятся на практике, потому кэширование не проблема, я бы даже сказал наоборот, его отсутствие позволяет обновлять версии пакетов до актуальных в апстриме каждый релиз, а не только когда меняется код. Обратите внимание, что в моём CI не используется кэширование вовсе, так что полная пересборка гарантирована в любом случае на каждом релизе.
Раскройте мысль, почему единый слой - не очень хорошо, кроме кэширования? Обычно это наоборот классическая рекомендация - уменьшение количества слоёв в итоговом образе. Особенно при установке зависимостей из apt. Либо так, либо мультистейдж, который слепит все слои в один при использовании инструкции FROM на следующем шаге (для образов приложений, когда мы будем их рассматривать, я рекомендую именно такой подход).
Единый слой хорошо именно в итоговом образе как вы и написали. Во время сборки он плох тем, что ломает кэши. С другой стороны вы вроде бы именно это и хотите, тут нету идеального решения.
Я обычно делаю мультистейдж, где есть сборщик и итоговый образ. В сборщике все по слоям заботясь о кэшах и времени сборки. А в итоговом уже все склеено в один слой, для оптимизации скачивания образа.
Всё верно, сборку образов приложений мы именно так и организовываем. Только я бы ещё добавил, что разбиваем минимум на три слоя:
Builder: в нём мы делаем компиляцию или подготавливаем окружение, если это Python, например
Base: стейдж в котором мы ставим на базовый образ все частные зависимости, тут как раз можно не стесняться разбивать на слои
Runtime: делаем FROM из стейджа сборки зависимостей, упаковывая в единый слой, а потом делаем COPY --from из нашего builder'а уже собранного приложения.
В приложении это оправдано, поскольку в наших пайплайнах сборка образа может триггериться на каждый коммит в каждый ПР репозитория приложения. Это зачастую очень большой объем срабатываний, требующий к тому же минимальное время (поскольку на нём завязан цикл обратной связи), а потому требуется максимальное кэширование.
В базовых образах частотность гораздо ниже, что позволяет нам не так сильно беспокоиться о времени сборки. Но что более важно, кэши порой бывают только вредят, поскольку блокируют обновление на свежие версии незакреплённых зависимостей, а значит если запустить пересборку, произойдёт кэшхит и мы останемся со старыми версиями. В образе приложения такой проблемы нет, поскольку мы привязываемся к конкретной версии базового образа, а значит и версии пакетов, которые с ним приезжают по факту закреплены на момент его релиза.
Часто нужен ещё слой, который выделит файлы, содержащие описание зависимостей (*.csproj, package.json и подобные) для последующего использования слоем Base.
Свежему docker этот слой вроде бы уже не нужен, но кто видел свежий докер на сборочных агентах? :-)
Немного путаница случилась, видимо. Зависимостями, которые ставит base, я называю системные пакеты например, а не окружение самого приложения, как например node_modules, это уже вотчина билдера.
И да, моя ошибка, перечисление выше - это не слои, а именно стейджи, слои могут быть какие угодно в каждом слое, кроме разве что последнего, где мы минимизируем их количество.
Понял, но 4й слой всё равно нужен.
Если речь про стейдж, то всё ещё не понимаю зачем четвёртый, если честно.
Что б предметно говорить приложу абстрактный пример для Python-приложения того о чём говорил (для ноды всё плюс минус то же самое, только npm вместо poetry):
FROM ghcr.io/ovsds-example-organizaton/python-dev:3.10-jammy-0.3.1 as builder
# копируем всё, что нам нужно для сборки окружения
COPY pyproject.toml /pyproject.toml
COPY poetry.lock /poetry.lock
COPY poetry.toml /poetry.toml
# сборка
RUN poetry install
FROM ghcr.io/ovsds-example-organizaton/python:3.10-jammy-0.3.1 as base
# много слоёв разнообразной локально донастройки
RUN apt-get update && \
apt-get install -y \
libpq5 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
RUN mkdir --parents /opt/app
RUN useradd --create-home --home-dir /opt/app appuser
RUN chown -R appuser:appuser /opt/app
...
# отдельный стейдж, что б и были кэши разбитые по слоям, но и в рантайме был один слой
FROM base as runtime
# копируем окружение
COPY --from=builder /.venv /opt/app/.venv
# копируем приложение
COPY bin /opt/app/bin
COPY lib /opt/app/lib
WORKDIR /opt/app
USER appuser
ENTRYPOINT [".venv/bin/python", "-m", "bin.main"]
PS: это не значит, что не может быть четвёртого слоя, ещё как может, просто пытаюсь понять о чём именно вы.
Это у вас простой случай и всего один poetry.toml на докерфайл. А теперь представьте что у вас два модуля со своими зависимостями, которые должны оказаться в одном контейнере...
Вопрос почему этот модуль не в зависимостях другого, или зачем нам два приложения в одном контейнере?
Если же это какая-то глобальная зависимость, которую нам нужно собрать, то делаем обычно такое в base
, не забыв почистить исходники.
Разумеется, один из модулей главный, а второй у него в зависимостях. При этом оба модуля идут в исходниках одном репозитории.
Стандартное решение для организации кода в крупных проектах. На C# случается постоянно (у нас принято создавать новый модуль/проект на каждый чих), на других языках реже - но может понадобиться где угодно.
Понял, просто локальные зависимости обычно решаются на стороне тулинга. Например, poetry умеет собирать зависимости локальные (тоесть делается всё тот же poetry install), не только из pypi/git. В node существует lerna для монореп.
В общем кажется, что четвёртый стейдж, о котором вы говорите, скорее не повсеместный, а частный случай. Как и сказал выше, естественно бывают частности и стейджей может быть гораздо больше, я описал скорее базовый случай.
Честно сказать я вообще не большой фанат монореп как подхода для разработки именно приложений. Базовые образы - да. Пакеты - не всегда, но тоже да. Приложения - скорее нет. Это холиварная тема, предлагаю в неё не пускаться, может в последующих статьях :D
CI/CD в каждый дом: сборочный цех базовых docker-образов