Выпуск 29. Июль 2015

От редактора | Содержание | Инструменты для поиска зависимостей в скрипте

(R)?ex на практике

В жизни каждого человека настает момент, когда ему становится лениво делать одно и то же на удаленных серверах и хочется автоматизации

В решении этой тяжелой жизненной проблемы нам поможет (R)?ex. Rex представляет собой систему управления удаленными серверами и является аналогом Ansible, но, в отличии от него, написан на Perl и работает на всех платформах кроме Windows.

Первым делом установка. На момент написания статьи текущая версия на CPAN 1.2.1. Но устанавливать его лучше из репозитория пакетным менеджером вашего дистрибутива. Благо, сборки есть под все популярные дистрибутивы.

На данный момент основная проблема Rex — это слабая документация. Вроде все вещи описаны, но описаны где попало. Что-то есть на сайте, что-то на CPAN, что-то в списке рассылки — не очень удобно. Так что в этой статье не будет каких-то особо тайных знаний, в основном все просто будет сведено в кучу.

Первые шаги

Во первых, Rex можно использовать для простого запуска команд на удаленном сервере:

rex -H "example.com example.ru" -e "say run 'uptime'"

Здесь мы запускаем команду uptime на серверах example.com и example.ru и получаем ее вывод в консоль.

Задачи в таком режиме выполняются последовательно, так что придется подождать, пока опросятся все сервера, особенно если их много.

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

По умолчанию Rex использует файл под названием Rexfile (удивительное совпадение).

Попробуем сделать что-то полезное.

Проверка синтаксиса

Первым делом укажу самое главное при написании командных файлов: проверка синтаксиса выполняется командой rex -T.

При ее выполнении будет выведено примерно следующее:

bash-3.2$ rex -T
Tasks
 checkout
 deploy     Deploy mysite
 sync
Server Groups
  myservers                      server1

В принципе, тут все очевидно. В случае ошибки синтаксиса появится обычное перловое сообщение об ошибке:

bash-3.2$ rex -T
syntax error at Rexfile line 22, near ""checkout" sub "
syntax error at Rexfile line 27, near "}"
Rexfile had compilation errors.
[2015-06-04 10:03:58] INFO - Exiting Rex...
[2015-06-04 10:03:58] INFO - Cleaning up...

Тоже все достаточно очевидно.

Режим отладки

В случае если с синтаксисом все правильно, но все равно ничего не работает — можно использовать режим отладки. Вызывается указанием -d в параметрах задачи.

rex -d stalled-task

Выведет гигантскую портянку со всеми действиями Rex, подставленными переменными и ответами удаленного сервера.

Теперь создадим Rexfile, который будет выполнять команду deploy. По этой команде Rex пойдет на удаленный сервер и сделает там git pull для сайта. Простейший вариант деплоя.

 use Rex -feature => ['1.0'];

 user "rex"; #Пользователь на удаленном сервере
 private_key "~/.ssh/id_rsa";
 public_key "~/.ssh/id_rsa.pub";
 key_auth;

 desc "Deploy the blog on example.ru";
 task "deploy", "example.ru", sub {
   run "cd /srv/www/example.ru && git pull origin gh-pages";
 };

Теперь можно сказать: rex deploy.

Дальше Rex залогинится под пользователем rex с использованием указанных ключей и выполнит команду, указанную в опции run. Если пользователь на удаленном сервере совпадает с текущим, то первую секцию можно опустить. Будет использоваться текущий пользователь и его настройки ключей для ssh. Но если планируется использование команды через cron, то указывать надо обязательно, иначе ничего работать не будет. Так же при использовании через cron нужно указывать полные пути к ключам.

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

Во первых, Rex с одинаковым успехом может управлять группой хостов как одним. Для этого добавим описание группы:

group myservers => "server1", "server2", "serverN";

И приведем объявление задачи к виду:

task "deploy", group => "myservers", sub { ... };

Так как Rex использует DSL на основе Perl, то в объявлении серверов отлично работают перечисления, и наш список серверов можно было сделать в виде:

group myservers => "server[1..10]";

В обычном режиме все операции с серверами будут производиться последовательно, что может занять достаточно приличное время. Для ускорения можно распараллеливать задачи, для этого служит опция parallelism($count) где $count это число тредов Rex. Каждый тред будет брать себе по серверу из списка и работать с ним. Правда, при этом смешивается вывод исполняемых команд (что логично), и определить что и где пошло не так, может быть сложно (если не настроить логгер и не использовать grep).

Чтобы выполнить команду локально, достаточно просто не указывать целевой сервер в атрибутах цели:

task "local_task", sub { ... };

Выполнит команду на хост машине.

Далее можно обновлять конфигурацию, копируя на хосты необходимые файлы (вы же не храните авторизационные данные в Git, правда?). Приведем содержимое раздела task к виду:

 run "cd /srv/www/example.ru && git pull origin gh-pages";
 file "/srv/www/example.ru/test.conf",
     source    => "files/test.conf";

Теперь при выполнении rex deploy будет выполнен pull из репозитория на удаленном сервере, а затем туда будет скопирован файл files/test.conf.

Синхронизация файлов

Таким же образом можно выкатывать все приложение, не используя git pull на удаленных хостах. Естественно, что делать это стоит не через описание всего приложения в секции file, а через старый добрый rsync.

Приведем наш Rexfile к виду:

use Rex -feature => ['1.0'];

# Подключаем модуль для работы с SCM, он поддерживает Git и SVN.
use Rex::Commands::SCM;
# Подключаем модуль для работы с rsync.
use Rex::Commands::Rsync;

user "rex"; #Пользователь на удаленном сервере
private_key "~/.ssh/id_rsa";
public_key "~/.ssh/id_rsa.pub";
key_auth;

# Объявляем репозиторий 
set repository => "example",
       url => "git@github.com:spb-pm/spb.pm.git",
       type => "git",
       username => "username",
       password => "password";

desc "Deploy the site on example.ru";
task "deploy", "example.ru", sub {
    # Делаем checkout из указанного выше репозитория
    say checkout "example",
        branch => 'gh-pages',
        path => "site_repo";

    # Заливаем файлы через rsync на удаленный хост
    say sync "./site_repo/*", "/srv/www/example.ru", {
            parameters => '--backup --delete',
    };
};

Теперь при выполнении rex deploy будет сделан git pull в директорию site_repo, в текущей директории и ее содержимое будет через rsync перенесено на удаленный сервер. В блоке parameters => ... задаются параметры rsync.

Через rsync можно получать и данные с удаленного сервера (может быть удобным для переодического получения логов и прочего в этом духе):

task "sync", "server01", sub {
   sync "/var/www/html/*", "html/", {
    download => 1,
    parameters => '--backup',
   };
 };

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

Передача параметров в задачу

Еще полезным моментом может быть передача параметров в задачи. Например, можно указать ветку, если мы разворачиваем тестовое окружение.

rex deploy --branch=test-branch --parameter2=foo

Пример секции task:

task "deploy", "myserver", sub {
    my $params = shift;
    say $params->{'branch'};
    say run "uptime";
};

Собственно, стандартная перловая передача hashref с параметрами.

Обработка ошибок

Статус выхода команды run доступен в переменной $?. Так же для run можно включить режим auto_die. Что он делает, понятно из названия.

task "deploy", "myserver", sub {
    say run "cp /tmp/foo /tmp/bar";
    say run "...";
};

В случае отсутствия исходного файла для cp операция не выдаст ничего, и выполнится следующая команда. Исправим это поведение:

task "deploy", "myserver", sub {
    say run "cp /tmp/foo /tmp/bar", (auto_die => TRUE);
    say run "...";
};

Теперь в случае ошибки мы получим красивый лог с описанием произошедшего.

sudo

Если нам нужно выполнить команду от root или пользователя, отличного от залогиненного, то надо использовать sudo:

sudo_password 'sudo password';

task "whoami", "example.ru", sub {
        say run "whoami";
        sudo {

                command => sub {
                        say run "whoami";
                },
                user => 'myuser',
        };
};

Выведет 2 строчки:

rex
myuser

Ускоряем работу

Rex может кешировать инвентори для выполнения задач. Полезно, когда используется много вызовов sudo или других медленных операций. Для этого надо вызвать его с опцией -c:

rex -c deploy

После работы скрипта будет создан каталог .cache, в котором будет хранится инвентарь для последующих вызовов скрипта.

Управление конфигурациями

Теперь рассмотрим вещи, ради которых действительно стоит использовать системы управления конфигурациями. По большому счету то, что приведено выше, реализуется небольшим bash-скриптом, который все знают и умеют, и нет особого смысла осваивать Rex.

Модули

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

Модуль Rex фактически является стандартным модулем Perl. Соответственно, большинство требований аналогично. Модели размещаются в каталоге lib директории, откуда выполняется Rexfile.

Для своих проектов я использую практически типовой набор ПО для создания окружения. Напишем модуль, который будет поднимать это окружение.

Создадим проект:

mkdir lib && cd lib && rexify MyRex::OS --create-module

В документации написано, что должна создаться директория lib с модулем и Rexfile, но на практике не создается.

lib
└── MyRex
       └── OS
           ├── __module__.pm
           └── meta.yml

Вот такое получится в результате работы этой команды. Файл meta.yml будет нужен только если вы собираетесь заливать свой модуль в репозиторий пакетов Rex. Файл __template__.pm — это файл нашего модуля.

Модуль будет устанавливать на сервер все необходимые пакеты и разворачивать окружение, приведем его к виду:

package MyRex::OS;

use Rex -feature => ['1.0'];

task "prepare" => sub {
    sudo sub {
        pkg [ 
             "build-essential",
             "supervisor",
             "mariadb-common",
                "git",
             "vim",
           ],
         ensure => 'present';
    };
};

1;

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

Опция ensure говорит нам, что делать с пакетами:

  • present — пакет должен быть установлен, какая версия, нам не важно;
  • latest — должен быть установлен пакет последней версии, если это не так — пакет будет автоматически обновлен;
  • absent — пакет не должен быть установлен, если пакет установлен — то он будет удален;
  • 2.3.4 — должна быть установлена конкретная версия пакета.

Обратите внимание на использование sudo вокруг pkg, по умолчанию все операции выполняются от текущего пользователя, и самостоятельно Rex не будет переключаться в режим суперпользователя.

Создадим в корне проекта Rexfile вида:

use Rex -feature => ['1.0'];

user "rex";
password "password";
sudo_password "password";

group "srv" => "192.168.1.102";

require MyRex::OS;

task "prepare", group => "srv", sub {
        MyRex::OS::prepare();
};

1;

В Rexfile мы объявляем группу для серверов и создаем команду prepare, которая будет выполнять команду prepare из модуля MyRex::OS на нужных нам серверах и в нужном окружении.

Теперь при запуске rex prepare на целевом сервере будут установлены все указанные выше пакеты.

Пришло время установить Perl.

Первым делом установим perlbrew и CPAN в системный Perl. Добавим в блок sudo в prepare нашего модуля:

append_if_no_such_line "/etc/environment",
    line  => "LC_ALL=en_US.UTF-8",
    regexp => [qr{^LC_ALL=}]; # this is an OR

run 'cpan -i App::perlbrew', (auto_die => TRUE);

Первая команда откроет /etc/environment и проверит, что там задан язык для LC_ALL. Если не задан, то выставит кодировку en_US.UTF-8.

Зачем это надо? По умолчанию эта переменная не проставлена, и многие ssh-клиенты ее не устанавливают. Соответственно, при установке perl мы получим кучу ругани и падение тестов, которые проверяют IO. На самом деле, для запуска задач через Rex эта проблема не актуальна, так как он выставляет LC_ALL=C, но при входе в консоль возможны варианты.

Вторая строка просто запускает cpan -i.

Установим Perl. Эту операцию я вынес в отдельный таск, т.к. там используется куча run, и простыня в prepare получается уж слишком большой.

task "install_perl" => sub {
    say "Installing perl";
    run "perlbrew init", (auto_die => TRUE);

    append_if_no_such_line "~/.bashrc",
            line  => "source ~/perl5/perlbrew/etc/bashrc";

       run "source ~/perl5/perlbrew/etc/bashrc";
    run "perlbrew install perl-5.20.2", (auto_die => TRUE);
    say "Done";
};

Тоже ничего сложного, стандартный процесс установки из мануала perlbrew. В append_if_no_such_line мы просто дописываем строку, если ее не существует.

Вызов этого таска можно добавить вне блока sudo в prepare через do_task "install_perl". Либо вызывать самостоятельно, по большому счету это выполняется один раз, и особой разницы нет.

Теперь мы можем подключать этот модуль в Rexfile любого нового проекта и получать единообразное окружение одной командой.

Ну и всякие мелочи напоследок.

say

Вывод команды say на больших логах выглядит чудовищно. Если сохранить вывод run в переменную и вывести его через say, то будет чуть менее ужасно. Но он настраивается:

sayformat('[%D] %h %s');


[2015-06-15 10:58:22] 192.168.1.150 total 4
[2015-06-15 10:58:22] 192.168.1.150 drwxr-xr-x 3 root root 4096 Jun 15 09:22 perl5

Локальные задачи

Если внутри таска на удаленных серверах надо выполнить задачу на хост-машине — используется функция LOCAL(&):

task "mytask", group => "srv" sub {
    # Это выполнится на удаленных серверах
    say run "uptime";

    # А это на локальном
    LOCAL {
      say run "uptime";
    };
 };

profile

Важным моментом при использовании Rex являет тот факт, что он подключается к серверу в режиме nologin. В этом режиме не подгружаются файл .bashrc и иже с ним. В данной ситуации есть три варианта действий:

  • делать source ~/.bashrc в команде;
  • вручную задать все нужные переменные окружения в Rexfile;
  • выставить настройку source_global_profile(0|1).

Настройка делает то же что и первый пункт списка, но автоматически.

Вывод команды при ошибке

По большей части нам все равно, что пишут команды в консоль при выполнении, не все равно становится, когда что-то ломается. Но каждый раз переключать c run "uptime" на say run "uptime" не интересно и лениво. Для решения этой проблемы есть специальная команда — last_command_output

task "mytask", group => "srv" sub {
    run "command-with-error";

    if ($?) {
        say last_command_output();
    }
 };

Для версий старше 1.2 — надо будет изменить заголовок файла на:

use Rex -feature => ['1.3', 'tty'];

поскольку в режиме notty не происходит перехват STDERR, а режим tty был отключен по умолчанию в каком-то из релизов 1.0+.

Заключение

В целом, Rex является очень мощным инструментом прямо из коробки. Он позволяет обойтись без строительства велосипедов, и при этом получить полный контроль над управлением инфраструктурой самых разных масштабов.

Единственное что он пока не умеет — управление хостами на Windows, но если это не требуется — то дорога в мир Continuous Delivery для вас открыта :)

Денис Федосеев


От редактора | Содержание | Инструменты для поиска зависимостей в скрипте
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье