Оптимизация образов Docker для производства

В среде производства Docker упрощает процессы создания, развертывания и запуска приложений в контейнерах. Контейнеры позволяют разработчикам собирать приложения и зависимости в единый пакет, который затем можно превратить в образ и реплицировать. Образы Docker собираются из файлов Dockerfiles. Dockerfile – это файл, который определяет, как будет выглядеть образ, какую базовую операционную систему он будет использовать и какие команды будут запускаться внутри него.

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

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

В этом мануале мы оптимизируем образы Docker в несколько простых этапов, сделаем их меньше, быстрее и лучше для производства. Мы создадим образы для Go API в нескольких различных контейнерах Docker: начиная с Ubuntu и образа для конкретного языка, а затем перейдя к дистрибутиву Alpine. Мы используем многоэтапную сборку для оптимизации образов для производства. Конечная цель этого мануала – показать разницу в размерах между использованием образов Ubuntu по умолчанию и их оптимизированных аналогов и продемонстрировать преимущество многоэтапных сборок. Позже вы сможете применить эти методы к своим собственным проектам и конвейерам непрерывной интеграции.

Примечание: В качестве примера здесь используется API, написанный на Go. Этот простой интерфейс поможет вам понять, как оптимизировать микросервисы Go через образы Docker. Этот мануал подходит не только для Go API, но и для многих других языков программирования.

Требования

1: Загрузка экземпляра Go API

Вы должны сначала загрузить пример API, из которого вы будете создавать образы Docker. Использование простого Go API покажет все ключевые этапы сборки и запуска приложения внутри контейнера Docker. Этот мануал использует Go, потому что это скомпилированный язык, такой как C ++ или Java, но в отличие от них, он занимает очень мало места.

Клонируйте API Go:

git clone https://github.com/do-community/mux-go-api.git

Теперь на вашем сервере есть каталог mux-go-api. Перейдите в него:

cd mux-go-api

Это домашний каталог для вашего проекта. Все образы Docker нужно создавать в этом каталоге. Внутри вы найдете исходный код API, написанный на Go, в файле api.go. Хотя этот API минимальный и имеет всего несколько конечных точек, он подойдет для имитации готового к работе API.

Теперь, когда вы загрузили образец Go API, можно создать базовый Docker-образ Ubuntu, с которым вы сможете сравнить оптимизированные образы, которые вы создадите позже.

2: Сборка базового образа Ubuntu

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

Начнем с написания Dockerfile, который помогает Docker создать образ Ubuntu, установить Go и запустить API. Обязательно создайте Dockerfile в каталоге клонированного репозитория. Если вы клонировали его в домашний каталог, путь должен быть $HOME/mux-go-api.

Создайте новый файл Dockerfile.ubuntu. Откройте его в текстовом редакторе:

nano ~/mux-go-api/Dockerfile.ubuntu

В этом файле нужно определить образ Ubuntu и установку Golang. После этого вы установите зависимости и соберете двоичный файл. Добавьте в Dockerfile.ubuntu следующие строки:

FROM ubuntu:18.04
RUN apt-get update -y \
&& apt-get install -y git gcc make golang-1.10
ENV GOROOT /usr/lib/go-1.10
ENV PATH $GOROOT/bin:$PATH
ENV GOPATH /root/go
ENV APIPATH /root/go/src/api
WORKDIR $APIPATH
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]

Команда FROM указывает, какую базовую операционную систему будет использовать образ. Затем команда RUN устанавливает язык Go при создании образа. ENV устанавливает конкретные переменные окружения, которые нужны компилятору Go для правильной работы. WORKDIR указывает каталог, в который нужно скопировать код, а COPY берет код из каталога, в котором находится Dockerfile.ubuntu, и копирует его в образ. Последняя команда RUN устанавливает зависимости Go, необходимые для компиляции исходного кода и запуска API.

Примечание: Операторы && для объединения команд RUN очень важны при оптимизации Dockerfiles, поскольку каждая команда RUN создает новый слой, а каждый новый слой увеличивает размер конечного образа.

Сохраните и закройте файл. Теперь вы можете запустить команду build для создания образа Docker из вашего Dockerfile:

docker build -f Dockerfile.ubuntu -t ubuntu .

Команда build создает образ из Dockerfile. Флаг -f указывает имя исходного Dockerfile, Dockerfile.ubuntu, а флаг -t добавляет заданный тег (в данном случае с его помощью файл будет помечен именем ubuntu). Последняя точка представляет текущий контекст, в котором находится Dockerfile.ubuntu.

Сборка займет некоторое время, вы можете пока сделать перерыв. Как только сборка будет завершена, у вас будет образ Ubuntu, готовый к запуску вашего API. Но окончательный размер образа может быть далек от идеала; для этого API любой образ в несколько сотен МБ будет считаться слишком большим.

Запустите следующую команду, чтобы получить список всех образов Docker и узнать размер полученного образа Ubuntu:

docker images
REPOSITORY  TAG     IMAGE ID        CREATED         SIZE
ubuntu      latest  61b2096f6871    33 seconds ago  636MB
. . .

Как подчеркивается в выводе, образ базового API Golang имеет размер 636 МБ, (ваш размер может незначительно отличаться). Такой размер будет существенно влиять на время развертывания и пропускную способность сети.

У вас теперь есть базовый образ Ubuntu со всеми необходимыми инструментами и зависимостями Go для запуска API, клонированного в разделе 1. В следующем разделе мы используем предварительно собранный образ Docker для упрощения Dockerfile и процесса сборки.

3: Сборка базового образа языка

Предварительно собранные образы – это обычные базовые образы, которые пользователи модифицировали, чтобы включить в них инструменты для конкретной ситуации. Затем пользователи могут помещать эти образы в репозиторий Docker Hub, а это позволяет другим использовать общий образ и не писать свои собственные файлы Dockerfiles. Это распространенный процесс в производстве. Вы можете найти в Docker Hub различные предварительно собранные образы практически для любого случая использования. Сейчас мы создадим образец API с помощью специального образа для Go, в котором уже установлены компилятор и зависимости.

Благодаря предварительно собранным базовым образам, в которых уже есть инструменты, необходимые для создания и запуска приложения, вы можете значительно сократить время сборки. Поскольку в базовом образе все необходимые инструменты уже установлены, вы можете пропустить их в Dockerfile, и тогда он будет выглядеть намного чище и время сборки, в конечном итоге, тоже будет меньше.

Создайте  Dockerfile.golang:

nano ~/mux-go-api/Dockerfile.golang

Этот файл будет значительно короче, чем предыдущий, поскольку в нем предварительно установлены все зависимости, инструменты и компилятор Go.

Теперь добавьте следующие строки:

FROM golang:1.10
WORKDIR /go/src/api
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]

В начале файла установлен оператор FROM, который теперь определяет golang:1.10. Это означает, что Docker возьмет предварительно собранный образ Go из Docker Hub, в котором уже установлены все необходимые инструменты Go.

Теперь соберите образ Docker с помощью команды:

docker build -f Dockerfile.golang -t golang .

Проверьте окончательный размер образа с помощью следующей команды:

docker images
REPOSITORY  TAG     IMAGE ID        CREATED         SIZE
golang      latest  eaee5f524da2    40 seconds ago  744MB
. . .

Несмотря на то, что сам Dockerfile более эффективен и время сборки короче, общий размер образа фактически увеличился. Предварительно собранный образ Golang занимает около 744 МБ.

Этот способ создания образов Docker является предпочтительным. Вы берете базовый образ, который сообщество утвердило в качестве стандарта для того или иного языка (в данном случае Go). Однако, чтобы подготовить образ к производству, вам нужно вырезать те компоненты, которые не нужны вашему приложению.

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

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

4: Сборка базовых образов Alpine

Одним из самых простых шагов по оптимизации контейнерных развертываний Docker является использование меньших базовых образов. Alpine – это легкий дистрибутив Linux, разработанный для обеспечения безопасности и эффективности использования ресурсов. Образ Alpine использует musl libc и BusyBox для сохранения компактности, требуя не более 8 МБ в контейнере. Крошечный размер обусловлен тем, что двоичные пакеты прореживаются и разделяются – это дает больший контроль над тем, что вы устанавливаете, и делает среду менее перегруженной и более эффективной.

Процесс создания образа Alpine аналогичен процессу создания образа Ubuntu в разделе 2. Сначала создайте новый файл Dockerfile.alpine:

nano ~/mux-go-api/Dockerfile.alpine

Вставьте в файл:

FROM alpine:3.8
RUN apk add --no-cache \
ca-certificates \
git \
gcc \
musl-dev \
openssl \
go
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
ENV APIPATH $GOPATH/src/api
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" "$APIPATH" && chmod -R 777 "$GOPATH"
WORKDIR $APIPATH
COPY . .
RUN \
go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]

Здесь добавляется команда apk add, которая позволяет использовать менеджер пакетов Alpine для установки Go и всех необходимых библиотек. Как и в образе Ubuntu, вам необходимо также установить переменные окружения.

Идем дальше и собираем образ:

docker build -f Dockerfile.alpine -t alpine .

Теперь проверьте размер образа:

docker images
REPOSITORY  TAG     IMAGE ID        CREATED         SIZE
alpine      latest  ee35a601158d    30 seconds ago  426MB
. . .

Размер уменьшился до 426MB.

Небольшой размер базового образа Alpine повлиял на конечный размер вашего образа, но есть несколько способов сделать его еще меньше.

Теперь попробуйте использовать предварительно собранный образ Alpine для Go. Это сделает Dockerfile короче, а также уменьшит размер конечного образа. Поскольку предварительно созданный образ Alpine для Go создается вместе со  скомпилированным экземпляром Go, его размер значительно меньше.

Начните с создания нового файла Dockerfile.golang-alpine:

nano ~/mux-go-api/Dockerfile.golang-alpine

Вставьте в файл:

FROM golang:1.10-alpine3.8
RUN apk add --no-cache --update git
WORKDIR /go/src/api
COPY . .
RUN go get -d -v \
&& go install -v \
&& go build
EXPOSE 3000
CMD ["./api"]

Небольшие различия между Dockerfile.golang-alpine и Dockerfile.alpine – это команда FROM и первая команда RUN. Теперь команда FROM задает образ golang с тегом 1.10-alpine3.8, а в RUN указана только команда для установки Git. Git необходим, чтобы сработала команда go get (во втором RUN в конце Dockerfile.golang-alpine).

Соберите образ:

docker build -f Dockerfile.golang-alpine -t golang-alpine .

Теперь запросите список образов:

docker images
REPOSITORY      TAG     IMAGE ID        CREATED         SIZE
golang-alpine   latest  97103a8b912b    49 seconds ago  288MB

Теперь размер образа уменьшился до 288 МБ.

Вам удалось значительно сократить размер, но вы можете сделать еще одну вещь, чтобы подготовить его к производству. Это называется многоэтапная (или многоступенчатая) сборка. Благодаря таким сборкам вы можете использовать один образ для сборки приложения, а другой, более легкий образ – при упаковке скомпилированного приложения для производства.

5: Многоэтапная сборка и исключение лишних инструментов

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

Начните с создания файла Dockerfile.multistage

nano ~/mux-go-api/Dockerfile.multistage

Следующие строки, которые нужно вставить в файл, вам знакомы – они мало чем отличаются от содержимого файла Dockerfile.golang-alpine. Но сейчас нужно также указать второй образ, в который нужно скопировать двоичный файл из первого образа.

FROM golang:1.10-alpine3.8 AS multistage
RUN apk add --no-cache --update git
WORKDIR /go/src/api
COPY . .
RUN go get -d -v \
&& go install -v \
&& go build
##
FROM alpine:3.8
COPY --from=multistage /go/bin/api /go/bin/
EXPOSE 3000
CMD ["/go/bin/api"]

Сохраните и закройте файл. Здесь есть две команды FROM. Первая идентична Dockerfile.golang-alpine, за исключением дополнительного оператора AS multistage. Это присвоит образу имя multistage, на которое вы будете ссылаться в нижней части файла Dockerfile.multistage. Во второй команде FROM берется базовый образ alpine и копируется поверх скомпилированного приложения Go из образа multistage. Этот процесс еще больше сократит размер конечного образа, делая его готовым к производству.

Соберите образ:

docker build -f Dockerfile.multistage -t prod .

Проверьте итоговый размер образа:

docker images
REPOSITORY      TAG     IMAGE ID        CREATED         SIZE
prod            latest  82fc005abc40    38 seconds ago  11.3MB
<none>          <none>  d7855c8f8280    38 seconds ago   294MB
. . .

Образ <none> — это образ multistage, созданный с помощью команды FROM golang:1.10-alpine3.8 AS multistage. Это всего лишь посредник, используемый для сборки и компиляции приложения Go. А образ prod в этом контексте является конечным, он содержит только скомпилированное приложение Go.

От начальных 744 МБ осталось 11,3 МБ. Отслеживать такой маленький образ и отправлять его по сети на рабочие серверы будет намного проще, чем образ размером более 700 МБ, и в долгосрочной перспективе вы сэкономите значительный объем ресурсов.

Заключение

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

Читайте также:

Tags: , , , ,