Практика применения CFEngine в реальном мире

в 3:44, , рубрики: cfengine, linux, системное администрирование, системы управления конфигурациями, метки:

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js