Привет всем,

Сегодня хотел бы поделится вводной информацией, относительно того как начать использоваться puppet.

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

Как работает puppet

Puppet, в отличии от ansible, работает по pull модели. Хосты на которых устанавливается puppet-агент, каждые пол часа обмениваются информацией с puppet-мастером для получения новой конфигурации. При этом клиенты puppet отправляют факты о себе мастеру. И в случаи наличия изменений применяют их у себя локально.

puppet-arch-schemes.jpg

Взаимодействие между клиентом/сервером осуществляется за счет создания SSL-соединения, на puppet-master поднимается https порт - 8140/TCP, который ожидает входящий трафик.

Но прежде чем начать обменится данными, puppet-агент установленный на хосте, должен быть правильно идентифицирован на главном сервере puppet (puppet-master). Для это каждый клиент генерит свой клиентcкий сертификат, в который включается имя хоста и отправляет его на puppet-сервер.

Администратор сервера просматривает этот сертификат и видит имя клиентского хоста, затем принимает решение о дальнейшем взаимодействии с ним. Обычно мы просто подписываем клиентский сертификат, и затем подписаный клиентский сертификат отправляется обратно на хост. puppet-setup-ssl.jpg

Теперь клиент/сервер готовы к взаимодействию. И обе стороны могут поднять SSL сессию.

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

Если же клиентский сертификат был ранее подписан puppet-сервером, то устанавливается SSL сессия с клиентом. Иначе полученный клиентский сертификат нужно будет подписать на стороне puppet-сервера. puppet-setup-ssl2.jpg

Сеанс связи между сервером/клиентом установлен. Агент запущенный на клиентских хостах собирает все факты о системе (установленная операционка, ip-адресс, и прочее) и отправляет их на puppet-server. puppet-setup-facts.png

Полученные факты с хостов сравниваются с манифестами на puppet-сервер. Делается это для того, что бы проверить соблюдается ли текущее состояние клиентского хоста с описанием манифеста. puppet-setup-facts2.png

В случаи же, если то состояние что описано в манифесте не соблюдено на хосте, компилируется новый пакет и этот пакет отправляется на клиенсткий хост. Полученный пакет выполняется на клиенте, для достижения нужного состояния. puppet-setup-facts3.png

И после применений, каждым хостом генерируется некий репорт(отчет), который отправляется мастеру.

Настройка/Работа с puppet

И так, мы ранее разобрались с тем как все работает, и много раз было упомянуто относительно состояния системы и манифестов.

Конфигурация нод (или состояние) в puppet описывается в манифестах на декларативном языке DSL.

  • Манифест (Manifest) это сушность объединяющая все ресурсы и классы, как правило заканчивается с расширением на .pp. Проводя параллель с Ansible, манифесты puppet можно сравнить с playbook в Ansible.

  • Ресурсы (Resources) это минимальный объект описывающий то состояние, которое мы хотим увидеть в конечном итоге. В экосистеме Ansible, task схож с ресурсом в puppet.

  • Классы (Class) - сушность внутри которой описывается несколько ресурсов, для упорядочивания и объединения в один объект. Классы также позволяют избежать повторного переиспользования написанного кода.

  • Модуль (Module) - это дерево каталогов содержащее манифесты, конфигурационные файлы, шаблоны. Модуль используется для реализации одной задачи, например для установки и настройки одного ПО. В мире Ansible, модуль можно сравнить с ролью.

Перед тем как начать знакомится более подробней, развернем небольшую инфраструктуру из трех серверов на Almalinux 8. Один сервер будет выступать в качестве puppet-сервера, а остальные две ноды будут подчиненными. puppet-setup-infra.png

Установка puppet-сервера

Для установки пакета puppet, сначала добавляем репозиторий:

[root@puppet-master ~]# yum install -y https://yum.puppetlabs.com/puppet8-release-el-8.noarch.rpm

Как можете заменить используема нами версия - 8.

Ну и теперь просто инсталим пакет:

[root@puppet-master ~]# yum install -y puppetserver

Puppet работает на java, окружение java ставить дополнительно не требуется. В качестве зависимости пакет openjdk c 11 версией java поставится автоматом.

По умолчанию puppetserver резервирует 2gb памяти, в зависимости от общего числа памяти на сервере можем отдать меньшее колличество. Например у меня на виртуальной машине общий объем памяти 4gb, под нужны puppetserver отдам 1gb.

[root@puppet-master ~]# vi /etc/sysconfig/puppetserver
---
# Modify this if you'd like to change the memory allocation, enable JMX, etc
JAVA_ARGS="-Xms1g -Xmx1g -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger"

Меняются значения в параметрах - -Xms1g -Xmx1g.

В файле глобальной конфигурации puppetserver нужно определить настройки мастер-сервера:

[root@puppet-master ~]# vim /etc/puppetlabs/puppet/puppet.conf
---
[master]
dns_alt_names = puppet-master,puppet-master.nixhub.ru

[main]
certname = puppet-master.nixhub.ru
server = puppet-master.nixhub.ru
runinterval = 30m

Самая базовая настройка конфигурации это указание dns-имен сервера. Без этого не сгенерится рутовый/промежуточный сертификат.

Ну и в файл /etc/hosts, добавляем запись:

[root@puppet-master ~]# vim /etc/hosts
---
# Pupper server
127.0.0.1       puppet-master.nixhub.ru         puppet-master

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

В 8 версии, рабочий каталог и бинарные файлы располагаются в /opt/puppetlabs/puppet/. Для удобства взаимодествия с утилитами, можно добавить этот путить к переменной окружения $PATH. Ну или же прост осоздать симлинк на нужные утилиты:

[root@puppet-master ~]# ln -s /opt/puppetlabs/bin/puppetserver /usr/sbin/puppetserver

Ну и теперь настроим CA puppet-сервера:

[root@puppet-master ~]# puppetserver ca setup
Generation succeeded. Find your files in /etc/puppetlabs/puppetserver/ca

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

[root@puppet-master ~]# firewall-cmd --add-port=8140/tcp --permanent
[root@puppet-master ~]# firewall-cmd --reload

Ну и запускаем сервис:

[root@puppet-master ~]# systemctl enable --now puppetserver

Настройка puppet-agent

Настройка агентов, такая тривиальная задача. Добавляем репозиторий puppet:

[root@puppet-hode01 ~]# yum install -y https://yum.puppetlabs.com/puppet8-release-el-8.noarch.rpm

Ставим агента:

[root@puppet-hode01 ~]# yum install -y puppet-agent

В конфигурационном файле агента, нужно прописать адрес puppet-сервера и имя сертификата нашего хоста (клиента):

[root@puppet-hode01 ~]# vi /etc/puppetlabs/puppet/puppet.conf
---
[main]
server = puppet-master.nixhub.ru
certname = puppet-node01.nixhub.ru
runinterval = 30

Опять же, не забываем в /etc/hosts добавить запись с адресом нашего сервера:

[root@puppet-hode01 ~]# vi /etc/hosts
---
# Puppet server
10.8.5.21       puppet-master.nixhub.ru

Затем запускаем агента,

[root@puppet-hode01 ~]# /opt/puppetlabs/bin/puppet resource service puppet ensure=running enable=true
Notice: /Service[puppet]/ensure: ensure changed 'stopped' to 'running'
service { 'puppet':
  ensure   => 'running',
  enable   => 'true',
  provider => 'systemd',
}

Подпись клиентского сертификата

Как упоминалось ранее, для идентификации удаленных клиентов используется модель подписи сертификатов. При первом запуске агента, клиент отправит свой сертификат на сервер.

Для его просмотра воспользуемся командой:

[root@puppet-master ~]# puppetserver ca list
Requested Certificates:
    puppet-node01.nixhub.ru       (SHA256)  FF:9F:96:FF:F7:6E:22:62:5B:00:CE:26:AF:44:9B:7E:8C:2E:5B:9A:EA:39:69:50:A4:15:B0:F8:B5:6C:4A:7D
    puppet-node02.nixhub.ru       (SHA256)  FA:52:AF:2E:07:46:03:26:8E:48:56:86:A9:8C:35:61:9C:79:0E:7F:48:81:4A:76:3C:B4:4D:F4:35:CF:DF:71

Как видно из вывода команды, у нас есть два запроса на подпись сертификата.

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

# Для подписи определенного запроса:
[root@puppet-master ~]# puppetserver ca sign --certname puppet-node01.nixhub.ru
Successfully signed certificate request for puppet-node01.nixhub.ru

# Для подписи всех запросов:
[root@puppet-master ~]# puppetserver ca sign --all
Successfully signed certificate request for puppet-node02.nixhub.ru

Структура каталогов сервера

Прежде чем приступить к созданию своего манифеста, рассмотрим структуру каталогов puppet-сервера.

Ключевые каталоги указываются в файле - /etc/puppetlabs/puppet/puppet.conf, по желанию их можно переопределить. Основной каталог, с которым чаще всего придется работать это - /etc/puppetlabs/code. В его состав включено два подкаталога environments и modules.

  • environments - содержит наши рабочие окружения, быть может в нашей инфраструктуре есть несколько сред, например - production, dev, test. И можем разделить выполнение задач по тем или иным окружениям.
  • modules - в данном каталоге как правило располагаются общие модули, доступные для импорта во все среды. Также в этот каталог добавляются модули скачанные из внешней библиотеки.

На данном этапе нам интересен раздел со средами. По умолчанию в puppet, уже имеется каталог с production средой. Мы можем использовать его как эталонный для создания своих сред (Агенты по умолчанию, стягивают каталоги с этого окружения).

/etc/puppetlabs/code/environments/
└── production
    ├── data
    ├── environment.conf
    ├── hiera.yaml
    ├── manifests
    └── modules

Внутри каталога среды имеется следующие подкаталоги:

  • data - содержит данные относящиеся к модулю hiera (переменные и прочее, об этом позже);
  • manifest - как уже понятно из названия, в данный каталог помещают различные манифесты. Обычно туда кладут файл site.pp, который как бы является entry-точкой входа в манифест;
  • modules - в содержимое данного каталога кладут различные модули, как написанные самостоятельно так и скачанные из сети. Затем эти модули импортирются(инклюдятся) в главный манифест site.pp, как например.
  • environment.conf - в данному файле определяются настройки для нашей среды.
  • hiera.yaml - а этот файл служит для настройки доп.компонента hiera. Обычно он нужен для выстраивания иерархии хостов, для их обработки.

Пишем первый манифест

После всей лирики, давайте перейдем к практике и выполним так называемый - Hello World. Пропишем ресурс file, который создаст файл с содержащий информацию по нашим серверам.

Итак, тут все просто открываем файл site.pp, в каталоге /etc/puppetlabs/code/environments/manifests/ и указываем:

# Для наглядности мое текущее расположение:
[root@puppet-master production]# pwd
/etc/puppetlabs/code/environments/production

# Пишем site.pp в manifests:
[root@puppet-master production]# vim manifests/site.pp
---
node default {
  file { '/tmp/server_info.txt':
    ensure 	=> present,
    content 	=> "${facts['networking']['fqdn']}, (${facts['networking']['ip']})\n",
    owner	=> "root",
    group	=> "root",
  }
}

Манифест начинается с контекста node default { ... }, который нам говорит на каких нодах мы хотим применить данные изменения. Слово default в ключевом значении node, в данном случаи указывает применения инструкций на всех нодах, известных puppet-сервер. Эта конструкция в терминалогии кода puppet называется - Node definitions. И по мимо значения default, мы можем указывать имя сертификата отдельного хоста. Также можем использовать регулярные выражения в контексте определения имен узлов.

Внутри Node definitions, идет контекст ресурса(resource) - file { ... }. Как уже упоминалось ранее, каждый ресурс описывает желаемое состояние, которое мы хотим видеть. В нашем же случаи берется тип ресурса - file, в его заголовке(title) идет уникальное значение - /tmp/server_info.txt. Заголовки, как раз таки и нужны для уникальной идентификации ресурса паппетом. Затем следуют атрибуты ресурса, как раз таки в атрибутах и описывается желаемое нами состояние. В моем примере:

  • ensure => present - данный атрибут, говорит нам что файл будет гарантированно создан;
  • content => "..." - указывает желаемое содержимое в виде одной строки. В нашем же случаи строка собирается из фактов, которые в свою очередь добавляют имя fqdn-хоста и ip-адресс этого же хоста.
  • owner/group - аттрибуты определяющие владельца и группу создаваемого файла.

Чтобы не ждать пока изменения будут применены, применим конфигурацию с клиентского хоста. Как я говорил ранее, по дефолту интервал обновления 30 минут.

[root@puppet-hode01 ~]# /opt/puppetlabs/bin/puppet agent -t
Info: Using environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for puppet-node01.nixhub.ru
Info: Applying configuration version '1693830717'
Notice: /Stage[main]/Main/Node[default]/File[/tmp/server_info.txt]/ensure: defined content as '{sha256}1d2de8d01c94900ce5176de145719a258b87851ee6fa4d46371d0dcab9d34edc'
Notice: Applied catalog in 0.02 seconds

Загляним во внутрь файла /tmp/server_info.txt,

[root@puppet-hode01 ~]# cat /tmp/server_info.txt
puppet-hode01.office.localdomain, (10.8.5.22)

Отлично файл с нужным контентом у нас отобразился.

Пишем первый модуль

Представим, что к нам пришла задача, в которой требуется поставить zabbix-агента на десяток серверов. И для решения этой задачи мы напишем соответствующий модуль.

Я проваливаюсь во внутрь каталога с окружением:

[root@puppet-master ~]# cd /etc/puppetlabs/code/environments/production/

Затем внутри окружения создаю новый модуль:

[root@puppet-master production]# mkdir -p modules/setup_zbx_agent/{manifests,templates,files}

Рекурсивно будет создан модуль с названием setup_zbx_agent, внутри каталога модуля будут созданы каталоги:

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

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

Начнем процесс с добавления статического файла в каталог - files.

[root@puppet-master ~]# cd /etc/puppetlabs/code/environments/production/
[root@puppet-master production]# vim modules/setup_zbx_agent/files/zabbix.repo
---
[zabbix]
name=Zabbix Official Repository - $basearch
baseurl=https://repo.zabbix.com/zabbix/6.0/rhel/8/$basearch/
enabled=1
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX-A14FE591

Здесь создаем файл репозитория zabbix, данные для репозитория нашел на официальном сайта.

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

В каталоге templates, создаем:

[root@puppet-master production]# vim modules/setup_zbx_agent/templates/zabbix_agentd.erb
---
# --
#  Managment via Puppet
# --

# Pid location
PidFile=/var/run/zabbix/zabbix_agentd.pid

# Log's patametres
LogFile=/var/log/zabbix/zabbix_agentd.log
LogFileSize=0

# Zabbix-server hostname/ip
Server=<%= @zbx_server  %>
ServerActive=<%= @zbx_server  %>

# Zabbix-agent hostname
Hostname=<%= @zbx_host %>

# Includes extentions:
Include=/etc/zabbix/zabbix_agentd.d/*.conf

В этом шаблоне используется всего две переменной - zbx_server, zbx_host. В первой переменной будет подставляться адрес zabbix сервера, во второй будет fqdn/хостнейм хоста на котором будет инсталирован заббикс агент.

Данные подставляются в шаблон за счет открывающего/закрывающего тега - <%=, %>. Доступ к переменной в шаблоне осуществляется через символ @, например - @zbx_server.

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

Начнем с описания первого класса:

[root@puppet-master production]# vim modules/setup_zbx_agent/manifests/install.pp
---
class setup_zbx_agent::install (
  $zbx_package          = 'zabbix-agent',
  $zbx_listen_port      = 10050,
){
  file { '/etc/yum.repos.d/zabbix.repo':
    ensure      => 'present',
    source      => "puppet:///modules/setup_zbx_agent/zabbix.repo",
  }

  package { "$zbx_package":
    ensure      => 'present',
    require     => File['/etc/yum.repos.d/zabbix.repo'],
  }

  exec { 'firewall':
    path        => '/bin/',
    command     => "firewall-cmd --add-port=${zbx_listen_port}/tcp --permanent && firewall-cmd --reload",
    onlyif      => "firewall-cmd --list-all | grep ${zbx_listen_port} >/dev/null && echo False || echo True",
    require     => Package["$zbx_package"],
  }
}

Здесь мы объявляем класс, имя класса состоит из названия модуля (setup_zbx_agent) + именем манифеста (install.pp). Внутри класса (круглые скобки) объявляются перемененные, которые будут использоваться по умолчанию. У нас две переменные:

  • zbx_package - с именем пакета zabbix-агента;
  • zbx_listen_port - порт, на котором будет запускаться заббикс агент.

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

  • file - копирует файл репозитория на сервер. В имя ресурса указываем путь расположения файла. А в атрибутах указываем желаемое состояние (ensure), и источник (require) откуда файл будет заполучен.
  • package - через этот ресурс ставить пакет заббикс агента. В имя ресурса передаем значение из переменной $zbx_package, в атрибутах также указывается состояние. И новый для нас атрибут - require, который создает зависимость от другого ресурса. То есть в нашем случаи данный ресурс выполнится, после ресурса - file выше.
  • exec - в нашем примере через этот ресурс мы добавляем правило в firewalld. На самом деле для firewalld в puppet есть готовый модуль, которой можно заюзать. Но для примера, так скажем на коленке решил использовать exec. Из атрибутов:
    • path - указываем исполняемый каталог,
    • command - атрибут содержащий команду для исполнения. У нас команда на перманентное добавление порта. Сам порт подставляется из значения переменной zbx_listen_port.
    • onlyif - данный отрибут выполняет команду перед выполнением основной команды. И если в результате его выполнения возвращается результат True, то будет выполнена основная команда. В нашем примере проверяется добавлен ли нужный порт.
    • require - также добавляется зависимость, но теперь только от ресурса package.

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

[root@puppet-master production]# vim modules/setup_zbx_agent/manifests/init.pp
---
class setup_zbx_agent (
  $zbx_server   = 'localhost',
  $zbx_host     = "${facts['networking']['fqdn']}",
){
  class {'setup_zbx_agent::install':}

  file { '/etc/zabbix/zabbix_agentd.conf':
    content     => template('setup_zbx_agent/zabbix_agentd.erb'),
    owner       => 'zabbix',
    group       => 'zabbix',
  }

  service { 'zabbix-agent':
    ensure      => 'running',
    subscribe   => File['/etc/zabbix/zabbix_agentd.conf']
  }
}

Объявляем новый класс, как правило первый класс в манифесте init.pp, принято называть именем модуля.

В этой классе объявляются переменные по умолчанию - zbx_server, zbx_host. В значения переменных подставляется адрес zabbix-сервера, и fqdn-имя клиентского хоста. В контексте выполнения модуля мы добавляем инклюд, подключаем класс который устанавливает агента. Ну а затем следует знакомые нам ресурсы.

  • file - из ранее созданного шаблона, создает конфигу zabbix-агента - /etc/zabbix/zabbix_agentd.conf.
  • service - данный ресурс, запускает сервис агента. У сервиса есть атрибут subscribe, который запускает текущий ресурс при каждом изменении целевого ресурса. В нашем случаи целевым является ресурс - file. Это означает, что при изменениях в шаблоне, каждый раз будет использоваться ресурс service.

Вобщем то на этом все, модуль готов. Остается только включить его в корневой манифест site.pp:

node default {
  class { 'setup_zbx_agent':
    zbx_server  => "10.8.10.7",
  }
}

Здесь мы для всех агентов, инклюдим класс setup_zbx_agent, в аргументах к классу переопределяем переменную - zbx_server.

Проверяем через апдейт на агенте:

[root@puppet-hode01 ~]# /opt/puppetlabs/bin/puppet agent -t

Ну и смотрим появился ли агент на сервере:

[root@puppet-hode01 ~]# systemctl status zabbix-agent
● zabbix-agent.service - Zabbix Agent
   Loaded: loaded (/usr/lib/systemd/system/zabbix-agent.service; disabled; vendor preset: disabled)
   Active: active (running) since Wed 2023-09-06 16:40:11 +06; 6s ago
  Process: 701651 ExecStart=/usr/sbin/zabbix_agentd -c $CONFFILE (code=exited, status=0/SUCCESS)
 Main PID: 701653 (zabbix_agentd)
    Tasks: 6 (limit: 11148)
   Memory: 3.7M
   CGroup: /system.slice/zabbix-agent.service
           ├─701653 /usr/sbin/zabbix_agentd -c /etc/zabbix/zabbix_agentd.conf
           ├─701654 /usr/sbin/zabbix_agentd: collector [idle 1 sec]
           ├─701655 /usr/sbin/zabbix_agentd: listener #1 [waiting for connection]
           ├─701656 /usr/sbin/zabbix_agentd: listener #2 [waiting for connection]
           ├─701657 /usr/sbin/zabbix_agentd: listener #3 [waiting for connection]
           └─701658 /usr/sbin/zabbix_agentd: active checks #1 [idle 1 sec]

Sep 06 16:40:11 puppet-hode01.office.localdomain systemd[1]: Starting Zabbix Agent...
Sep 06 16:40:11 puppet-hode01.office.localdomain systemd[1]: Started Zabbix Agent.

Для понимания общей картинки взаимосвязей между объектами, придумал такую схемку: puppet-setup-module.png

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


Дополнительный ссылки