Среда разработки: делаем рабочие процессы удобнее

Разработка 20 окт. 2016 г.

Vagrant

Среда разработки: делаем рабочие процессы удобнее

Для удобного процесса разработки, быстрого переключения между проектами и эффективного взаимодействия бекэнд и фронтенд команд мы в WB—Tech работаем в виртуальном окружении Vagrant и VirtualBox.

Vagrant — кросс-платформенное ПО для создания виртуальной среды разработки. Для ускорения развертывания виртуальной машины можно использовать компилированные, версированные боксы. Версийность боксов в Vagrant описывается при помощи JSON документа.

{
    "name": "box_name",
    "description": "This box description.",
    "versions": [
        {
            "version": "42.0",
            "providers": [
                {
                    "name": "virtualbox",
                    "url": "http://somewhere.com/precise64_010_virtualbox.box",
                    "checksum_type": "sha1",
                    "checksum": "foo"
                }
            ]
        }
    ]
}

В самом Vagrantfile указать путь к метаданным в атрибуте config.vm.box_url.

config.vm.box = "box_name"
config.vm.box_version = "42.0"
config.vm.box_url = "http://somewhere.com/path/to/metadata.json"

Лайфхак: при обновлении версии бокса мы используем Nginx с дополнительным модулем, потому что описывать документ каждый раз вручную не практично. Формирование метаданных сделано при помощи простого скрипта на Lua. Мы хостим боксы самостоятельно с помощью Lua.

Почему именно Lua

Lua — скриптовый язык программирования. По возможностям, идеологии и реализации язык ближе всего к JavaScript, однако отличается более мощными и гибкими конструкциями.

Несмотря на то что Lua не содержит понятия класса и объекта в явном виде, механизмы ООП, в том числе множественное наследование, легко реализуются с использованием метатаблиц. Эти таблицы также отвечают за перегрузку операций и прочее.

Реализуемая модель ООП — прототипная (как и в JavaScript). Интерпретатор языка — свободно распространяемый с открытыми исходными текстами на языке C.

Установка и зависимости

Описание приведено для операционных систем семейства Debian.

  • Прежде всего нужен Nginx с модулем lua-nginx-module. Установите готовый пакет nginx-extras, либо соберите вручную.
  • Потребуется интерпретатор Lua, чтобы протестировать скрипт в интерактивной консоли.
  • Для компиляции модулей потребуется утилита make.
  • Менеджер пакетов luarocks: для поиска файлов в директории используем модуль luaposix, для конвертации словаря в JSON — JSON4Lua.
$ sudo apt-get -y install make nginx-extras lua5.1 luarocks
$ # install lua modules
$ sudo luarocks install luaposix
$ sudo luarocks install JSON4Lua

«Здравствуй, подлунный мир!»

Фраза “Hello world!” на Lua так же проста, как и на Python.

Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> print "Hello world!"
Hello world!

Теперь попробуем тоже самое при помощи полнофункционального Nginx Lua API.

server {
    listen    80;

    location /hello-world {
        content_by_lua '
            ngx.header.content_type = "text/plain"
            ngx.say("Hello world!")
        ';
    }
}
$ curl http://10.1.1.111/hello-world
Hello world!

Для исполнения скрипта служит директива content_by_lua, для которого Nginx получает ответ через API. Если скрипт большой, не обязательно описывать его внутри конфигурации, можно подключить через директиву content_by_lua_file.

Vagrant метаданные

Настройка Nginx

На сервере мы складываем боксы в директорию hosted, создавая поддиректорию для каждого проекта. Сами боксы со строго указанным форматом имени {provider}-{version.subversion}.box.

Формируется такое дерево.

$ tree hosted/
hosted/
├── foo
│   ├── docker-1.0.box
│   ├── docker-1.3.box
│   ├── virtualbox-1.0.box
│   ├── virtualbox-1.4.box
│   └── virtualbox-1.7.box
└── bar
    ├── virtualbox-1.0.box
    ├── virtualbox-1.1.box
    └── virtualbox-1.2.box

2 directories, 8 files

Настроим Nginx так, чтобы для любого бокса начиналось скачивание, а для имени проекта возвращались вычисленные метаданные.

server {
    listen    80;

    set $box_url 'http://10.1.1.111/%s/%s-%s.box';
    set $box_prefix '/home/vagrant/proj/hosted/';

    location ~ /*\.box$ {
        root /home/vagrant/proj/hosted;
        # just return box
    }

    location ~ /(?<box_name>\w+)/?$ {
        content_by_lua_file /home/vagrant/proj/app/handler.lua;
    }
}

Переменные $box_url и $box_prefix будут использоваться при формировании метаданных.

Вычисление метаданных с помощью Lua

Теперь сформируем метаданные для версирования Vagrant боксов. Идея в том, что по запросу Lua будет осуществлять поиск сохраненных боксов в заданной директории на сервере, вычислять их хеш-суммы и создавать ответ в формате метаданных Vagrant.

Используя glob из библиотеки posix, найдем все боксы.

local box_root = ngx.var.box_prefix .. ngx.var.box_name .. '/'
local posix = require "posix"
local glob = posix.glob (box_root .. '*.box')

-- Если боксы не найдены, можно сразу возвращать 404
if not glob then
    ngx.status = ngx.HTTP_NOT_FOUND
    return ngx.exit (ngx.HTTP_NOT_FOUND)
end

Итерациями пройдем по найденным боксам и сформируем словарь с найденными версиями.

local versions = {}
-- Discover the boxes
for _, box in ipairs (glob) do
    -- Обрабатываем найденый бокс, определяя версию и формируя описание
    local provider, version = make_provider (box)
    if version then
        if versions[version] == nil then
            -- Если версия встречается впервые, создаем запись для новой версии
            versions[version] = {
                version = version,
                providers = {provider}
            }
        else
            -- Если версия уже была описана, обновляем список провайдеров
            table.insert (versions[version]['providers'], provider)
        end
    end
end

Для вычисления хеш-суммы больших файлов боксов используем утилиты
OC — sha1sum, sha256sum, md5sum с помощью вызова процесса через io.popen.

local hash = 'sha1'

function get_hash (filepath)
    -- Вычисляем хешсумму используя вызов консольной утилиты sha1sum
    local command = string.format ('%ssum %s | cut -d " " -f1', hash, filepath)
    local hashsum = assert (io.popen (command, 'r'))
    local result = string.gsub (hashsum:read ('*a'), '\n', '')
    hashsum:close ()
    return result
end

Функция make_provider выполняется для каждого найденного бокса. Подразумевается, что боксы хранятся на сервере со строго заданным форматом имени: {provider}-{version.subversion}.box

Разбираем версию и имя провайдера, после чего формируем словарь, описывающий данный бокс.

local function make_provider (filepath)
    -- Make vagrant provider from given file
    local box_provider, box_version = string.match (
        filepath, string.format ('%s(%%a+)-(.+).box', box_root))
    return {
        -- Название провайдера virtualbox или docker
        name = box_provider,
        -- Прямая ссылка на бокс, которую будет запрашивать vagrant
        url = string.format (ngx.var.box_url, ngx.var.box_name, box_provider, box_version),
        -- Алгоритм хешсуммы sha1, sha256, md5
        checksum_type = hash,
        -- Строка со значением хешсуммы
        checksum = get_hash(filepath)
    }, box_version
end

Обработав все боксы и сформировав список версий, обернем все в дополнительный словарь.

-- Make result response
local vagrant = {
    name = ngx.var.box_name,
    description = string.format ("Boxes for %s proj", ngx.var.box_name),
    versions = {}
}
for _, version in pairs (versions) do
    table.insert (vagrant['versions'], version)
end

Ответ сервера JSON с найденными версиями.

ngx.header.content_type = "application/json; charset=utf-8"
local json = require "json"
ngx.say (json.encode (vagrant))

Полученный скрипт формирует JSON ответ c метаинформацией о боксах.

$ curl http://10.1.1.111/example | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   943    0   943    0     0  13412      0 --:--:-- --:--:-- --:--:-- 13471
{
  "versions": [
    {
      "version": "1.7",
      "providers": [
        {
          "url": "http://10.1.1.111/example/virtualbox-1.7.box",
          "checksum": "3221c0fd58a4b2430efc5eeaf09cb8eaf877f3a9",
          "name": "virtualbox",
          "checksum_type": "sha1"
        }
      ]
    },
    {
      "version": "1.3",
      "providers": [
        {
          "url": "http://10.1.1.111/example/docker-1.3.box",
          "checksum": "def7148aa7ded879dbf5944af4785c2b09aba97a",
          "name": "docker",
          "checksum_type": "sha1"
        }
      ]
    },
    {
      "version": "1.4",
      "providers": [
        {
          "url": "http://10.1.1.111/example/virtualbox-1.4.box",
          "checksum": "63b06d8c065f5c2522c356d4d6ceb718ec3f8198",
          "name": "virtualbox",
          "checksum_type": "sha1"
        }
      ]
    },
    {
      "version": "1.0",
      "providers": [
        {
          "url": "http://10.1.1.111/example/docker-1.0.box",
          "checksum": "65cb550765d251604dcfeedc36ea61f66ce205c4",
          "name": "docker",
          "checksum_type": "sha1"
        },
        {
          "url": "http://10.1.1.111/example/virtualbox-1.0.box",
          "checksum": "c0a9d5c3d6679cfcc4b1374e3ad42465f3dd596e",
          "name": "virtualbox",
          "checksum_type": "sha1"
        }
      ]
    }
  ],
  "name": "example",
  "description": "Boxes for example proj"
}

Полный пример скрипта можно посмотреть в репозитории на Github. И при желании поиграться, запустив настроенный Vagrant.


Другие статьи серии о о программных решениях, которые мы используем в своей работе:

Если хотите убедиться, что все делаете правильно или проконсультироваться по поводу разработки вашего проекта, напишите нам.

Подпишитесь на блог WB—Tech

Никакого спама, только анонсы новых статей!

Кирилл Гришанин

Последние 10 лет руковожу командой аналитиков, дизайнеров и разработчиков