Компиляция вашего приложения Go для контейнеров

Отрывок из книги «Мощные приложения командной строки в Go» Рикардо Херарди

📘 Если вам понравился этот отрывок, подумайте о том, чтобы купить книгу или присоединиться к беседе на DevTalk (или и к тому, и к другому).

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

Доступны различные среды выполнения контейнеров, такие как Podman и Docker. Если вы запускаете эти примеры в системе Linux, вы можете использовать любой из них взаимозаменяемо. Если вы работаете в Windows или macOS, Docker предоставляет настольную версию, которая упрощает запуск. Вы также можете использовать Podman в этих операционных системах, но для этого вам необходимо установить виртуальную машину. Мы не будем рассматривать здесь процесс установки среды выполнения контейнера. Для получения более подробной информации ознакомьтесь с документацией соответствующего проекта.

Чтобы распространять ваше приложение в виде контейнера, вам необходимо создать образ контейнера. Вы можете сделать это несколькими способами, но наиболее распространенным способом является использование файла Dockerfile, который содержит рецепт создания образа. Затем вы передаете этот файл в качестве входных данных командам docker или podman для создания образа. Для получения более подробной информации о том, как создать Dockerfile, обратитесь к его документации.

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

Чтобы сделать двоичный файл еще более подходящим для запуска в контейнере, вы можете передать дополнительные параметры сборки. Например, вы активируете статически связанный двоичный файл, установив CGO_ENABLED=0, и вы можете передать дополнительные параметры компоновщика, используя флаг -ldflags. Чтобы уменьшить размер двоичного файла, используйте параметры -ldflags=”-s -w” для удаления символов отладки из двоичного файла. Прежде чем приступить к работе, внимательно изучите некоторые параметры сборки, которые вы будете использовать:

  • CGO_ENABLED=0: включает статически связанные двоичные файлы, чтобы сделать приложение более переносимым. Это позволяет вам использовать двоичный файл с исходными изображениями, которые не поддерживают общие библиотеки, при создании образа контейнера.
  • GOOS=linux: Поскольку контейнеры работают под управлением Linux, установите этот параметр, чтобы включить повторяющиеся сборки даже при сборке приложения на другой платформе.
  • -ldflags=”-s -w”: Параметр -ldflags позволяет указать дополнительные параметры компоновщика, которые go build использует на этапе компоновки процесса сборки. В этом случае опция -s -w удаляет из двоичного файла символы отладки, уменьшая его размер. Без этих символов сложнее отлаживать приложение, но обычно это не является серьезной проблемой при работе в контейнере. Чтобы просмотреть все параметры компоновщика, которые вы можете использовать, запустите go tool link.
  • -tags=containers: Это относится к вашему приложению Pomodoro. Создайте приложение, используя файлы, указанные с помощью тега container, чтобы удалить зависимость от SQLite и уведомлений, как вы делали в разделе Условная сборка приложения.

Теперь создайте свой двоичный файл, используя эти параметры:

​ ​$ ​​CGO_ENABLED=0​​ ​​GOOS=linux​​ ​​go​​ ​​build​​ ​​-ldflags=​​"-s -w"​​ ​​-tags=containers​

Проверьте этот файл, чтобы проверить его свойства и размер:

​ ​$ ​​ls​​ ​​-lh​​ ​​pomo​
​ -rwxr-xr-x 1 ricardo users 7.2M Feb 28 12:06 pomo
​ ​$ ​​file​​ ​​pomo​
​ pomo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
​ ​  ...​​ ​​...​​ ​​,​​ ​​stripped​

Обратите внимание, что размер файла составляет около 7 МБ, и файл статически связан и разделен.

Сравните это со сборкой приложения без этих опций:

​ ​$ ​​go​​ ​​build​
​ ​$ ​​ls​​ ​​-lh​​ ​​pomo​
​ -rwxr-xr-x 1 ricardo users 13M Feb 28 12:09 pomo
​ ​$ ​​file​​ ​​pomo​
​ pomo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
​   dynamically linked,
​ interpreter /lib64/ld-linux-x86-64.so.2, ..., for GNU/Linux 4.4.0,
​   not stripped

Бинарный файл, оптимизированный для контейнеров, почти на 50 % меньше исходного. Он также статически связан и лишен символов отладки.

Получив двоичный файл, вы создадите образ контейнера с помощью Dockerfile. Вернитесь в корневой каталог главы и создайте новый подкаталог containers:

​ ​$ ​​cd​​ ​​$HOME/pragprog.com/rggo/distributing​
​ ​$ ​​mkdir​​ ​​containers​

Создайте и отредактируйте файл Dockerfile в этом подкаталоге. Добавьте следующее содержимое, чтобы создать новый образ из базового образа alpine:latest, создайте pomo обычного пользователя для запуска приложения и скопируйте созданный ранее двоичный файл pomo/pomo в образ в каталоге /app:

раздача/контейнеры/Dockerfile

​ FROM alpine:latest
​ RUN mkdir /app && adduser -h /app -D  pomo
​ WORKDIR /app
​ COPY --chown=pomo /pomo/pomo .
​ CMD ["/app/pomo"]

Создайте свой образ с помощью команды сборки docker, предоставив этот Dockerfile в качестве входных данных:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile​​ ​​.​
​ STEP 1: FROM alpine:latest
​ STEP 2: RUN mkdir /app && adduser -h /app -D  pomo
​ ​-->​​ ​​500286ad2c9​
​ STEP 3: WORKDIR /app
​ ​-->​​ ​​175d6b43663​
​ STEP 4: COPY --chown=pomo /pomo/pomo .
​ ​-->​​ ​​2b05fa6dbba​
​ STEP 5: CMD ["/app/pomo"]
​ STEP 6: COMMIT pomo/pomo:latest
​ ​-->​​ ​​998e1c2cc75​
​ 998e1c2cc75dc865f57890cb6294c2f25725da97ce8535909216ea27a4a56a38

Эта команда создает образ с тегом pomo/pomo:latest. Перечислите его, используя образы докеров:

​ ​$ ​​docker​​ ​​images​
​ REPOSITORY                     TAG     IMAGE ID      CREATED         SIZE
​ localhost/pomo/pomo            latest  998e1c2cc75d  47 minutes ago  13.4 MB
​ docker.io/library/alpine       latest  e50c909a8df2  4 weeks ago     5.88 MB

Запустите приложение с помощью Docker, указав флаги -it для включения эмулятора терминала, необходимого для запуска интерактивного интерфейса командной строки Pomodoro:

​ ​$ ​​docker​​ ​​run​​ ​​--rm​​ ​​-it​​ ​​localhost/pomo/pomo​

Вы также можете использовать Docker для сборки приложения с официальным образом Go и многоэтапным Dockerfile. Многоэтапный Dockerfile создает экземпляр контейнера для компиляции приложения, а затем копирует полученный файл во второй образ, аналогичный предыдущему созданному вами Dockerfile. Создайте новый файл с именем Dockerfile.builder в подкаталоге контейнеров. Определите многоэтапную сборку, используя следующий код:

раздача/контейнеры/Dockerfile.builder

​ ​FROM​​ golang:1.15 AS builder​
​ ​RUN ​mkdir /distributing
​ ​WORKDIR​​ /distributing​
​ ​COPY​​ notify/ notify/​
​ ​COPY​​ pomo/ pomo/​
​ ​WORKDIR​​ /distributing/pomo​
​ ​RUN ​CGO_ENABLED=0 GOOS=linux go build -ldflags=​"-s -w"​ -tags=containers
​ 
​ ​FROM​​ alpine:latest​
​ ​RUN ​mkdir /app && adduser -h /app -D  pomo
​ ​WORKDIR​​ /app​
​ ​COPY​​ --chown=pomo --from=builder /distributing/pomo/pomo .​
​ ​CMD​​ ["/app/pomo"]​

Теперь используйте этот образ для создания двоичного файла и образа контейнера для вашего приложения:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile.builder​​ ​​.​
​ STEP 1: FROM golang:1.15 AS builder
​ STEP 2: RUN mkdir /distributing
​ ​-->​​ ​​e8e2ea98b04​
​ STEP 3: WORKDIR /distributing
​ ​-->​​ ​​81cee711389​
​ STEP 4: COPY notify/ notify/
​ ​-->​​ ​​ac86b302a7a​
​ STEP 5: COPY pomo/ pomo/
​ ​-->​​ ​​5353bc4d73e​
​ STEP 6: WORKDIR /distributing/pomo
​ ​-->​​ ​​bfddd5217bf​
​ STEP 7: RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -tags=containers
​ go: downloading github.com/spf13/viper v1.7.1
​ go: downloading github.com/spf13/cobra v1.1.1
​ go: downloading github.com/mitchellh/go-homedir v1.1.0
​ go: downloading github.com/mum4k/termdash v0.13.0
​ go: downloading github.com/spf13/afero v1.1.2
​ go: downloading github.com/spf13/cast v1.3.0
​ go: downloading github.com/pelletier/go-toml v1.2.0
​ go: downloading gopkg.in/yaml.v2 v2.2.8
​ go: downloading github.com/mitchellh/mapstructure v1.1.2
​ go: downloading github.com/spf13/pflag v1.0.5
​ go: downloading golang.org/x/text v0.3.4
​ go: downloading github.com/subosito/gotenv v1.2.0
​ go: downloading github.com/magiconair/properties v1.8.1
​ go: downloading github.com/fsnotify/fsnotify v1.4.7
​ go: downloading github.com/mattn/go-runewidth v0.0.9
​ go: downloading github.com/spf13/jwalterweatherman v1.0.0
​ go: downloading github.com/hashicorp/hcl v1.0.0
​ go: downloading github.com/gdamore/tcell/v2 v2.0.0
​ go: downloading gopkg.in/ini.v1 v1.51.0
​ go: downloading golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba
​ go: downloading github.com/gdamore/encoding v1.0.0
​ go: downloading github.com/lucasb-eyer/go-colorful v1.0.3
​ ​-->​​ ​​de7b70a3753​
​ STEP 8: FROM alpine:latest
​ STEP 9: RUN mkdir /app && adduser -h /app -D  pomo
​ ​-->​​ ​​Using​​ ​​cache​​ ​​500286ad2c9f1242184343eedb016d53e36e1401675eb6769fb9c64146...​
​ ​-->​​ ​​500286ad2c9​
​ STEP 10: WORKDIR /app
​ ​-->​​ ​​Using​​ ​​cache​​ ​​175d6b43663f6db66fd8e61d80a82e5976b27078b79d59feebcc517d44...​
​ ​-->​​ ​​175d6b43663​
​ STEP 11: COPY --chown=pomo --from=builder /distributing/pomo/pomo .
​ ​-->​​ ​​0292f63c58f​
​ STEP 12: CMD ["/app/pomo"]
​ STEP 13: COMMIT pomo/pomo:latest
​ ​-->​​ ​​3c3ec9fafb8​
​ 3c3ec9fafb8f463aa2776f1e45c216dc60f7490df1875c133bb962ffcceab050

В результате получается тот же образ, что и раньше, но с этим новым Dockerfile вам не нужно вручную компилировать приложение перед созданием образа. Многоступенчатая сборка делает все за вас повторяемым и последовательным образом.

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

Чтобы создать такой образ, вы будете использовать многоэтапный Dockerfile. Поэтому скопируйте файл containers/Dockerfile.builder в новый файл containers/Dockerfile.scratch и отредактируйте этот новый файл, заменив изображение второго этапа в команде FROM нулями. В этом образе нет каталогов или пользователей, поэтому замените оставшиеся команды на команду для копирования двоичного файла в корневой каталог. Когда вы закончите, ваш Dockerfile будет выглядеть так:

раздача/контейнеры/Dockerfile.scratch

​ ​FROM​​ golang:1.15 AS builder​
​ ​RUN ​mkdir /distributing
​ ​WORKDIR​​ /distributing​
​ ​COPY​​ notify/ notify/​
​ ​COPY​​ pomo/ pomo/​
​ ​WORKDIR​​ /distributing/pomo​
​ ​RUN ​CGO_ENABLED=0 GOOS=linux go build -ldflags=​"-s -w"​ -tags=containers
​ 
​ ​FROM​​ scratch​
​ ​WORKDIR​​ /​
​ ​COPY​​ --from=builder /distributing/pomo/pomo .​
​ ​CMD​​ ["/pomo"]​

Создайте свой образ, используя этот Dockerfile, как вы делали это раньше:

​ ​$ ​​docker​​ ​​build​​ ​​-t​​ ​​pomo/pomo:latest​​ ​​-f​​ ​​containers/Dockerfile.scratch​​ ​​.​
​ STEP 1: FROM golang:1.15 AS builder
​ STEP 2: RUN mkdir /distributing
​ ​-->​​ ​​9021735fd16​
​ ​...​​ ​​TRUNCATED​​ ​​OUTPUT​​ ​​...​
​ STEP 8: FROM scratch
​ STEP 9: WORKDIR /
​ ​-->​​ ​​00b6e665a3f​
​ STEP 10: COPY --from=builder /distributing/pomo/pomo .
​ ​-->​​ ​​c6bbaccb87b​
​ STEP 11: CMD ["/pomo"]
​ STEP 12: COMMIT pomo/pomo:latest
​ ​-->​​ ​​4068859c281​

Проверьте свое новое изображение и обратите внимание, что его размер близок к размеру двоичного файла, потому что это единственный файл в образе:

​ ​$ ​​docker​​ ​​images​
​ REPOSITORY                      TAG     IMAGE ID      CREATED         SIZE
​ localhost/pomo/pomo             latest  4068859c281e  5 seconds ago   8.34 MB

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

Далее, давайте рассмотрим, как распространять ваше приложение с исходным кодом.

Мы надеемся, что вам понравился этот отрывок из книги Мощные приложения командной строки в Go Рикардо Херарди. Вы можете продолжить чтение на Medium или приобрести электронную книгу напрямую у Pragmatic Programmers.