Въведение в Puppet

Puppet е система за управление на конфигурация. Тя се използва за приведеждане на хостове в желаното състояние и поддържане на това състояние.

Работя с Puppet повече от пет години. Текстът всъщност е преведен и пренареден компилация на ключовите моменти от официалната документация, която ще позволи на новаците бързо да се запознаят със същността на Puppet.

Въведение в Puppet

Основна информация

Схемата на работа на Puppet е клиент-сървърна, въпреки че се поддържа и вариант на работа без сървър с ограничена функционалност.

Използва се модел на работа pull: по подразбиране на всеки половин час клиентите се свързват със сървъра за конфигурация и я прилагат. Ако сте работили с Ansible, там се използва друг, push-модел: администраторът инициира процеса на прилагане на конфигурацията, самите клиенти няма да прилагат нищо.

При мрежовото взаимодействие се използва двустранно TLS криптиране: сървърът и клиентът имат свои собствени частни ключове и съответстващите им сертификати. Обикновено сървърът издава сертификати за клиентите, но в принципа е възможно използването и на външно CA.

Запознаване с манифестите

В терминологията на Puppet к папет-сървъра се свързват нодове (nodes). Конфигурацията за нодовете се пише в манифестите на специален език за програмиране — Puppet DSL.

Puppet DSL е декларативен език. С него се описва желаното състояние на нода под формата на деклариране на отделни ресурси, например:

  • Файлът съществува и има определено съдържание.
  • Пакетът е инсталиран.
  • Услугата е стартирана.

Ресурсите могат да бъдат взаимосвързани:

  • Има зависимости, които влияят на реда на прилагане на ресурсите.
    Например, "първо инсталирай пакета, след това поправи конфигурационния файл, а след това стартирай услугата."
  • Има известия — ако ресурсът се променя, той изпраща известия на ресурсите, които са абонирани за него.
    Например, ако конфигурационният файл се променя, услугата може автоматично да се рестартира.

Освен това в Puppet DSL има функции и променливи, както и условни оператори и селектори. Поддържат се и различни механизми за шаблониране — EPP и ERB.

Puppet е написан на Ruby, затова много конструкции и термини са взети от там. Ruby позволява разширяване на Puppet — добавяне на сложна логика, нови типове ресурси, функции.

По време на работа с Puppet, манифестите за всяка конкретна нода на сървъра се компилират в каталог. Директория — това е списък на ресурсите и техните взаимовръзки след изчисляване на стойностите на функциите, променливите и разкриване на условните оператори.

Синтаксис и кодстил

Ето разделите на официалната документация, които ще помогнат да разберете синтаксиса, ако приведените примери не са достатъчни:

Ето пример за това как изглежда манифестът:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Отстъпи и преводи на редове не са задължителна част от манифеста, но има препоръчан стилов гайд.Кратко резюме:

  • Двустранни отстъпи, табулации не се използват.
  • Фигурните скобки се отделят с интервал, двоеточието не се отделя с интервал.
  • Запетаи след всеки параметър, включително последния. Всеки параметър — на отделен ред. Изключение се прави за случая без параметри и един параметър: може да се пише на един ред и без запетая (т.е. resource { 'title': } и resource { 'title': param => value }).
  • Стрелките при параметрите трябва да бъдат на едно ниво.
  • Стрелките на взаимовръзките на ресурсите се пишат преди тях.

Разположение на файловете на папетсервера

За допълнителни обяснения ще въведа понятието „коренна директория“. Коренната директория е директорията, в която се намира конфигурацията на Puppet за конкретната нода.

Коренната директория варира в зависимост от версията на Puppet и използването на среди. Средите са независими набори от конфигурации, които се съхраняват в отделни директории. Обикновено се използват в комбинация с git, в такъв случай средите се създават от git клонове. Следователно, всяка нода попада в една или друга среда. Това се настройва на самата нода или в ENC, което ще обясня в следващата статия.

  • В третата версия („старият Puppet“) базовата директория беше /etc/puppet. Използването на среди е опционално — ние, например, не ги използваме с стария Puppet. Ако се използват среди, те обикновено се съхраняват в /etc/puppet/environments, коренната директория ще бъде директорията на средата. Ако не се използват среди, коренната директория ще бъде базовата.
  • Започвайки от четвърта версия („новият Puppet“), използването на среди стана задължително, а базовата директория бе преместена в /etc/puppetlabs/code. Следовательно, средите се съхраняват в /etc/puppetlabs/code/environments, кореновата директория е директорията на средата.

В кореновата директория трябва да има подпапка manifests, в която се намират един или повече манифести с описание на нодовете. Освен това, там трябва да има подпапка modules, в която се намират модулите. Какво представляват модулите, ще разкажа малко по-късно. Освен това, в старата Папета също може да има подпапка файлове, в която се намират различни файлове, които копираме на нодовете. В новата Папета всички файлове обаче са извлечени в модулите.

Файловете на манифестите имат разширение .pp.

Пара боеви примери

Описание на нода и ресурс на него

На нода server1.testdomain трябва да бъде създаден файл /etc/issue с съдържание Debian GNU/Linux n l. Файлът трябва да принадлежи на потребителя и групата root, правата за достъп трябва да бъдат 644.

Пишем манифест:

node 'server1.testdomain' {   # конфигурационен блок, свързан с нода server1.testdomain
    file { 'etc/issue':   # описваме файла /etc/issue
        ensure  => present,   # този файл трябва да съществува
        content => 'Debian GNU/Linux n l',   # той трябва да има такова съдържание
        owner   => root,   # собственик на файла
        group   => root,   # група собственик
        mode    => '0644',   # права на файла. Те са зададени като низ (в кавички), защото иначе числото с 0 в началото ще бъде възприето като записано в осмична система и всичко ще премине не така, както е било замислено
    }
}

Взаимовръзки на ресурсите на нода

На нода server2.testdomain трябва да бъде стартиран nginx, работещ с подготвена предварително конфигурация.

Декомпозиране на задачата:

  • Трябва да бъде инсталиран пакет nginx.
  • Трябва да бъдат копирани конфигурационни файлове от сървъра.
  • Трябва да бъде стартиран сервис nginx.
  • В случай на обновление на конфигурацията, трябва да се рестартира сервисът.

Пишем манифест:

нода 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    пакет { 'nginx':   # описываем пакет nginx
        осигурете => инсталиран,   # той трябва да бъде инсталиран
    }
  # Пряка стрелка (->) указва, че ресурсът по-долу трябва
  # да бъде създаден след ресурса, описан по-горе.
  # Такива зависимости са транзитивни.
    -> файл { ' /etc /nginx':   # описваме файла /etc /nginx
        осигурете  => директория,   # това трябва да бъде директория
        източник  => 'puppet:// /modules /example /nginx-conf',   # нейното съдържание трябва да се взема от папет-сървъра по указан адрес
        рекурсивно => истина,   # копирайте файловете рекурсивно
        прочистване   => истина,   # трябва да се изтриват излишните файлове (тези, които не са в източника)
        принуда   => истина,   # изтриване на излишни директории
    }
  # Вълнистата стрелка (~>) указва, че ресурсът по-долу трябва
  # да се подпише за промените в ресурса, описан по-горе.
  # Вълнистата стрелка включва пряката (->).
    ~> услуга { 'nginx':   # описваме услугата nginx
        осигурете => работеща,   # той трябва да бъде пуснат
        активиране => истина,   # трябва да се стартира автоматично при стартиране на системата
    }
  # Когато ресурс от типа услуга получи известие,
  # съответната услуга се рестартира.
}

За да работи това, е необходимо приблизително такова разположение на файловете на папет-сървъра:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Типове ресурси

Пълен списък с поддържани типове ресурси се намира в документацията, тук ще опиша пет основни типа, които в моята практика са достатъчни за решаване на повечето задачи.

файл

Управлява файлове, директории, симлинкове, тяхното съдържание, права на достъп.

Параметри:

  • име на ресурса — път до файла (по желание)
  • път — път до файла (ако не е зададен в името)
  • осигурете — тип файл:
    • отсъства — изтриване на файла
    • представен — трябва да бъде файл от всякакъв тип (ако файла няма, ще бъде създаден обикновен файл)
    • файл — обикновен файл
    • директория — директория
    • link — симлинк
  • content — съдържание на файла (подходящо само за обикновени файлове, не може да се използва заедно с source или target)
  • source — линк до пътя, от който трябва да се копира съдържанието на файла (не може да се използва заедно с content или target). Може да бъде зададен както под формата на URI със схема puppet: (в този случай ще бъдат използвани файлове от папет-сървъра), така и със схемата http: (надявам се, че е ясно какво ще бъде в този случай), и дори със схемата file: или под формата на абсолютен път без схема (в този случай ще бъде използван файл от локалната файловата система на нода)
  • target — къде трябва да сочи симлинка (не може да се използва заедно с content или source)
  • собственик — потребител, на когото трябва да принадлежи файлът
  • група — група, на която трябва да принадлежи файлът
  • mode — права върху файла (под формата на стринг)
  • рекурсивно — включва рекурсивна обработка на директории
  • изчисти — включва изтриване на файлове, които не са описани в Puppet
  • сила — включва изтриване на директории, които не са описани в Puppet

пакет

Инсталира и премахва пакети. Може да обработва известия — преинсталира пакета, ако е зададен параметър преинсталирай_при_обновление.

Параметри:

  • име на ресурса — име на пакета (по избор)
  • name — име на пакета (ако не е зададено в името)
  • доставчик — пакетен мениджър, който трябва да се използва
  • осигурете — желано състояние на пакета:
    • представен, инсталиран — инсталирана всяка версия
    • latest — инсталирана последната версия
    • отсъства — премахнат (apt-get remove)
    • изчистен — премахнат заедно с конфигурационните файлове (apt-get purge)
    • задържан — версията на пакета е блокирана (apt-mark hold)
    • всяка друга стринг — инсталирана посочената версия
  • преинсталирай_при_обновление — ако истинно, то при получаване на известие пакетът ще бъде преинсталиран. Полезно за дистрибуции, базирани на източник, където повторната компилация на пакети може да бъде необходима при промяна на параметрите на компилация. По подразбиране неверно.

service

Управлява услугите. Може да обработва известия — рестартира услугата.

Параметри:

  • име на ресурса — услуга, която трябва да се управлява (по избор)
  • name — услуга, която трябва да се управлява (ако не е зададена в името)
  • осигурете — желано състояние на услугата:
    • работеща — стартирана
    • спряна — спрян
  • включи — управлява възможността за стартиране на услугата:
    • истинно — включен автоматичен старт (systemctl enable)
    • маскирай — маскирана (systemctl mask)
    • неверно — изключен автоматичен старт (systemctl disable)
  • рестартиране — команда за рестартиране на услугата
  • status — команда за проверка на статуса на услугата
  • имаРестарт — да се укаже дали инитскриптът на услугата поддържа рестартиране. Ако неверно и е зададен параметър рестартиране — се използва стойността на този параметър. Ако неверно и параметърът рестартиране не е зададен — услугата се спира и стартира за рестартиране (но в systemd се използва команда systemctl restart).
  • имаСтатус — да се укаже дали инитскриптът на услугата поддържа команда status. Ако неверно, то се използва стойността на параметъра status. По подразбиране истинно.

exec

Стартира външни команди. Ако не се зададат параметри създава, самоако, освен ако или самообновяване, командата ще се изпълнява при всяко изпълнение на Puppet. Може да обработва известия — стартира командата.

Параметри:

  • име на ресурса — команда, която трябва да се изпълни (по избор)
  • command — команда, която трябва да се изпълни (ако не е зададена в името)
  • път — пътища, по които да се търси изпълним файл
  • самоако — ако командата, посочена в този параметър, завърши с нулев код на връщане, основната команда ще бъде изпълнена
  • освен ако — ако командата, посочена в този параметър, завърши с ненулев код на връщане, основната команда ще бъде изпълнена
  • създава — ако посоченият в този параметър файл не съществува, основната команда ще бъде изпълнена
  • самообновяване — ако истинно, командата ще бъде стартирана само в случай, че този exec получи уведомление от други ресурси
  • cwd — директорията, от която да се стартира командата
  • user — потребителят, от който да се стартира командата
  • доставчик — с помощта на какво да се стартира командата:
    • posix — просто се създава дъщерен процес, задължително посочване път
    • shell — командата се стартира в шелл /bin/sh, не е необходимо да се посочва път, може да се използва глоблинг, пайпове и други функции на шелла. Обикновено се определя автоматично, ако има всякакви спецсимволи (|, ;, &&, || и така нататък).

cron

Управлява кронджобовете.

Параметри:

  • име на ресурса — просто някакъв идентификатор
  • осигурете — състояние на кронджоба:
    • представен — създайте, ако не съществува
    • отсъства — изтрийте, ако съществува
  • command — коя команда да се изпълнява
  • среда — в каква среда да се изпълнява командата (списък от променливи на средата и техните стойности чрез =)
  • user — от кой потребител да се изпълнява командата
  • минута, час, седмица, месец, ден от месеца — кога да се стартира крон. Ако някой от тези атрибути не е посочен, неговото значение в кронтаб ще бъде *.

В Puppet 6.0 cron практически е премахнат от кутията в puppetserver, затова няма документация на общия сайт. Но той съществува в кутията в puppet-agent, затова не е нужно да го инсталирате отделно. Документацията за него може да бъде видяна в документацията на петата версия на Puppet, или на GitHub..

По отношение на ресурсите на общо

Изисквания за уникалност на ресурсите

Най-честата грешка, с която се сблъскваме — Duplicate declaration. Тази грешка настъпва, когато в каталога попаднат два или повече ресурса от един и същи тип с едно и също име.

Затова ще кажа още веднъж: в манифестите за една нода не трябва да има ресурси от един и същи тип с едно и също име (title)!

Понякога е необходимо да се инсталират пакети с еднакво име, но с различни пакетни мениджъри. В такъв случай е необходимо да се използва параметър name, за да се избегне грешката:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

В другите типове ресурси съществуват аналогични параметри, които помагат да се избегне дублиране, — name у service, command у exec, и така нататък.

Метапараметри

Някои специални параметри съществуват за всеки тип ресурс, независимо от неговата същност.

Пълен списък с метапараметри в документацията на Puppet.

Кратък списък:

  • require — в този параметър се посочват ресурсите, от които зависи този ресурс.
  • before — в този параметър се посочват ресурсите, които зависят от този ресурс.
  • subscribe — в този параметър се посочват ресурсите, от които този ресурс получава уведомления.
  • notify — в този параметър се посочват ресурсите, които получават уведомления от този ресурс.

Всички посочени метапараметри приемат либо една връзка към ресурс, либо масив от връзки в квадратни скобки.

Връзки към ресурси

Връзка към ресурс — това просто е споменаване на ресурса. Използват се основно за указване на зависимости. Връзка към несъществуващ ресурс ще предизвика грешка при компилация.

Синтаксисът на връзката е следният: тип ресурс с голяма буква (ако в наименованието на типа има двойни двоеточия, то с голяма буква се пише всяка част от името между двоеточията), след това в квадратни скобки името на ресурса (регистърът на името не се променя!). Не трябва да има интервали, квадратните скобки се пишат незабавно след името на типа.

Пример:

file { 'file1': ensure => present }
file { 'file2':
  ensure => directory,
  before => File['file1'],
}
file { 'file3': ensure => absent }
File['file1'] -> File['file3']

Зависимости и уведомления

Документацията е тук.

Както вече беше споменато, простите зависимости между ресурсите са транзитивни. Между другото, внимавайте при задаването на зависимости — възможно е да се създадат циклични зависимости, което ще предизвика грешка при компилация.

В отличие от зависимостите, уведомленията не са транзитивни. За уведомленията важат следните правила:

  • Ако ресурс получи уведомление, той се обновява. Действията при обновяване зависят от типа ресурс — exec стартира команда, service рестартира услуга, пакет премества пакет. Ако не е определено действие при обновяване за ресурса, то нищо не се случва.
  • При един проход на Puppet ресурсът се обновява не повече от един път. Това е възможно, тъй като уведомленията включват в себе си зависимости, а графикът на зависимостите не съдържа цикли.
  • Ако Puppet променява състоянието на ресурса, ресурсът изпраща известия на всички ресурси, записани за него.
  • Ако ресурсът бъде актуализиран, той изпраща известия на всички ресурси, записани за него.

Обработка на неуточнени параметри

Обикновено, ако някой параметър на ресурса няма стойност по подразбиране и този параметър не е указан в манифеста, Puppet няма да промени това свойство на съответния ресурс на нода. Например, ако ресурсът от тип файл не указва параметър собственик, Puppet няма да променя притежателя на съответния файл.

Запознаване с класове, променливи и дефиниции

Нека предположим, че имаме няколко нода, на които има еднаква част от конфигурацията, но има и различия — иначе бихме могли да опишем всичко това в един блок node {}. Разбира се, можем просто да копираме еднаквите части от конфигурацията, но в общия случай това е лошо решение — конфигурацията нараства и при промяна на общата част от конфигурацията ще трябва да коригираме същото на много места. Лесно е да се сбърка и изобщо принципът DRY (don’t repeat yourself) не е измислен напразно.

За решаване на такъв проблем съществува конструкция, наречена клас.

Класове

Клас — това е именуван блок от Puppet-код. Класовете са необходими за повторно използване на кода.

Първо, класът трябва да бъде описан. Самото описание не добавя ресурси никъде. Класът се описва в манифестите:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

След това класът може да бъде използван:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Пример от предишната задача — да изнесем инсталацията и настройките на nginx в клас:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recurse => true,
        purge  => true,
        force  => true,
    }
    -> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Променливи

Класът от предишния пример не е много гъвкав, защото винаги предоставя същата конфигурация на nginx. Нека да направим така, че пътят към конфигурацията да стане променлива, тогава този клас може да се използва за инсталация на nginx с всяка конфигурация.

Това може да се постигне чрез променливи.

Внимание: променливите в Puppet са неизменяеми!

Освен това, може да се обърнете към променлива само след като е била декларирана, в противен случай стойността на променливата ще бъде undef.

Пример за работа с променливи:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

В Puppet има имена на пространства, а в променливите, съответно, има обхват на видимост: променлива с едно и също име може да бъде дефинирана в различни имена на пространства. При разрешаване на стойността на променливата, тя се търси в текущото пространство, след това в обхващащото, и така нататък.

Примери за имена на пространства:

  • глобално — тук попадат променливите извън описанието на класа или нода;
  • името на пространството на нода в описанието на нода;
  • името на пространството на клас в описанието на класа.

За да избегнем неясноти при достъпа до променливата, можем да посочим пространството на името в именуването на променливата:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Съгласихме се, че пътят към конфигурацията на nginx е в променливата $nginx_conf_source. Тогава класът ще изглежда по следния начин:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # тук използваме променливата вместо фиксиран низ
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

Въпреки това, приведеното примери е лошо, защото има нещо „тайно знание“ за това, че вътре в класа използва променлива с такава име. Много по-правилно е да направим това знание общо — класовете могат да имат параметри.

Параметри на класа — това са променливи в именното пространство на класа, те се задават в заглавието на класа и могат да бъдат използвани като обикновени променливи в тялото на класа. Стойностите на параметрите се задават при използване на класа в манифеста.

На параметъра може да се зададе стойност по умолчание. Ако параметърът няма стойност по умолчание и стойността не е зададена при използването, това ще предизвика грешка при компилация.

Нека параметризираме класа от примерa по-горе и добавим два параметра: първият, задължителен — пътят към конфигурацията, и вторият, незадължителен — името на пакета с nginx (в Debian, например, има пакети nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

В Puppet променливите са типизирани. Има много типове данни. Типовете данни обикновено се използват за валидация на стойностите на параметрите, предавани на класове и дефиниции. Ако предадената параметрична стойност не отговаря на указан тип, ще възникне грешка при компилация.

Типът се изписва непосредствено пред името на параметъра:

клас пример (
  Стринг $param1,
  Цяло $param2,
  Масив $param3,
  Хаш $param4,
  Хаш[Стринг, Стринг] $param5,
) {
  ...
}

Класове: включване на име на клас срещу клас{‘име на клас’:}

Всеки клас е ресурс от тип клас. Както и при всички други типове ресурси, на една нода не може да съществува два экземпляра на един и същи клас.

Ако се опитате да добавите клас на една и съща нода два пъти с помощта на клас { 'име на клас':} (независимо дали с различни или идентични параметри), ще има грешка при компилация. Но в случая на използване на клас в стил ресурс, можете веднага в манифеста да зададете всички негови параметри явно.

Но ако използвате include, класът може да се добавя неограничен брой пъти. Въпросът е, че include е идемпотентна функция, която проверява дали класът е добавен в каталога. Ако класът не е в каталога – добавя го, а ако вече съществува, тогава нищо не прави. Но при използване на include не могат да се задават параметри на класа по време на обявяването му – всички задължителни параметри трябва да бъдат зададени във външен източник на данни – Hiera или ENC. За тях ще говорим в следващата статия.

Дефайни

Както беше споменато в предишния раздел, един и същи клас не може да присъства на нода повече от един път. Въпреки това в някои случаи е необходимо да можете да прилагате един и същи кодов блок с различни параметри на една нода. С други думи, има нужда от собствен тип ресурс.

Например, за да инсталираме PHP модул, ние в Авито правим следното:

  1. Инсталираме пакета с този модул.
  2. Създаваме конфигурационен файл за този модул.
  3. Създаваме симлинк за конфигурацията на php-fpm.
  4. Създаваме симлинк за конфигурацията на php cli.

В такива случаи се използва конструкция като дефайн (define, defined type, defined resource type). Дефайн е подобен на клас, но има разлики: първо, всеки дефайн е тип ресурс, а не ресурс; второ, всеки дефайн има неявен параметър $title, където попада името на ресурса при неговото обявяване. Както и при класовете, дефайнът първо трябва да бъде описан, след което може да бъде използван.

Оптимизиран пример с модул за PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

В дефайне най-лесно можете да улавяте грешки с дублиране на декларации. Това се случва, ако дефайнът има ресурс с константно име и на някакъв нод има два или повече екземпляра на този дефайн.

За да се защитите от това, е просто: всички ресурси вътре в дефайна трябва да имат наименование, зависещо от $title. Като алтернатива — идемпотентно добавяне на ресурси, в най-простият случай е достатъчно да изнесете общите за всички екземпляри дефайна ресурси в отделен клас и да включите този клас в дефайна — функцията include е идемпотентна.

Има и други методи за постигане на идемпотентност при добавяне на ресурси, а именно използването на функциите defined и ensure_resources, но за това ще говоря в следващата серия.

Зависимости и уведомления за класове и дефайнове

Класовете и дефайновете добавят следните правила за обработка на зависимости и уведомления:

  • зависимост от клас/дефин добавя зависимости от всички ресурси на класа/дефина;
  • зависимост на клас/дефин добавя зависимости на всички ресурси на класа/дефина;
  • уведомление на клас/дефин уведомява всички ресурси на класа/дефина;
  • подписка на клас/дефин подписва на всички ресурси на класа/дефина.

Условни оператори и селектори

Документацията е тук.

ако

Тук всичко е просто:

if ИЗРАЖЕНИЕ1 {
  ...
} elsif ИЗРАЖЕНИЕ2 {
  ...
} else {
  ...
}

освен ако

unless — това е ако в обратна посока: блокът код ще се изпълни, ако изразът е лъжен.

unless ИЗРАЖЕНИЕ {
  ...
}

case

Тук също няма нищо сложно. Като стойности можете да използвате обикновени стойности (низове, числа и т.н.), регулярни изрази, както и типове данни.

case ИЗРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  по умолчанию: { ... }
}

Селектори

Селекторът е езикова конструкция, подобна на case, само че вместо изпълнение на блок код, той връща стойност.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, по умолчанию => 3 }

Модули

Когато конфигурацията е малка, тя може лесно да се държи в един манифест. Но колкото повече конфигурация описваме, толкова повече класове и нодове се появяват в манифеста, той нараства и става неудобен за работа.

Освен това, има проблем с повторното използване на кода - когато целият код е в един манифест, е трудно да се споделя с други. За решаване на тези два проблема в Puppet има такова понятие, като модули.

Модули — това са комплекти класове, дефайнини и други Puppet-обекти, извлечени в отделна директория. С други думи, модулът е независим фрагмент от Puppet-логиката. Например, може да има модул за работа с nginx, и в него ще има само това, което е необходимо за работа с nginx, а може да има и модул за работа с PHP и така нататък.

Модулите получават версии, също така се поддържат зависимости между модулите. Има публичен репозитарий на модули — Puppet Forge.

На puppet-сервера модулите се намират в поддиректорията modules на коренната директория. Вътре в всеки модул стандартната схема на директории е — manifests, files, templates, lib и така нататък.

Структура на файловете в модула

В корена на модула могат да бъдат следните директории с описателни имена:

  • manifests — в нея се намират манифестите
  • файлове — в нея се намират файловете
  • templates — в нея се намират шаблоните
  • lib — в нея се намира Ruby-код

Това не е изчерпателен списък на директориите и файловете, но за тази статия е достатъчно засега.

Имена на ресурсите и файловете в модула

Документацията е тук.

Ресурсите (класове, дефайни) в модула не могат да имат произволни имена. Освен това, има директно съответствие между името на ресурса и името на файла, в който Puppet ще търси описанието на този ресурс. Ако нарушавате правилата за именуване, Puppet просто няма да намери описанието на ресурсите и ще получите грешка при компилация.

Правилата са прости:

  • Всички ресурси в модула трябва да са в името на модула. Ако модулът се нарича foo, то всички ресурси в него трябва да се наричат foo::, или просто foo.
  • Ресурсът с името на модула трябва да бъде в файла init.pp.
  • За останалите ресурси схемата на именуване на файловете е следната:
    • предикатът с името на модула се отхвърля
    • всички двойни двоеточия, ако има, се заменят със слешове
    • добавя се разширение .pp

Ще демонстрирам с пример. Да предположим, че пиша модул nginx. В него има следните ресурси:

  • клас nginx описан в манифеста init.pp;
  • клас nginx::service описан в манифеста service.pp;
  • дефайн nginx::server описан в манифеста server.pp;
  • дефайн nginx::server::location описан в манифеста server/location.pp.

Шаблони

Сигурен съм, че и вие знаете какво представляват шаблоните, няма да ги обяснявам подробно тук. Но във всеки случай ще оставя линк към Уикипедия.

Как да използваме шаблони: стойността на шаблона може да бъде разкрита с помощта на функция template, на която се предава пътят към шаблона. За ресурси от тип файл използваме заедно с параметъра content. Например, така:

file { '/tmp/example': content => template('modulename/templatename.erb')

Пътят от вида / предполага файл /modules//templates/.

Освен това, има функция inline_template — на нея се предава текстът на шаблона, а не името на файла.

Вътре в шаблоните може да се използват всички променливи на Puppet в текущата област на видимост.

Puppet поддържа шаблони в формати ERB и EPP:

Накратко за ERB

Управляващи конструкции:

  • <%= ВЫРАЖЕНИЕ %> — да се вмъкне стойността на израза
  • <% ВЫРАЖЕНИЕ %> — да се изчисли стойността на израза (без да се вмъква). Тук обикновено влизат условни оператори (if), цикли (each).
  • <%# КОММЕНТАРИЙ %>

Изразите в ERB се пишат на Ruby (всъщност, ERB — това е Embedded Ruby).

За достъп до променливи от манифеста трябва да добавите @ к името на променливата. За да премахнете новия ред, появяващ се след управляващата конструкция, трябва да използвате затварящия таг -%>.

Пример за използване на шаблон

Да предположим, че пиша модул за управление на ZooKeeper. Класът, отговорен за създаването на конфигурацията, изглежда приблизително така:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure  => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

А съответният му шаблон zoo.cfg.erb — изглежда така:

0 -%>

server.=":::"



dataDir=


=""

Факти и вложени променливи

Често конкретната част от конфигурацията зависи от това, какво се случва в момента на ноду. Например, в зависимост от версията на Debian, която е инсталирана, трябва да се инсталира съответната версия на пакета. Може да следите всичко това ръчно, пренаписвайки манифестите при промяна на нодовете. Но този подход не е сериозен, автоматизацията е много по-добра.

За получаване на информация за нодовете в Puppet има механизъм, наречен факти. Фактите са информация за нода, налична в манифестите под формата на обикновени променливи в глобалното пространство от имена. Например, име на хост, версия на операционната система, архитектура на процесора, списък с потребители, списък на мрежовите интерфейси и техните адреси и много, много друго. Фактите са налични в манифестите и шаблоните като обикновени променливи.

Пример за работа с факти:

notify { "Работеща ОС ${facts['os']['name']} версия ${facts['os']['release']['full']}": }
# ресурс от типа notify просто извежда съобщение в лог-a

Формално казано, фактът има име (низ) и стойност (налични са различни типове: низове, масиви, речници). Има набор вградени факти. Може също така да пишете свои собствени. Събирачите на факти се описват като функции на Ruby, или като изпълними файлове. Също така фактите могат да бъдат представени под формата на текстови файлове с данни на нодовете.

По време на работа Puppet агентът първо копира от Puppet сървъра на нода всички налични събирачи на факти, след което ги стартира и изпраща на сървъра събраните факти; чак след това сървърът започва компилация на каталога.

Фактите под формата на изпълними файлове

Тези факти се поставят в модули в директория facts.d. Разбира се, файловете трябва да са изпълними. При стартиране те трябва да извеждат на стандартния изход информация или във формат YAML, или във формат 'ключ=стойност'.

Не забравяйте, че фактите се разпространяват на всички нодове, които са под управлението на Puppet сървъра, на който се разгръща вашият модул. Затова в скрипта се погрижете да проверите, че в системата има всички необходими програми и файлове за работата на вашия факт.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Фактите на Ruby

Тези факти се поставят в модули в директория lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Текстовите факти

Тези факти се поставят на нодовете в директория /etc/facter/facts.d в стария Puppet или /etc/puppetlabs/facts.d в новия Puppet.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Обращение към фактите

Можете да се обърнете към фактите по два начина:

  • чрез речник $facts: $facts['fqdn'];
  • използвайки името на факта като име на променлива: $fqdn.

Най-добре е да използвате речник $facts, а още по-добре е да посочите глобалното пространство от имена ($::facts).

Ето необходимия раздел от документацията.

Вградени променливи

Освен фактите, има и някои променливи, достъпни в глобалното пространство от имена.

  • доверени факти — променливи, които се взимат от сертификата на клиента (тъй като сертификатът обикновено се издава на папет-сервера, агентът не може просто да смени сертификата си, затова променливите са "доверени"): името на сертификата, името на хоста и домейна, разширенията от сертификата.
  • серверни факти — променливи, свързани с информацията за сървера — версия, име, IP адрес на сървера, среда.
  • агентски факти — променливи, добавени директно от puppet-agent, а не от facter — името на сертификата, версията на агента, версията на папета.
  • основни променливи — променливи на папетмастера (sic!). Там е нещо подобно на серверни факти, плюс налични стойности на конфигурационни параметри.
  • компилаторски променливи — променливи на компилатора, които се различават в различните области на видимост: името на текущия модул и името на модула, в който е направено извикването към текущия обект. Те могат да се използват, например, за да се провери, че вашите частни класове не се използват директно от други модули.

Допълнение 1: как да стартираме и дебъгваме всичко това?

В статията имаше много примери за puppet-код, но се говореше съвсем малко за това как да се стартира този код. Какво ж, поправям се.

За работа с Puppet е достатъчен агент, но за повечето случаи ще е нужен и сървър.

Агент

Най-малко от пета версия пакетите puppet-agent от официалното хранилище на Puppetlabs съдържат в себе си всички зависимости (ruby и съответните gem'ове), така че няма трудности с инсталацията (говоря за дистрибуции, базирани на Debian — не ползваме RPM-базирани дистрибуции).

В най-простия случай за прилагане на puppet-конфигурация е достатъчно да стартирате агента в безсървърен режим: при условие, че puppet-кодът е копиран на нодата, стартирате puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

Разбира се, по-добре е да се вдигне сървър и да се пуснат агентите на нодовете в режим на демон — тогава на всеки половин час те ще прилагат конфигурацията, изтеглена от сървъра.

Може да се имитира push-модел на работа — да влезете на интересуващия ви нод и да стартирате sudo puppet agent -t. Ключът -t (--test) всъщност включва няколко опции, които могат да се включват и поотделно. Сред тези опции са следните:

  • да не работи в режим на демон (по подразбиране агентът се стартира в режим на демон);
  • да завърши работа след прилагане на каталога (по подразбиране агентът продължава работа и прилага конфигурацията на всеки половин час);
  • да пише подробен лог на работата;
  • да показва измененията в файловете.

Агентът има режим на работа без изменения — може да се ползва в случай, че не сте сигурни, че сте написали правилната конфигурация и искате да проверите какво точно ще промени агентът по време на работа. Този режим се включва с параметъра --noop в командния ред: sudo puppet agent -t --noop.

Освен това, може да се включи отладъчен лог на работата — в него puppet пише за всички действия, които извършва: за ресурса, който в момента обработва, за параметрите на този ресурс, за програмите, които стартира. Разбира се, това е параметър --debug.

Сървър

Не смятам да разглеждам в тази статия пълната настройка на puppets сървър и деплой на него код, ще кажа само, че от кутията се инсталира напълно работеща версия на сървъра, която не изисква допълнителна настройка за работа в условията на малък брой ноди (да кажем, до сто). По-голям брой ноди вече ще изискват тунинг — по подразбиране puppetserver стартира не повече от четири работника, за по-голяма производителност трябва да се увеличи броят им и да не се забравят лимитите на паметта, в противен случай голяма част от времето сървърът ще прави garbage collect.

Деплой на код — ако трябва бързо и просто, вижте (на r10k)[https://github.com/puppetlabs/r10k], за малки инсталации това би трябвало да е достатъчно.

Допълнение 2: препоръки за писане на код

  1. Изнасяйте цялата логика в класове и дефиниции.
  2. Дръжте класовете и дефинициите в модули, а не в манифестите с описание на нодите.
  3. Използвайте фактите.
  4. Не правете if конструкции по хостимен.
  5. Не се колебайте да добавяте параметри за класовете и дефинициите — това е по-добре от неявната логика, скрита в тялото на класа/дефиницията.

А защо препоръчвам да го правите по този начин — ще обясня в следващата статия.

Заключение

На този етап завършваме с въведението. В следващата статия ще говоря за Hiera, ENC и PuppetDB.

Само регистрирани потребители могат да участват в анкетата. Влезте, моля.

Всъщност материалите са много повече — мога да напиша статии на следните теми, гласувайте за това, за какво ви е интересно да прочетете:

  • 59,1%Напреднали конструкти на Puppet — малко следващо ниво: цикли, мапинг и други ламбда изрази, колектори на ресурси, експортирани ресурси и междухостово взаимодействие чрез Puppet, тагове, доставчици, абстрактни типове данни.
  • 31,8%«Аз съм админ на мамка» или как в Авито събрахме заедно няколко Puppet сървъра с различни версии, а и принципно част за администрирането на Puppet сървъра.
  • 81,8%Как пишем код за Puppet: инструментална обвивка, документация, тестване, CI/CD.

Гласували са 22 потребители. Въздържали са се 9 потребители.

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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster