[Перевод] 10 самых распространенных проблем при линтинге Dockerfile'ов

Весной 2023 года разработчики Depot сообщили о том, что теперь с помощью их сервиса можно проверять Dockerfile при каждой сборке. Depot — это сервис удаленной сборки контейнеров, который может создавать образы Docker до 20 раз быстрее, чем сборка образов Docker внутри обычных CI-провайдеров. Это молодая компания, которая образовалась в начале 2022 года, но уже сейчас она состоит в венчурном фонде Y Combinator и привлекла 1,8 млн долларов инвестиций.

a61106320bf83fa1d9616ec469dad6c7.png

После добавления возможности проверять Dockerfiles в Depot разработчики сервиса столкнулись со множеством сложностей. В итоге они выделили 10 наиболее распространенных проблем при линтинге Dockerfile’ов и описали их в статье, которую мы перевели для вас.

В статье разработчики Depot разбирают каждую проблему, объясняют, почему она возникает и как ее решить. Авторы отмечают, что со временем список может измениться, но даже в таком виде он послужит хорошей отправной точкой для оптимизации Dockerfile’ов.

Линтинг Dockerfile’ов

В Depot мы используем два линтера Dockerfile’ов: hadolint и набор правил для линтера Dockerfile’ов, разработанный Semgrep. Благодаря им процесс становится более интеллектуальным и автоматизированным.

Hadolint можно запустить локально — задать ему определенные правила и снабдить конфиг-файлом. Также доступен пользовательский веб-интерфейс. Предварительно hadolint необходимо установить. Это можно сделать с помощью brew либо использовать готовый Docker-образ и просто «скормить» ему Dockerfile:

hadolint Dockerfile

# или использовать Docker-образ

docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile

1. Объединяйте связанные инструкции RUN

В hadolint эта ошибка известна как DL3059. Это самая распространенная проблема, с которой мы сталкиваемся в своей практике. Она затрагивает почти 30% всех Dockerfile’ов. Суть в том, что несколько RUN-инструкций следуют друг за другом, хотя их можно было бы объединить в одну. Пример:

RUN download_a_really_big_file
RUN remove_the_really_big_file

Чтобы понять, в чем тут дело, нужно вспомнить, как в Docker устроено кэширование слоев. Каждый новый оператор RUN в Dockerfile — это отдельный слой в конечном образе.

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

Другая проблема с DL3059 — установка пакетов в два захода. Например:

RUN fetch_package_registry_list
RUN install_some_package

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

Решение DL3059

При работе с большими файлами, которые добавляются и удаляются во время docker build, разумно объединять эти операции в один атомарный оператор RUN:

RUN download_a_really_big_file && \
         remove_the_really_big_file

Это уменьшает конечный размер образа, поскольку удаляется промежуточный слой с большим файлом, ведь все манипуляции с ним проводятся в рамках одного RUN-выражения. Если объединять RUN-операторы, которые включают кэшируемые вещи, с вещами, которые часто обновляются (тем самым инвалидируя кэш), могут возникнуть тонкости при работе с кэшем. В таких случаях кэшируемую часть лучше держать в отдельном операторе RUN.

В примере с реестром пакетов получение списка пакетов и установку нужного пакета можно совместить в одном операторе RUN:

RUN fetch_package_registry_list && \
         install_some_package

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

2. Явно указывайте версии при установке с apt-get

Еще одна неоднозначная проблема с линтингом Dockfile’ов получила в hadolint номер DL3008. Она присутствует примерно в трети всех Dockerfile’ов. Проблема возникает, если при установке с помощью apt-get не прописывать версии пакетов явно. Например:

FROM ubuntu:22.04
RUN apt-get update && \
         apt-get install -y some-package

Если не указывать версию явно, docker build не будет ее проверять — так можно оказаться в ситуации, когда версия пакета отличается от нужной. Это может привести к проблемам при сборке Dockerfile’а или запуске получившегося образа.

Решение DL3008

FROM ubuntu:22.04
RUN apt-get update && \
         apt-get install -y some-package=1.2.*

Явно указывая версию пакета, мы заставляем build скачать именно ее. Это позволяет контролировать, какие именно пакеты устанавливаются в Dockerfile, а также их зависимости.

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

3. Используйте --no-install-recommends, чтобы избежать установки ненужных пакетов

Еще одна распространенная ошибка линтера — DL3015: установка лишних пакетов с помощью apt-get. Она присутствует в 22% всех Dockerfile’ов. Проблема возникает, когда apt-get install запускается без флага --no-install-recommends. Например:

FROM ubuntu:22.04
RUN apt-get update && \
        apt-get install -y some-package

Если не указан флаг --no-install-recommends, apt-get устанавливает рекомендуемые пакеты в дополнение к основному. То есть размер Docker-образа может сильно вырасти из-за установки в него лишних пакетов.

Решение DL3015

FROM ubuntu:22.04
RUN apt-get update && \
    apt-get install -y some-package --no-install-recommends

Решение очевидно: указывать флаг --no-install-recommends в apt-get install. Это предотвратит установку рекомендуемых пакетов и уменьшит размер образа. При этом необходимо понимать, какие именно пакеты идут в качестве рекомендуемых, чтобы не забыть о зависимостях.

4. Не используйте кэш при вызове pip install

Когда заходит речь о выполнении команды pip install во время сборки, встает вопрос послойного кэширования. Соответствующая ошибка hadolint DL3042 присутствует в 18% всех Dockerfile’ов. Проблема возникает, когда мы забываем запретить pip install использовать кэш в Dockerfile. Например:

FROM python:3.11
RUN pip3 install mysql-connector-python

В этом случае pip install не только установит пакет, но и сохранит его в кэше. Если пакетов много, вырастет итоговый размер Docker-образа.

Решение DL3042

FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python

Нет никакого смысла кэшировать Pip-пакеты при сборке Docker-образа, поскольку их никто не будет переустанавливать. Вместо этого можно использовать кэш Docker-слоя. Отключение кэша уменьшает размер конечного образа.

5. Удаляйте списки apt-get после установки пакетов

Как уже говорилось в другой нашей статье, уменьшение размеров образов контейнеров часто связано с тем, как проходит docker build. Ошибка hadolint DL3009 присутствует в 16% всех Dockerfile’ов. Проблема возникает, если после установки пакетов не удалить списки apt-get. Например:

FROM ubuntu:22.04
RUN apt-get update && \
         apt-get install -y some-package --no-install-recommends

Приведенный выше пример для DL3015 можно еще больше оптимизировать, уменьшив итоговый размер образа. Если не очистить кэш apt-get, тот попадет в слой для RUN, занимая ценное место. 

Решение DL3009

FROM ubuntu:22.04
RUN apt-get update && \
    apt-get install -y some-package --no-install-recommends && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

Здесь установка пакета объединяется с очисткой кэша apt-get — обе операции проходят в одном атомарном RUN-выражении. Это позволяет уменьшить размер конечного образа, удалив из него кэш apt-get, и избежать добавления лишних слоев.

6. Используйте WORKDIR вместо RUN cd some-path

Другая распространенная проблема линтинга Dockerfile’ов идет под номером DL3003. Она возникает, когда вместо WORKDIR используется RUN cd. Ошибка встречается в 14% всех Dockerfile’ов. Типичный пример:

FROM ubuntu:22.04
RUN cd /usr/src/app && git clone git@github.com:depot/some-repo.git

Каждое RUN-выражение выполняется в собственной командной оболочке, и большинство команд умеют работать с абсолютными путями.

Решение DL3003

FROM ubuntu:22.04
WORKDIR /usr/src/app
RUN git clone git@github.com:depot/some-repo.git

При смене директорий можно использовать WORKDIR, который запускает shell в указанной директории. Единственное исключение — когда нужно сделать что-то в подоболочке. В этом случае придется использовать cd.

7. Явно указывайте версии пакетов при установке через pip

Ошибка DL3013 повторяет логику DL3008 в применении к pip вместо apt-get install. Ошибка встречается в 13% всех Dockerfile’ов. Типичный пример:

FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python

Если не указывать версию явно, docker build не будет ее проверять, и тогда вы рискуете оказаться в ситуации, когда версия пакета отличается от нужной. Как говорилось в DL3008, если установить версию, отличную от изначально заданной при создании Dockerfile, это может привести к неожиданному поведению.

Решение DL3013

FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python==8.1.0

Если явно указать версию mysql-connector-python, docker build будет вынуждена использовать именно ее независимо от того, что есть в кэше слоя Docker.

8. Используйте нотацию JSON для аргументов CMD и ENTRYPOINT

Ошибка линтинга DL3025 сводится к корректности при запуске образа. Она присутствует в 12% всех Dockerfile’ов. Вот типичные примеры выражений, в которых она встречается:

FROM ubuntu:22.04
ENTRYPOINT foo run-server
FROM ubuntu:22.04
CMD foo run-server

Если не использовать JSON-нотацию для аргументов CMD и ENTRYPOINT, исполняемые файлы не будут получать сигналы от ОС. Это особенно актуально, когда необходимо сообщить работающему контейнеру об окончании его работы — послать SIGTERM.

Решение DL3025

FROM ubuntu:22.04
ENTRYPOINT ["foo", "run-server"]
FROM ubuntu:22.04
CMD ["foo", "run-server"]

Если использовать JSON-нотацию, исполняемый файл станет PID 1 в контейнере и сможет получать сигналы от ОС. Еще пара моментов, на которые следует обратить внимание:

  1. CMD не обрабатывает переменные окружения в shell-формате (например, $FOO_BAR) из-за побочного эффекта, связанного с использованием sh -c в качестве точки входа по умолчанию. Так что необходимо самостоятельно обрабатывать переменные окружения вне CMD-выражения.

  1. CMD-выражение парсируется как массив JSON, поэтому для передачи аргументов необходимо использовать двойные кавычки (» ») вместо одинарных (' ').

9. Используйте apt-get или apt-cache вместо apt

Команда apt предназначена для конечного пользователя и не должна использоваться в RUN-выражениях Dockerfile’ов. Ошибка линтинга DL3027 означает, что в Dockerfile вместо apt-get или apt-cache используется apt. Она встречается в 9% всех Dockerfile’ов. Типичный пример:

FROM ubuntu:22.04
RUN apt install -y some-package=1.2.*

Решение DL3027

FROM ubuntu:22.04
RUN apt-get install -y some-package=1.2.*

apt может по-разному вести себя в дистрибутивах Linux. Поэтому лучше использовать более базовые apt-get или apt-cache.

10. Явно задавайте версии пакетов при установке через apk add

Как отмечалось в DL3008 и DL3013, при установке пакетов следует явно указывать их версии. Это справедливо и для apk add в Dockerfile’ах на основе Alpine. Ошибка встречается в 8% всех Dockerfile’ов. Типичный пример:

Решение DL3018

FROM alpine:3.7
RUN apk --no-cache add some-package=~1.2.3

Обоснование то же самое: если версия указана явно, docker build извлечет именно ее независимо от того, что есть в кэше Docker-слоя. Следует отметить, что для образов на базе Alpine используется частичная привязка с помощью ~. Можно привязать и к конкретной версии, задав ее в виде some-package=1.2.3. Однако, если пакет удален, сборка завершится с ошибкой.

Заключение

В этой статье были рассмотрены 10 самых распространенных проблем с линтингом Dockerfile’ов, с которыми мы сталкиваемся в своей практике. Они различаются по серьезности и последствиям. У каждой проблемы есть свои нюансы, но, если устранить их, вы сможете оптимизировать Dockerfile’ы и ускорить сборку образов.

Например, явное указание версий гарантирует определенное состояние при сборке Docker-образов, но его недостаток — потенциальные сложности с получением обновлений безопасности. Использование --no-install-recommends позволяет избежать лишних зависимостей, которые не нужны или не используются. Минус — можно упустить зависимость, которая необходима.

Надеемся, эта статья подкинула вам пару идей о том, как улучшить Dockerfile«ы и как с этим может помочь линтинг. Если хотите больше узнать о том, чем занимается Depot, рекомендуем ознакомиться с нашей недавней публикацией о линтинге и создании Dockerfile’ов.

P. S.

Читайте также в нашем блоге:

© Habrahabr.ru