Как стать автором
Обновить

Комментарии 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, это уже вотчина билдера.

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

Если речь про стейдж, то всё ещё не понимаю зачем четвёртый, если честно.

Что б предметно говорить приложу абстрактный пример для 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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий