- PVSM.RU - https://www.pvsm.ru -
В июне на конференции РИТ++ мы с коллегой Игорем Должиковым [1] делились опытом автоматизации процесса разработки сервисов на Go — от первого коммита и до релиза в продакшн-окружение Kubernetes [2] (да-да, видео начинается с 07:16, и нам тоже это не нравится). С момента публикации мастер-класса время от времени я получаю вопросы по тем или иным темам, затронутым в нем. Пожалуй, самые горячие вопросы достойны отдельного рассмотрения, и сегодня я хотела бы поговорить о процессе сборки приложения. Затрагиваемые темы актуальны не только при подготовке сервисов, но и вообще для любых приложений, написанных на Go.
Всё, что описано в этой статье, актуально для текущей версии Go — 1.9.
При подготовке приложения к продакшн нужно быть максимально уверенным в том, что в сборку попадёт именно тот код, который ожидает разработчик. По умолчанию Go по-прежнему не умеет работать с управлением зависимостями, а значит, при компиляции приложения все “внешние” зависимости инструменты Go будут искать внутри директории $GOPATH/src
. Как узнать текущее значение GOPATH
, если вы в нем не уверены? Это значение можно найти в списке переменных, выводимых командой go env
.
Кроме того, код самого проекта также должен находиться внутри $GOPATH/src
, и я рекомендую заранее продумать путь, по которому он будет лежать. Когда проект окажется под управлением системы контроля версий, при его затягивании с использованием, например, команды go get
, он должен попадать именно по тому пути, который мы определили для проекта изначально. Например, код сервиса, который хранится в моем гитхаб-аккаунте в репозитории rumyantseva/mif [3] развернут у меня внутри $GOPATH/src/github.com/rumyantseva/mif
. Если бы этот же код лежал внутри репозитория mif некоторого закрытого хранилища example.com в неймспейсе services, то путь к нему на машине разработчика выглядел бы, скорее всего, как $GOPATH/src/example.com/services/mif
. Для того, чтобы избежать в будущем проблем или неоднозначностей, правило расположения кода необходимо соблюдать.
Разные проекты можно хранить как внутри одной и той же директории GOPATH
, так и внутри нескольких. Соответственно, во втором случае значение GOPATH
придется переопределять. Для того, чтобы сделать это, необходимо будет переустановить соответствующую переменную окружения в нужное вам значение. В случае, если GOPATH
не задан вообще, Go будет считать рабочим каталогом директорию go
, находящуюся в домашнем каталоге пользователя. Чтобы лучше в этом всём разобраться, проведем несколько экспериментов с консолью:
$ # По умолчанию 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 [4], который позволяет проводить сборки вне зависимости от расположения кода.
Итак, в случае, если мы пишем приложение, использующее внешние (относительно текущего репозитория) зависимости, для успешной сборки нам необходимо, чтобы все эти зависимости находились внутри GOPATH
. Притянуть зависимости автоматически можно с помощью вызова таких команд, как go get
или go install
внутри текущего рабочего проекта. При этом мы скачаем код зависимостей, находящихся в дефолтных ветках репозиториев. Этого достаточно для ситуации «здесь и сейчас», но в общем случае никто не гарантирует, что через 5 минут в тех же самых ветках внешних зависимостей не появятся обратно-несовместимые изменения. А значит, следующая попытка развернуть приложение (например, на билд-машине) может закончиться провалом. Что нам поможет в этой ситуации? Конечно же, вендоринг.
Про директорию vendor
неоднократно писали и на Хабре, и много где еще, и повторяться я не буду. Однако, кратко напомню, что все те же зависимости, которые мы притягивали в GOPATH/src
, можно сложить и в директорию vendor текущего приложения. Как это сделать? Или вручную, или с помощью менеджера управления зависимостями. В качестве примера утилиты для работы с зависимостями наконец-то можно упомянуть dep [5], официальный эксперимент Go. Несмотря на статус «официального эксперимента» dep по-прежнему не является ни абсолютно стабильным, ни рекомендуемым. Тем не менее, мы рискнули попробовать dep в наших рабочих проектах, и нам понравилось! Если вы впервые сталкиваетесь с вопросом управления зависимостями, я очень рекомендую вам десятиминутное видео [6] с конференции 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 [7] для некоторого сервиса. Я подготовила репозиторий go-zeroservice [8], содержащий «нулевой» сервис, единственная функциональность которого — запуститься и показать информацию о сборке.
Команда make build [9] собирает бинарный файл, подставляя указанную в Makefile версию, хэш последнего коммита и текущее время. При этом перед make build вызывается команда clean, которая удаляет уже существующий бинарник (если он был). Для обновления зависимостей предусмотрена команда make vendor [10], которая установит dep, если его еще нет, и выполнит команду dep ensure для актуализации пакетов внутри vendor. Для проверки качества кода предлагается команда make check [11], которая установит и запустит металинтер [12].
В наших продакшн-проектах мы выносим в Makefile любые более или менее повторяющиеся действия — запуск проверок на стандарты кодирования и запуск тестов, запуск менеджера управления пакетами, сборку приложения под нужную ОС с нужными флагами, команды для сборки и запуска Docker-контейнера и даже команды, позволяющие запустить релиз сервиса в Kubernetes с использованием Helm.
Наличие таких команд на разных окружениях позволяет быстро производить необходимые действия, например, запускать тесты и собирать и запускать контейнер на локальном окружении разработчика, или запускать тесты и сборку и проводить релиз в рамках процессов CI/CD. В случае с go-zeroservice можно посмотреть файл .travis.yml [13], который запускает сборку сервиса в рамках Travis CI и как раз состоит из команд, описанных в Makefile.
Итак, для того, чтобы разобраться с вопросами сборки Go-приложений для продакшн, требуется не так уж много. Во-первых, нужно определиться с расположением кода приложения внутри GOPATH
и убедиться, что оно соответствует расположению кода в системе контроля версий. Во-вторых, нужно решить, каким образом будет производиться взаимодействие с управлением внешними зависимостями и выбрать подходящий инструмент [14]. В-третьих, удобно, когда под рукой есть все инструкции, которые приходится выполнять более или менее регулярно и на разных окружениях, в хранении таких инструкции помогает, например, Makefile.
Надеюсь, эта статья поможет новичкам мира Go быстрее разобраться с вопросами подготовки релизов. А если вы используете другие практики для сборки приложений, не стесняйтесь поделиться ими в комментариях и заранее спасибо за это.
P.S. Кстати, мы придумали продолжение мастер-класса про Go и Kubernetes и планируем представить его в сентябре на конференции DevFest Siberia [15]. Присоединяйтесь к нам! ;-)
Автор: len
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/golang/263318
Ссылки в тексте:
[1] Игорем Должиковым: https://github.com/takama
[2] от первого коммита и до релиза в продакшн-окружение Kubernetes: https://youtu.be/0ndWw1udpsA?t=7m16s
[3] rumyantseva/mif: https://github.com/rumyantseva/mif
[4] gb: https://getgb.io
[5] dep: https://github.com/golang/dep
[6] десятиминутное видео: https://www.youtube.com/watch?v=eZwR8qr2BfI
[7] конкретный пример Makefile: https://github.com/rumyantseva/go-zeroservice/blob/1.0.0/Makefile
[8] go-zeroservice: https://github.com/rumyantseva/go-zeroservice
[9] make build: https://github.com/rumyantseva/go-zeroservice/blob/60f058495e10077e6b9eb58de8849b3bdd409008/Makefile#L13
[10] make vendor: https://github.com/rumyantseva/go-zeroservice/blob/0de2ee014eebf069d6f977544abb92648146bbc8/Makefile#L23
[11] make check: https://github.com/rumyantseva/go-zeroservice/blob/0de2ee014eebf069d6f977544abb92648146bbc8/Makefile#L9
[12] металинтер: https://github.com/alecthomas/gometalinter
[13] .travis.yml: https://github.com/rumyantseva/go-zeroservice/blob/1.0.0/.travis.yml
[14] выбрать подходящий инструмент: https://github.com/golang/go/wiki/PackageManagementTools
[15] DevFest Siberia: https://gdg-siberia.com
[16] Источник: https://habrahabr.ru/post/337158/
Нажмите здесь для печати.