ADG Оставить заявку
Блог DevOps 4 мин чтения

Docker 17.06 CE: multi-stage builds в stable - пересобираем production-образы

Docker 17.06 CE выходит со stable multi-stage builds. Образ Go-сервиса с 800 МБ builder-слоя превращается в 12 МБ финального - пересобираем все production-образы по новой схеме.

Контекст момента

Docker 17.06 CE - multi-stage builds переведены в stable, secrets в Docker Swarm, улучшен stack deploy с поддержкой compose v3.3

Docker 17.06 CE вышел на этой неделе. Главное что нас интересовало - multi-stage builds переведены из experimental в stable. Ждали именно этого: когда фича была experimental, ставить её в производственный пайплайн было рискованно. Теперь ограничений нет, и мы взялись переделывать образы.

Проблема с размером образов которую все знали но терпели

До multi-stage builds типичный Dockerfile для Go-сервиса выглядел примерно так: либо собираешь прямо в образе с полным golang-окружением и получаешь финальный образ на 700-800 МБ, либо городишь два отдельных Dockerfile - один для сборки, второй для запуска - и пишешь shell-скрипт который копирует бинарник между ними.

Второй вариант работал, но это был костыль. Два файла которые нужно держать в синхронизации, build-скрипт который делает docker cp или монтирует том, CI-пайплайн который про это знает. При малейшем изменении в процессе сборки нужно было обновлять все три места. Мы в итоге делали именно так для нескольких сервисов, и каждый раз при онбординге нового разработчика уходило время на объяснение «а почему тут два Dockerfile».

Как теперь выглядит multi-stage build

Механика простая: в одном Dockerfile можно описать несколько этапов через FROM, каждый с именем (AS builder), и копировать артефакты между ними через COPY --from=builder.

FROM golang:1.8 AS builder
WORKDIR /go/src/app
COPY . .
RUN go build -o /go/bin/app

FROM debian:jessie-slim
COPY --from=builder /go/bin/app /usr/local/bin/app
CMD ["app"]

Финальный образ содержит только бинарник и минимальный runtime. Golang-тулчейн, все зависимости которые тянулись при сборке, исходники - всё это остаётся в builder-слое и в финальный образ не попадает.

Для одного из наших Go-сервисов результат: образ c golang:1.8 as builder весит в районе 800 МБ, финальный на debian:jessie-slim - 12 МБ. Разница не в процентах, а в порядке величины. То же самое работает для Java (убрать JDK, оставить JRE), Node.js (убрать devDependencies и build-тулы), C++ (убрать заголовки и компилятор).

Что пересобирали и в каком порядке

Прошлись по всем production-образам которые мы собираем для клиентов на managed. Примерно треть оказалась кандидатами на переход - те где в образе был toolchain от которого в runtime ничего не нужно.

Go-сервисы переписали в первую очередь - там выигрыш самый очевидный, и нет runtime-зависимостей от языка, бинарник статически слинкован. Образы с Node.js - следом: npm install с devDependencies, webpack, потом копируем только dist/ и node_modules без dev-пакетов в финальный образ.

Один момент который нужно держать в голове. При копировании бинарника Go в debian:slim или alpine встречается проблема с динамической линковкой: если сборка шла в golang-образе с CGO включённым, бинарник ссылается на системные библиотеки которых в alpine нет. Решение - или CGO_ENABLED=0 при сборке, или финальный образ на debian:slim вместо alpine. Мы выбрали по ситуации: для сервисов без cgo-зависимостей переходим на scratch или alpine, для остальных - slim.

Secrets в Swarm и stack deploy

Помимо multi-stage builds в 17.06 пришло ещё несколько вещей.

Docker secrets для Swarm стали значительно удобнее - теперь можно создавать и ротировать секреты через docker secret create, сервисы получают их как файлы в /run/secrets/. Это лучше чем тащить пароли через environment variables, которые видны в docker inspect и утекают в логи при ошибке конфигурации. Мы уже используем это на нескольких Swarm-кластерах.

stack deploy с compose v3.3 - поддержка configs и secrets в compose-файле, что позволяет описывать production-окружение полностью декларативно. Раньше часть вещей приходилось делать через отдельные команды после деплоя.

Параллельно: Kubernetes или Swarm

Мы смотрели на этот вопрос в апреле и не дали однозначного ответа - потому что его нет. С multi-stage builds интереснее другое: сам формат Dockerfile никуда не денется независимо от оркестратора. Переход с Swarm на Kubernetes или обратно не меняет то как собирается образ - это отдельный слой.

Состояние после переключения

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

  • Pull-время образов при деплое сократилось заметно - меньше данных гонять по сети
  • Registry занимает меньше места - не радикально, но приятно
  • Dockerfile стал читаемым: всё в одном файле, два FROM, понятная структура

Образы для разработки оставили как были - там размер менее критичен, зато удобно иметь все инструменты под рукой внутри контейнера. Multi-stage builds хорошо решают именно production-проблему.

Экспериментальный флаг снят, фича stable - можно спокойно встраивать в CI и не объяснять коллегам почему это работает только с особым демоном.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.