Статьи

Создание минимальных Docker-контейнеров для приложений Go

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

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

Нам нужно что-то протестировать для нашего приложения, поэтому давайте сделаем что-то довольно маленькое: мы собираемся извлечь google.com и вывести размер html, который мы получаем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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: Докеризация

Следуя официальному образу Docker для Go, мы написали бы «встроенный» Dockerfile так:

1
FROM golang:onbuild

Изображения «onbuild» предполагают, что структура вашего проекта стандартна, и будет создавать ваше приложение как обычное приложение Go. Если вы хотите больше контроля, вы можете использовать их стандартный базовый образ Go и скомпилировать себя:

1
2
3
4
5
6
FROM golang:latest
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o main .
CMD ["/app/main"]

Это хорошо, если у вас есть Makefile или что-то еще нестандартное, что вам нужно делать при создании приложения. Мы могли бы загрузить некоторые ресурсы из CDN, или добавить их из другого проекта, или, возможно, мы хотим запустить наши тесты в контейнере.

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

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

Основа составляет 514,9 МБ, и наше приложение добавляет к этому всего 5,8 МБ. Ух ты. Таким образом, для нашего скомпилированного приложения нам все еще нужно 514,9 МБ зависимостей? Как это произошло?

Ответ в том, что наше приложение было скомпилировано внутри контейнера. Это означает, что контейнеру требуется установить Go, и это означает, что ему нужны зависимости Go, а это значит, что нам нужен менеджер пакетов и действительно целая ОС. Фактически, если вы посмотрите на Dockerfile для golang: 1.4, он начинается с Debian Jessie, устанавливает компилятор GCC и некоторые инструменты сборки, сворачивает Go и устанавливает его. Таким образом, у нас есть целый сервер Debian и набор инструментов Go для запуска нашего крошечного приложения. Что мы можем сделать?

Часть 3: Компиляция!

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

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

А Dockerfile.scratch это просто:

1
2
3
FROM scratch
ADD main /
CMD ["/main"]

Так что за царапина? Скретч — это специальное изображение в докере, которое пусто. Это действительно 0B:

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

Кроме того, наш контейнер просто 5,6 МБ! Здорово! Но есть одна проблема:

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

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

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

Мы отключаем cgo, который дает нам статический двоичный файл. Мы также устанавливаем ОС на Linux (в случае, если кто-то собирает это на Mac или Windows), а флаг -a означает перестройку всех пакетов, которые мы используем, что означает, что весь импорт будет перестроен с отключенным cgo. Эти настройки изменились в Go 1.4, но я нашел обходной путь в GitHub Issue . Теперь у нас есть статический двоичный файл! Давайте попробуем это:

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

Отлично, что теперь? Вот почему я решил использовать SSL в нашем примере. Это действительно распространенный метод для этого сценария: для выполнения запросов SSL нам нужны корневые сертификаты SSL. Так как же мы добавим их в наш контейнер?

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

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

Теперь просто восстановите наш образ и запустите его, и это работает! Здорово! Давайте посмотрим, насколько велико наше приложение сейчас:

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

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

Вывод

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

Я действительно с нетерпением жду успехов, которые сообщество делает в создании как минимальных операционных систем для гостей-контейнеров, так и агрессивного снижения требований для всех типов языков. Отличительной чертой общедоступного Docker-хаба является то, что им можно легко поделиться со всеми.

Ссылка: Создание минимальных Docker-контейнеров для приложений Go от нашего партнера JCG Флориана Мотлика в блоге Codeship Blog .