Хабрахабр

[Из песочницы] Создание минимального Docker-контейнера для Go-приложений

Привет, Хабр! Предлагаю вашему вниманию перевод статьи основателя сервиса Meetspaceapp Nick Gauthier «Building Minimal Docker Containers for Go Applications».

Время чтения: 6 минут

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

Часть 1: Наше «приложение»

Для тестирования нам потребуется какое-нибудь маленькое приложение. Давайте будем фетчить google.com и выводить размер HTML.

package main import ( "fmt" "io/ioutil" "net/http" "os"
) func main() { resp, err := http.Get("https://google.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body))
} func check(err error)
}

Если мы запустимся, то получим только какое-то число. У меня вышло около 17К. Я целенаправленно решил использовать SSL, но причину объясню позднее.

Часть 2: Докеризация

Используя официальный образ Go мы напишем “onbuild” Dockerfile:

FROM golang:onbuild

“Onbuild” образ предполагает, что у вашего проекта стандартная структура и создаст стандартное Go-приложение. Если же вам нужна большая гибкость, можно использовать стандартный образ Go и самостоятельно его скомпилировать:

FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"]

Хорошо бы здесь еще создать Makefile или что-то еще подобное, что вы используете для билда приложений. Мы могли бы загрузить какие-нибудь ресурсы с CDN или импортировать их из другого проекта, или, может, мы хотим запускать тесты в контейнере…
Как вы видите, докеризация Go довольно несложная, особенно если учесть, что у нас не используется сервисы и порты, к которым надо подключаться. Но есть один серьезный недостаток у официальных образов – они реально большие. Давайте посмотрим:

REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE
example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB
example-golang latest 02e19291523e 19 minutes ago 520.7MB
golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB
golang 1.4.2 121a93c90463 9 days ago 514.9MB
golang latest 121a93c90463 9 days ago 514.9MB

Базовый образ занимает 514,9МБ, а наше приложение добавляет еще 5,8МБ. Как так выходит, что для нашего скомпилированного приложения требуется 515МБ зависимостей?
Дело в том, что наше приложение было скомпилировано внутри контейнера. Это означает, что контейнеру требуется установить Go. Следовательно, ему нужны зависимости Go, а так же менеджер пакетов и реально целая ОС. Фактически, если вы посмотрите Dockerfile для golang:1.4, — он ставится с Debian Jessie, устанавливает компилятор GCC и инструменты сборки, скачивает Go и устанавливает его. Таким образом, мы получаем целый сервер Debian и набор инструментов Go для запуска нашего крошечного приложения. Что можно с этим сделать?

Часть 3: Компилируй!

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

go build -o main .
docker build -t example-scratch -f Dockerfile.scratch .

И простой Dockerfile.scratch:

FROM scratch
ADD main /
CMD ["/main"]

Что такое scratch? Scratch — это специальный пустой образ в докере. Его размер 0B:

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest ca1ad50c9256 About a minute ago 5.60MB
scratch latest 511136ea3c5a 22 months ago 0B

В итоге наш контейнер занимает всего лишь 5,6 МБ. Отлично! Но есть одна проблема:

$ docker run -it example-scratch
no such file or directory

Что это значит? Мне потребовалось некоторое время, чтобы понять, что наш бинарный файл Go ищет библиотеки в той операционной системе, в которой запущен. Мы скомпилировали наше приложение, но оно по-прежнему динамически связано с библиотеками, которые необходимо запустить (т. е. со всеми библиотеками C). К сожалению, scratch пуст, поэтому нет ни библиотек, ни путей загрузки. Нам нужно изменить скрипт сборки, чтобы статически компилировать наше приложение со всеми встроенными библиотеками:

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

Мы отключаем cgo, который отдает нам статический бинарник. Также мы указываем Linux в качестве ОС (на случай, если кто-то билдит его на Mac или Windows). Флаг -a означает перестройку всех пакетов, которые мы используем, что перестроит весь импорт с отключенным cgo. Теперь у нас есть статический бинарник. Давайте запустим:

$ docker run -it example-scratch
Get https://google.com: x509: failed to load system roots and no roots provided

А это еще что? Вот почему я решил использовать SSL в нашем примере. Это действительно распространенный «косяк» для подобных сценариев: для выполнения запросов SSL нам нужны рутовые сертификаты SSL. Так как же мы добавим их в наш контейнер?
В зависимости от операционной системы сертификаты могут лежать в разных местах. Для многих дистрибутивов Linux это /etc/ssl/certs/ca-certificates.crt. Итак, во-первых, мы скопируем ca-certificates.crt с нашего компьютера (или виртуальной машины Linux, или поставщика онлайн-сертификатов) в наш репозиторий. Затем мы добавим ADD в наш Dockerfile, чтобы переместить этот файл туда, где Go его ожидает:

FROM scratch
ADD ca-certificates.crt /etc/ssl/certs/
ADD main /
CMD ["/main"]

Теперь просто пересоздадим наш образ и запустим его. Работает! Давайте посмотрим размер нашего приложения теперь:

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
example-scratch latest ca1ad50c9256 About a minute ago 6.12MB
example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB
example-golang latest 02e19291523e 19 minutes ago 520.7MB
golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB
golang 1.4.2 121a93c90463 9 days ago 514.9MB
golang latest 121a93c90463 9 days ago 514.9MB
scratch latest 511136ea3c5a 22 months ago 0B

Мы добавили чуть больше пол мегабайта (и большая часть которого – от статического файла, а не от корневых сертификатов). У нас получился реально маленький контейнер — его будет очень удобно перемещать между реестрами.

Заключение

Наша цель состояла в том, чтобы уменьшить размер контейнера для приложения Go. Особенность Go заключается в том, что он может создавать статически связанный двоичный файл, полностью содержащий приложение. Другие языки тоже могут так, но далеко не все. Применение подобной техники уменьшения размера контейнера в других языках будет зависеть от их минимальных требований. Например, приложение Java или JVM может быть скомпилировано вне контейнера и затем внедрено в контейнер, который содержит только JVM (и ее зависимости). Но даже так будет меньше, чем контейнер с JDK.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть