Компилиране на вашето Go приложение за контейнери

Откъс от Мощни приложения за команден ред в Go от Рикардо Герарди

📘 Ако харесвате този откъс, моля, обмислете „купуването на книгата“ или присъединяването към „разговора в DevTalk“ (или и двете).

Друг алтернативен начин за разпространение на вашето приложение, който става все по-популярен през последните години, е да позволите на вашите потребители да стартират приложението в Linux контейнери. Контейнерите пакетират вашето приложение и всички необходими зависимости, като използват стандартен формат на изображението и изпълняват приложението изолирано от други процеси, изпълнявани на същата система. Контейнерите използват ресурси на ядрото на Linux като пространства от имена и Cgroups, за да осигурят изолация и управление на ресурсите.

Има различни налични времена за изпълнение на контейнери, като 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. Създайте приложението, като използвате файловете, посочени с маркера на контейнера, за да премахнете зависимостта от 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​

Забележете, че размерът на файла е около 7MB и файлът е статично свързан и премахнат.

Сравнете това със създаването на приложението без тези опции:

​ ​$ ​​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. Превключете обратно към основната директория на главата и създайте нови контейнери на поддиректория:

​ ​$ ​​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, за да активирате терминален емулатор, който е необходим за стартиране на интерактивния CLI на 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 с scratch. Това изображение няма директории или потребители, така че заменете останалите команди с команда за копиране на двоичния файл в основната директория. Когато сте готови, вашият 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.