Не игнорируйте .dockerignore – это дорого и потенциально опасно

При создании сервисов разработчики часто сталкиваются с необходимостью оптимизировать образы Docker, чтобы уcкорить их сборку. Один из путей оптимизации заключается в использовании .dockerignore. Данная статья является переводом и адаптацией англоязычной статьи Алексея Леденева.

Образы Docker могут выполняться практически на любом сервере, поэтому вопросы их оптимизации часто обходятся стороной. Существует множество преимуществ использования файла .dockerignore. Он может помочь уменьшить размер образа, ускорить выполнение docker build и избежать случайного разглашения секретной информации (читайте далее, чтобы понять, что имеется ввиду).

Чтобы понять, почему применение .dockerignore эффективно, необходимо понять суть контекста сборки.

Контекст сборки образа Docker

Для сборки нового образа Docker используется команда docker build. У команды build есть аргумент, который задает контекст сборки (build context) .

Что такое контекст сборки для Docker?

Напомним, что Docker является клиент-серверным приложением, которое состоит из Docker-клиента и Docker-сервера (Docker-демона). Инструмент командной строки Docker-клиента командами сообщает Docker-серверу, что нужно делать. Одной из команд является build – создание нового образа Docker. Docker-сервер может работать на той же машине, что и клиент, либо на удаленной машине.

Почему это важно и как это связано с контекстом сборки Docker?

Чтобы создать новый образ, Docker-серверу нужен доступ к файлам, из которых будет создаваться образ. Поэтому необходимо каким-то образом передать эти файлы на Docker-сервер. Эти файлы являются контекстом сборки Docker. Docker-клиент упаковывает все файлы контекста сборки в архив tar и загружает этот архив на Docker-сервер. По умолчанию, клиент использует текущий каталог в качестве контекста сборки.

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

Влияние на сборку Docker

Первая строка ответа, которую вы видите при запуске команды docker build, выглядит следующим образом:

Sending build context to Docker daemon 45.3 MB
Step 1: FROM ...

Поясним смысл этого шага. Фактически, каждый раз при запуске команды docker build Docker-клиент создает новый архив контекста сборки и посылает его на Docker-сервер. Поэтому вы всегда несете временные издержки, возникающие при создании архива и пересылки его по сети на сервер.

Практический совет: не добавляйте файлы в контекст сборки, если они не будут использоваться для сборки Docker-образа.

Файл .dockerignore

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

Зачем это нужно?

Действительно, зачем вам это нужно? Современные компьютеры работают быстро, сети тоже довольно быстрые (хочется надеяться), а хранилища дешевые. Поэтому, издержки могут быть не такими большим, верно? Я попробую убедить вас, что это нужно.

Причина №1: Размер образа Docker

Разработка ПО в последнее время движется в сторону непрерывной доставки, эластичной инфраструктуры и микросервисной архитектуры.

Как это связано?

Современные системы состоят из множества компонентов или микросервисов, каждый из которых запускается внутри контейнера Linux. Это могут быть десятки или сотни сервисов и еще больше экземпляров сервисов. Эти экземпляры могут быть созданы и развернуты независимо друг от друга – это можно сделать для каждого отдельного коммита кода. Эластичность инфраструктуры означает, что в систему можно добавить или удалить из нее новые вычислительные узлы, а микросервисы можно перемещать с одного узла на другой, чтобы реагировать на требования масштабирования и доступности. Как результат, образы Docker часто собираются и переносятся.

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

Причина №2: Непреднамеренное раскрытие секретной информации

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

Если вы копируете файлы в образ Docker с помощью команд ADD . или COPY ., вы можете случайно включить свои исходные файлы, всю историю git (каталог git), секретные файлы (например, .aws, .env, закрытые ключи), кэш и другие файлы не только в контекст сборки, но и в конечный образ Docker.

Сегодня на DockerHub доступно множество образов Docker, в которых открыт исходный код приложений, пароли, ключи и учетные данные (например Twitter Vine).

Причина №3: Инвалидация кэша при сборке Docker

Традиционно, вся кодовая база приложения вливается в образ с помощью команды типа:

COPY . /usr/src/app

В этом случае мы копируем весь контекст сборки в образ. Также, важно понимать, что каждая команда в Dockerfile создает новый слой. Поэтому, если какой-либо из включенных файлов во всем контексте сборки меняется, это изменение инвалидирует кэш сборки для слоя COPY . /opt/myapp, и в следующей сборке будет создан новый слой образа.

Пример. Вы изменяете файл README, а в результате Docker выполняет пересборку всего контейнера, если README не исключен из контекста сборки. Другой пример – файлы IDE IntelliJ или PyCharm, которые хранятся в каталоге .idea. Среда постоянно обновляет содержимое этого каталога, а значит, даже если нет изменений в исходных файлах, то в контексте сборки изменения будут, если каталог .idea не исключен из контекста.

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

Синтаксис .dockerignore

Файл .dockerignore похож на файл .gitignore, используемый git. Как и gitignore, он позволяет определять шаблон для каталогов и файлов, которые должны быть проигнорированы Docker-клиентом при создании контекста сборки. Синтаксис файла .dockerignore для описания шаблонов игнорирования похож на .gitignore, но есть и отличия.

Синтаксис сопоставления шаблонов в .dockerignore основывается на функции Go filepath.Match() и включает некоторые дополнения.

Так выглядит полный синтаксис для .dockerignore:

pattern:
{ term }
term:
'*' соответствует любой последовательности символов, не являющихся разделителями
'?' соответствует одному любому символу, не являющемуся разделителем
'[' [ '^' ] { character-range } ']'
character class (не должен быть пустым)
c соответствует символу c (c != '*', '?', '\\', '[')
'\\' c соответствует символу c

character-range:
c соответствует символу c (c != '\\', '-', ']')
'\\' c соответствует символу c
lo '-' hi соответствует символу c для lo <= c <= hi

additions:
'**' соответствует любому количеству директорий (включая ноль)
'!' строки, начинающиеся с ! (восклицательного знака) можно использовать для определения исключений из правил игнорирования
'#' строки, начинающиеся с этого символа, игнорируются: используйте его для комментирования

Используйте символ ! с осторожностью. Используя его с шаблонами перед и после строки с символом ! можно создать более сложные правила.

Примеры

# игнорировать каталоги .git и .cache 
.git
.cache
# игнорировать все файлы *.class во всех каталогах, включая корневой каталог сборки 
**/*.class
# игнорировать все файлы markdown (md) кроме всех файлов README*.md, исключая README-secret.md
*.md
!README*.md
README-secret.md

Рекомендации

Если вы находите этот пост полезным, поделитесь им с друзьями.