Hyrje në Puppet

Puppet — një sistem menaxhimi të konfigurimit. Përdoret për të sjellë hostet në gjendjen e dëshiruar dhe për ta mbajtur atë gjendje.

Kam punuar me Puppet për më shumë se pesë vjet. Ky tekst është një përmbledhje esenciale e pikave kyçe nga dokumentacioni zyrtar që do t’u ndihmojë fillestarëve të kuptojnë shpejt thelbin e Puppet.

Hyrje në Puppet

Informacion bazik

Skema e punës së Puppet është klient-server, megjithatë mbështetet edhe një variant pune pa server me funksionalitet të kufizuar.

Përdor një model të punës pull: në mënyrë default çdo gjysmë ore klientët i drejtohen serverit për konfigurim dhe e zbatojnë atë. Nëse keni punuar me Ansible, aty përdoret një model tjetër, push: administratori iniciaton procesin e zbatuar të konfigurimit, klientët vetë nuk do të zbatojnë asgjë.

Gjatë komunikimit në rrjet përdoret TLS-kriptimi dyshes: si serveri ashtu edhe klienti kanë çelësat e tyre private dhe certifikatat përkatëse. Zakonisht, serveri lëshon certifikata për klientët, por në principe është e mundur të përdoren edhe CA të jashtme.

Njoftimi me manifestet

Në terminologjinë e Puppet klientët e serverit të Puppet lidhen nodet (noded). Konfigurimi për nodet shkruhet në manifestet në një gjuhë specifike programimi — Puppet DSL.

Puppet DSL është një gjuhë deklarative. Në të përshkruhet gjendja e dëshiruar e nodit nëpërmjet shpalljes së burimeve të veçanta, për shembull:

  • Një skedar ekziston dhe ka përmbajtje të caktuar.
  • Një paketë është instaluar.
  • Një shërbim është aktivizuar.

Burimet mund të jenë të ndërlidhura:

  • Ekzistojnë varësi, ato ndikojnë në rendin e zbatimit të burimeve.
    Për shembull, "së pari instaloni paketën, pastaj rregulloni skedarin e konfigurimit, pas kësaj aktivizoni shërbimin".
  • Ekzistojnë njoftime — nëse një burim ndryshon, ai dërgon njoftime tek burimet e regjistruara për të.
    Për shembull, nëse ndryshohet skedari i konfigurimit, mund të rindezësh automatikisht shërbimin.

Për më tepër, në Puppet DSL ka funksione dhe variabla, si dhe operatorë kushtorë dhe selektorë. Po ashtu, mbështeten mekanizma të ndryshëm të modelimit — EPP dhe ERB.

Puppet është shkruar në Ruby, prandaj shumë konstruktete dhe terma janë marrë nga aty. Ruby lejon zgjerimin e Puppet — të shkruani logjikë të ndërlikuar, tipa të rinj burimesh, funksione.

Gjatë punës, manifestet për secilën nodë të veçantë në server kompilohen në një katalog. Katalóg — është një listë burimesh dhe lidhjesh të tyre pas llogaritjes së vlerave të funksioneve, variablave dhe hapjes së operatorëve kushtorë.

Sintaksa dhe stilimi i kodit

Ja seksionet e dokumentacionit zyrtar që do t'ju ndihmojnë të kuptoni sintaksën, nëse shembujt e dhënë nuk janë të mjaftueshëm:

Ja një shembull se si duket një manifest:

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

E hyra dhe përkthimi i rreshtave nuk janë një pjesë e detyrueshme e manifestit, megjithatë ekziston një udhëzues i rekomanduar i stilit:Përmbledhje:

  • Hapësirat dyfish, tabulat nuk përdoren.
  • Kllapat { ndahen nga një hapësirë, ikona e dypikës nuk ndahet nga një hapësirë.
  • Vizatimi i sa mëposhtë: pas çdo parametri, përfshirë të fundit. Çdo parametër — në një linjë të veçantë. Përjashtim bëhet për rastin pa parametra dhe një parametër: mund të shkruhet në një linjë dhe pa vizatimin e fundit (p.sh. resource { 'title': } dhe resource { 'title': param => value }).
  • Shigjetat e parametrave duhet të jenë në të njëjtin nivel.
  • Shigjetat e lidhjeve të burimeve shkruhen përpara tyre.

Pozicioni i skedareve në serverin e Puppet

Për shpjegime të mëtejshme, do të fut një koncept "direktorja rrënjë". Direktorja rrënjë është direktorja ku ndodhet konfigurimi i Puppet për një nodë të veçantë.

Direktorja rrënjë ndryshon në varësi të versionit të Puppet dhe përdorimit të ambienteve. Ambientet janë grupe të pavarura konfigurimi që ruhten në direktoriet e ndara. Zakonisht përdoren në kombinim me git, në këtë rast ambientet krijohen nga degët e git. Përkatësisht, çdo nodë ndodhet në një ambient të caktuar. Kjo konfigurohet në nodë vetë, ose në ENCs, për të cilin do të flas në artikullin tjetër.

  • Në versionin e tretë ("Puppet i vjetër") direktorja e bazës ishte /etc/puppet. Përdorimi i ambienteve është opcional — ne, për shembull, nuk i përdorim me Puppet e vjetër. Nëse përdoren ambientet, zakonisht ato ruhen në /etc/puppet/environments, direktorja rrënjë do të jetë direktoria e ambientit. Nëse ambientet nuk përdoren, direktorja rrënjë do të jetë baza.
  • Duke filluar nga versioni i katërt ("Puppet i ri") përdorimi i ambienteve është bërë i detyrueshëm, dhe direktorja bazë është zhvendosur në /etc/puppetlabs/code. Prandaj, ambientet ruhen në /etc/puppetlabs/code/environments, direktorja kryesore është direktorja e mjedisit.

Në direktorën kryesore duhet të jetë një nën-direktor. manifests, ku ndodhen një ose disa manifesto që përshkruajnë nodet. Për më tepër, duhet të ketë një nën-direktor modules, ku ndodhen modulët. Çfarë janë modulët, do të flas më vonë. Për më tepër, në Pappetën e vjetër gjithashtu mund të ketë një nën-direktor files, ku ndodhen skedarë të ndryshëm që ne kopjojmë në nodet. Në Pappetën e re, të gjitha skedarët janë transferuar në modulë.

Skedarët e manifesto kanë zgjerimin .pp.

Një çift shembuj praktikë

Përshkrimi i nodit dhe burimeve në të

Në nodë server1.testdomain duhet të krijohet një skedar /etc/issue me përmbajtjen Debian GNU/Linux n l. Skedari duhet t'i përkasë përdoruesit dhe grupit root, të drejtat e aksesit duhet të jenë 644.

Shkruajmë manifestin:

node 'server1.testdomain' {   # blloku i konfigurimit që i përket nodit server1.testdomain
    file { '/etc/issue':   # përshkruajmë skedarin /etc/issue
        ensure  => present,   # ky skedar duhet të ekzistojë
        content => 'Debian GNU/Linux n l',   # ai duhet të ketë këtë përmbajtje
        owner   => root,   # përdoruesi pronar
        group   => root,   # grupa pronare
        mode    => '0644',   # të drejtat mbi skedar. Ato janë gjithmonë përshkruar si një varg (në thonjëza), sepse ndryshe numri me 0 në fillim do të perceptohet si i shkruar në sistemin oktal, dhe gjithçka do të shkojë ndryshe nga si është menduar
    }
}

Marrëdhëniet e burimeve në nodë

Në nodë server2.testdomain duhet të jetë i instaluar nginx, që punon me konfigurimin e përgatitur më parë.

Dekonstruojmë detyrën:

  • Duhet të instalohet paketa nginx.
  • Duhet të kopjohen skedarët e konfigurimit nga serveri.
  • Duhet të nisë shërbimi nginx.
  • Në rastin e përditësimit të konfigurimeve, duhet të ri-nisni shërbimin.

Shkruajmë manifestin:

node 'server2.testdomain' {   # blloku i konfigurimit që i përket nodit server2.testdomain
    package { 'nginx':   # përshkruajmë paketën nginx
        ensure => installed,   # ai duhet të jetë instaluar
    }
  # Arrow direkt (-&) tregon se burimi në vijën më poshtë duhet
  # të krijohet pas burimit të përshkruar më sipër.
  # Këto varësi janë tranzitive.
    -> file { '/etc/nginx':   # përshkruajmë skedarin /etc/nginx
        ensure  => directory,   # kjo duhet të jetë një direktor
        source  => 'puppet:///modules/example/nginx-conf',   # përmbajtja e saj duhet të merret nga serveri i puppet në adresën e dhënë
        recurse => true,   # kopjoni skedarët në mënyrë rekursive
        purge   => true,   # duhet të hiqni skedarët e tepërt (ato që nuk janë në burim)
        force   => true,   # hiqni direktorët e tepërt
    }
  # Arrow valë (~&) tregon se burimi në vijën më poshtë duhet
  # të regjistrohet pas ndryshimeve në burimin e përshkruar më sipër.
  # Arrow valë përmban edhe direktën (->).
    ~> service { 'nginx':   # përshkruajmë shërbimin nginx
        ensure => running,   # ai duhet të jetë duke punuar
        enable => true,   # ai duhet të nisë automatikisht kur sistemi startohet
    }
  # Kur një burim i tipit shërbim merr njoftim,
  # shërbimi përkatës ri-nisët.
}

Për të punuar kështu, duhet një vendosje e tillë e skedarëve në serverin puppet:

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

Llojet e burimeve

Lista e plotë e llojeve të mbështetura të burimeve ndodhet në dokumentacion, këtu do të përshkruaj pesë lloje themelore, të cilat në praktiken time janë të mjaftueshme për zgjidhjen e shumicës së detyrave.

file

Menaxhon skedarët, direktorët, lidhjet simbolike, përmbajtjen e tyre dhe të drejtat e aksesit.

Parametrat:

  • emri i burimit — rruga e skedarit (opsionale)
  • path — rruga e skedarit (nëse nuk është dhënë në emër)
  • ensure — tipi i skedarit:
    • absent — fshij skedarin
    • pranishëm — duhet të jetë skedar i çdo lloji (nëse skedari nuk ekziston, do të krijohet një skedar i zakonshëm)
    • file — skedar i zakonshëm
    • directory — direktor
    • link — lidhje simbolike
  • content — përmbajtja e skedarit (përshtatet vetëm për skedarët e zakonshëm, nuk mund të përdoret së bashku me source ose target)
  • source — lidhja në rrugën nga e cila duhet të kopjohet përmbajtja e skedarit (nuk mund të përdoret së bashku me content ose target). Mund të jepet si një URI me skemën puppet: (atëherë do të përdoren skedarët nga serveri i puppet), ashtu edhe me skemën http: (shpresoj të jetë e qartë se çfarë do të ndodhë në këtë rast), dhe gjithashtu me skemën file: apo si një rrugë absolute pa skemë (atëherë do të përdoret skedari nga sistemi lokal në nodë)
  • target — ku duhet të tregojë lidhja simbolike (nuk mund të përdoret së bashku me content ose source)
  • owner — përdoruesi, të cilit duhet t'i përkasë skedari
  • grup — grupi, të cilit duhet t'i përkasë skedari
  • mode — të drejtat mbi skedarin (në formën e një vargu)
  • recurse — përfshin përpunimin rekursiv të direktorëve
  • purge — përfshin heqjen e skedarëve që nuk përshkruhen në Puppet
  • force — përfshin heqjen e direktorëve që nuk përshkruhen në Puppet

paketë

Instalon dhe heq paketat. Mund të trajtojë njoftimet - ri-instaloni paketën nëse është caktuar parametri reinstall_on_refresh.

Parametrat:

  • emri i burimit — emri i paketës (opsionale)
  • emri — emri i paketës (nëse nuk është caktuar në emër)
  • provider — menaxher pakete që duhet të përdoret
  • ensure — gjendja e dëshiruar e paketës:
    • pranishëm, installed — çdo version i instaluar
    • latest — versioni më i fundit i instaluar
    • absent — hequr (apt-get remove)
    • por hequr — është larguar së bashku me skedarët e konfigurimit (apt-get purge)
    • mbajtur — versione e paketës e bllokuar (apt-mark hold)
    • çdo rresht tjetër — është instaluar të dhënë versioni
  • reinstall_on_refresh — nëse e vërtetë, atëherë me marrjen e njoftimit, paketa do të riinstalohet. E dobishme për shpërndarje me bazë source, ku rikonstruksioni i paketimeve mund të jetë e nevojshme kur ndryshojnë parametrat e ndërtimit. Në mënyrë të paracaktuar false.

service

Menaxhon shërbimet. Mund të përpunojë njoftime - ripërtërin shërbimin.

Parametrat:

  • emri i burimit — shërbimi që duhet të menaxhohet (opsionale)
  • emri — shërbimi që duhet të menaxhohet (nëse nuk është specifikuar në emër)
  • ensure — gjendja e dëshiruar e shërbimit:
    • running — i nisur
    • stopped — i ndaluar
  • enable — menaxhon mundësinë e nisjes së shërbimit:
    • e vërtetë — nisja automatike është e aktivizuar (systemctl enable)
    • mask — e maskuar (systemctl mask)
    • false — nisja automatike është e çaktivizuar (systemctl disable)
  • restart — komandë për ripërtëritjen e shërbimit
  • status — komandë për kontrollin e statusit të shërbimit
  • hasrestart — specifikon nëse skripti i init të shërbimit mbështet ripërtëritjen. Nëse false dhe parametri është specifikuar restart — përdoret vlera e këtij parametri. Nëse false dhe parametri restart nuk është specifikuar - shërbimi ndalohet dhe rifillon për ripërtëritje (por në systemd përdoret komanda systemctl restart).
  • hasstatus — specifikon nëse skripti i init të shërbimit mbështet komandën status. Nëse false, atëherë përdoret vlera e parametrit status. Në mënyrë të paracaktuar e vërtetë.

exec

Nis komandat e jashtme. Nëse nuk specifikoni parametrat creates, onlyif, unless ose refreshonly, komanda do të ekzekutohet në çdo kalim të Puppet. Mund të përpunojë njoftime - dëgjon komandën.

Parametrat:

  • emri i burimit — komanda që duhet të ekzekutohet (opsionale)
  • command — komanda që duhet të ekzekutohet (nëse nuk është specifikuar në emër)
  • path — rrugët ku të kërkohet skedari eksekuese
  • onlyif — nëse komanda e specifikuar në këtë parametrin përfundon me kodin e kthimit zero, komanda kryesore do të ekzekutohet
  • unless — nëse komanda e specifikuar në këtë parametrin përfundon me kodin e kthimit jo zero, komanda kryesore do të ekzekutohet
  • creates — nëse skedari i specifikuar në këtë parametrin nuk ekziston, komanda kryesore do të ekzekutohet
  • refreshonly — nëse e vërtetë, atëherë komanda do të ekzekutohet vetëm në rast se ky exec merr njoftimin nga burime të tjera
  • cwd — direktoria nga e cila duhet të ekzekutohet komanda
  • përdorues — përdoruesi nga i cili duhet të ekzekutohet komanda
  • provider — me çfarë duhet të ekzekutohet komanda:
    • posix — thjesht krijon një proces të ri, duhet të specifikohet path
    • shell — komanda ekzekutohet në shell /bin/sh, mund të mos specifikohet path, mund të përdoren globbing, piping dhe tipare të tjera të shell-it. Zakonisht përcaktohet automatikisht, nëse ka simbole speciale (|, ;, &&, || etj).

cron

Menaxhon punët e crontab.

Parametrat:

  • emri i burimit — thjesht një identifikues
  • ensure — gjendja e punës së crontab:
    • pranishëm — krijohet nëse nuk ekziston
    • absent — fshihet nëse ekziston
  • command — cilën komandë do të ekzekutohet
  • environment — në cilin ambient do të ekzekutohet komanda (lista e variableve të mjedisit dhe vlerave të tyre përmes =)
  • përdorues — nga cili përdorues do të ekzekutohet komanda
  • minute, ora, dita e javës, muaj, dita e muajit — kur do të ekzekutohet crontab. Nëse ndonjë nga këta atribute nuk është specifikuar, vlera e tij në crontab do të jetë *.

Në Puppet 6.0 cron siç ka është hequr nga kuti në puppetserver, prandaj nuk ka dokumentacion në faqen e përgjithshme. Por ajo është në kuti në puppet-agent, prandaj nuk ka nevojë ta instaloni veçmas. Dokumentacionin për të mund ta shihni në dokumentacionin për versionin e pestë të Puppet, ose në GitHub.

Për burimet në përgjithësi

Kërkesat për unikësinë e burimeve

Gabimi më i zakonshëm me të cilin përballemi është — Deklaratë e dyfishtë. Ky gabim ndodh kur dy ose më shumë burime të njëjtit lloj bien në katalog me të njëjtin emër.

Prandaj, do ta them përsëri: në manifestet për një nodë nuk duhet të ketë burime të njëjtit lloj me të njëjtin emër (titulli)!

Nd sometimes e nevojshme të instalohet paketa me të njëjtin emër, por me menaxherë të ndryshëm paketash. Në këtë rast, duhet të përdorni parametrin emri, për të shmangur gabimin:

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

Në lloje të tjera burimesh ka parametrat analogë që ndihmojnë në shmangien e dublimeve, — emri u service, command u exec, dhe kështu me radhë.

Metaparametrat

Disa parametra të veçantë ka çdo lloj burimi, pavarësisht nga entiteti i tij.

Lista e plotë e metaparametrave në dokumentacionin e Puppet.

Lista përmbledhëse:

  • require — në këtë parametër shkruhen burimet nga të cilat varet ky burim.
  • para — në këtë parametër shkruhen burimet të cilat varen nga ky burim.
  • subscribe — në këtë parametër shkruhen burimet nga të cilat ky burim merr njoftime.
  • notify — në këtë parametër shkruhen burimet që marrin njoftime nga ky burim.

Të gjithë metaparametrat e përmendur pranojnë ose një lidhje në burim, ose një array lidhjesh në katrorët [ ] .

Ссылки на ресурсы

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

Синтаксис у ссылки следующий: тип ресурса с большой буквы (если в названии типа содержатся двойные двоеточия, то с большой буквы пишется каждая часть названия между двоеточиями), дальше в квадратных скобках название ресурса (регистр названия не меняется!). Пробелов быть не должно, квадратные скобки пишутся сразу после названия типа.

Shembuj:

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

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

Документация здесь.

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

В отличие от зависимостей, уведомления не транзитивны. Для уведомлений действуют следующие правила:

  • Если ресурс получает уведомление, он обновляется. Действия при обновлении зависят от типа ресурса — exec запускает команду, service перезапускает сервис, paketë переустанавливает пакет. Если для ресурса не определено действие при обновлении, то ничего не происходит.
  • За один прогон Паппета ресурс обновляется не больше одного раза. Это возможно, так как уведомления включают в себя зависимости, а граф зависимостей не содержит циклов.
  • Если Паппет меняет состояние ресурса, то ресурс отправляет уведомления всем подписанным на него ресурсам.
  • Если ресурс обновляется, то он отправляет уведомления всем подписанным на него ресурсам.

Обработка неуказанных параметров

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

Знакомство с классами, переменными и дефайнами

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

Для решения такой проблемы есть такая конструкция, как klasa.

Классы

Klasa — это именованный блок паппет-кода. Классы нужны для переиспользования кода.

Сначала класс нужно описать. Само по себе описание не добавляет никуда никакие ресурсы. Класс описывается в манифестах:

# Описание класса начинается с ключевого слова 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',
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Variablat

Класс из предыдущего примера совсем не гибок, потому что он всегда приносит одну и ту же конфигурацию 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,   # këtu përdorim variablën në vend të një string-u të fikson
        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
}

Megjithatë, shembulli i dhënë është i dobët sepse ka një "njohuri sekrete" në lidhje me faktin se ndokund brenda klasës përdoret një variabël me një emër të caktuar. Më e drejtë do të ishte të bëhej kjo njohuri e përbashkët - klasa mund të ketë parametra.

Parametrat e klasës janë variabla në hapësirën e emrave të klasës, ata përcaktohen në krye të klasës dhe mund të përdoren si variabla të zakonshëm brenda klasës. Vlerat e parametrave specifikohen kur përdoret klasa në manifest.

Parametrit mund t'i jepet një vlerë për default. Nëse parametri nuk ka vlerë për default dhe nuk specifikohet një vlerë gjatë përdorimit, kjo do të shkaktojë një gabim kompilimi.

Le të parametrizojmë klasën nga shembulli i mësipërm dhe të shtojmë dy parametra: të parin, të detyrueshëm - rruga në konfigurim, dhe të dytin, jo të detyrueshëm - emri i paketës për nginx (për shembull, në Debian ka paketa 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',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

Në Puppet, variablat janë të tipizuara. Ka shumë lloje të të dhënave. Llojet e të dhënave zakonisht përdoren për të validuar vlerat e parametrave që kalohen në klasa dhe definita. Nëse parametri i dhënë nuk është në përputhje me llojin e specifikuar, do të ndodhë një gabim kompilimi.

Lloji shkruhet menjëherë përpara emrit të parametrave:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Klasat: përfshini emrin e klasës vs class{'classname':}

Çdo klasë është një burim i tipit class. Ashtu si në rastin e llotave të tjera të burimeve, në një nodë nuk mund të ketë dy instanca të të njëjtës klasë.

Nëse provoni të shtoni klasën në të njëjtën nodë dy herë duke përdorur class { 'classname':} (pa marrë parasysh nëse janë me parametra të ndryshëm ose të njëjtë), do të ketë një gabim kompilimi. Mirëpo, në rastin e përdorimit të klasës në stilin e burimit, mund të specifikoni qartë të gjitha parametrat e saj në manifest.

Megjithatë, nëse përdoret include, atëherë klasa mund të shtohet sa herë të dëshironi. Çështja është se include është një funksion idempotent që kontrollon nëse klasa është shtuar në katalogn. Nëse klasa nuk është në katalog - e shton, dhe nëse tashmë ekziston, nuk bën asgjë. Por në rastin e përdorimit të include nuk është e mundur të specifikoni parametrat e klasës gjatë shpalljes së klasës - të gjithë parametrat e detyrueshëm duhet të shihen nga një burim të dhënash të jashtëm - Hiera ose ENC. Për ta diskutuam në artikullin tjetër.

Definat

Siç u përmend në bllokun e mëparshëm, të njëjtën klasë nuk mund të jetë në një nodë më shumë se një herë. Megjithatë, në disa raste nevojitet të keni mundësinë për të aplikuar të njëjtin bllok kod me parametra të ndryshme në një nodë. Në fjalë të tjera, ka një nevojë për një lloj të vetë burimit.

Për shembull, për të instaluar një modifikues PHP, ne në Avito bëjmë si më poshtë:

  1. Instalojmë paketën me këtë modifikues.
  2. Krijojmë një skedar konfigurimi për këtë modifikues.
  3. Krijojmë një simlink për konfigurimin për php-fpm.
  4. Krijojmë një simlink për konfigurimin për php cli.

Në raste të tilla përdoret një ndërtim i tillë si definimi (definimi, tip i definuar, tip burimi e definuar). Definimi është i ngjashëm me klasën, por ka dallime: së pari, çdo definim është një tip burimi, jo një burim; së dyti, çdo definim ka një parametër të implicit $title, ku emri i burimit hyn gjatë shpalljes së tij. Ashtu si në rastin e klasave, së pari duhet të përshkruani definimin, pas së cilës mund ta përdorni.

Një shembuj i thjeshtë me modulin për 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'],  # triggers e paketave php të debian vetë krijojnë simlinks dhe rilaktojnë shërbimin php-fpm - ne nuk kemi nevojë për këtë, pasi simlinks dhe shërbimin e menaxhojmë me 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' }
}

Në definim është më e thjeshtë të kapësh gabimin e Deklaratës së Dubluar. Kjo ndodh nëse në definim ka një burim me një emër konstant, dhe në ndonjë nodë ka dy ose më shumë instanca të këtij definimi.

Mund t'i mbrosh nga kjo lehtë: të gjitha burimet brenda definicioni duhet të kenë një emër që varet nga $title. Si alternativë – shtimi idempotent i burimeve, në rastin më të thjeshtë është mjaft të nxjerrësh burimet e përbashkëta për të gjithë instancat e definicionit në një klasë të veçantë dhe ta përfshish atë në definicion – funksioni include është idempotent.

Ka edhe mënyra të tjera për të arritur idempotencën gjatë shtimit të burimeve, përkatësisht përdorimi i funksioneve defined dhe ensure_resources, por për këtë do të flas në serinë e ardhshme.

Varësitë dhe njoftimet për klasat dhe definicionet

Klasat dhe definicionet shtojnë rregulla të mëposhtme për trajtimin e varësive dhe njoftimeve:

  • varësia e një klase/definimi shton varësi nga të gjitha burimet e klasës/definimit;
  • varësia e klasës/definimit shton varësi të gjitha burimeve të klasës/definimit;
  • njoftimi i klasës/definimit njofton të gjitha burimet e klasës/definimit;
  • abonimi në klasë/definim abonon në të gjitha burimet e klasës/definimit.

Operatorët kondicional dhe selektorët

Документация здесь.

nëse

Këtu gjithçka është e thjeshtë:

if SHPREHJA1 {
  ...
} elsif SHPREHJA2 {
  ...
} else {
  ...
}

unless

unless është e kundërta e if: blloku i kodit do të ekzekutohet nëse shprehja është e gabuar.

unless SHPREHJA {
  ...
}

rast

Këtu gjithashtu nuk ka asgjë të komplikuar. Si vlera mund të përdoren vlera të zakonshme (string, numra dhe kështu me radhë), shprehje të zakonshme, si dhe tipe të dhënash.

case SHPREHJA {
  VLERË1: { ... }
  VLERË2, VLERË3: { ... }
  default: { ... }
}

Seletorët

Seletori është një konstrukt gjuhësor, i ngjashëm me rast, por në vend të ekzekutimit të një blloku kodi, ai kthen një vlerë.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Module

Kur konfigurimi është i vogël, është e lehtë ta mbash në një manifest. Por sa më shumë konfigurim të përshkruajmë, aq më shumë klasa dhe nënmbidha shtohen në manifest, ai zgjeron, dhe bëhet e vështirë të punosh me të.

Për më tepër, ka një problem me ripërdorimin e kodit – kur të gjithë kodi është në një manifest, është e vështirë të ndash këtë kod me të tjerët. Për të zgjidhur këto dy probleme, në Puppet ekziston një entitet i tillë si modulet.

Module — janë grupe klasash, definimesh dhe entitetesh të tjera të Puppet, të shpërndara në një direktor të veçantë. Në terma të tjerë, një modul është një copë e pavarur e logjikës së Puppet. Për shembull, mund të ketë një modul për të punuar me nginx, dhe në të do të jetë vetëm ajo që nevojitet për punën me nginx, dhe mund të ketë një modul për të punuar me PHP, etj.

Modulet versionohen, po ashtu mbështeten varësitë e moduleve nga njëri-tjetri. Ka një depo të hapur për modulet — Puppet Forge.

Në serverin puppet, modulet ndodhen në nën direktorinë modules të drejtorisë rrënjësore. Brenda secilit modul, skema standarde e drejtorive është – manifests, files, templates, lib dhe kështu me radhë.

Struktura e skedarëve në modul

Në rrënjën e modulit mund të jenë drejtoritë e mëposhtme me emra që flasin vetë:

  • manifests — në të ndodhen manifestet
  • files — në të ndodhen skedarët
  • templates — në të ndodhen shabllonat
  • lib — në të ndodhet kodi Ruby

Ky është një listë jo e plotë e drejtorive dhe skedarëve, por për këtë artikull është mjaft.

Emrat e burimeve dhe emrat e skedarëve në modul

Документация здесь.

Burimet (klasa, definime) në modul nuk mund të quhen si të duash. Për më tepër, ka një përputhje direkte midis emrit të burimit dhe emrit të skedarit ku Puppet do të kërkojë përshkrimin e këtij burimi. Nëse shkelin rregullat e emërtimit, atëherë Puppet thjesht nuk do ta gjejë përshkrimin e burimeve, dhe do të rezultojë një gabim kompilimi.

Rregullat janë të thjeshta:

  • Të gjitha burimet në modul duhet të jenë në hapësirën e emrave të modulit. Nëse moduli quhet foo, të gjitha burimet në të duhet të quhen foo::, ose thjesht foo.
  • Burimi me emrin e modulit duhet të jetë në skedarin init.pp.
  • Për burimet e tjera, skema e emërtesës së skedarëve është si vijon:
    • prefiksi me emrin e modulit është hequr
    • të gjitha dy dyfishta, nëse ka, zëvendësohen me shkronja
    • shtohet zgjerimi .pp

Të tregoj një shembull. Supozoni, unë po shkruaj modul nginx. Në të ka këto burime:

  • klasa nginx përshkruar në manifest init.pp;
  • klasa nginx::service përshkruar në manifest service.pp;
  • definimi nginx::server përshkruar në manifest server.pp;
  • definimi nginx::server::location përshkruar në manifest server/location.pp.

Shabllonat

Një siguri e tillë, ju e dini, çfarë janë shabllonat, nuk do ta zgjas këtu, por për rastin do të lë një lidhje në Wikipedia.

Si të përdorni shabllonat: vlera e shabllonit mund të zbulohet me funksionin template, i cili merr si argument rrugën për shabllon. Për burime të tipit file këtu përdoret së bashku me parametrin content. Për shembull, kështu:

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

Rruga në formën / nënkupton skedarin /modules//templates/.

Për më tepër, ekziston një funksion inline_template — ai merr tekstin e shabllonit si input, jo emrin e skedarit.

Brenda shablloneve mund të përdoren të gjitha variablat e Puppet në hapësirën aktuale të emrave.

Puppet mbështet shabllone në formatin ERB dhe EPP:

Këtu shkurtimisht për ERB

Struktura kontrolluese:

  • <%= ВЫРАЖЕНИЕ %> — futni vlerën e shprehjes
  • <% ВЫРАЖЕНИЕ %> — llogaritni vlerën e shprehjes (pa e futur atë). Këtu zakonisht shkojnë operatorët kushtorë (if), ciklet (each).
  • <%# КОММЕНТАРИЙ %>

Shprehjet në ERB shkruhen në Ruby (në të vërtetë, ERB është Ruby i Futur).

Për të hyrë në variablat nga manifesti, duhet të shtoni @ në emrin e variablit. Për të hequr rreshtin e ri, i cili shfaqet pas strukturës kontrolluese, duhet të përdorni etiketën përmbyllëse -%>.

Shembulli i përdorimit të modelit

Le të themi se po shkruaj një modul për menaxhimin e ZooKeeper. Klasa që është përgjegjëse për krijimin e konfiguracionit duket më afër kësaj:

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'),
  }
}

Dhe modeli përkatës zoo.cfg.erb — është kështu:

0 -%>

server.=::;



dataDir=


=

Faktet dhe variablat e futur

Shpesh herë, një pjesë e caktuar e konfiguracionit varet nga ajo që po ndodh për momentin në nodë. Për shembull, në varësi të versionit të Debian-it, është e nevojshme të instaloni versionin përkatës të paketës. Mund të mbani nën kontroll të gjitha këto manualisht, duke riparë manifestet në rast të ndryshimit të nodave. Por ky është një qasje jo serioze, automatizimi është shumë më mirë.

Për të marrë informacionin mbi nodat në Puppet ka një mekanizëm të tillë, si faktet. Faktet — janë informacion mbi nodën, e cila është e disponueshme në manifestet si variabla të zakonshme në hapësirën e emrave globale. Për shembull, emri i hostit, versioni i sistemit operativ, arkitektura e procesorit, lista e përdoruesve, lista e ndërfaqeve të rrjetit dhe adresat e tyre, dhe shumë, shumë më tepër. Faktet janë të disponueshme në manifestet dhe formatet si variabla të zakonshme.

Shembulli i punës me faktet:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# Burimi i tipit notify thjesht shfaq një mesazh në log

Në mënyrë formale, një fakt ka emrin (string) dhe vlerën (disponohen lloje të ndryshme: strings, arrays, dictionaries). Ka një grup faktesh të futur. Gjithashtu, mund të shkruani tuajat. Mblidhësit e faktit përshkruhen si funksione në Ruby, ose si skedarë ekzekutues. Po ashtu, faktet mund të paraqiten si skedarë të dhënash në nodat.

Në procesin e punës, agjenti i puppetit së pari kopjon me puppets-server në nodë të gjitha mblidhësit e faktit të disponueshëm, pastaj i ekzekuton ato dhe dërgon faktet e mbledhura në server; vetëm pas kësaj, serveri fillon kompilimin e katalogut.

Faktet si skedarë ekzekutues

Këto fakte vendosen në module në direktorinë facts.d. Sigurisht, skedarët duhet të jenë të ekzekutueshëm. Gjatë ekzekutimit, ata duhet të shpërndajnë informacion në standardin e daljes, ose në formatin YAML, ose në formatin 'çelës=vlerë'.

Mos harroni se faktet përhapet në të gjitha nodet, të cilat janë nën menaxhimin e puppets-server, në të cilin do të publikoni modulin tuaj. Prandaj, në skript, duhet të kontrolloni që në sistem të ketë të gjitha programet dhe skedarët e nevojshëm për funksionimin e faktit tuaj.

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

Faktet në Ruby

Këto fakte vendosen në module në direktorinë 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

Faktet tekstuale

Këto fakte vendosen në nodat në direktorinë /etc/facter/facts.d në Puppet-in e vjetër ose /etc/puppetlabs/facts.d në Puppet-in e ri.

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

Qasja në fakte

Qasja në fakte mund të bëhet në dy mënyra:

  • nëpërmjet fjalorit $facts: $facts['fqdn'];
  • duke përdorur emrin e faktit si emrin e variablës: $fqdn.

Më së miri është të përdorni fjalorin $facts, edhe më mirë është të tregoni hapësirën e emrave globale ($::facts).

Ja seksi përkatës i dokumentacionit.

Variablat e futur

Përveç faktit, ka gjithashtu disa variabla, të cilat janë të disponueshme në hapësirën e emrave globale.

  • faktet e besuara — janë variabla që merren nga certifikata e klientit (pasi certifikata zakonisht lëshohet në puppets-server, agjenti nuk mund të ndryshojë thjesht certifikatën e tij, prandaj variablat janë 'të besuara'): emri i certifikatës, emri i hostit dhe i domenit, zgjedhjet nga certifikata.
  • faktet e serverit — variabla që lidhim me informacionin mbi serverin — versioni, emri, adresa IP e serverit, mjedisi.
  • faktet e agjentit — variabla që shtohen direkt nga puppet-agent, e jo nga facter — emri i certifikatës, versioni i agentit, versioni i puppetit.
  • variablat e masterit — variabla e puppets-master (sic!). Aty është rreth e njëjtë si në faktet e serverit, plus janë të disponueshme vlerat e parametrave të konfigurimit.
  • variablat e kompilimit — variabla të kompilatorit, të cilat ndryshojnë në çdo zonë të dukshmërisë: emri i modulit aktual dhe emri i modulit nga i cili është bërë referimi në objektin aktual. Ato mund të përdoren, për shembull, për të kontrolluar se klasat tuaja private nuk përdoren direkt nga modulet e tjera.

Shtesa 1: si ta nisni dhe ta debug-oni këtë gjithçka?

Në artikull ishte shumë shembuj të kodit puppet, por nuk u përmend fare se si të nisej ky kod. mirë, po e rregulloj.

Për të punuar me Puppet, mjafton agen, por për shumicën e rasteve do të nevojitet edhe një server.

Agenti

Të paktën nga versioni e pestë, paketat puppet-agent nga repozitorin zyrtar të Puppetlabs përmbajnë të gjitha varësitë (ruby dhe gem-at përkatëse), prandaj nuk ka asnjë vështirësi në instalim (po flas për distribucionet e bazuara në Debian — ne nuk përdorim distribucione të bazuara në RPM).

Në rastin më të thjeshtë, për të aplikuar konfigurimin puppet mjafton të nisni agjentin në modin pa server: me kushtin që kodi puppet të jetë kopjuar në nodë, ju nisni puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Njoftim: Katalogu i kompiluar për atikhonov.localdomain në ambientin prodhimit në 0.01 sekonda
Njoftim: Hello world!
Njoftim: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: e caktuar 'mesazhi' si 'Hello world!'
Njoftim: Aplikuar katalogu në 0.01 sekonda

Është më mirë, sigurisht, të ngrini server dhe të nisni agentët në nodë në modin demon - atëherë një herë në gjysmë ore ata do të aplikojnë konfigurimin, të përmirësuar nga serveri.

Mund të imitosh modelin push të punës - hyni në nodën që ju intereson dhe nisni sudo puppet agent -t. Çelësi -t (--test) në të vërtetë përfshin disa mundësi, të cilat mund të aktivizoni edhe veçmas. Ndër këto mundësi janë:

  • nuk punon në modin demon (në mënyrë default agjenti nisihet në modin demon);
  • të përfundojë punën pas aplikimit të katalogut (në mënyrë default agjenti do të vazhdojë të punojë dhe të aplikojë konfigurimin çdo gjysmë ore);
  • të shkruajë një log të detajuar të punës;
  • të tregojë ndryshimet në skedarë.

Agjenti ka një mod të punës pa ndryshime - mund ta përdorni kur nuk jeni të sigurt se keni shkruar një konfigurim të saktë dhe dëshironi të kontrolloni se çfarë do të ndryshojë agjenti gjatë punës. Ky mod aktivizohet me parametrin --noop në linjën e komandës: sudo puppet agent -t --noop.

Për më tepër, mund të aktivizoni logun e shkallëzimit - në të puppet shkruan për të gjitha veprimet që ai kryen: për burimin që po përpunon në atë moment, për parametrat e atij burimi, për programet që nisi. Sigurisht, ky është parametrin --debug.

Serveri

Nuk do të diskutoj konfigurimin e plote të puppets-server dhe dërgimin e kodit në të, do të thosha vetëm se nga kuti instalohet një version funksional i serverit, që nuk kërkon konfigurim shtesë për të punuar në kushte të një numri të vogël nodash (le të themi, deri në njëqind). Një numër më të madh nodash do të kërkojë rregullim - në default puppetserver nisi nuk më shumë se katër punëtorë, për një performancë më të madhe duhet të rriteni numrin e tyre dhe të mos harroni të rritni kufijtë e memories, përndryshe serveri do të kalojë pjesën më të madhe të kohës duke bërë garbage collection.

Dërgimi i kodit - nëse keni nevojë për një mundësim të shpejtë dhe të thjeshtë, shihni (në r10k)[https://github.com/puppetlabs/r10k], për instalime të vogla duhet të mjaftojë.

Shtesa 2: rekomandime për shkrimin e kodit

  1. Nxirrni të gjitha logjikën në klasa dhe definita.
  2. Mbani klasat dhe definitat në modulet, jo në manifestet me përshkrimin e nodave.
  3. Përdorni faktet.
  4. Mos bëni if-e sipas emrave të hosteve.
  5. Mos hezitoni të shtoni parametra për klasat dhe definitat - është më mirë sesa logjika e paqartë, e fshehur në trupin e klasës/definitës.

Dhe pse unë rekomandoj ta bëni kështu - do ta shpjegoj në artikullin e ardhshëm.

Përfundimi

Këtu e mbyllim me hyrjen. Në artikullin e ardhshëm do të flas për Hiera, ENC dhe PuppetDB.

Vetëm përdoruesit e regjistruar mund të marrin pjesë në anketë. Hyni, ju lutemi.

Në të vërtetë, ka shumë më tepër materiale - mund të shkruaj artikuj mbi temat në vijim, votojeni për atë që ju intereson të lexoni:

  • 59,1%Konstruksione të avancuara puppet - disa gjëra nga niveli tjetër: ciklet, mapimet dhe shprehjet e tjera lambda, kolektorët e burimeve, burimet e eksportuara dhe ndërveprimi ndërmjet hosteve përmes Puppet, etiketat, ofruesit, tipet e dhënash abstrakte.
  • 31,8%«Unë jam admin i mamit» ose si në Avito ne lidhëm disa servera puppet të versioneve të ndryshme, bëj edhe një pjesë mbi administrimin e puppets-server.
  • 81,8%Si shkruajmë kodin puppet: ndihma instrumentale, dokumentimi, testimi, CI/CD.

22 përdorues votuan. 9 përdorues abstenuan.

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster