Встраивание ресурсов в приложение

23 minute read

Перевод статьи "Embedding Assets in Go"

В этой статье я продемонстрирую, как можно встраивать ресурсы в само Go приложение. В частности, я покажу пример встраивания shell скрипта для chef-runner(одного из моих открытых проектов). Моя задача состоит в том, чтобы подтолкнуть вас к использованию этой замечательной технологии в ваших проектах. Кроме того, я постараюсь дать вам представлении о выборе правильных инструментов для подобных задач. Давайте начнем.

Встречайте chef-runner. Быстрый способ запуска Chef Cookbooks.

Самый большой плюс chef-runner'a -это ускорение разработки и тестирования Chef cookbooks. Изначально я разрабатывал этот инструмент как быструю альтернативу медленной команде vagrant provision. С тех пор прошло много времени и теперь chef-runner может использоваться как для настройки виртуальных машин в Vagrant так и удаленных хостов, например инстансов EC2.

Одно из преимуществ chef-runner'a в том что он может установить Chef на машину до ее полного резервирования. Это позволяет использовать его на голых серверах без ПО, только с операционной системой. Для реализации этой возможности chef-runner использует скаченный в локальную папку Omnibus installer(также известный как install.sh скрипт), который будет скопирован на целевую машину где он должен будет установить Chef.

Чуть позже я решил немного изменить схему работы. Вместо скачивания скрипта из интернета будет лучше встроить его как часть исходника chef-runner'a. Это дает множество преимуществ:

  • Простота. Код становится проще, не нужно реализовывать логику загрузки, не нужно реализовывать кеширование.
  • Прозрачность. Всегда используется тот скрипт, который вы добавили в репозиторий.
  • Скорость. Нет необходимости что-то скачивать для каждого проекта.

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

chef-runner написан на Go. Я знал, что есть возможность встроить ресурсы в Go приложение и решил ознакомиться с различными вариантами и пакетами для этого. В конце концов, я решил использовать комбинацию go-bindata и go generate.

Встраивание ресурсов с go-bindata

go-bindata конвертирует все текстовые или бинарные файлы в исходники Go. И это действительно замечательный инструмент для встраивания различных данных в ваше приложение. go-bindata можно использовать для встраивания CSS, JavaScript и изображений в ваше веб-приложение. В результате, у вас будет единственный бинарный файл, который можно просто деплоить как и любую другую Go программу.

Я использовал go-bindata для добавления Omnibus installer как ресурса к chef-runner'у в виде пакета omnibus. Все что мне нужно сделать для этого - скачать скрипт и выполнить команду go-bindata для генерирования Go кода. Более точно все выглядит так:

# inside $GOPATH/src/github.com/mlafeldt/chef-runner
$ cd chef/omnibus/ 
$ mkdir assets 
$ curl https://www.chef.io/chef/install.sh > assets/install.sh 
$ go-bindata -pkg omnibus -o assets.go assets/

После этого я должен адаптировать chef/omnibus/omnibus.go для использования директории с ресурсами напрямую, не загружая их. Данные из ресурсов доступны через функцию Asset, которая определена в сгенерированном файле assets.go. В результате код будет выглядеть вот так:

// chef/omnibus/omnibus.go

package omnibus

type Installer struct { 
    ChefVersion string 
    SandboxPath string 
    RootPath string 
    Sudo bool 
}

func (i Installer) writeOmnibusScript() error { 
    script := path.Join(i.SandboxPath, "install.sh") 
    log.Debugf("Writing Omnibus script to %s\n", script) 
    data, err := Asset("assets/install.sh") 
    if err != nil { 
        return err 
    } 
    return ioutil.WriteFile(script, []byte(data), 0644) 
}

И это все что мне нужно было сделать. Я даже был удивлен, что понадобилось так мало усилий.

Тем не менее, есть небольшой момент, который мне не нравится в функции Asset, сгенерированной go-bindata. Дело в том, что эта функция будет отображаться в документации к пакету, что немного сбивает с толку. Один из вариантов, это хранить ресурсы в отдельном пакете.

Если вы хотите узнать больше о фичах go-bindata, то сходите и почитайте README. Есть очень полезная возможность включить режим отладки, в котором данные будут загружены из исходного файла на диске с использованием того же API. Это удобно использовать во время разработки.

Еще один взаимосвязанный проект - go-bindata-assetfs. Его можно использовать для раздачи файлов через net/http, которые были встроены с помощью go-bindata (внутри все реализованно через интерфейс http.FileSystem). Этот пакет может быть полезен для множества разных кейсов.

go generate: используем кодогенерацию

Хотя такие инструменты как go-bindata могут работать сами по себе, порой возникает необходимость интегрировать их в свой собственный бил процесс. Для этого вы можете использовать например Make, который будет работать как клей в вашей системе сборки. Но было бы намного круче, если бы Go сам предоставлял необходимые инструменты для генерации кода. И, на самом деле, так и есть!

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

Пример комментария, который я добавил в пакет omnibus:

// chef/omnibus/omnibus.go

package omnibus

//go:generate go-bindata -pkg $GOPACKAGE -o assets.go assets/

Теперь при запуске go generate выполниться команда go-bindata с указанными параметрами ($GOPACKAGE будет заменено на текущее имя пакета, например "omnibus"). Давайте сгенерируем немного кода:

$ go generate -x ./chef/omnibus
go-bindata -pkg omnibus -o assets.go assets/

Флаг -x означает, что во время выполнения go generate будут выводится все выполняемые ею действия. И эти действия должны быть вам знакомы.

Как и в большинстве Go инструментов, вы можете запустить go generate ./... для прохода по всем пакетам в вашем проекте. И если так случилось, что у вас есть Makefile, то сейчас самое время добавить цель make generate для сокращения и для использования в других целях в Makefile.

Есть только одни неудобный момент работы с go generate, про котрый вы должны помнить: нет никакой интеграции с go get, как можно было бы ожидать. Если вы хотите, чтобы ваш проект был "go gettable", то вам нужно положить в систему контроля версий и сгенерированный файлы, как это сделал я с assets.go.

Для более детальной информации по go generate используйте go help generate или, что еще лучше, почитайте эту статью в блоге Go.

Заключение

В целом, в Go есть удобные средства для встраивания ресурсов, которыми легко пользоваться благодаря существующим инструментам. Это и go-bindata, которая позволяет конвертировать любые данные в встраиваемые ресурсы, и go generate, который позволяет автоматизировать генерацию Go кода красивым способом. Я уверен, в недалеком будущем мы будем использовать эти инструменты еще чаще.

И еще кое-что. В одном из своих открытых проектах я использовал Python script для генерации Go map'ы со списком дистров, поддерживаемых Packagecloud. Так как я генерировал эту map еще до компиляции, то на этапы исполнения у меня получилось сэкономить на API вызовах. Я рекомендую вам ознакомиться с исходным кодом проекта, чтобы быть в курсе какие еще бывают практики встраивания ресурсов в свои приложения.