Миграция с Nagios на Icinga2 в Австралии

Всем привет.

Я — сисадмин linux, переехал из России в Австралию по независимой профессиональной визе в 2015 году, но статья будет не о том, как поросёнку завести трактор. Таких статей уже и так достаточно (тем не менее, если будет интерес — напишу и про это), так что я хотел бы рассказать о том, как на своей работе в Австралии в должности linux-ops-инженера я был инициатором миграции с одной системы мониторинга на другую. Конкретно — Nagios => Icinga2.

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

К сожалению тэг «code» не подсвечивает код Puppet и yaml, так что пришлось использовать «plaintext».

Ничто не предвещало беды утром 21 декабря 2016 года. Я, как обычно, читал Хабр незарегистрированным анонимусом в первые полчаса рабочего дня, поглощая кофе и наткнулся на эту статью.

Поскольку в моей компании как раз использовался Nagios, я, недолго думая, создал тикет в Redmine и скинул ссылку в общий чат, поскольку счёл это важным. Инициатива наказуема даже в Австралии, так что ведущий инженер повесил эту проблему на меня, раз уж я её обнаружил.

Скрин из РедмайнМиграция с Nagios на Icinga2 в Австралии

У нас в отделе перед изложением своего мнения принято предложить как минимум одну альтернативу, даже если выбор очевиден, так что я начал с гуглирования какие вообще системы мониторинга на данный момент актуальны, поскольку в России на последнем месте работы у меня была своя собственная самописная система, очень примитивная, но тем не менее вполне себе рабочая и выполняющая все возложенные на неё задачи. Python, Питерский Политех и метро рулят. Нет, метро — отстой. Это личное (11 лет работы) и достойно отдельной статьи, но не сейчас.

Немного о правилах внесения изменений в конфигурацию инфраструктуры на моём текущем месте. У нас используется Puppet, Gitlab и принцип Infrastructure as a Code, так что:

  • Никаких ручных изменений через SSH путём ручного изменения каких-либо файлов на виртуальных машинах. За три года работы я за это получал по шапке много раз, последний — неделю назад и не думаю, что это был последний раз. Ну в самом деле — поправить одну строку в конфиге, перезапустить службу и посмотреть решилась ли проблема — 10 секунд. Создать новую ветку в Gitlab, запушить изменения, дождаться, пока r10k отработает на Puppetmaster, запустить Puppet —environment=mybranch и ещё пару минут подождать пока всё это отработает — 5 минут минимум.
  • Любые изменения делаются путём создания Merge Request в Gitlab и необходимо получение одобрения минимум от одного члена команды. Серьёзные изменения по решению тим-лида требуют два или три одобрения.
  • Все изменения так или иначе являются текстовыми (поскольку манифесты Puppet, скрипты и данные Hiera — это текст), бинарные файлы крайне не рекомендуются и для одобрения таких файлов нужны веские причины.

Итак, варианты, которые я рассмотрел:

  • Munin — если в инфраструктуре больше 10 серверов, администрирование превращается в ад (из этой статьи. У меня не было особого желания проверять это, так что я поверил на слово).
  • Zabbix — давно уже присматривался, ещё в России, но тогда он был избыточен для моих задач. Здесь — пришлось отбросить по причине использования Puppet в качестве менеджера конфигурации и Gitlab в качестве системы контроля версий. На тот момент, насколько я понял — Zabbix хранит всю конфигурацию в базе данных, в связи с чем было непонятно как управлять конфигурацией в текущих условиях и как отслеживать изменения.
  • Prometheus — то, к чему мы придём в итоге, судя по настроению в отделе, но на тот момент я не осилил это и не смог продемонстрировать реально работающий образец (Proof of Concept), так что пришлось отказаться.
  • Ещё было несколько других вариантов, которые либо требовали полной переработки системы, либо были в зачаточном состоянии / заброшены и по этой же причине были отвергнуты.

В итоге я остановился на Icinga2 по трём причинам:

1 — совместимость с Nrpe (клиентской службой, которая запускает проверки по командам от Nagios). Это было очень важно, потому что у нас на тот момент было 135 (сейчас на 2019 их 165) виртуальных машин с кучей самописных сервисов/проверок и переделывать это всё было бы лютым геморроем.
2 — все конфигурационные файлы текстовые, что позволяет легко править это дело, создавать merge requests с возможность видеть что добавлено или удалено.
3 — это живой и развивающийся OpenSource проект. У нас очень любят OpenSource и делают посильный вклад в него путём создания Pull Requests и Issues для решения проблем.

Итак, поехали, Icinga2.

Первое, с чем пришлось столкнуться — инертность коллег. Все привыкли к Нагиосу/Наджиосу (хотя даже здесь не смогли придти к компромису как это произносить) и интерфейсу CheckMK. У icinga интерфейс выглядит капитально иначе (это был минус), но есть возможность гибко настраивать то, что нужно видеть с помощью фильтров буквально по любому параметру (это был плюс, но я за него знатно повоевал).

ФильтрыМиграция с Nagios на Icinga2 в Австралии

Оцените отношение размера скролл-бара к размеру поля для прокрутки.

Второе — все привыкли видеть всю инфраструктуру на одном мониторе, потому что CheckMk позволяет работать с несколькими хостами Nagios, но интерфейс Icinga так не умел (на самом деле умел, но об этом ниже). Альтернативой была вещь под названием Thruk, но её дизайн вызывал рвотные позывы у всех членов команды, кроме одного — того, кто это предложил (не я).

В топку Thruk — единогласное решение командыМиграция с Nagios на Icinga2 в Австралии

Спустя пару дней мозгового шторма я предложил идею кластерного мониторинга, когда есть один мастер-хост в production-зоне и два подчинённых — один в dev/test и один внешний хост, расположенный у другого провайдера с целью мониторить наши сервисы с точки зрения клиента или постороннего наблюдателя. Такая конфигурация позволяла видеть все проблемы в одном web-интерфейсе и вполне себе работала, но Puppet… Проблема с Puppet заключалась в том, что мастер-хост теперь должен был знать о всех хостах и службах/проверках в системе и должен был распределять их между зонами (dev-test, staging-prod, ext), но отправка изменений через Icinga API занимает пару секунд, а вот компиляция каталога Puppet всех сервисов для всех хостов — пару минут. Это мне до сих пор вменяют в вину, хотя я уже несколько раз объяснял как всё работает и почему это всё так долго.

Третье — куча SnowFlakes (снежинок) — вещей, которые выбиваются из общей системы, потому что в них есть что-то особенное, поэтому общие правила к ним неприменимы. Решалось путём лобовой атаки — если есть тревоги, но по факту всё в порядке, значит тут надо копать глубже и разбираться почему оно у меня алёртит, хотя не должно. Или наоборот — почему Нагиос паникует, а Icinga — нет.

Четвертое — Нагиос работал тут до меня три года и к нему доверия было изначально больше, чем к моей новомодной хипстерской системе, так что каждый раз, когда Icinga поднимала панику — никто ничего не делал, пока Нагиос не возбуждался по тому же вопросу. Но очень редко Icinga выдавала реальные тревоги раньше, чем Nagios и я считаю это серьёзным косяком, о котором расскажу в разделе «Выводы».

В итоге, ввод в эксплуатацию затянулся больше, чем на 5 месяцев (планировалось 28 июня 2018, по факту — 3 декабря 2018), в основном из-за «parity check» — той хрени, когда есть несколько сервисов в Нагиос, о которых никто ничего не слышал последние пару лет, но ИМЕННО СЕЙЧАС они, блин, выдали crit без какой-либо причины и мне пришлось объясняться почему их нет на моей панели и пришлось добавить их в Icinga, чтобы «parity check is complete» (Все службы/проверки в Нагиос соответствуют службам/проверкам в Icinga)

Внедрение:
Первое — война Code vs Data, типа Puppet Style. Все данные, прямо вот вообще все, должны быть в Hiera и никак иначе. Весь код — в файлах .pp. Переменные, абстракции, фунции — всё идёт в pp.
В итоге — у нас есть куча виртуальных машин (165 на момент написания статьи) и 68 web-приложений, которые надо мониторить на предмет работоспособности и на действительности SSL-сертификатов. Но из-за исторического геморроя информация для мониторинга приложений берётся из отдельного gitlab-репозитория и формат данных не менялся со времён Puppet 3, что создаёт дополнительные сложности в конфигурациии.

Puppet-code для приложений, берегите глаза

define profiles::services::monitoring::docker_apps(
  Hash $app_list,
  Hash $apps_accessible_from,
  Hash $apps_access_list,
  Hash $webhost_defaults,
  Hash $webcheck_defaults,
  Hash $service_overrides,
  Hash $targets,
  Hash $app_checks,
  )
{
#### APPS ####
  $zone = $name
  $app_list.each | String $app_name, Hash $app_data |
  {

    $notify_group = { 'notify_group' => ($webcheck_defaults[$zone]['notify_group'] + pick($app_data['notify_group'], {} )) } # adds notifications for default group (systems) + any group defined in int/pm_docker_apps.eyaml

    $data = merge($webhost_defaults, $apps_accessible_from, $app_data)

    $site_domain = $app_data['site_domain']

    $regexp = pick($app_data['check_regex'], 'html')        # Pick a regex to check

    $check_url = $app_data['check_url'] ? {
      undef   => { 'http_uri' => '/' },
      default => { 'http_uri' => $app_data['check_url'] }
    }

    $check_regex = $regexp ?{
      'absent' => {},
      default  => {'http_expect_body_regex' => $regexp}
    }

    $site_domain.each | String $vhost, Hash $vdata | {        # Split an app by domains if there are two or more
      $vhost_name = {'http_vhost' => $vhost}

      $vars = $data['vars'] + $vhost_name + $check_regex + $check_url

      $web_ipaddress = is_array($vdata['web_ipaddress']) ? {  # Make IP-address an array if it's not, because askizzy has 2 ips and it's an array
        true  => $vdata['web_ipaddress'],
        false => [$vdata['web_ipaddress']],
      }

      $access_from_zones = [$zone] + $apps_access_list[$data['accessible_from']] # Merge default zone (where the app is defined) and extra zones if they exist
      $web_ipaddress.each | String $ip_address | {            # For each IP (if we have multiple)
        $suffix = length($web_ipaddress) ? {                  # If we have more than one - add IP as a suffix to this hostname to avoid duplicating resources
          1       => '',
          default => "_${ip_address}"
        }
        $octets = split($ip_address, '.')
        $ip_tag = "${octets[2]}.${octets[3]}" # Using last octet only causes a collision between nginx-vip 203.15.70.94 and ext. ip 49.255.194.94

        $access_from_zones.each | $zone_prefix |{
          $zone_target = $targets[$zone_prefix]

          $nginx_vip_name = "${zone_prefix}_nginx-vip-${ip_tag}" # If it's a host for ext - prefix becomes 'ext_' (ext_nginx-vip...)
          $nginx_host_vip = {
            $nginx_vip_name => {
              ensure        => present,
              target        => $zone_target,
              address       => $ip_address,
              check_command => 'hostalive',
              groups        => ['nginx_vip',],
            }
          }

          $ssl_vars = $app_checks['ssl']
          $regex_vars = $app_checks['http'] + $vars + $webcheck_defaults[$zone] + $notify_group

          if !defined( Profiles::Services::Monitoring::Host[$nginx_vip_name] ) {
          ensure_resources('profiles::services::monitoring::host', $nginx_host_vip)
          }

          if !defined( Icinga2::Object::Service["${nginx_vip_name}_ssl"] ) {
            icinga2::object::service {"${nginx_vip_name}_ssl":
              ensure         => $data['ensure'],
              assign         => ["host.name == $nginx_vip_name",],
              groups         => ['webchecks',],
              check_command  => 'ssl',
              check_interval => $service_overrides['ssl']['check_interval'],
              target         => $targets['services'],
              apply          => true,
              vars           => $ssl_vars
            }
          }
          if $regexp != 'absent'{
            if !defined(Icinga2::Object::Service["${vhost}${$suffix} regex"]){
              icinga2::object::service {"${vhost}${$suffix} regex":
                ensure          => $data['ensure'],
                assign          => ["match(*_nginx-vip-${ip_tag}, host.name)",],
                groups          => ['webchecks',],
                check_command   => 'http',
                check_interval  => $service_overrides['regex']['check_interval'],
                target          => $targets['services'],
                enable_flapping => true,
                apply           => true,
                vars            => $regex_vars
              }
            }
          }
        }
      }
    }
  }
}

Код конфигурации хостов и сервисов тоже выглядит ужасно:

monitoring/config.pp


class profiles::services::monitoring::config(
Array $default_config,
Array $hostgroups,
Hash $hosts = {},
Hash $host_defaults,
Hash $services,
Hash $service_defaults,
Hash $service_overrides,
Hash $webcheck_defaults,
Hash $servicegroups,
String $servicegroup_target,
Hash $user_defaults,
Hash $users,
Hash $oncall,
Hash $usergroup_defaults,
Hash $usergroups,
Hash $notifications,
Hash $notification_defaults,
Hash $notification_commands,
Hash $timeperiods,
Hash $webhost_defaults,
Hash $apps_access_list,
Hash $check_commands,
Hash $hosts_api = {},
Hash $targets = {},
Hash $host_api_defaults = {},
)
{
# Profiles::Services::Monitoring::Hostgroup <<| |>> # will be enabled when we move to icinga completely
#### APPS ####
case $location {
'int', 'ext': {
$apps_by_zone = {}
}
'pm': {
$int_apps         = hiera('int_docker_apps')
$int_app_defaults = hiera('int_docker_app_common')
$st_apps          = hiera('staging_docker_apps')
$srs_apps         = hiera('pm_docker_apps_srs')
$pm_apps          = hiera('pm_docker_apps') + $st_apps + $srs_apps
$pm_app_defaults  = hiera('pm_docker_app_common')
$apps_by_zone = {
'int' => $int_apps,
'pm'  => $pm_apps,
}
$app_access_by_zone = {
'int' => {'accessible_from' => $int_app_defaults['accessible_from']},
'pm'  => {'accessible_from' => $pm_app_defaults['accessible_from']},
}
}
default: {
fail('Please ensure the node has $location fact set (int, pm, ext)')
}
}
file { '/etc/icinga2/conf.d/':
ensure  => directory,
recurse => true,
purge   => true,
owner   => 'icinga',
group   => 'icinga',
mode    => '0750',
notify  => Service['icinga2'],
}
$default_config.each | String $file_name |{
file {"/etc/icinga2/conf.d/${file_name}":
ensure => present,
source => "puppet:///modules/profiles/services/monitoring/default_config/${file_name}",
owner  => 'icinga',
group  => 'icinga',
mode    => '0640',
}
}
$app_checks = {
'ssl' => $services['webchecks']['checks']['ssl']['vars'],
'http' => $services['webchecks']['checks']['http_regexp']['vars']
}
$apps_by_zone.each | String $zone, Hash $app_list | {
profiles::services::monitoring::docker_apps{$zone:
app_list             => $app_list,
apps_accessible_from => $app_access_by_zone[$zone],
apps_access_list     => $apps_access_list,
webhost_defaults     => $webhost_defaults,
webcheck_defaults    => $webcheck_defaults,
service_overrides    => $service_overrides,
targets              => $targets,
app_checks           => $app_checks,
}
}
####    HOSTS    ####
# Profiles::Services::Monitoring::Host <<| |>> # This is for spaceship invasion when it's ready.
$hosts_has_large_disks = query_nodes('mountpoints.*.size_bytes >= 1099511627776')
$hosts.each | String $hostgroup, Hash $list_of_hosts_with_settings | {           # Splitting site lists by hostgroups - docker_host/gluster_host/etc
$list_of_hosts_in_group = $list_of_hosts_with_settings['hosts']
$hostgroup_settings     = $list_of_hosts_with_settings['settings']
$merged_hostgroup_settings = deep_merge($host_defaults, $list_of_hosts_with_settings['settings'])
$list_of_hosts_in_group.each | String $host_name, Hash $host_settings |{  # Splitting grouplists by hosts
# Is this host in the array $hosts_has_large_disks ? If so set host.vars.has_large_disks
if ( $hosts_has_large_disks.reduce(false) | $found, $value| { ( $value =~ "^${host_name}" ) or $found } ) {
$vars_has_large_disks = { 'has_large_disks' => true }
} else {
$vars_has_large_disks = {}
}
$host_data = deep_merge($merged_hostgroup_settings, $host_settings)
$hostgroup_settings_vars = pick($hostgroup_settings['vars'], {})
$host_settings_vars = pick($host_settings['vars'], {})
$host_notify_group = delete_undef_values($host_defaults['vars']['notify_group'] + $hostgroup_settings_vars['notify_group'] + $host_settings_vars['notify_group'])
$host_data_vars = delete_undef_values(deep_merge($host_data['vars'] , {'notify_group' => $host_notify_group}, $vars_has_large_disks)) # Merging vars separately
$hostgroups = delete_undef_values([$hostgroup] + $host_data['groups'])
profiles::services::monitoring::host{$host_name:
ensure             => $host_data['ensure'],
display_name       => $host_data['display_name'],
address            => $host_data['address'],
groups             => $hostgroups,
target             => $host_data['target'],
check_command      => $host_data['check_command'],
check_interval     => $host_data['check_interval'],
max_check_attempts => $host_data['max_check_attempts'],
vars               => $host_data_vars,
template           => $host_data['template'],
}
}
}
if !empty($hosts_api){                                                                # All hosts managed by API
$hosts_api.each | String $zone, Hash $hosts_api_zone | {                            # Split api hosts by zones
$hosts_api_zone.each | String $hostgroup, Hash $list_of_hosts_with_settings | {   # Splitting site lists by hostgroups - docker_host/gluster_host/etc
$list_of_hosts_in_group = $list_of_hosts_with_settings['hosts']
$hostgroup_settings     = $list_of_hosts_with_settings['settings']
$merged_hostgroup_settings = deep_merge($host_api_defaults, $list_of_hosts_with_settings['settings'])
$list_of_hosts_in_group.each | String $host_name, Hash $host_settings |{        # Splitting grouplists by hosts
# Is this host in the array $hosts_has_large_disks ? If so set host.vars.has_large_disks
if ( $hosts_has_large_disks.reduce(false) | $found, $value| { ( $value =~ "^${host_name}" ) or $found } ) {
$vars_has_large_disks = { 'has_large_disks' => true }
} else {
$vars_has_large_disks = {}
}
$host_data = deep_merge($merged_hostgroup_settings, $host_settings)
$hostgroup_settings_vars = pick($hostgroup_settings['vars'], {})
$host_settings_vars = pick($host_settings['vars'], {})
$host_api_notify_group = delete_undef_values($host_defaults['vars']['notify_group'] + $hostgroup_settings_vars['notify_group'] + $host_settings_vars['notify_group'])
$host_data_vars = delete_undef_values(deep_merge($host_data['vars'] , {'notify_group' => $host_api_notify_group}, $vars_has_large_disks))
$hostgroups = delete_undef_values([$hostgroup] + $host_data['groups'])
if defined(Profiles::Services::Monitoring::Host[$host_name]){
$hostname = "${host_name}_from_${zone}"
}
else
{
$hostname = $host_name
}
profiles::services::monitoring::host{$hostname:
ensure             => $host_data['ensure'],
display_name       => $host_data['display_name'],
address            => $host_data['address'],
groups             => $hostgroups,
target             => "${host_data['target_base']}/${zone}/hosts.conf",
check_command      => $host_data['check_command'],
check_interval     => $host_data['check_interval'],
max_check_attempts => $host_data['max_check_attempts'],
vars               => $host_data_vars,
template           => $host_data['template'],
}
}
}
}
}
#### END OF HOSTS ####
####   SERVICES   ####
$services.each | String $service_group, Hash $s_list |{             # Service_group and list of services in that group
$service_list = $s_list['checks']                                 # List of actual checks, separately from SG settings
$service_list.each | String $service_name, Hash $data |{
$merged_defaults = merge($service_defaults, $s_list['settings']) # global service defaults + service group defaults
$merged_data = merge($merged_defaults, $data)
$settings_vars = pick($s_list['settings']['vars'], {})
$this_service_vars = pick($data['vars'], {})
$all_service_vars = delete_undef_values($service_defaults['vars'] + $settings_vars + $this_service_vars)
# If we override default check_timeout, but not nrpe_timeout, make nrpe_timeout the same as check_timeout
if ( $merged_data['check_timeout'] and ! $this_service_vars['nrpe_timeout'] ) {
# NB: Icinga will convert 1m to 60 automatically!
$nrpe = { 'nrpe_timeout' => $merged_data['check_timeout'] }
} else {
$nrpe = {}
}
# By default we use nrpe and all commands are run via nrpe. So vars.nrpe_command = $service_name is a default value
# If it's server-side Icinga command - we don't need 'nrpe_command'
# but there is no harm to have that var and the code is shorter
if $merged_data['check_command'] == 'nrpe'{
$check_command = $merged_data['vars']['nrpe_command'] ? {
undef   => { 'nrpe_command' => $service_name },
default => { 'nrpe_command' => $merged_data['vars']['nrpe_command'] }
}
}else{
$check_command = {}
}
# Assembling $vars from Global Default service settings, servicegroup settings, this particular check settings and let's not forget nrpe settings.
if $all_service_vars['graphite_template'] {
$graphite_template = {'check_command' => $all_service_vars['graphite_template']}
}else{
$graphite_template = {'check_command' => $service_name}
}
$service_notify = [] + pick($settings_vars['notify_group'], []) + pick($this_service_vars['notify_group'], []) # pick is required everywhere, otherwise becomes "The value '' cannot be converted to Numeric"
$service_notify_group = $service_notify ? {
[]      => $service_defaults['vars']['notify_group'],
default => $service_notify
} # Assing default group (systems) if no other groups are defined
$vars = $all_service_vars + $nrpe + $check_command + $graphite_template + {'notify_group' => $service_notify_group}
# This needs to be merged separately, because merging it as part of MERGED_DATA overwrites arrays instead of merging them, so we lose some "assign" and "ignore" values
$assign = delete_undef_values($service_defaults['assign'] + $s_list['settings']['assign'] + $data['assign'])
$ignore = delete_undef_values($service_defaults['ignore'] + $s_list['settings']['ignore'] + $data['ignore'])
icinga2::object::service {$service_name:
ensure             => $merged_data['ensure'],
apply              => $merged_data['apply'],
enable_flapping    => $merged_data['enable_flapping'],
assign             => $assign,
ignore             => $ignore,
groups             => [$service_group],
check_command      => $merged_data['check_command'],
check_interval     => $merged_data['check_interval'],
check_timeout      => $merged_data['check_timeout'],
check_period       => $merged_data['check_period'],
display_name       => $merged_data['display_name'],
event_command      => $merged_data['event_command'],
retry_interval     => $merged_data['retry_interval'],
max_check_attempts => $merged_data['max_check_attempts'],
target             => $merged_data['target'],
vars               => $vars,
template           => $merged_data['template'],
}
}
}
#### END OF SERVICES ####
#### OTHER BORING STUFF ####
$servicegroups.each | $servicegroup, $description |{
icinga2::object::servicegroup{ $servicegroup:
target       => $servicegroup_target,
display_name => $description
}
}
$hostgroups.each| String $hostgroup |{
profiles::services::monitoring::hostgroup { $hostgroup:}
}
$notifications.each | String $name, Hash $settings |{
$assign = pick($notification_defaults['assign'], []) + $settings['assign']
$ignore = pick($notification_defaults['ignore'], []) + $settings['ignore']
$merged_settings = $settings + $notification_defaults
icinga2::object::notification{$name:
target       => $merged_settings['target'],
apply        => $merged_settings['apply'],
apply_target => $merged_settings['apply_target'],
command      => $merged_settings['command'],
interval     => $merged_settings['interval'],
states       => $merged_settings['states'],
types        => $merged_settings['types'],
assign       => delete_undef_values($assign),
ignore       => delete_undef_values($ignore),
user_groups  => $merged_settings['user_groups'],
period       => $merged_settings['period'],
vars         => $merged_settings['vars'],
}
}
# Merging notification settings for users with other settings
$users_oncall = deep_merge($users, $oncall)
# Magic. Do not touch.
create_resources('icinga2::object::user', $users_oncall, $user_defaults)
create_resources('icinga2::object::usergroup', $usergroups, $usergroup_defaults)
create_resources('icinga2::object::timeperiod',$timeperiods)
create_resources('icinga2::object::checkcommand', $check_commands)
create_resources('icinga2::object::notificationcommand', $notification_commands)
profiles::services::sudoers { 'icinga_runs_ping_l2':
ensure            => present,
sudoersd_template => 'profiles/os/redhat/centos7/sudoers/icinga.erb',
}
}

Я до сих пор работаю над этой лапшой и улучшаю её по мере возможности. Однако именно такой код позволил использовать простой и понятный синтаксис в Hiera:

Данные

profiles::services::monitoring::config::services:
perf_checks:
settings:
check_interval: '2m'
assign:
- 'host.vars.type == linux'
checks:
procs: {}
load: {}
memory: {}
disk:
check_interval: '5m'
vars:
notification_period: '24x7'
disk_iops:
vars:
notifications:
- 'silent'
cpu:
vars:
notifications:
- 'silent'
dns_fqdn:
check_interval: '15m'
ignore:
- 'xenserver in host.groups'
vars:
notifications:
- 'silent'
iftraffic_nrpe:
vars:
notifications:
- 'silent'
logging:
settings:
assign:
- 'logserver in host.groups'
checks:
rsyslog: {}
nginx_limit_req_other: {}
nginx_limit_req_s2s: {}
nginx_limit_req_s2x: {}
nginx_limit_req_srs: {}
logstash: {}
logstash_api:
vars:
notifications:
- 'silent'

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

В каждой проверке можно переопределить любую опцию и всё это в итоге складывается ещё и с дефолтными настройками всех проверок в целом. Поэтому в config.pp и написана такая лапша — там идёт слияние всех дефолтных настроек с настройками групп и потом ещё с каждой индивидуальной проверкой.

Так же очень важным изменением стала возможность использовать функции в настройках, к примеру, функция подмены порта, адреса и url для проверки http_regex.

http_regexp:
assign:
- 'host.vars.http_regex'
- 'static_sites in host.groups'
check_command: 'http'
check_interval: '1m'
retry_interval: '20s'
max_check_attempts: 6
http_port: '{{ if(host.vars.http_port) { return host.vars.http_port } else { return 443 } }}'
vars:
notification_period: 'host.vars.notification_period'
http_vhost: '{{ if(host.vars.http_vhost) { return host.vars.http_vhost } else { return host.name } }}'
http_ssl: '{{ if(host.vars.http_ssl) { return false } else { return true } }}'
http_expect_body_regex: 'host.vars.http_regex'
http_uri: '{{ if(host.vars.http_uri) { return host.vars.http_uri } else { return "/" } }}'
http_onredirect: 'follow'
http_warn_time: 8
http_critical_time: 15
http_timeout: 30
http_sni: true

Это означает — если в определении хоста есть переменная http_port — использовать её, иначе 443. Например, web-интерфейс jabber висит на 9090, а Unifi — на 7443.
http_vhost означает игнорировать DNS и брать этот адрес.
Если в хосте указан uri — то идти по нему, иначе брать «/».

С http_ssl вышла забавная история — эта зараза никак не хотела отключаться по требованию. Я долго тупил в эту строку, пока до меня не дошло, что в переменная в определении хоста:

http_ssl: false

Подставляется в выражение

if(host.vars.http_ssl) { return false } else { return true }

как false и в итоге получается

if(false) { return false } else { return true }

то есть проверка ssl получается всегда активна. Решилось заменой синтаксиса:

http_ssl: no

Выводы:

Плюсы:

  • У нас теперь одна система мониторинга, а не две, как было последние 7-8 месяцев, или одна, устаревшая и уязвимая.
  • Структура данных хостов / служб(проверок)теперь (на мой взгляд) намного более читаема и понятна. Для других это оказалось не так очевидно, так что пришлось запилить пару страниц в местной вики для разъяснения как оно всё работает и что где править.
  • Есть возможность гибкой настройки проверок с помощью переменных и функций, например для проверки http_regexp искомый паттерн, код возврата, url и порт можно задавать в настройках хоста.
  • Есть несколько панелей(dashboards), для каждой из готорых можно определить свой список отображаемых тревог и управлять всем этим через Puppet и merge requests.

Минусы:

  • Инертность членов команды — Нагиос работал, работал и работал, а эта твоя Исинга постоянно глючит и тормозит. А как тут посмотреть историю? А, блин, она же не обновляется… (Реальная проблема — история тревог не обновляется автоматом, только по F5)
  • Инертность системы — когда я кликаю в web-интерфейсе на «обновить» (check now) — результат выполнения зависит от погоды на Марсе, особенно на сложных сервисах, которые требуют десятки секунд для выполнения. Подобный результат — нормальное дело. Миграция с Nagios на Icinga2 в Австралии
  • В целом по полугодовой статистике работы двух систем бок о бок Нагиос всегда отрабатывал быстрей, чем Icinga и это очень меня раздражало. Как мне кажется, там что-то намутили с таймерами и проверка раз в пять минут по факту идёт раз в 5:30 или что-то в этом духе.
  • Если перезапустить сервис в любой момент времени (systemctl restart icinga2) — все проверки, которые на тот момент были в процессе выполнения, выдадут тревогу critical <terminated by signal 15> на экран и со стороны это выглядит, как будто вообще всё упало (подтверждённый баг).

Но в целом — оно работает.

Источник: habr.com