Всем привет,

В продолжении знакомства с 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

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

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