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 Samael500/ngx-vagrant. А при желании поиграться, запустив настроенный Vagrant.

О том, как ещё можно интересно использовать связку Nginx и Lua, читайте в статье Применение Nginx + Lua для обработки контактной формы.