Готовим сборку Go-приложения в продакшн

в 6:35, , рубрики: Go, golang, gopath, makefile, сборка приложений, управление зависимостями

В июне на конференции РИТ++ мы с коллегой Игорем Должиковым делились опытом автоматизации процесса разработки сервисов на Go — от первого коммита и до релиза в продакшн-окружение Kubernetes (да-да, видео начинается с 07:16, и нам тоже это не нравится). С момента публикации мастер-класса время от времени я получаю вопросы по тем или иным темам, затронутым в нем. Пожалуй, самые горячие вопросы достойны отдельного рассмотрения, и сегодня я хотела бы поговорить о процессе сборки приложения. Затрагиваемые темы актуальны не только при подготовке сервисов, но и вообще для любых приложений, написанных на Go.

Всё, что описано в этой статье, актуально для текущей версии Go — 1.9.

Заветный GOPATH и расположение кода

При подготовке приложения к продакшн нужно быть максимально уверенным в том, что в сборку попадёт именно тот код, который ожидает разработчик. По умолчанию Go по-прежнему не умеет работать с управлением зависимостями, а значит, при компиляции приложения все “внешние” зависимости инструменты Go будут искать внутри директории $GOPATH/src. Как узнать текущее значение GOPATH, если вы в нем не уверены? Это значение можно найти в списке переменных, выводимых командой go env.

Кроме того, код самого проекта также должен находиться внутри $GOPATH/src, и я рекомендую заранее продумать путь, по которому он будет лежать. Когда проект окажется под управлением системы контроля версий, при его затягивании с использованием, например, команды go get, он должен попадать именно по тому пути, который мы определили для проекта изначально. Например, код сервиса, который хранится в моем гитхаб-аккаунте в репозитории rumyantseva/mif развернут у меня внутри $GOPATH/src/github.com/rumyantseva/mif. Если бы этот же код лежал внутри репозитория mif некоторого закрытого хранилища example.com в неймспейсе services, то путь к нему на машине разработчика выглядел бы, скорее всего, как $GOPATH/src/example.com/services/mif. Для того, чтобы избежать в будущем проблем или неоднозначностей, правило расположения кода необходимо соблюдать.

Разные проекты можно хранить как внутри одной и той же директории GOPATH, так и внутри нескольких. Соответственно, во втором случае значение GOPATH придется переопределять. Для того, чтобы сделать это, необходимо будет переустановить соответствующую переменную окружения в нужное вам значение. В случае, если GOPATH не задан вообще, Go будет считать рабочим каталогом директорию go, находящуюся в домашнем каталоге пользователя. Чтобы лучше в этом всём разобраться, проведем несколько экспериментов с консолью:

Разбираемся с GOPATH

$ # По умолчанию GOPATH определен как $HOME/go:
$ go env | grep GOPATH
GOPATH="/Users/elena/go"
$
$ # Изменим значение переменной окружения GOPATH и посмотрим, что будет:
$ GOPATH=/Users/tmp/something
$ go env | grep GOPATH
GOPATH="/Users/tmp/something"
$
$ # Теперь попробуем задать переменную непосредственно в процессе вызова команды go env:
$ GOPATH=/pampam go env | grep GOPATH
GOPATH="/pampam"
$
$ # А в рамках текущей сессии значение GOPATH по-прежнему не изменилось:
elena:~ $ go env | grep GOPATH
GOPATH="/Users/tmp/test"
$
$ # Уберем значение GOPATH вообще и посмотрим, что будет:
$ GOPATH=
$ go env | grep GOPATH
GOPATH="/Users/elena/go"
$ # Мы вернулись к значению по умолчанию :)

Однако, если идея GOPATH вам всё же не по вкусу, можно обратиться к таким инструментам, как gb, который позволяет проводить сборки вне зависимости от расположения кода.

Работа с внешними зависимостями

Итак, в случае, если мы пишем приложение, использующее внешние (относительно текущего репозитория) зависимости, для успешной сборки нам необходимо, чтобы все эти зависимости находились внутри GOPATH. Притянуть зависимости автоматически можно с помощью вызова таких команд, как go get или go install внутри текущего рабочего проекта. При этом мы скачаем код зависимостей, находящихся в дефолтных ветках репозиториев. Этого достаточно для ситуации «здесь и сейчас», но в общем случае никто не гарантирует, что через 5 минут в тех же самых ветках внешних зависимостей не появятся обратно-несовместимые изменения. А значит, следующая попытка развернуть приложение (например, на билд-машине) может закончиться провалом. Что нам поможет в этой ситуации? Конечно же, вендоринг.

Про директорию vendor неоднократно писали и на Хабре, и много где еще, и повторяться я не буду. Однако, кратко напомню, что все те же зависимости, которые мы притягивали в GOPATH/src, можно сложить и в директорию vendor текущего приложения. Как это сделать? Или вручную, или с помощью менеджера управления зависимостями. В качестве примера утилиты для работы с зависимостями наконец-то можно упомянуть dep, официальный эксперимент Go. Несмотря на статус «официального эксперимента» dep по-прежнему не является ни абсолютно стабильным, ни рекомендуемым. Тем не менее, мы рискнули попробовать dep в наших рабочих проектах, и нам понравилось! Если вы впервые сталкиваетесь с вопросом управления зависимостями, я очень рекомендую вам десятиминутное видео с конференции Gophercon 2017, в котором подробно и наглядно показаны принципы работы dep.

Итак, по итогам использования менеджера управления зависимостями, мы получили директорию vendor, полную пакетов, и некоторый набор метафайлов, описывающих наши зависимости. Казалось бы, метафайлов с описанием используемых тегов и даже хэшей коммитов нам достаточно, и естественным желанием было бы убрать директорию vendor из-под управления системы контроля версий. Однако, в настоящих продакшн-проектах, не стоит этого делать. Хранение кода вместе с vendor — единственный путь защиты от таких аварий, как падение гитхаба или полное удаление своего репозитория сторонним разработчиком.

Версионирование бинарников

Пожалуй, почти все популярные консольные утилиты поддерживают команду или флаг version, позволяющую вывести информацию о текущий версии бинарника. Эта же практика может пригодиться и в случае с запущенным сервисом. Кроме того, иногда бывает полезно «зашить» в бинарник не только информацию о семантической версии, но и хэш коммита, дату сборки и другие полезные данные.

Подобную информацию удобно хранить в виде констант (или переменных — по вкусу), например, в специальном пакете. А при вызове линкера с флагом -X, мы сможем автоматически подменить значения этих констант или переменных.

Небольшой пример. Создадим файл hello.go с таким содержимым:

package main

import "fmt"

var hello = "Hello"
var world = "World"

func main() {
    fmt.Printf("%s, %s!n", hello, world)
}

При обычном запуске, например, с помощью команды go run hello.go, мы получим строку «Hello, World!»:

$ go run hello.go
Hello, World!

А теперь добавим вызов линкера с флагами -X и новыми значениями переменных:

$ go run -ldflags="-X main.hello=Привет -X main.world=Мир" hello.go
Привет, Мир!

Подменять можно не только переменные из пакета main, но и переменные и константы из любых пакетов вообще. Таким образом, во время сборки приложения можно зашить в него любую необходимую метаинформацию.

Набор инструкций по сборке приложения

Несмотря на солидный возраст, утилита make не теряет актуальности и популярности у тех, кому приходится собирать приложения (хотя бы под *nix). Вот и среди Go-разработчиков этот инструмент весьма распространен.

Рассмотрим конкретный пример Makefile для некоторого сервиса. Я подготовила репозиторий go-zeroservice, содержащий «нулевой» сервис, единственная функциональность которого — запуститься и показать информацию о сборке.

Команда make build собирает бинарный файл, подставляя указанную в Makefile версию, хэш последнего коммита и текущее время. При этом перед make build вызывается команда clean, которая удаляет уже существующий бинарник (если он был). Для обновления зависимостей предусмотрена команда make vendor, которая установит dep, если его еще нет, и выполнит команду dep ensure для актуализации пакетов внутри vendor. Для проверки качества кода предлагается команда make check, которая установит и запустит металинтер.

В наших продакшн-проектах мы выносим в Makefile любые более или менее повторяющиеся действия — запуск проверок на стандарты кодирования и запуск тестов, запуск менеджера управления пакетами, сборку приложения под нужную ОС с нужными флагами, команды для сборки и запуска Docker-контейнера и даже команды, позволяющие запустить релиз сервиса в Kubernetes с использованием Helm.

Наличие таких команд на разных окружениях позволяет быстро производить необходимые действия, например, запускать тесты и собирать и запускать контейнер на локальном окружении разработчика, или запускать тесты и сборку и проводить релиз в рамках процессов CI/CD. В случае с go-zeroservice можно посмотреть файл .travis.yml, который запускает сборку сервиса в рамках Travis CI и как раз состоит из команд, описанных в Makefile.

Заключение

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

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

P.S. Кстати, мы придумали продолжение мастер-класса про Go и Kubernetes и планируем представить его в сентябре на конференции DevFest Siberia. Присоединяйтесь к нам! ;-)

Автор: len

Источник

Поделиться

* - обязательные к заполнению поля