CFEngine
Продолжим начатый пользователем alex_www в двух предыдущих статьях рассказ о CFEngine. В этой речь пойдёт о практике применения CFEngine и некоторых нюансах его настройки в условиях реального мира. Для уменьшения объёма текста я предполагаю, что основными понятиями из мира CFEngine вы владеете, возможно даже пробовали его где-то использовать. В качестве букваря могу посоветовать книгу Диего Замбони (Diego Zamboni) "Learning CFEngine 3", она небольшая, очень понятная и читается на одном дыхании.
В статье даны примеры для настройки с чистого листа на Debian GNU/Linux с использованием Git. Если вы хотите дополнить статью примерами для своих любимых дистрибутивов и VCS, то присылайте личные сообщения или высказывайтесь в комментариях. По возможности я буду их добавлять в основной текст с указанием авторства.
Установка
Самый простой способ установить CFEngine3 — сделать это через официальный репозиторий. Для этого сначала нужно добавить ключ, которым подписаны пакеты:
cd /tmp
wget http://cfengine.com/pub/gpg.key
cat gpg.key # не ныряй в незнакомых местах
apt-key add gpg.key
rm gpg.key # чисто там, где не сорят
… а потом добавить репозиторий и поставить CFE:
echo "deb http://cfengine.com/pub/apt community main" > /etc/apt/sources.list.d/cfengine-community.list
chmod 644 /etc/apt/sources.list.d/cfengine-community.list # некоторые любят umask 0700
apt-get update
apt-get install cfengine-community
Инициализация
Чтобы инициализировать CFE, нужно выполнить (например, вручную) первый запуск cf-agent. Если вам приходится часто вводить в строй новые серверы, то установку CFE и bootstrap лучше всего производить из preseed или же из скрипта, запускающегося один раз при первом старте системы; хорошим примером такого скрипта может послужить скрипт, создающий серверные ключи для OpenSSH.
/var/cfengine/bin/cf-agent -IC --bootstrap <policy_hub_address>
Вместо <policy_hub_address>
нужно подставить соответствующий IP-адрес или доменное имя вашего policy hub, причём если вы инициализируете сам policy hub, то нужно указать тот IP, на котором он потом будет обслуживать клиентов. В случае доменного имени, оно будет разрешено в IP-адрес, который и будет использоваться в дальнейшем, так что проблемы с возможной недоступностью DNS-серверов не станут помехой.
Параметр -I
включает создание краткого отчёта о выполнении, а -C
— раскраску вывода в консоли. Оба не являются обязательными, но я их использую в интерактивных сессиях для собственного удобства. Ещё один полезный параметр запуска это -v
, подробный режим. Он имеет приоритет перед -I
и выдаёт очень подробную информацию о ходе выполнения промисов. Здорово помогает при отладке.
Первичная настройка
После завершения инициализации policy hub, ещё до соединения с первым клиентом, нужно настроить кое-какие мелочи. Дело в том, что параметры по умолчанию в файле def.cf
(здесь и далее для всех относительных путей корнем считается /var/cfengine/masterfiles
, если не указано иное) неплохо подходят для экспериментов «на коленке» или для демонстрации возможностей, однако для прода эти параметры подходят мало. Чуть ниже, я напишу об организации процесса разработки промисов и выкатывания их в прод, а пока что лучше всего сделать локальную копию /var/cfengine/masterfiles
и работать с ней.
Первым делом укажем наше полное доменное имя. Несмотря на усилия команды разработчиков сделать хорошую систему автоопределения текущих параметров системы, она неидеальна, поэтому везде, где можно и целессобразно лучше всего не полагаться на автоматику и указывать данные вручную. Начнём с bundle common def
, и укажем domain
, mailto
, mailfrom
и smtpserver
:
'domain' string => 'example.org';
'mailto' string => 'sysadmin-queue@${def.domain}'; # на этот адрес CFE будет пытаться слать сообщения в случае необходимости
'mailfrom' string => 'root@cfe-policy-server.${def.domain}';
'smtpserver' string => 'internal-mail-collector.${def.domain}';
Безопасность
CFE может обеспечивать безопасность соединений и аутентификацию клиентов, однако какими-то исключительно развитыми средствами для этого не обладает, так как имеющихся за глаза хватает для выполнения поставленной задачи. Так или иначе, в коммуникациях с клиентами используется система с доверенными ключами, аналогичная OpenSSH, и списки доверенных IP-сетей и доменов. По умолчанию соединения разрешёны для всех хостов в домене policy hub и для /16 его основного IP. Все ключи, полученные по успешно установленным соединениям считаются доверенными. Такой подход может в условиях работы в доверенной сети может сильно облегчить разворачивание с нуля и очень удобен на стадии R&D, но крайне небезопасен в реально жизни. Учитывая эфемерность IP-адресов и географическую распределённость контролируемых машин я предпочитаю использовать следующий подход: CFEngine (cf-serverd
, если быть точнее) принимает соединения с любого IP-адреса и доверяет только тем ключам, которые ему известны заранее (то есть, находятся в /var/cfengine/ppkeys
):
'acl' slist => {
'0.0.0.0/0',
};
comment => 'Connections are allowed from any IP',
handle => 'common_def_vars_acl';
'trustkeysfrom' slist => {
# NEVER ADD ANYTHING HERE. DON'T TRUST STRANGERS!
},
comment => 'Only keys in /var/cfengine/ppkeys are trusted',
handle => 'common_def_vars_truskeysfrom';
В случае, когда нужно ограничивать доступ по IP-адресам я предпочитаю использовать фаерволл, как более подходящий для решения задачи инструмент.
Чтобы добавить ключ клиента в доверенные нужно скопировать содержимое файла /var/cfengine/ppkeys/localhost.pub
(обычный RSA-ключ в Base64) на policy hub и запустить cf-key -t /path/to/client_key.pub
. Программа cf-key
сама добавит его в /var/cfengine/ppkeys
с правильным именем и правами.
Стандартная библиотека
Автоматизация контроля за конфигурациями, позволяющая легко вносить масштабные изменения, с такой же лёгкостью приводит и к масштабным ошибкам. Поэтому необходимо разумное количество «ремней безопасности» и «больших красных кнопок». Один из таких «ремней» — это внесение стандартной библиотеки в VCS вместе с вашим кодом. При установке пакета с новой версией, содержимое каталогов /var/cfengine/masterfiles
и /var/cfengine/inputs
не обновляется, так как в результате будет невозможно гарантировать непротиворечивость конфигурации. Поэтому один из важных этапов обновления — это слияние изменений стандартной библиотеки с вашей копией и именно здесь вам пригодится вся помощь, какую ваша VCS может вам предложить, а так же умение пользоваться утилитами diff
и patch
.
package_latests
Одним из механизмов, который я часто использую для гарантий обновлений некоторых пакетов является бандл package_latest
из стандартной бибилотеки. К сожалению, там есть баг, из-за которого бандл в Дебиане не работает. Фикс весьма тривиален. В файле lib/3.6/packages.cf
нужно найти код бандла packages_latest
и привести его к такому виду (можно использовать патч из багрепорта):
debian::
"$(package)"
package_policy => "addupdate",
package_version => "999999999:9999999999",
package_method => apt_get_permissive;
Bug 6870
Ещё одним досадным багом, который я обнаружил в проде, является баг 6870. Суть проблемы заключается в том, что CFEngine устанавливает некоторые классы на основании PTR-записей IP-адресов на интерфейсах. Как можно догадаться, такое поведение системы весьма и весьма небезопасно, да и противоречит постулату CFEngine о неприемлимости внешних знаний. Однако, в своей книге Диего Замбони учит определять хост исполнения по классам вида host1_example_org
, а исправление этого бага может разломать слишком много работающих сейчас систем. Поэтому, пока разработчики не предоставили более надёжного способа, мы его создадим сами. Вот код, который можно добавить в свою версию стандартной библиотеки в файл lib/3.6/bug_6870.cf
:
bundle common bug_6870_workaround {
classes:
'bug6870_workaround_${sys.host}' expression => 'any';
}
Затем нужно добавить имя файла в ${stdlib_common.inputs}
по аналогии с уже перечисленными там файлами и после этого уже использовать класс bug6870_workaround_host1_example_org
не опасаясь неожиданных пересечений с PTR-записями других IP.
Жизненный цикл
Теперь, когда всё готово для творчества, я расскажу немного об организации процесса разработки промисов и немного о более приземлённых, бытовых вещах.
Инструменты
Прежде всего, промисы надо в чём-то писать. Для этого, скорее всего, подойдёт ваш любимый текстовый редактор. Я предпочитаю Vim и использую плагины, написанные Нилом Ватсоном (Niel Watson), а мой коллега Валера Островерхов (Val Astraverkhau) создал очень хороший плагин для поддержки CFEngine в Sublime Text 2 и 3. Пользователям Emacs будет небезынтересна лекция Тэда Златанова (Ted Zlatanov) об использовании Emacs в качестве CFEngine IDE.
Так же вам понадобится хорошая система контроля версий. Меня всем устраивает Git, но я уверен, что подойдёт любая современная VCS. Требования тут те же, что и к разработке обычного софта, так что берите то, с чем вам удобно работать.
Организация файлов и точка входа
Скажу сразу, что предложенный мной способ не единственно верный. Знакомый программистам на Perl принцип TIMTOWTDI применим и здесь; и как нигде, здесь уместна полемика. Вот примерная структура директорий, относительно корня проекта:
/bin
/masterfiles
/masterfiles/cfe_internal
/masterfiles/cfe_internal/ha
/masterfiles/controls
/masterfiles/controls/3.4
/masterfiles/inventory
/masterfiles/example_org
/masterfiles/lib
/masterfiles/lib/3.5
/masterfiles/lib/3.6
/masterfiles/services
/masterfiles/services/autorun
/masterfiles/sketches
/masterfiles/sketches/meta
/masterfiles/templates
/masterfiles/update
/static
/static/bird-lg
/static/firewall-configs
/static/ssh-keys
/templates
В директории /masterfiles/example_org
лежит тот код, который мы пишем. Остальные поддиректории в /masterfiles
— это части стандартной поставки, которые я стараюсь не менять без крайней необходимости. Все нестандартные темплейты вынесены в /templates
, а в /static
, как видно из названия, хранится «статичная» информация — публичные SSH-ключи, настройки фаерволлов, пользовательские настройки, конфигурационные файлы и прочее, что не меняется от хоста к хосту. В директории /bin
лежит пара сервисных скриптов, включая «большую красную кнопку» — скрипт, перекладывающий все нужные файлы туда, откуда CFEngine сможет их раздать клиентам.
Точка входа расположена в /masterfiles/example_org/main.cf
, где содержатся два промиса: bundle common example_org
, в котором перечислены используемые файлы и происходит классификация серверов, и bundle agent example_org_main
, где в зависимости от класса управление передаётся нужному бандлу, описывающему как именно серверы этого класса должны быть сконфигурированы.
Для указания точки входа в файле promises.cf
нужно внести следующие изменения:
body common control {
bundlesequence => {
# [...]
@{example_org.bundles},
};
inputs => {
# [...]
'example_org/main.cf',
@{example_org.inputs},
};
}
Сам же example_org/main.cf
выглядит примерно так:
bundle common example_org {
vars:
'inputs' slist => {
'example_org/add_default_users.cf',
'example_org/basic_packages.cf',
'example_org/configure_dns.cf',
'example_org/configure_firewall.cf',
'example_org/configure_ftp.cf',
'example_org/configure_ssh.cf',
'example_org/cve_2015_0235.cf',
'example_org/lib.cf',
};
'bundles' slist => {
'example_org',
'example_org_main',
};
classes:
'ftp_server' or => {classmatch('BUG6870_ftp.*')};
'dns_server' expression => classmatch('BUG6870_dns.*');
reports:
verbose_mode::
'${this.bundle}: defining inputs="${inputs}"';
'${this.bundle}: defining bundles="${bundles}"';
ftp_server::
'This host assumes FTP server role';
dns_server::
'This host assumes DNS server role';
}
bundle agent example_org_main {
methods:
any::
'example_org_update_motd' usebundle => 'update_motd';
'example_org_basic_packages' usebundle => 'basic_packages';
'example_org_add_default_users' usebundle => 'add_default_users';
'example_org_configure_firewall' usebundle => 'configure_firewall';
'example_org_configure_ssh' usebundle => 'configure_ssh';
'example_org_cve_2015_0235' usebundle => 'cve_2015_0235';
any.Min30_35::
'heartbeat' usebundle => 'heartbeat';
# FTP servers configuration
ftp_server::
'example_org_configure_ftp' usebundle => 'configure_ftp';
# DNS servers configuration
dns_server::
'example_org_configure_dns' usebundle => 'configure_dns';
}
Этот пример был составлен из нескольких настоящих проектов для демонстрации, в реальности, конечно же, всё немного сложнее и больше. Цель такого подхода — минимизировать количество изменений в стандартной библиотеке, чтобы потом было проще поддерживать изменения в ней.
Отладка и тестирование
Поскольку цена ошибки велика, прежде, чем нажать на «большую красную кнопку», стоит протестировать свои промисы. Для тестов у меня есть небольшой полигон: несколько виртуальных машин, которые я запускаю на своём рабочем компьютере. На них я проверяю правильность выполнения промисов и занимаюсь экспериментами.
В разработке промисов я придерживаюсь стиля школы отладки через printf, то есть пользуюсь промисами типа report
очень широко. Ещё один незаменимый инструмент — это комплектная утилита cf-promises
. Кроме формальной валидации синтаксиса, она умеет показывать все доступные во время исполнения классы (параметр --show-classes
) и переменные с их содержимым (параметр --show-vars
). Ну и конечно же, запуск cf-agent
в подробном режиме (параметр --verbose
).
Обновления
Обновить версию CFEngine на всех подконтрольных машинах можно двумя способами: либо через пакетный менеджер, либо используя механизмы самого CFEngine. Я предпочитаю пакетный менеджер, для чего у меня есть специальный промис, который я подключаю только на время обновлений, суть его сводится к вызову package_latest
. На мой взгляд такой подход лучше всего соответствует концепциям CFE.
Большая Красная Кнопка
Выкатывание в прод — это всегда немного волнительный момент, даже если оно происходит по многу раз за день, и я не осуждаю людей, у которых для этого существует какой-то ритуал. В случае массовых изменений конфигурации, это может определить смысл жизни на следующие несколько суток, если что-то пойдёт не так. Поэтому никакой автоматики, никаких хуков для Git. Только ручной режим, как залог уверенности, что всё сделано правильно, оттестированно и готово для прода. У меня в качестве кнопки выступает скрипт deploy.sh
с правами 0600
, чтобы его никак нельзя было запустить случайно. Набирать руками в консоли bash bin/deploy.sh
— это мой ритуал и последняя возможность отменить запуск. Сам скрипт весьма тривиален: при помощи rsync
он синхронизирует masterfiles
, static
и templates
с содержимым /var/cfengine/{masterfiles,static,templates}
и запускает две команды: cf-agent -KIC -f update.cf
и cf-agent -KIC -f promises.cf
. Так я могу быть уверенным, что как минимум policy hub может выполнить промисы и раздать их всем клиентам.
Заключение
Это далеко не все тонкости и премудрости, но этого вполне достаточно, чтобы начать внедрение CFEngine у себя. За рамками статьи остались такие интересные темы как "Design Center", отчёты, внутреннее устройство, разные сценарии использования и многое другое. Если CFEngine представляет какой-то интерес для хабрасообщества, я с удовольствием расскажу о нём больше, а пока, если у вас есть какие-то острые вопросы прямо сейчас, не стесняйтесь задавать их в комментариях. Я и мой коллега по lastops cagliostro постараемся на них ответить.
Автор: dsx