Глава 7. Управление настройками при помощи Ansible

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

Почти для всякого предприятия практически гарантирована необходимость выполнения одной задачи в течении срока службы стандартной системы Linux - установка и настройка некой службы. Она может просто включать настройку уже имеющейся системной службы, либо, возможно, даже установку самой такой службы с последующими работами после установки.

В данной главе мы рассмотрим следующие вопросы более глубокого изучения управления настроийки через Ansible:

  • Установку нового программного обеспечения

  • Внесение изменений в конфигурацию при помощи Ansible

  • Управление настройкой в масштабввах некого предприятия

Технические требования

Данная глава содержит примеры, основывающиеся на следующих технологиях:

  • Сервер Ubuntu 18.04 LTS

  • CentOS 7.6

  • Ansible 2.8

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

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

Все обсуждаемые в этой книге примеры доступны в GitHub.

Установка нового программного обеспечения

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

Давайте начнём здесь свой обзор с самого простого варианта - установки собственного пакета операционной системы.

Установка пакета из репозиториев операционной системы

Допустим что вы раскручиваете некую новую службу, которой требуется сервер базы данных - к примеру, MariaDB. Маловероятно что вы будете обладать установленной и включённой MariaDB во всех своих образах SOE, а следовательно, самым первым этапом, прежде чем вы сделаете нечто ещё, будет установка надлежащего программного пакета.

Оба наших примера операционных систем из этой книги (а в действительности и многи прочие производные) содержат собственные пакеты для MariaDB, а потому мы можем запросто установить её с их помощью. Когда дело доходит до установки пакета, естественно, существует необходимость понимания того что будет происходить за сценой нашей целевой операционной системы. К примеру, в Ubuntu мы знаем, что обычно мы выполняем установку выбираемую при помощи диспетчера пакетов APT. Таким образом, когда мы пожелали выполнять установку вручную, включая соответствующего клиента для целей управления, мы бы вызвали такую команду:


# sudo apt install mariadb-server mariadb-client
		

Конечно же, в CentOS дела обстоят совершенно иначе - даже хотя пакеты под MariaDB и доступны, сама команда их установки вместо этого будет следующей:


# sudo yum install mariadb mariadb-server
		

Хотя Ansible и способен автоматизировать большинство дел в вашем корпоративном Linux, он не может абстрагироваться от некоторых фундаментальных отличий между различными операционными системами. Тем не менее, к счастью, Ansible делает всё это достаточно простым. Рассмотрим такую опись:


[servers]
ubuntu-testhost
centos-testhost
 	   

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

На основании своей работы с шаблонами в этой книге, рассмотрим такую роль:


---
- name: Install MariaDB Server on Ubuntu or Debian
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - mariadb-server
    - mariadb-client
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Install MariaDB Server on CentOS or RHEL
  yum:
    name: "{{ item }}"
    state: present
  loop:
    - mariadb-server
    - mariadb
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'
 	   

Эта искусно упакованная роль буде верно работать и в Ubuntu, и в CentOS (а в конечном счёте и в RHEL - Red Hat Enterprise Linux, и Debian, если потребуется) и учитывает как различные диспетчеры пакетов, так и отличия в наименованиях пакетов. Естественно, если вам посчастливилось обладать полностью унифицированной средой, которая полностью унифицирована (например, только на основании сервера Ubuntu), тода такой код может быть упрощён и далее.

[Совет]Совет

В Ansible имеется модуль с названием package, который предпринимает попытку определения верного диспетчера пакетов для его использования на основании той операционной системы, для которой выполняется данный плейбук. Хотя это и удаляет необходимость для отдельных задач на основании yum и apt, как те что мы только то обсуждали, вам всё ещё принимать во внимание различия в наименованиях пакетов между отличающимися операционными системами Linux, а потому вам всё ещё может требоваться выражение when.

Для вызова данной роли мы определим простой плейбук седующим образом:


---
- name: Install MariaDB
  hosts: all
  become: yes

  roles:
    - installmariadb
 	   

Теперь мы можем запустить этот плейбук и посмотреть что произойдёт:

 

Рисунок 7-1



Из полученного выше вывода мы можем увидеть что наши задачи не подходят к пропускаемым системам, в то время как успешная установка нужного нам пакета в результате приводит к значению состояния changed. К тому же, обратите внимание, что при установке в нашей тестовой системе CentOS пакета клиента MariaDB с названием mariadb соответствующая задача выдала состояние ok. Причиной этого является то, что наш определённый в role loop выполняет при их установке итерации по очереди для всех перечисленных пакетов; в CentOS пакет mariadb находится в зависимости от пакета mariadb-server, а потому он был установлен при выполнении этой конкретной задачи.

Хотя и указание его вручную и может показаться избыточным, оставление его в нашей роли не повредит, ибо оно гарантирует, что несмотря на то что произойдёт, этот пакет клиента имеется в наличии. К тому же это некий вид самостоятельного документирования - через несколько лет кто- то может вернуться опять к этому плейбуку и он поймёт, что нужны пакеты и клиента, и сервера MariaDB, даже если он и не осведомлён о данном нюансе операционной системы CentOS 7.

Прежде чем мы начнём полагаться на эту роль, обратите внимание на исключение пакета. Как мы уже обсуждали это ранее, задачи Ansible идемпотентны. В случае данного примера, когда мы запускаем свой плейбук повторно, мы обнаружим что всеми возвращаемыми нам результатами будет ok. В приводимом ниже случае Ansible определил, что все выбранные пакеты уже установлены и не предпринимает вторую попытку установки:

 

Рисунок 7-2



Тем не менее, что если вам требуется что- то привести в порядок? Допустим, по соображениям безопасности, представленный в средствах стандартного образа пакет устарел, или его требуется удалить по причинам безопасности. При этих обстоятельствах не будет достаточным просто удалить данные плейбук или роль. В то время как присутствие нашего образца роли обеспечивает установку пакетов, собственно удаление этой роли не является обратным действием для этого. Короче говоря, нам придётся вручную выполнять деинсталяцию или удалять изменения когда они больше не требуются. Для обратных действий к нашей установке потребуется некая роль, подобная следующей:


---
- name: Uninstall MariaDB Server on Ubuntu or Debian
  apt:
    name: "{{ item }}"
    state: absent
  loop:
    - mariadb-server
    - mariadb-client
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Uninstall MariaDB Server on CentOS or RHEL
  yum:
    name: "{{ item }}"
    state: absent
  loop:
    - mariadb-server
    - mariadb
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'
 	   

Обратите внимание на почти идентичную сущность данной роли, за исключением того, что теперь мы применяем state: absent вместо state: present. Это общее место для большинства задач Ansible, которые вы можете выполнять - когда вы желаете определить некую процедуру для отката или иного обращения какого- то изменения, вам потребуется записать его отдельно. Теперь, когда мы запускаем предыдущую роль, вызывая её из подходящего плейбука, мы можем обнаружить что указанные пакеты чисто деинсталлируются, как это отображается на приводимом далее снимке экрана:

 

Рисунок 7-3



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

В своём следующем разделе мы рассмотрим как обрабатывать это в соответствии с теми принципами автоматизации, которые мы установили ранее.

Установка не натуральных пакетов

К счастью, установка не собственных пакетов при использовании Ansible не намного сложнее, чем для установки натуральных. В идеале, в некой среде предприятия все необходимые пакеты будут обслуживаться из собственного внутреннего репозитория, и мы на самом деле рассмотрим его позднее в своей книге. В таком случае, в сочетании с ролями Ansible, подобным тем, что мы обсуждали в своём предыдущем разделе, будет применяться соответствующий репозиторий предприятия.

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

Допустим, вы выполняете для своего предприятия оценку программного обеспечения резервного копирования Duplicati и вам требуется устанавливать самую последнюю бета- версию для осуществления кое- каких проверок. Очевидно, что вы имеете возможность её выгрузки вручную с собственной страницы редакций, скопировать её в свой целевой сервер и установить её вручную. Тем не менее, это не эффективный и уж точно не пригодный для повтора процесс. К счастью, сами применявшиеся нами ранее модули apt и yum поддерживают установку пакетов как из локального пути, так и с удалённого URL.

Итак, для проверки установки бета- версии 2.0.4.23 Duplicati вы можете воспользоваться некой ролью, подобной следующей:


---
- name: Install Duplicati beta on Ubuntu
  apt:
    deb: https://github.com/duplicati/duplicati/releases/download/v2.0.4.23-2.0.4.23_beta_2019-07-14/duplicati_2.0.4.23-1_all.deb
  when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'

- name: Install Duplicati beta on CentOS or RHEL
  yum:
    name: https://github.com/duplicati/duplicati/releases/download/v2.0.4.23-2.0.4.23_beta_2019-07-14/duplicati-2.0.4.23-2.0.4.23_beta_20190714.noarch.rpm
    state: present
  when: ansible_distribution == 'CentOS' or ansible_distribution == 'Red Hat Enterprise Linux'
 	   

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

 

Рисунок 7-4



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

Установка программного обеспечения без пакетов

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

К счастью, и здесь Ansible способен прийти вам на выручку - рассмотрим такую роль:


---
- name: download virtualmin install script
  get_url:
   url: http://software.virtualmin.com/gpl/scripts/install.sh
   dest: /root/install.sh
   mode: 0755

- name: virtualmin install (takes around 10 mins) you can see progress using: tail -f /root/virtualmin-install.log
  shell: /root/install.sh --force --hostname {{ inventory_hostname }} --minimal --yes
  args:
    chdir: /root
 	   

Здесь мы воспользовались модулем get_url Ansible для выгрузки необходимого сценария установки, а затем применили для его запуска модуль shell. Обратите также внимание, что в названия задач мы можем поместить полезные инструкции - хотя это и не заменит хорошей документации, это невероятно полезно, так как сообщает всем запускающим данный сценарий как проверять ход данной установки с помощью команды tail.

[Замечание]Замечание

Отметьте для себя, что использованный модуль shell требует при своём применении некого внимания - так как он не имеет возможности понимать запускалась ли данная задача оболочки ранее, он запускает эту команду при каждом исполнении своего плейбука. Таким образом, когда вы запустите приведённую ранее роль повторно, он попытается установить Virtualmin опять. При использовании своей задачи shell вам надлежит применять некое выражение when, чтобы обеспечить её выполнение только один раз определённым условием - возможно, в предыдущем примере, когда /usr/sbin/virtualmin (который устанавливается install.sh) отсутствует.

Данный метод можно расширить практически на любое программное обеспечение, которое вы только можете себе представить - вы даже можете выгружать некий tarball исходного кода, раскрывать его и собирать его код при помощи последовательностей вызовов модулей shell в Ansible. Конечно же, это не самый желательный случай, но мы обозначаем его здесь, чтобы показать что Ansible способен содействовать созданию вами повторяемых установок, даже когда у вас нет доступа к предварительно упакованному в формате RPM или DEB программному обеспечению.

Таким образом может устанавливаться практически любое программное обеспечение - в конце концов, сам процесс установки программного обеспечения состоит в выгрузке некого файла (или архива), размещения его в правильном месте и его настройке. По существу, именно этим и занимаются диспетчеры пакетов, такие как yum или apt позади сцены, причём такой вид деятельности способен обрабатывать и сам Ansible, что мы и продемонстрировали здесь. В своём следующем разделе мы изучим применение Ansible для внесения изменений в настройки систем, в которых мы собрали и/ или установили программное обеспечение.

Проведение изменений конфигураций через Ansible

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

Давайте подробнее рассмотрим некоторые основополагающие примеры из мириада изменений конфигураций, которые могут потребоваться.

Проведение небольших изменений конфигураций через Ansible

Когда наступает время внесения изменений настроек, зачастую вашим первым портом вызовов выступает модуль Ansible lineinfile и он способен обрабатывать множество мелких изменений, которые могут быть необходимыми. Рассмотрим пример развёртывания сервера MariaDB, который мы уже запускали ранее в этой главе. Несмотря на то что мы успешно установили все пакеты, они были установлены со своими настройками по умолчанию, а это скорее всего не подходит ни для каких ситуаций.

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

Для изменения данной настройки, самый первый момент, который нам следует установить, это гда располагается установленная по умолчанию конфигурация и как она выглядит. Исходя из этого мы определим задачу Ansible по переделке такой конфигурации.

В качестве примера давайте выберем сервер Ubuntu, его адрес привязки службы настраивается в файле /etc/mysql/mariadb.conf.d/50-server.cnf - значение установленной по умолчанию директивы выглядит следующим образом:


bind-address       = 127.0.0.1
 	   

Следовательно, для изменения этого мы можем воспользоваться простой ролью, подобной следующей:


---
- name: Reconfigure MariaDB Server to listen for external connections
  lineinfile:
    path: /etc/mysql/mariadb.conf.d/50-server.cnf
    regexp: '^bind-address\s+='
    line: 'bind-address = 0.0.0.0'
    insertafter: '^\[mysqld\]'
    state: present

- name: Restart MariaDB to pick up configuration changes
  service:
    name: mariadb
    state: restarted
 	   

Давайте разобъём задачу lineinfile на части и рассмотрим их более подробно:

  • path: Сообщает своему модулю какой файл настроек изменять.

  • regexp: Применяется для определения имеющейся изменяемой строки, когда она присутствует, чтобы у нас не было двух конфликтующих директив bind-address.

  • line: Значение строки для замены/ вставки в изменяемый файл настроек.

  • insertafter: Когда не найдено соответствие regexp (то есть данная строка не представлена в изменяемом файле), данная директива обеспечит вставку модулем lineinfile некой новой строки после имеющегося оператора [mysqld], тем самым обеспечивая его наличие в верной части данного файла.

  • state: Установка в значение present обеспечивает что данная строка присутствует в изменяемом файле, даже когда нет первоначального совпадения regexp - в такой ситуации в изменяемый файл добавляется некая строка в соответствии со значением line.

Следуя данным изменениям, мы знаем, что наш сервер MariaDB теперь не подхватит никакие изменения настроек пока мы его не перезапустим, а поэтому давайте сделаем именно это в самом конце данной роли. Теперь, если мы запустим её, мы сможем обнаружить, что они сразу вступили в действие, что отображается на следующем снимке экрана:

 

Рисунок 7-5



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

Поддержка целостности конфигурации

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

Регулярные выражения сами по себе не являются понятными всем и каждому и хороши лишь в той степени как они написаны. В своём предыдущем примере мы применяли приводимую далее строку для поиска директивы bind-address с целью её изменения. Само по себе регулярное выражение ^bind-address\s+= означает поиск строк в рассматриваемом файле которая обладает следующим:

  • Должна обладать литеральной строкой bind-address в самом начале своей строки (что обозначается символом ^)

  • Иметь один или более пробелов после означенной литеральной строки bind-address

  • Обладать знаком = после этих пробелов

Основная мысль, стоящая за этим регулярным выражением состоит в обеспечении того, что мы игнорируем комментарии, подобные следующему:


#bind-address = 0.0.0.0
 	   

Однако, MariaDB достаточно терпима к пробельным символам в своих файлах настроек, а то регулярное выражение, которое мы определили откажет в наличии совпадения со следующими превращениями данной строки, причём все они допустимы в равной степени:


bind-address=127.0.0.1
 bind-address = 127.0.0.1
 	   

В подобном случае, так как значение параметра regexp не находит совпадения, наша роль добавит какую- то новую строку в изменяемом файле настроек со значением директивы bind-address = 0.0.0.0. Поскольку MariaDB рассматривает предыдущие примеры как допустимые настройки, мы в конце концов получаем две директивы конфигурации в своём фале, что может давать вам неожиданные последствия. Различные пакеты программного обеспечения также будут обрабатывать это по- разному, добавляя путаницу. Существуют и прочие сложности, которые также следует принимать во внимание. Многие службы Linux обладают очень сложными настройками, которые часто дополнительно разбиты на множество файлов для упрощения управления ими. Та документация, которая поставляется с естественным для нашей проверочной системы Ubuntu пакетом сервера MariaDB постулирует следующее:


# The MariaDB/MySQL tools read configuration files in the following order:
# 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4. "~/.my.cnf" to set user-specific options.
 	   

Однако этот порядок настроек диктуется в файле /etc/mysql/mariadb.cnf, который в самом низу имеет директивы для включения перечисленных в строках 2 и 3 из предыдущего блока кода файлов. Вполне может оказаться, что некто (из лучших побуждений или ещё по какой- то причине), может просто пройтись и перезаписать /etc/mysql/mariadb.cnf более новой версией, которая удаляет операторы include для этих подкаталогов, а вместо этого включает в себя следующее:


[mysqld]
bind-address = 127.0.0.1
 	   

Поскольку наша роль пользуется lineinfile целиком не имея сведений об этом файле, она честно настроит соответствующий параметр из /etc/mysql/mariadb.conf.d/50-server.cnf, причём не разбираясь с тем, что на этот файл больше нет ссылок и опять- таки, получаемые результаты в этом сервере будут - в лучшем случае - непредсказуемыми.

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

В качестве альтернативы давайте возьмём - например - предлагавшиеся нами в Главе 5, Применение Ansible для создания шаблонов виртуальных машин развёртывания настройки демона SSH. В этом случае мы предложили некую простую роль (для справки снова отображаемую ниже), которая должна была запрещать регистрацию в SSH от имени root, что является одним из рекомендуемых параметров безопасности для устанавливаемого демона SSH:


---
- name: Disable root logins over SSH
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin no"
    state: present
 	   

Обратите внимание, что наше regexp, обладает теми же самыми упущениями, что и прочие роли, которые привносятся пробельными символами. Когда sshd обладает дублированными параметрами в своём файле настроек, он берёт самое первое правильное значение. Таким образом, если бы я знал что представленная в предыдущем блоке кода роль выполнялась для некой системы, всё что мне потребовалось бы сделать, это поместить данный строки в самом верху /etc/ssh/sshd_config:


# Override Ansible roles
  PermitRootLogin yes
 	   

Итак, наша роль Ansible честно будет выполнена для данного сервера выдаст сообщение что она успешно управилась с настройками имеющегося демона SSH, в то время как на практике мы переписали его и включили регистрацию от имени root.

Эти примеры показывают нам два момента. Прежде всего, при работе с регулярными выражениями будьте предельно внимательны. Чем тщательнее вы можете сработать, в особенности, когда дело касается пробельных символов. Очевидно, в некой идеальной ситуации, это бы не требовалось, однако неожиданные изменения, подобные приведённым, способны разрушать множество систем. Чтобы воспрепятствовать возможности нашего предыдущего примера с демоном SSH, мы можем испытать такое регулярное выражение:


^\s*PermitRootLogin\s+
 	   

Оно учтёт ноль или более пробелов перед ключевым словом PermitRootLogin, а затем примет во внимание один или более пробелов после, причём всё это при том что sshd обладает встроенным равнодушием к проблельным символам. Однако, регулярные выражения слишком буквальны, а ведь мы не учли ещё и табуляцию!

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

Управление конфигурацией в масштабах предприятия

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

Мы начнём с рассмотрения методов масштабирования для простых статичных изменений настроек (то есть одинаковых для всех серверов в среде) в своём следующем разделе.

Проведение масштабируемых изменений статических конфигураций

Крайне важно чтобы изменения вносимых нами изменений настроек управлялись версиями, причём повторяемыми и стабильными - поэтому давайте рассмотрим достигающий этой цели подход. Давайте начнём с простого примера с пересмотром наших настроек конфигурации демона SSH. На большинстве наших серверов они могут быть статическими, поскольку такие требования, как ограничения на регистрацию от имени root и запрета регистраций на основании паролей, скорее всего, могут применяются для повсеместного состояния целиком. Точно так же, сам демон SSH обычно настраивается через один центральный файл - /etc/ssh/sshd_config.

В неком сервере Ubuntu устанавливаемые по умолчанию настройки очень простые и состоят всего лишь из шести строк, когда мы удалим все лишние пробельные символы и комментарии. Давайте внесём некие изменения в этот файл , с тем чтобы отклонять удалённые регистрации root, запрете X11Forwarding и оставить включёнными лишь регистрацию на основании ключей, примерно так:


ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
PasswordAuthentication no
PermitRootLogin no
 	   

Мы сохраним этот файл внутри структуры своего каталога roles/ и развернём его с помощью следующих задач роли:


---
- name: Copy SSHd configuration to target host
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644

- name: Restart SSH daemon
  service:
    name: ssh
    state: restarted
 	   

Здесь мы пользуемся модулем Ansible copy для копирования созданного нами ранее и сохранённого внутри самой роли файла sshd_config в наш целевой хост и обеспечения что его обладатель и режим подходящие для самого демона SSH. Наконец, мы перезапустим этот демон SSH чтобы подхватить данные изменения (обратите внимание, что это название службы допустимо для сервера Ubuntu и может быть иным в прочих дистрибутивах Linux.) Таким образом, наша законченная структура каталога roles выглядит следующим образом:


roles/
└── securesshd
    ├── files
    │   └── sshd_config
    └── tasks
        └── main.yml
 	   

Теперь мы можем запустить её для развёртывания данной конфигурации в нашем проверочном хосте следующим образом:

 

Рисунок 7-6



Сейчас, развёртывание настроек таким манером сулит нам ряд преимуществ над теми методами, которые мы исследовали ранее, перечислим их:

  • Такая роль сама по себе фиксируется системой контроля версий, тем самым неявным образом привнося файл сам настроек (в соответствующем каталоге files/ данной роли) под управлением версий.

  • Задачи нашей роли очень простые - кто угодно способен запросто подцепить этот код и разобраться в нём, причём без какой- бы то ни было необходимости расшифровки регулярных выражений.

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

  • Все машины обладают идентичными настройками, причём не только в отношении директив, но также и в плане упорядоченности и форматов, тем самым обеспечивая простой аудит настроек в рамках предприятия.

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

 

Рисунок 7-7



Из предыдущего снимка экрана мы можем увидеть, что Ansible определил, что наш файл настроек SSH не изменялся с момента последнего прогона, а следовательно возвращается значение состояния ok. Тем не менее, значение состояния changed для задачи Restart SSH daemon, в отличии от него, указывает на факт перезапуска данного демона SSH, даже несмотря на то, что никакие изменения не вносились. Перезапуск службы обычно носит разрушительный характер, а потому его следует избегать до тех пор, пока он не превращается в неизбежность. В таком случае, мы бы не желали перезапускать свой демон SSH пока в не выполнены какие- то изменения настроек.

Рекомендуемым для этого способом является применение handler (обработчика) Некий handler является конструкцией Ansible, которая во многом схожа с задачей, за исключением того, что он вызывается только при внесении изменений {возникновения события}. Кроме того, при выполнении множества изменений настроек, обработчик может получать уведомления множество раз (по одному на каждое принятое изменение), и всё же, механизм Ansible соединяет все вызовы обработчика в пакет и запускает такой обработчик лишь раз, только после завершения задач. Это обеспечивает то, что когда он применяется для перезапуска некой службы, как в случае рассматриваемого нами примера, такая служба перезапускается только однажды, причём лишь после внесения изменений. Давайте рассмотрим это следующим образом:

  1. Прежде всего, удалим из своей роли задачу перезапуска данной службы и добавим выражение notify для уведомления создаваемого обработчика (мы создадим его через мгновение). Получаемые в результате задачи должны выглядеть подобно следующему:

    
    ---
    - name: Copy SSHd configuration to target host
      copy:
        src: files/sshd_config
        dest: /etc/ssh/sshd_config
        owner: root
        group: root
        mode: 0644
      notify:
        - Restart SSH daemon
    		
  2. Теперь нам требуется в своей роли создать каталог handlers/ и добавить в неё свой ранее удалённй код обработчика, чтобы он выглядел так:

    
    ---
    - name: Restart SSH daemon
      service:
        name: ssh
        state: restarted
    		
  3. Получаемый в результате каталог roles должен выглядеть примерно так:

    
    roles/
    └── securesshd
        ├── files
        │   └── sshd_config
        ├── handlers
        │   └── main.yml
        └── tasks
            └── main.yml
    		
  4. Теперь, когда мы запустим свой плейбук дважды для одного и того же сервера (изначально вернув настройки SSH в их первоначальный вид), мы обнаружим, что наш демон SSH перезапускается лишь в той ситуации, когда мы на самом деле вносим в настройки изменения, что отображено на снимке экрана ниже:

     

    Рисунок 7-8



Для последующей демонстрации обработчиком, перед тем как динуться далее, давайте рассмотрим такое расширение в своих задачах роли:


---
- name: Copy SSHd configuration to target host
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify:
    - Restart SSH daemon

- name: Perform an additional modification
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^\# Configured by Ansible'
    line: '# Configured by Ansible on {{ inventory_hostname }}'
    insertbefore: BOF
    state: present
  notify:
    - Restart SSH daemon
		

Здесь мы развернули свой файл настроек и выполнили дополнительные изменения. Мы поместили комментарий в самом заголовке того файла, который содержит переменную Ansible с названием хоста для своего целевого хоста.

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

 

Рисунок 7-9



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

[Совет]Совет

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

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

Проведение масштабируемых изменений динамических конфигураций

Хотя наши предыдущие примеры и разрешили большинство задач по выполнению автоматических изменений настроек в масштабах предприятия, следует отметить, что наш окончательный образец был в какой- то степени не эффективен. Мы развёртывали некий статичный, управляемый версиями файл настроек и снова вносили изменения при помощи модуля lineinfile.

Это позволяло нам вставлять некую переменную Ansible в соответствующий файл, что во многих обстоятельствах чрезвычайно полезно, в особенности, когда настраиваются более сложные службы. Тем не менее, это - в лучшем случае - не элегантно расщеплять такие изменения на две задачи. Кроме того, повторный возврат к использованию модуля lineinfile снова вносит нам уже обсуждавшиеся ранее риски и означает что нам потребовалось бы по одной задаче lineinfile для каждой из переменных, которые мы пожелали бы вставлять в некую настройку.

К счастью, Ansible содержит и ответ на такую задачу. В данном случае нас спасает понятие шаблонов Jinja2.

Jinja2 является языком шаблонов Python, который чрезвычайно мощный и простой в применении. Поскольку Ansible почти целиком закодирован на Python, он сам по себе приводит к применению шаблонов Jinja2. Итак, чем является некий шаблон Jinja2? На своём самом основополагающем уровне, это некий статический файл настроек, подобный тому, который мы развёртывали ранее для своего демона SSH, но при этом с возможностью подстановок переменной. Естественно, Jinja2 намного мощнее этого - он, по существу, сам по себе реальный язык программирования, с распространёнными в языках программирования конструкциями, такими как циклы for и построениями if...elif...else, которые вы можете обнаружить и в прочих языках программирования. Это превращает его в необычайно мощное и гибкое средство, причём позволяет опускать целые разделы настроек (например), в зависимости от вычислений оператора if.

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

Давайте вернёмся на минутку обратно к своему примеру демона SSH, в котором мы пожелали поместить в комментарии в самом заголовке файла название целевого имени хоста. Хотя это до некоторой степени неестественный пример, его перевод от примера copy/lineinfile к отдельной задаче template продемонстрирует те преимущества, которые привносят предлагаемые шаблоны. Здесь мы можем продолжать с более сложным примером. Для начала давайте определим свой шаблон Jinja2 для соответствующего файла sshd_config следующим манером:


# Configured by Ansible {{ inventory_hostname }}
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
PasswordAuthentication no
PermitRootLogin no
 	   

Обратите внимание на то, что этот файл идентичен уже развёрнутому нами ранее при помощи модуля copy, за единственным исключением, что мы включили соответствующий комментарий заголовка данного файла и воспользовались соответствующей конструкцией переменной Ansible (выделяемой парой фигурных скобок) для вставки необходимой переменной inventory_hostname.

Теперь, из соображений здравого смысла, мы назовём данный файл sshd_config.j2 , чтобы быть уверенными что мы способны отличать шаблоны от простых файлов настроек. Шаблоны обычно помещаются в каталог template/ внутри соответствующей роли, а тем самым являются предметом контроля версий тем же самым манером, что и плейбуки, роли и все связанные с ними обычные файлы конфигурации.

Тепер, вместо того чтобы копировать обычный файл и затем выполнять подстановки при помощи одной или более задач lineinfile, мы можем воспользоваться соответствующим модулем template Ansible для развёртывания данного шаблона и синтаксического разбора всех конструкций Jinja2.

Таким образом, наши задачи теперь выглядят так:


---
- name: Copy SSHd configuration to target host
  template:
    src: templates/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify:
    - Restart SSH daemon
 	   

Обратите внимание на то, что эти задачи почти идентичны нашим более ранним задачам copy, а также на то что мы вызываем свой обработчик, как и ранее.

Завершённая структура каталога модуля выглядит подобно следующей:


roles
└── securesshd
    ├── handlers
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    └── templates
        └── sshd_config.j2
 	   

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

 

Рисунок 7-10



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

Это становится невероятно мощным по мере усложнения наших настроек, так как вне зависимости от того насколько велик и сложен данный шаблон, самой роли требуется лишь единственная задача template. Возвращаясь к нашему серверу MariaDB, представим себе что мы желает установить ряд параметров для каждого сервера чтобы воспользоваться соответствующими регулировками для различных развёртываемых нами рабочих нагрузок. Возможно, мы пожелаем настроить следующее:

  • Значение привязки сервера, определяемое в bind-address

  • Максимальный размер двоичного журнала, определяемого в max_binlog_size

  • Значение порта TCP, по которому осуществляет ожидание MariaDB, задаваемое в port

Все эти параметры определяются в /etc/mysql/mariadb.conf.d/50-server.cnf. Однако, как уже обсуждалось ранее, нам требуется обеспечить целостность /etc/mysql/mariadb.cnf для того чтобы гарантировать включение этого (и прочих) файлов там, с целью снижения возможностей того что некто перепишет нашу конфигурацию. Давайте начнём сборку своих шаблонов - прежде всего упростим версию своего файла 50-server.cnf при помощи некоторых подстановок переменных. Самая первая часть данного файла приводится в идущем следом коде - обратите внимание на параметры port и bind-address, которые теперь определяются переменными Ansible, выделяемые обычным способом парой фигурных скобок:


[server]
[mysqld]
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = {{ mariadb_port }}
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
bind-address = {{ mariadb_bind_address }}
 	   

Вторая часть данного кода выглядит приводимым ниже образом - вы можете наблюдать здесь присутствие переменной mariadb_max_binlog_size, в то время как все прочие параметры остаются статическими:


key_buffer_size = 16M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 8
myisam_recover_options = BACKUP
query_cache_limit = 1M
query_cache_size = 16M
log_error = /var/log/mysql/error.log
expire_logs_days = 10
max_binlog_size = {{ mariadb_max_binlog_size }}
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
[embedded]
[mariadb]
[mariadb-10.1]
 	   

Теперь мы также добавляем следующим образом некую шаблонную версию /etc/mysql/mariadb.cnf:


[client-server]
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/
 	   

Это файл выглядит кратким, но он служит действительно важной цели. Именно этот файл служба MariaDB считывает самым первым при своей загрузке и именно он приводит ссылки на все прочие файлы или каталоги, которые подлежат включению. Если мы не обеспечим контроль над этим файлом при помощи Ansible, тогда все кто обладают достаточными полномочиями смогут зарегистрироваться и полностью обойти наши определяемые Ansible настройки. Всякий раз когда вы развёртываете конфигурацию при помощи Ansible, важно рассматривать такие моменты как этот, ибо в противном случае имеется возможность обхода вашим настроек конфигурации администраторами из лучших побуждений (или по каким- то ещё причинам).

[Совет]Совет

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

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


---
- name: Copy MariaDB configuration files to host
  template:
    src: {{ item.src }}
    dest: {{ item.dest }}
    owner: root
    group: root
    mode: 0644
  loop:
    - { src: 'templates/mariadb.cnf.j2', dest: '/etc/mysql/mariadb.cnf' }
    - { src: 'templates/50-server.cnf.j2', dest: '/etc/mysql/mariadb.conf.d/50-server.cnf' }
  notify:
    - Restart MariaDB Server
 	   

Наконец, мы определяем некий обработчик для перезапуска MariaDB при наличии изменений в настройках следующим образом:


---
- name: Restart MariaDB Server
  service:
    name: mariadb
    state: restarted
 	   

Теперь, прежде чем запустить всё это, несколько слов о переменных. В Ansible переменные могут задаваться на большом числе уровней. В подобных данному случае, когда мы применяем различные настройки для различных хостов с разными целями, существенно определять такие переменные на уровне самого хоста или группы хостов. Тем не менее, что если так произойдёт, что некто забыл поместить их в соответствующей описи, или в ином подходящем месте? К счастью, мы можем здесь воспользоваться для своих целей установленным порядком первенства переменных Ansible и задать для своей роли устанавливаемые по умолчанию значения переменных. Это второй из самых нижних уровней старшинства, а потому он почти всегда перекрывается прочими имеющимися где- то ещё настройками, но в то же самое он обеспечивает некую защитную сеть на тот случай, если они случайно пропущены. Что касается написанных нами ранее шаблонов, когда эти переменные нигде не определены, соответствующий файл настроек будет недопустимым и его серверу MariaDB будет отказано в запуске - такой случай желательно избегать.

Давайте теперь определим значения по умолчанию для таких переменных в своей роли в defaults/main.yml таким образом:


---
mariadb_bind_address: "127.0.0.1"
mariadb_port: "3306"
mariadb_max_binlog_size: "100M"
 	   

По завершению этого структура нашей роли выглядит подобно следующему:


roles/
└── configuremariadb
    ├── defaults
    │   └── main.yml
    ├── handlers
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    └── templates
        ├── 50-server.conf.j2
        └── mariadb.cnf.j2
 	   

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


[dbservers]
ubuntu-testhost

[dbservers:vars]
mariadb_port=3307
mariadb_bind_address=0.0.0.0
mariadb_max_binlog_size=250M
 	   

Покончив с этим, мы, наконец, можем запустить свой плейбук и посмотреть что произойдёт. Результаты приводятся на снимке экрана ниже:

 

Рисунок 7-11



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

Выводы

Управление настройками по всему штату корпоративного Linux заполнено западнями и потенциалом для сноса настроек. Они могут вызываться персоналом из лучших побуждений, даже в случаях исправления поломок, когда исправления приходится вносить наспех. Тем не менее, они также могут вноситься и из вредоносных побуждений в стремлении обойти требования безопасности. Достойное применение Ansible, в особенности с применением шаблонов, позволяет строить простые для чтения, краткие плейбуки, которые позволяют легко обеспечивать надёжное, воспроизводимое, подверженное аудиту и с контролем версий управление конфигурациями - все те основные принципы, которые мы излагали ранее в этой книге для достойной практики автоматизации предприятия.

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

Вопросы

  1. Какие различные модули Ansible обычно применяются для внесения изменений в файлы настроек?

  2. Как работают шаблоны в Ansible?

  3. Зачем вам следует рассматривать структуру файла настроек при внесении изменений с применением Ansible?

  4. В чём состоят засады использования регулярных выражений при внесении изменений в файлы?

  5. Как ведут себя шаблоны, когда в них нет переменных?

  6. Как вы можете убедиться в допустимости развёртываемого вами шаблона настроек до его фиксации на диске?

  7. Как можно быстро осуществить аудит настроек 100 машин с известным шаблоном при помощи Ansible?

Последующее чтение

Для более глубокого понимания Ansible, будьте добры ознакомиться с Mastering Ansible, Third Edition — James Freeman и Jesse Keating {Прим. пер.: рекомендуем также свой перевод этого 3 издания Полного руководства Ansible Джеймса Фримана и Джесса Китинга}.