Создание минимального 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) {
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

Если мы запустимся, то получим только какое-то число. У меня вышло около 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.
Источник: https://habr.com/ru/post/460535/


Интересные статьи

Интересные статьи

Привет! В предыдущем посте я рассказал вам о движке Armory, теперь создадим свой первый уровень в нем. На самом деле, создание уровней в Armory ничем практически не отлич...
Дрозофилы, или плодовые мушки — отличный материал для исследований. Просто потому, что они очень быстро размножаются, давая потомство, и эволюционные изменения можно отслеживать в течение нед...
Даже сегодня люди по-прежнему продолжают создавать новые игры для старых консолей. Мы называем их «homebrew». Иногда это способ реализовать детскую мечту о создании игры для консоли, на которой...
Битрикс24 — популярная в малом бизнесе CRM c большими возможностями даже на бесплатном тарифе. Благодаря API Битрикс24 (даже в облачной редакции) можно легко интегрировать с другими системами.
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?