Всем привет,
В продолжении знакомства с puppet, хотелось бы сегодня поделиться примером использовании такого замечательного дополнения, как hiera.
Немного теории
Hiera - это уже встроенный в puppet компонент позволяющий определить переменные или классы под определенные сервера или на группу серверов.
В более ранних версиях puppet, для использования иерархии, требовалось подключать hiera как плагин. Затем в последующих версиях она была включена, так скажем в ядро puppet.
Название компонента hiera обусловлено иерархической структурой, с помошью которой мы можем определить общие классы и переменные для всех хостов или определенной группы хостов. Или же можем определить классы и переменные точечно для каких-то хостов.
Также с помошью hiera мы можем добавить универсальности нашим модулям, за счет отделения данных об инфраструктере в hiera. При этом в модуле остается код, описывающий только логику. Такой подход дает возможность избежать нам хардкодинга в модуле.
Базовый пример структуры hiera:
---
hierarchy:
- name: "Per-node data"
path: "nodes/%{trusted.certname}.yaml"
- name: "Per-OS defaults"
path: "os/%{facts.os.family}.yaml"
- name: "Common data"
path: "common.yaml"
Исходя из этой структуры можно сделать вывод, что сначала будут применяться переменные и классы самого верхнего уровня (Per-node data
), точечно на определенные специфические хосты.
Далее принятся переменные в зависимости от семейства OS. (Например для RedHat или Debian)
Ну и самый нижний слой (общий/common), данные этого слоя будут влиять на все хосты.
Например, если же переменные представлены на нескольких уровняx, то приоритет в выборе переменной будет у наивысшего уровня. Так как в hiera используется поиск по первому успешному совпадению, и поиск следует по уровням сверху вниз.
Пишем модуль
И на этот раз я не придумал нечего оригинальнее, как взять пример с установкой zabbix-агента. Мы просто адаптируем пример из прошлой заметки под использование hiera.
Обновляем hiera.yaml
Находясь внутри нашего окружения, создаем файлик с иерархией будущей инфраструктуры - hiera.yaml
.
[root@puppetmaster production]# pwd
/etc/puppetlabs/code/environments/production
[root@puppetmaster production]# vim hiera.yaml
---
version: 5
defaults:
datadir: data
data_hash: yaml_data
hierarchy:
- name: "Commons classes and vars, by OS"
path: "os/%{facts.os.family}.yaml"
- name: "Commons classes and vars"
paths:
- "common-vars.yaml"
- "common-classes.yaml"
Если разобрать этот файл, то тут по мимо указании версии version
, имееться два контекста:
defaults
- контекст который настраивает hiera. В нашем случаи имееются директивы:datadir
- поле, которое настраивает рабочий каталог в hiera. То есть в какой директории будет производиться поиск данных. В значении этой переменной обычно указывается путь, относительно файла -hiera.yaml
data_hash
- этот параметр задает тип источника данных. Это может быть yaml или json. В зависимости от источника данные будут считаны и переданы puppet в виде хеша.
hierarchy
- контекст в котором уже идет описание нашей иерархии. В нашем примере определено два уровня. Определяется уровень иерархии при помощи двух директив:name
- Это поле по сути опциональное, в его значение помещаем текст с кратким описанием уровня.path
- а в значении этого параметра помещаем путь к файлу с данными. У нас в верхнем уровне используется составной путь, который собирается из значения фактаfacts.os.family
и расширения файла.yaml
.paths
- здесь определяется массив, в который мы можем передать несколько файлов. Этот пример содержит два файла с данными. С этими файлами познакомимся позже
Собственно, вот такая иерархия получилась. На основании ранней полученной теории можно сделать вывод, что вначале у нас будет выполнятся поиск переменных относящийся к определенному семейству ОС. Затем уже на последнем (нижнем) уровне будет поиск переменных относящихся ко всем хостам.
Создаем объекты hieradata
В значении переменной datadir
мы ранее указывали каталог - data
, внутри этого каталога и будем создавать последующие объекты. Поэтому если в вашем текущем окружении нету данного каталога, то создаем:
[root@puppetmaster production]# mkdir data
Начнем с самого первого уровня:
- name: "Commons classes and vars, by OS"
path: "os/%{facts.os.family}.yaml"
Переменные относящиеся в определенным операционкам, решил разместить в отдельной поддиректории - os
. Создадим этот каталог,
[root@puppetmaster production]# mkdir data/os
Для тестов я поднял две виртуалки от разных семейств, поэтому внутри data/os/
создаем два yaml
файла, под каждое из семейств:
[root@puppetmaster production]# touch data/os/Debian.yaml
[root@puppetmaster production]# touch data/os/RedHat.yaml
Как и говорилось ранее эти файлы будут собираться из значения факта - facts.os.family
+ .yaml
. Какие данные возвращаются в фактах можно проверить при помощи утилиты facter
, утилитка ставится вмести с puppet-агентом.
[root@puppetmaster production]# facter os.family
RedHat
Второй уровень содержит данные, которые будут относится ко всем puppet-хостам,
- name: "Commons classes and vars"
paths:
- "common-vars.yaml"
- "common-classes.yaml"
И тут имеется два файла:
common-vars.yaml
- будет содержать только переменнные относящиеся ко всем хостам,common-classes.yaml
- здесь же будут определены классы, также влияющие на все хосты. В принципе, все эти данные можно положить в один условный файлcommon.yaml
, но для удобства решил пойти иначе. Создаем файлы общей конфигурации:
[root@puppetmaster production]# touch data/common-vars.yaml
[root@puppetmaster production]# touch data/common-classes.yaml
И на этом этапе пока что все, заполнять данными будем в процессе написания модуля.
Подключаем hiera
Для того что бы puppet сервер начал работать с hiera, в файл site.pp
нужно сделать импорт hiera-классов.
[root@puppetmaster production]# vim manifests/site.pp
---
hiera_include('classes')
При таком сетапе, далее можем не определять классы и хосты в этом файле. Так как всю логику node definition
мы переложили в hiera.
Теперь классы, которые должны быть применимы на все хосты, определяются в файле hiera - common-classes.yaml
. Отредактируем этот файл, и включим в него модуль, который напишем далее.
[root@puppetmaster production]# vim data/common-classes.yaml
---
classes:
- zbx_agent
Пишем модуль
Создадим структуру нашего модуля:
[root@puppetmaster production]# mkdir modules/zbx_agent/{manifests,templates} -p
Напомню, в каталоге templates
будут лежать файл конфигурации zabbix-agent, и файлы репозитория для zabbix. А в manifests
будет содержатся структура описывающая логику модуля.
Создадим начальную “точку входа” в наш модуль, напишем файл - init.pp
:
[root@puppetmaster production]# vim modules/zbx_agent/manifests/init.pp
---
class zbx_agent (
String $package_name,
String $server_name,
String $agent_port,
String $agent_log_path,
String $agent_hostname = "${facts['networking']['fqdn']}",
String $os_release_name = "${facts['os']['distro']['codename']}",
String $zbx_repo_template,
String $repo_path,
String $firewall_type,
String $firewall_command,
String $firewall_command_path,
) {
class {'zbx_agent::install':} ->
class {'zbx_agent::config':}
}
Здесь мы определяем класс с именем zbx_agent
. Далее следует перечисление параметров класса, все параметры имеют тип строки String
.
$package_name
- в этом параметре будет определяться имя пакета zabbix-агента,$server_name
- этот параметр будет определять имя zabbix-сервера,$agent_port
- этот параметр будет содержать порт, на котором будет запущен zabbix-агент,$agent_log_path
- в значении этого параметра предполагается хранить путь к фс, куда zabbix-агент будет писать логи,$zbx_repo_template
- тут в значении предпогалается, что будет храниться имя шаблона для репозитория,$repo_path
- здесь в значении должен быть путь к фс, где репозиторий zabbix будет создан,$firewall_type
- тут будет указывать тип используемого фаервола. Предполагается, что у нас естьufw
илиfirewalld
,$firewall_command
- команда, открывающая порт zabbix-agent на фаерволе,$firewall_command_path
- путь к утилите взаимодействия с фаером,$agent_hostname
- в значении этого параметра из фактов будет определяться имя сервера,$os_release_name
- аналогично имени, тут из фактов будет определяться кодовое имя релиза операционки.
Переменные выстроены в таком порядке, начиная от общих, которые могут быть применимы на всех семействах операционных систем. И далее следуют уже зависимые от семейства.
В данном классе мы только проинициализировали параметры, теперь определим их значения используя манифест в hiera. Для общих параметров, будем использовать - common-vars.yaml
, отредактируем файл:
[root@puppetmaster production]# vim data/common-vars.yaml
---
zbx_agent::package_name: "zabbix-agent"
zbx_agent::server_name: "zbx.nixhub.ru"
zbx_agent::agent_port: "10050"
zbx_agent::agent_log_path: "/var/log/zabbix"
Параметры $agent_hostname
, $os_release_name
не трогаем, их значения уже определены.
Далее поднимаемся на следующий уровень hiera, и определяем значения под каждое семейство операционок.
Для Debian-бразных отредактируем файл - Debian.yaml
:
[root@puppetmaster production]# vim data/os/Debian.yaml
---
zbx_agent::zbx_repo_template: "zabbix_agent_debian.repo.erb"
zbx_agent::repo_path: "/etc/apt/sources.list.d/zabbix.list"
zbx_agent::firewall_type: "ufw"
zbx_agent::firewall_command: "ufw allow %{hiera('zbx_agent::agent_port')}"
zbx_agent::firewall_command_path: "/usr/sbin/"
Для Rhel-образные файл - RedHat.yaml
:
[root@puppetmaster production]# vim data/os/RedHat.yaml
---
zbx_agent::zbx_repo_template: "zabbix_agent_redhat.repo.erb"
zbx_agent::repo_path: "/etc/yum.repos.d/zabbix-agent.repo"
zbx_agent::firewall_type: firewalld
zbx_agent::firewall_command: "firewall-cmd --add-port=%{hiera('zbx_agent::agent_port')}/tcp --permanent && firewall-cmd --reload"
zbx_agent::firewall_command_path: "/bin/"
Какие именно значения заключены в эти параметры перечислили ранее. Из особеностей стоит отметить, способ подстановки значений из других (соседних) уровней. Реализовать это можно за счет такой конструкции:
%{hiera('zbx_agent::agent_port')
В процессе выполнения, этот вызов найдет значение для этой переменной и вставит его в строку по принципу шаблона.
Контекст исполнения нашего модуля содержит вложенные классы:
{
class {'zbx_agent::install':} ->
class {'zbx_agent::config':}
}
Класс zbx_agent::install
включает в себя логику, описывающую процесс установки заббикс агента на хост. Так скажем не отходя от кассы, напишем сразу этот класс:
[root@puppetmaster production]# vim modules/zbx_agent/manifests/install.pp
---
class zbx_agent::install {
file { $zbx_agent::repo_path:
content => template("zbx_agent/$zbx_agent::zbx_repo_template")
}
package { $zbx_agent::package_name:
ensure => 'present',
require => File["$zbx_agent::repo_path"],
}
}
Здесь у нас имеется два ресурса file
и package
:
-
file
- создает файл репозитория из шаблона. В заголовке данного ресурса будет содержатся значение переменной$zbx_agent::repo_path
. Если вспомнить, то ранее в hiera захардкодили значение для Rhel и Debian. Далее описывается атрибутcontent
в значении которого подставится шаблон, а имя шаблона ссылается на значение переменной$zbx_agent::zbx_repo_template
. (Шаблоны напишем в конце) -
package
- этот ресурс поставит нам пакет, в заголовке этого ресурса будет передано значение из переменной$zbx_agent::package_name
. В теле ресурса представлены атрибутыpresent
иrequire
, которые описывают представление и зависимость от ресурсаfile
.
Следом идет класс zbx_agent::config
, задача которого прикрутить конфигу zabbix-агента, и запустить его.
[root@puppetmaster production]# vim modules/zbx_agent/manifests/config.pp
---
class zbx_agent::config {
file { '/etc/zabbix/zabbix_agentd.conf':
content => template('zbx_agent/zabbix_agentd.conf.erb'),
owner => 'zabbix',
group => 'zabbix',
}
file { "$zbx_agent::agent_log_path":
ensure => 'directory',
owner => 'zabbix',
group => 'zabbix',
mode => '0750'
}
service { 'zabbix-agent':
ensure => 'running',
subscribe => File['/etc/zabbix/zabbix_agentd.conf']
}
exec { "$zbx_agent::firewall_type":
path => "${zbx_agent::firewall_command_path}",
command => "${zbx_agent::firewall_command}",
require => Package["${zbx_agent::package_name}"],
}
}
Быстро пройдемся по ресурсам этого класса:
file
- из шаблона создаст конфигурация для zabbix-агента. Следом идет еще один подобный ресурсfile
, но он уже создает директорию к логам. В процессе написания этого модуля выяснилось что у каждого семества операционок имеется свои пути с логам zabbix-агента. И что бы прийти к единому варианту, пришлось действовать по такому решению.service
- запускаем сервис zabbix агент. И смотрит на наличия изменений в файле с конфигом.exec
- этот ресурс выполняет shell-команду, в данном контексте просто выполнит команду на добавление порта в firewall.
И в заключение нам остается только написать шаблоны. Создадим шаблоны репозиториев:
Для Debian совместимых:
[root@puppetmaster production]# vim modules/zbx_agent/templates/zabbix_agent_debian.repo.erb
---
## Managet by Puppet
## --
deb https://repo.zabbix.com/zabbix/6.0/ubuntu <%= scope.lookupvar("zbx_agent::os_release_name") %> main
deb-src https://repo.zabbix.com/zabbix/6.0/ubuntu <%= scope.lookupvar("zbx_agent::os_release_name") %> main
Для Rhel совместимых:
[root@puppetmaster production]# vim modules/zbx_agent/templates/zabbix_agent_redhat.repo.erb
---
## Managet by Puppet
## --
[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
Создаем файл конфигурации агента:
[root@puppetmaster production]# vim modules/zbx_agent/templates/zabbix_agentd.conf.erb
---
# --
# Managment via Puppet
# --
# Pid location
PidFile=/var/run/zabbix/zabbix_agentd.pid
# Log's patametres
LogFile=<%= scope.lookupvar("zbx_agent::agent_log_path") %>/zabbix_agentd.log
LogFileSize=0
# Zabbix-server hostname/ip
Server=<%= scope.lookupvar("zbx_agent::server_name") %>
ServerActive=<%= scope.lookupvar("zbx_agent::server_name") %>
# Zabbix-agent hostname
Hostname=<%= scope.lookupvar("zbx_agent::agent_hostname") %>
Здесь представлен минимальный набор параметров, необходимый для работы агента.
Проверяем модуль
В качестве марионеток у меня подготовлено и настроено две виртуалки с Ubuntu и Almalinux, подключаемся к каждой по ssh, затем выполним команду:
[root@puppetnode01 ~]# /opt/puppetlabs/bin/puppet agent -t
Info: Refreshing CA certificate
Info: CA certificate is unmodified, using existing CA certificate
Info: Refreshing CRL
Info: CRL is unmodified, using existing CRL
Info: Using environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Caching catalog for puppetnode01.nixhub.ru
Info: Applying configuration version '1699537129'
Notice: /Stage[main]/Zbx_agent::Config/Exec[firewalld]/returns: executed successfully (corrective)
Notice: Applied catalog in 1.30 seconds
Если на этом этапе у вас нечего не сфейлилось, то значит изменения были применены к ноде. Иначе нужно будет изучать ошибки, и фиксить по ходу написания.
На основе этого примера, можно реализовать установку или конфигурацию других софтов. Но не стоит воспринимать его как эталонный, ведь всегда есть что улучшить в нем.