Интеграция Jenkins и Vagrant для сборки проектов в виртуальном окружении

Дата публикации: 2015-03-21

Недавно передо мной встала задача автоматизировать сборку одного проекта, разработанного на Haskell-фреймворке Yesod. Особенных проблем с разработкой в гетерогенном окружении (Windows + Linux) не возникало, а все подводные камни успешно обходились, пока дело не дошло до разворачивания проекта в рабочем окружении. В качестве хостинга мы используем DigitalOcean — вполне хорошее и дешёвое решение, и к нему тоже до сих пор никаких претензий до сих пор не возникало. Но вот на этот раз возникли проблемы...

Haskell компилируется в нативный код. Это значит, во-первых, что получаемые бинарники достаточно быстро работают (что хорошо), а во-вторых — что они требуют компиляции в среде, приближенной к реальной (что уже не так хорошо) или кросскомпиляции. Ранее мы применяли очень простую практику — для простеньких "домашних" проектов стенд, на котором приложение развёрнуто, сам его и компилирует. Пока мы работали со Scala и sbt — это вызывало минимальные проблемы (пришлось немножко подтюнить shell-скрипт для sbt, чтобы он отдавал JVM побольше системной памяти). А вот с Haskell не срослось совсем — для сборки простейшего сайта на Yesod (а, вернее, каких-то нужных для него библиотек) требуется более 1 гигабайта виртуальной памяти! Когда cabal (сборочный инструмент Haskell) эту память не может найти, он просто падает.

Ну что ж, прошлось подходить к проблеме более серьёзно. Сборку приложения было решено проводить в контролируемых условиях, а выкладывать только бинарники. Проще всего с точки зрения поддержки проводить сборку на той же операционной системе, на которой приложение впоследствии запускается. Значит, будем для сборки использовать 64-битную Ubuntu 14.04 Trusty.

Какой самый удобный способ иметь портабельную повторяемую контролируемую среду с другой операционной системой? Конечно же, это Vagrant! А какое средство является самым приемлемым для того, чтобы контролировать процесс сборки? Конечно же, это Jenkins.

В этом посте я расскажу, как их друг с другом подружить, ну и организовать полноценное разворачивание приложения с виртуального стенда Vagrant на облако DigitalOcean (разумеется, рекомендации будут пригодны и для других облачных провайдеров и просто машин).

Настраиваем целевую машину

Я — сторонник подхода Continous Deployment, когда свежие версии программного обеспечения автоматически разворачиваются на рабочих машинах прямо из репозитория с кодом. Поэтому важной частью интеграции является настройка целевой машины (т.е. машины в облаке DigitalOcean) для того, чтобы она могла безопасно принимать сборки приложения со сборочного стенда.

Встречаются рекомендации об организации собственного репозитория с dev-пакетами, куда сборочная машина бы складывала обновления, а некая активность на целевой машине бы их оттуда загружала и переустанавливала. Я пока что не достиг такого мастерства в Linux-инфраструктуре, чтобы разворачивать собственные репозитории с пакетами, так что используем более простой способ — старый добрый проверенный SSH.

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

Я не буду давать конкретных советов по настройке этого аспекта, а просто дам ссылку на инструкцию в сети. Приватную часть ключа нужно будет передать на виртуальную машину-сборщик, а публичную — оставить в облаке.

Настройка Vagrant

Теперь приступим к настройке Vagrant (первым делом его, конечно же, следует установить). Базовым механизмом настройки подотчётных виртуальных машин Vagrant является т.н. provision — это когда Vagrant выполняет пользовательские скрипты в окружении виртуальной машины, или предпринимает другие действия по её настройке (в зависимости от провайдера — помимо shell поддерживаются chef и puppet). Ну а общие параметры виртуальной машины можно настроить прямо в Vagrantfile — основном файле любого образа Vagrant. Вот пример того Vagrantfile, который я применяю для сборки своего проекта:

# -*- mode: ruby -*-

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/trusty64"
  config.vm.provision "shell", path: "provision.sh"
  config.vm.provider "virtualbox" do |vb|
    vb.name = "vagrant-trusty64"
    vb.memory = 2048
  end

  # Disable the default ssh forwarding and enable another one
  config.vm.network :forwarded_port, guest: 22, id: "ssh", disabled: true
  config.vm.network :forwarded_port, guest: 22, host: 2210
end

Особенно важными являются строки config.vm.network — они отключают умолчальное перенаправление порта ssh и форсят его перенаправление на порт 2210. По умолчанию Vagrant перенаправляет порт sshd виртуальной машины на 2222 порт хоста. Если этот порт занят — он использует другой свободный порт. Если же у вас несколько виртуальных машин запускается одновременно, то в итоге бывает сложно предсказать, кто из них на каком порту сидит. Поэтому важно фиксировать порт, по которому будет доступен сервер ssh виртуальной машины.

А вот скрипт provision.sh, который отвечает за установку пакетов и настройку окружения виртуальной машины (обратите внимание, этот скрипт будет запущен от имени root):

# Копируем ssh-ключи и настраиваем known hosts, чтобы работал git:
cp /vagrant/id_rsa /home/vagrant/.ssh/
cp /vagrant/id_rsa.pub /home/vagrant/.ssh/
cp /vagrant/known_hosts /home/vagrant/.ssh/

chown -R vagrant /home/vagrant/.ssh/
chmod 700 /home/vagrant/.ssh/ && chmod 600 /home/vagrant/.ssh/*

# Устанавливаем пакеты для того, чтобы пользоваться свежим компилятором Haskell, и обновляем инструменты:
sudo add-apt-repository -y ppa:hvr/ghc
apt-get update
apt-get install -y cabal-install-1.22 ghc-7.8.3 git postgresql postgresql-server-dev-9.3 openjdk-7-jre-headless zlib1g-dev
ln -s /usr/bin/cabal-1.22 /usr/bin/cabal

echo export PATH=/opt/ghc/7.8.3/bin:/home/vagrant/.cabal/bin:$PATH >> /home/vagrant/.profile
sudo -Hu vagrant PATH=/opt/ghc/7.8.3/bin:/home/vagrant/.cabal/bin:$PATH cabal update
sudo -Hu vagrant PATH=/opt/ghc/7.8.3/bin:/home/vagrant/.cabal/bin:$PATH cabal install alex happy yesod-bin

# Создаём postgresql-окружение, нужное для тестирования:
sudo -u postgres psql -c "create user \"codingteam-site\" password 'codingteam-site';"
sudo -u postgres psql -c "create database \"codingteam-site_test\" owner \"codingteam-site\" encoding 'utf-8';"

# Создаём workspace для Jenkins:
mkdir /opt/jenkins
chown vagrant /opt/jenkins

Настройка Jenkins

Наконец-то переходим к самому главному — к настройке Jenkins. Jenkins сам по себе умеет работать в режиме кластера - когда есть один главный узел, управляющий группой дочерних slave'ов. Запуск и взаимодействие со slave-ами может происходить в различных режимах, но нас интересует один из них — когда на slave по SSH закачивается специальная программа, которая и будет управлять slave'ом.

Однако прежде, чем мы сможем подключиться по SSH, нужно сначала запустить машину. Для этой цели есть несколько "облачных" плагинов для Jenkins, которые реализуют взаимодействие с различными облачными провайдерами, а также Vagrant и т.н. Scripted Cloud. К сожалению, плагин с поддержкой Vagrant на текущий момент морально устарел, так что мы будем использовать именно Scripted Cloud. Он позволяет осуществлять интеграцию с любого рода внешними исполнителями, которых можно запустить и остановить при помощи скриптов.

Для начала составим скрипты для запуска и остановки облака. Я использую для них PowerShell и размещаю их прямо в каталоге с Vagrantfile. Вот эти скрипты:

# Start.ps1
cd $PSScriptRoot
vagrant up
# Stop.ps1
cd $PSScriptRoot
vagrant halt

Остаётся только создать новое "облако" с помощью Scripted Cloud, указать ему эту пару скриптов, отметить параметры подключения — и всё, система готова к работе. Scripted Cloud можно настроить таким образом, чтобы он стартовал машину по требованию и выключал её, когда она не нужна.

Разворачивание собранных артефактов

Для разворачивания собранных артефактов я использую стандартные программы ssh и scp для копирования артефактов и перезапуска сервисов. Вот пример скрипта, который обновляет артефакты приложения и перезагружает его:

ssh -p 2222 user@remotecomputer.ru sudo /sbin/stop site-user || true
ssh -p 2222 user@remotecomputer.ru rm -rf /opt/site/*
scp -P 2222 -r static user@remotecomputer.ru:/opt/site/static
ssh -p 2222 user@remotecomputer.ru mkdir /opt/site/config
scp -P 2222 config/client_session_key.aes user@remotecomputer.ru:/opt/site/config/client_session_key.aes
scp -P 2222 dist/build/site/site user@remotecomputer.ru:/opt/site/site
ssh -p 2222 user@remotecomputer.ru sudo /sbin/start site

Заключение

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