Wprowadzenie do Marionetki

Puppet to system zarządzania konfiguracją. Służy do doprowadzenia hostów do pożądanego stanu i utrzymania tego stanu.

Z Puppetem współpracuję już ponad pięć lat. Ten tekst jest zasadniczo przetłumaczoną i uporządkowaną kompilacją kluczowych punktów z oficjalnej dokumentacji, która pozwoli początkującym szybko zrozumieć istotę Puppet.

Wprowadzenie do Marionetki

Podstawowe informacje

System operacyjny Puppet to klient-serwer, chociaż obsługuje także działanie bezserwerowe z ograniczoną funkcjonalnością.

Stosowany jest model działania typu pull: domyślnie raz na pół godziny klienci kontaktują się z serwerem w celu konfiguracji i jej zastosowania. Jeśli pracowałeś z Ansible, to używają innego modelu push: administrator inicjuje proces stosowania konfiguracji, sami klienci nic nie będą stosować.

Podczas komunikacji sieciowej stosowane jest dwukierunkowe szyfrowanie TLS: serwer i klient posiadają własne klucze prywatne i odpowiadające im certyfikaty. Zwykle serwer wystawia certyfikaty dla klientów, ale w zasadzie możliwe jest skorzystanie z zewnętrznego urzędu certyfikacji.

Wprowadzenie do manifestów

W terminologii marionetkowej na marionetkowy serwer łączyć węzły (węzły). Zapisano konfigurację węzłów w manifestach w specjalnym języku programowania - Puppet DSL.

Puppet DSL jest językiem deklaratywnym. Opisuje pożądany stan węzła w postaci deklaracji poszczególnych zasobów, np.:

  • Plik istnieje i ma określoną treść.
  • Pakiet jest zainstalowany.
  • Usługa została uruchomiona.

Zasoby można ze sobą łączyć:

  • Istnieją zależności, wpływają one na kolejność wykorzystania zasobów.
    Na przykład „najpierw zainstaluj pakiet, następnie edytuj plik konfiguracyjny, a następnie uruchom usługę”.
  • Istnieją powiadomienia - jeśli zasób się zmienił, wysyła powiadomienia do subskrybowanych przez niego zasobów.
    Na przykład, jeśli plik konfiguracyjny ulegnie zmianie, możesz automatycznie ponownie uruchomić usługę.

Dodatkowo Puppet DSL zawiera funkcje i zmienne, a także instrukcje warunkowe i selektory. Obsługiwane są również różne mechanizmy szablonowania - EPP i ERB.

Puppet jest napisany w języku Ruby, więc wiele konstrukcji i terminów zostało z niego zaczerpniętych. Ruby pozwala na rozbudowę Puppet - dodanie złożonej logiki, nowych typów zasobów, funkcji.

Kiedy Puppet jest uruchomiony, manifesty dla każdego konkretnego węzła na serwerze są kompilowane do katalogu. katalog to lista zasobów i ich zależności po obliczeniu wartości funkcji, zmiennych i rozwinięciu instrukcji warunkowych.

Składnia i styl kodu

Oto sekcje oficjalnej dokumentacji, które pomogą Ci zrozumieć składnię, jeśli podane przykłady nie wystarczą:

Oto przykład wyglądu manifestu:

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

Wcięcia i podziały wierszy nie są wymaganą częścią manifestu, ale są zalecane przewodnik po stylu. Streszczenie:

  • Wcięcia dwuspacjaowe, tabulatory nie są używane.
  • Nawiasy klamrowe oddziela się spacją, dwukropki nie są oddzielane spacją.
  • Przecinki po każdym parametrze, łącznie z ostatnim. Każdy parametr znajduje się w osobnej linii. Wyjątek stanowi przypadek bez parametrów i jednego parametru: można pisać w jednej linii i bez przecinka (tzn. resource { 'title': } и resource { 'title': param => value }).
  • Strzałki na parametrach powinny znajdować się na tym samym poziomie.
  • Przed nimi są zapisane strzałki relacji zasobów.

Lokalizacja plików na pappetserverze

W celu dalszego wyjaśnienia przedstawię koncepcję „katalogu głównego”. Katalog główny to katalog zawierający konfigurację Puppet dla określonego węzła.

Katalog główny różni się w zależności od wersji Puppet i używanych środowisk. Środowiska to niezależne zestawy konfiguracji przechowywane w oddzielnych katalogach. Zwykle używane w połączeniu z git, w którym to przypadku środowiska tworzone są z gałęzi git. W związku z tym każdy węzeł znajduje się w tym czy innym środowisku. Można to skonfigurować na samym węźle lub w ENC, o czym powiem w następnym artykule.

  • W trzeciej wersji („stary Puppet”) katalogiem bazowym był /etc/puppet. Korzystanie ze środowisk jest opcjonalne – np. nie używamy ich ze starym Puppetem. Jeśli używane są środowiska, zwykle są one przechowywane w /etc/puppet/environments, katalog główny będzie katalogiem środowiska. Jeśli środowiska nie są używane, katalog główny będzie katalogiem podstawowym.
  • Począwszy od czwartej wersji („nowej Puppet”) korzystanie ze środowisk stało się obowiązkowe, a katalog bazowy został przeniesiony do /etc/puppetlabs/code. W związku z tym środowiska są przechowywane w /etc/puppetlabs/code/environments, katalog główny jest katalogiem środowiska.

W katalogu głównym musi znajdować się podkatalog manifests, który zawiera jeden lub więcej manifestów opisujących węzły. Ponadto powinien istnieć podkatalog modules, który zawiera moduły. Nieco później powiem Ci, jakie moduły są. Ponadto stary Puppet może również posiadać podkatalog files, który zawiera różne pliki, które kopiujemy do węzłów. W nowym Puppet wszystkie pliki są umieszczone w modułach.

Pliki manifestu mają rozszerzenie .pp.

Kilka przykładów walki

Opis węzła i znajdującego się na nim zasobu

Na węźle server1.testdomain należy utworzyć plik /etc/issue z treścią Debian GNU/Linux n l. Plik musi należeć do użytkownika i grupy root, prawa dostępu muszą być 644.

Piszemy manifest:

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 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relacje między zasobami w węźle

Na węźle server2.testdomain nginx musi być uruchomiony, pracować z wcześniej przygotowaną konfiguracją.

Rozłóżmy problem:

  • Pakiet wymaga zainstalowania nginx.
  • Konieczne jest skopiowanie plików konfiguracyjnych z serwera.
  • Usługa musi działać nginx.
  • Jeśli konfiguracja zostanie zaktualizowana, należy ponownie uruchomić usługę.

Piszemy manifest:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Aby to zadziałało, potrzebujesz w przybliżeniu następującej lokalizacji pliku na serwerze lalek:

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

Typy zasobów

Pełną listę obsługiwanych typów zasobów można znaleźć tutaj w dokumentacji, tutaj opiszę pięć podstawowych typów, które w mojej praktyce wystarczą do rozwiązania większości problemów.

filet

Zarządza plikami, katalogami, dowiązaniami symbolicznymi, ich zawartością i prawami dostępu.

Opcje:

  • Nazwa zasobu — ścieżka do pliku (opcjonalnie)
  • ścieżka — ścieżka do pliku (jeśli nie jest podana w nazwie)
  • zapewnić - Typ pliku:
    • absent - usuń plik
    • present — musi istnieć plik dowolnego typu (w przypadku braku pliku zostanie utworzony zwykły plik)
    • file - zwykły plik
    • directory - katalog
    • link - dowiązanie symboliczne
  • zawartość — zawartość pliku (odpowiednia tylko dla zwykłych plików, nie może być używana razem z źródło lub cel)
  • źródło — link do ścieżki, z której chcesz skopiować zawartość pliku (nie można używać razem z zawartość lub cel). Można określić jako identyfikator URI ze schematem puppet: (wtedy zostaną użyte pliki z serwera marionetek) i ze schematem http: (Mam nadzieję, że jest jasne, co się stanie w tym przypadku), a nawet ze schematem file: lub jako ścieżka bezwzględna bez schematu (wtedy wykorzystany zostanie plik z lokalnego FS na węźle)
  • cel — gdzie powinno wskazywać dowiązanie symboliczne (nie można używać razem z zawartość lub źródło)
  • właściciel — użytkownik, który powinien być właścicielem pliku
  • grupa — grupa, do której powinien należeć plik
  • tryb — uprawnienia do plików (jako ciąg znaków)
  • powtarzać się - umożliwia rekurencyjne przetwarzanie katalogów
  • oczyścić - umożliwia usuwanie plików, które nie są opisane w Puppet
  • siła - umożliwia usuwanie katalogów, które nie są opisane w Puppet

pakiet

Instaluje i usuwa pakiety. Możliwość obsługi powiadomień - ponownie instaluje pakiet, jeśli parametr jest określony zainstaluj ponownie przy_odświeżaniu.

Opcje:

  • Nazwa zasobu — nazwa pakietu (opcjonalnie)
  • Nazwa — nazwa pakietu (jeśli nie została podana w nazwie)
  • dostawca — menedżer pakietów do użycia
  • zapewnić — pożądany stan opakowania:
    • present, installed - zainstalowana dowolna wersja
    • latest - zainstalowana najnowsza wersja
    • absent - usunięto (apt-get remove)
    • purged — usunięte wraz z plikami konfiguracyjnymi (apt-get purge)
    • held - wersja pakietu jest zablokowana (apt-mark hold)
    • любая другая строка — zainstalowana jest określona wersja
  • zainstaluj ponownie przy_odświeżaniu - Jeśli true, wówczas po otrzymaniu powiadomienia pakiet zostanie ponownie zainstalowany. Przydatne w przypadku dystrybucji opartych na źródłach, gdzie przy zmianie parametrów kompilacji może być konieczne przebudowanie pakietów. Domyślny false.

usługa

Zarządza usługami. Możliwość przetwarzania powiadomień - restartuje usługę.

Opcje:

  • Nazwa zasobu — usługa do zarządzania (opcjonalnie)
  • Nazwa — usługa, którą należy zarządzać (jeśli nie jest określona w nazwie)
  • zapewnić — pożądany stan usługi:
    • running - wystrzelony
    • stopped - zatrzymany
  • umożliwiać — kontroluje możliwość uruchomienia usługi:
    • true — automatyczne uruchamianie jest włączone (systemctl enable)
    • mask - przebrany (systemctl mask)
    • false — automatyczne uruchamianie jest wyłączone (systemctl disable)
  • restart - polecenie ponownego uruchomienia usługi
  • status — polecenie sprawdzenia stanu usługi
  • hasrestart — wskaż, czy skrypt inicjujący usługę obsługuje ponowne uruchomienie. Jeśli false i parametr jest określony restart — używana jest wartość tego parametru. Jeśli false i parametr restart nie określono - usługa jest zatrzymywana i uruchamiana ponownie (ale systemd używa polecenia systemctl restart).
  • hasstatus — wskaż, czy skrypt inicjujący usługę obsługuje to polecenie status. Jeśli false, wówczas używana jest wartość parametru status. Domyślny true.

exec

Uruchamia polecenia zewnętrzne. Jeśli nie określisz parametrów tworzy, tylko, jeżeli, chyba że lub tylko odświeżyć, polecenie będzie uruchamiane przy każdym uruchomieniu Puppet. Możliwość przetwarzania powiadomień - uruchamia polecenie.

Opcje:

  • Nazwa zasobu — polecenie do wykonania (opcjonalnie)
  • komenda — polecenie do wykonania (jeśli nie jest podane w nazwie)
  • ścieżka — ścieżki, w których należy szukać pliku wykonywalnego
  • tylko, jeżeli — jeżeli polecenie określone w tym parametrze zostanie zakończone zerowym kodem powrotu, zostanie wykonane polecenie główne
  • chyba że — jeżeli polecenie określone w tym parametrze zostanie zakończone niezerowym kodem powrotu, zostanie wykonane polecenie główne
  • tworzy — jeżeli plik określony w tym parametrze nie istnieje, zostanie wykonane polecenie główne
  • tylko odświeżyć - Jeśli true, polecenie zostanie wykonane tylko wtedy, gdy ten plik exec otrzyma powiadomienie z innych zasobów
  • cwd — katalog, z którego ma zostać uruchomione polecenie
  • użytkownik — użytkownik, od którego ma zostać uruchomione polecenie
  • dostawca - jak uruchomić polecenie:
    • posix — po prostu tworzony jest proces potomny, pamiętaj o określeniu ścieżka
    • powłoka - polecenie zostaje uruchomione w powłoce /bin/sh, nie można określić ścieżka, możesz używać efektu globowania, potoków i innych funkcji powłoki. Zwykle wykrywane automatycznie, jeśli występują jakieś znaki specjalne (|, ;, &&, || itp).

cron

Kontroluje cronjoby.

Opcje:

  • Nazwa zasobu - po prostu jakiś identyfikator
  • zapewnić — stan korony:
    • present - utwórz, jeśli nie istnieje
    • absent - usuń, jeśli istnieje
  • komenda - jakie polecenie uruchomić
  • środowisko — w jakim środowisku uruchomić polecenie (lista zmiennych środowiskowych i ich wartości poprzez =)
  • użytkownik — od którego użytkownika uruchomić polecenie
  • chwila, godzina, dzień powszedni, miesiąc, dzień miesiąca — kiedy uruchomić cron. Jeśli którykolwiek z tych atrybutów nie zostanie określony, jego wartość w pliku crontab będzie taka *.

W marionetce 6.0 cron jak gdyby wyjęty z pudełka w marionetkowym serwerze, więc na ogólnej stronie nie ma dokumentacji. Ale on jest w pudełku w marionetkowym agencie, więc nie ma potrzeby instalowania go osobno. Możesz zobaczyć jego dokumentację w dokumentacji piątej wersji PuppetLub na GitHubie.

Ogólnie o zasobach

Wymagania dotyczące unikalności zasobów

Najczęstszym błędem, z jakim się spotykamy, jest tzw Duplikat deklaracji. Ten błąd występuje, gdy w katalogu pojawiają się co najmniej dwa zasoby tego samego typu i o tej samej nazwie.

Dlatego napiszę jeszcze raz: manifesty dla tego samego węzła nie powinny zawierać zasobów tego samego typu i o tym samym tytule!

Czasami istnieje potrzeba zainstalowania pakietów o tej samej nazwie, ale z różnymi menedżerami pakietów. W takim przypadku musisz użyć parametru nameaby uniknąć błędu:

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

Inne typy zasobów mają podobne opcje, które pomagają uniknąć duplikacji name у usługa, command у exec, i tak dalej.

Metaparametry

Każdy typ zasobu ma pewne specjalne parametry, niezależnie od jego charakteru.

Pełna lista metaparametrów w dokumentacji Puppet.

Ostateczna lista:

  • wymagać — ten parametr wskazuje, od jakich zasobów zależy ten zasób.
  • zanim - Ten parametr określa, które zasoby zależą od tego zasobu.
  • subskrybuj — ten parametr określa, z jakich zasobów dany zasób otrzymuje powiadomienia.
  • powiadom — Ten parametr określa, które zasoby otrzymują powiadomienia z tego zasobu.

Wszystkie wymienione metaparametry akceptują pojedyncze łącze do zasobu lub tablicę łączy w nawiasach kwadratowych.

Linki do zasobów

Link do zasobu to po prostu wzmianka o zasobie. Stosowane są głównie do wskazywania zależności. Odwoływanie się do nieistniejącego zasobu spowoduje błąd kompilacji.

Składnia linku jest następująca: typ zasobu pisany dużą literą (jeżeli nazwa typu zawiera podwójne dwukropki, to każda część nazwy między dwukropkami jest pisana wielką literą), następnie nazwa zasobu w nawiasie kwadratowym (w przypadku nazwy nie zmienia!). Nie powinno być spacji, nawiasy kwadratowe wpisuje się bezpośrednio po nazwie typu.

Przykład:

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

Zależności i powiadomienia

Dokumentacja tutaj.

Jak wspomniano wcześniej, proste zależności między zasobami są przechodnie. Swoją drogą, bądź ostrożny przy dodawaniu zależności - możesz tworzyć zależności cykliczne, co spowoduje błąd kompilacji.

W przeciwieństwie do zależności, powiadomienia nie są przechodnie. W przypadku powiadomień obowiązują następujące zasady:

  • Jeśli zasób otrzyma powiadomienie, zostanie zaktualizowany. Akcje aktualizacji zależą od typu zasobu − exec uruchamia polecenie, usługa restartuje usługę, pakiet ponownie instaluje pakiet. Jeśli dla zasobu nie zdefiniowano akcji aktualizacji, nic się nie dzieje.
  • Podczas jednego uruchomienia Puppet zasób jest aktualizowany nie więcej niż raz. Jest to możliwe, ponieważ powiadomienia zawierają zależności, a wykres zależności nie zawiera cykli.
  • Jeśli Puppet zmieni stan zasobu, zasób wysyła powiadomienia do wszystkich subskrybowanych przez niego zasobów.
  • Jeśli zasób zostanie zaktualizowany, wysyła powiadomienia do wszystkich subskrybowanych przez niego zasobów.

Obsługa nieokreślonych parametrów

Z reguły, jeśli jakiś parametr zasobu nie ma wartości domyślnej i parametr ten nie jest określony w manifeście, Puppet nie zmieni tej właściwości dla odpowiedniego zasobu w węźle. Na przykład, jeśli zasób typu filet parametr nieokreślony owner, to Puppet nie zmieni właściciela odpowiedniego pliku.

Wprowadzenie do klas, zmiennych i definicji

Załóżmy, że mamy kilka węzłów, które mają tę samą część konfiguracji, ale są też różnice - w przeciwnym razie moglibyśmy opisać to wszystko w jednym bloku node {}. Można oczywiście po prostu skopiować identyczne części konfiguracji, ale generalnie jest to złe rozwiązanie – konfiguracja rośnie i jeśli zmienimy ogólną część konfiguracji, będziemy musieli edytować to samo w wielu miejscach. Jednocześnie łatwo jest popełnić błąd i w ogóle zasada DRY (nie powtarzaj się) została wymyślona nie bez powodu.

Aby rozwiązać ten problem, istnieje taki projekt jak klasa.

Классы

Klasa to nazwany blok kodu poppetowego. Aby ponownie wykorzystać kod, potrzebne są klasy.

Najpierw należy opisać klasę. Sam opis nie dodaje nigdzie żadnych zasobów. Klasa jest opisana w manifestach:

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

Następnie można użyć klasy:

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

Przykład z poprzedniego zadania - przenieśmy instalację i konfigurację nginx do klasy:

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
}

Zmienne

Klasa z poprzedniego przykładu nie jest wcale elastyczna, ponieważ zawsze przynosi tę samą konfigurację Nginx. Stwórzmy ścieżkę do zmiennej konfiguracyjnej, wtedy tej klasy można użyć do zainstalowania nginx z dowolną konfiguracją.

To może być zrobione za pomocą zmiennych.

Uwaga: zmienne w Puppet są niezmienne!

Ponadto dostęp do zmiennej można uzyskać dopiero po jej zadeklarowaniu, w przeciwnym razie wartość zmiennej zostanie zachowana undef.

Przykład pracy ze zmiennymi:

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

Marionetka ma przestrzenie nazw, a zmienne odpowiednio mają obszar widoczności: Zmienna o tej samej nazwie może być zdefiniowana w różnych przestrzeniach nazw. Podczas rozpoznawania wartości zmiennej, zmienna jest przeszukiwana w bieżącej przestrzeni nazw, następnie w otaczającej ją przestrzeni nazw i tak dalej.

Przykłady przestrzeni nazw:

  • global - trafiają tam zmienne spoza opisu klasy lub węzła;
  • przestrzeń nazw węzła w opisie węzła;
  • przestrzeń nazw klasy w opisie klasy.

Aby uniknąć dwuznaczności podczas uzyskiwania dostępu do zmiennej, możesz określić przestrzeń nazw w nazwie zmiennej:

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

Załóżmy, że ścieżka do konfiguracji nginx leży w zmiennej $nginx_conf_source. Wtedy klasa będzie wyglądać następująco:

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
}

Podany przykład jest jednak zły, bo istnieje jakaś „tajemnicza wiedza”, że gdzieś wewnątrz klasy używana jest zmienna o takiej a takiej nazwie. O wiele bardziej poprawne jest uogólnienie tej wiedzy - klasy mogą mieć parametry.

Parametry klasy są zmiennymi w przestrzeni nazw klasy, są one określone w nagłówku klasy i mogą być używane jak zwykłe zmienne w treści klasy. Wartości parametrów są określone podczas korzystania z klasy w manifeście.

Parametr można ustawić na wartość domyślną. Jeśli parametr nie ma wartości domyślnej i wartość ta nie zostanie ustawiona podczas użycia, spowoduje to błąd kompilacji.

Sparametryzujmy klasę z powyższego przykładu i dodajmy dwa parametry: pierwszy, wymagany, to ścieżka do konfiguracji, a drugi, opcjonalny, to nazwa pakietu z nginx (w Debianie są np. pakiety 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',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

W Puppet zmienne są wpisywane. Jeść wiele typów danych. Typy danych są zwykle używane do sprawdzania wartości parametrów przekazywanych do klas i definicji. Jeśli przekazany parametr nie pasuje do określonego typu, wystąpi błąd kompilacji.

Typ jest zapisywany bezpośrednio przed nazwą parametru:

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

Klasy: uwzględnij nazwę klasy vs class{'classname':}

Każda klasa jest zasobem typu klasa. Podobnie jak w przypadku każdego innego typu zasobu, w tym samym węźle nie mogą znajdować się dwie instancje tej samej klasy.

Jeśli spróbujesz dwukrotnie dodać klasę do tego samego węzła, używając class { 'classname':} (bez różnicy, z różnymi lub identycznymi parametrami), wystąpi błąd kompilacji. Jeśli jednak użyjesz klasy w stylu zasobu, możesz natychmiast jawnie ustawić wszystkie jej parametry w manifeście.

Jeśli jednak użyjesz include, wówczas klasę można dodać dowolną liczbę razy. Fakt jest taki include jest funkcją idempotentną, która sprawdza, czy klasa została dodana do katalogu. Jeśli klasy nie ma w katalogu, dodaje ją, a jeśli już istnieje, nie robi nic. Ale w przypadku użycia include Podczas deklaracji klasy nie można ustawić parametrów klasy - wszystkie wymagane parametry muszą być ustawione w zewnętrznym źródle danych - Hiera lub ENC. Porozmawiamy o nich w następnym artykule.

Definiuje

Jak powiedziano w poprzednim bloku, ta sama klasa nie może występować w węźle więcej niż raz. Jednak w niektórych przypadkach musisz mieć możliwość użycia tego samego bloku kodu z różnymi parametrami w tym samym węźle. Innymi słowy, istnieje zapotrzebowanie na własny typ zasobu.

Przykładowo, aby zainstalować moduł PHP, wykonujemy w Avito następujące czynności:

  1. Zainstaluj paczkę z tym modułem.
  2. Utwórzmy plik konfiguracyjny dla tego modułu.
  3. Tworzymy dowiązanie symboliczne do konfiguracji dla php-fpm.
  4. Tworzymy dowiązanie symboliczne do konfiguracji dla php cli.

W takich przypadkach projekt taki jak definiować (zdefiniuj, zdefiniowany typ, zdefiniowany typ zasobu). A Define jest podobne do klasy, ale są różnice: po pierwsze, każde Define jest typem zasobu, a nie zasobem; po drugie, każda definicja ma ukryty parametr $title, gdzie znajduje się nazwa zasobu, gdy jest zadeklarowana. Podobnie jak w przypadku klas, najpierw należy opisać definicję, po czym można z niej skorzystać.

Uproszczony przykład z modułem dla 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' }
}

Najłatwiejszym sposobem wyłapania błędu deklaracji Duplicate jest użycie opcji Definiuj. Dzieje się tak, jeśli definicja zawiera zasób o stałej nazwie, a w jakimś węźle znajdują się dwie lub więcej instancji tej definicji.

Łatwo się przed tym zabezpieczyć: wszystkie zasoby w definicji muszą mieć nazwę zależną od $title. Alternatywą jest idempotentne dodanie zasobów; w najprostszym przypadku wystarczy przenieść zasoby wspólne dla wszystkich wystąpień definicji do osobnej klasy i uwzględnić tę klasę w definicji - funkcja include idempotentny.

Istnieją inne sposoby osiągnięcia idempotencji podczas dodawania zasobów, a mianowicie użycie funkcji defined и ensure_resources, ale o tym opowiem w następnym odcinku.

Zależności i powiadomienia dla klas i definicji

Klasy i definicje dodają następujące reguły do ​​obsługi zależności i powiadomień:

  • zależność od klasy/definicji dodaje zależności od wszystkich zasobów klasy/definicji;
  • zależność klasa/definicja dodaje zależności do wszystkich zasobów klasy/definicji;
  • powiadomienie klasy/definicji powiadamia wszystkie zasoby klasy/definicji;
  • Subskrypcja class/define subskrybuje wszystkie zasoby klasy/define.

Instrukcje warunkowe i selektory

Dokumentacja tutaj.

if

Tutaj wszystko jest proste:

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

chyba że

chyba że jest to if odwrotne: blok kodu zostanie wykonany, jeśli wyrażenie będzie fałszywe.

unless ВЫРАЖЕНИЕ {
  ...
}

walizka

Tutaj też nie ma nic skomplikowanego. Jako wartości możesz używać wartości regularnych (łańcuchów, liczb itp.), wyrażeń regularnych i typów danych.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Selektory

Selektor jest konstrukcją językową podobną do case, ale zamiast wykonać blok kodu, zwraca wartość.

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

Moduły

Gdy konfiguracja jest niewielka, można ją łatwo przechowywać w jednym manifeście. Jednak im więcej konfiguracji opiszemy, tym więcej klas i węzłów będzie w manifeście, tym będzie on większy i praca z nim stanie się niewygodna.

Do tego dochodzi problem ponownego wykorzystania kodu – gdy cały kod znajduje się w jednym manifeście, trudno jest udostępnić ten kod innym. Aby rozwiązać te dwa problemy, Puppet ma jednostkę zwaną modułami.

Moduły - są to zestawy klas, definicji i innych bytów Puppet umieszczonych w osobnym katalogu. Innymi słowy, moduł jest niezależnym elementem logiki marionetkowej. Na przykład może istnieć moduł do pracy z nginxem i będzie on zawierał to, co i tylko to, co jest potrzebne do pracy z nginxem, może też istnieć moduł do pracy z PHP i tak dalej.

Moduły są wersjonowane i obsługiwane są także wzajemne zależności modułów. Istnieje otwarte repozytorium modułów - Kuźnia lalek.

Na serwerze marionetkowym moduły znajdują się w podkatalogu modułów w katalogu głównym. Wewnątrz każdego modułu znajduje się standardowy schemat katalogów - manifesty, pliki, szablony, biblioteki i tak dalej.

Struktura plików w module

W katalogu głównym modułu mogą znajdować się następujące katalogi o nazwach opisowych:

  • manifests - zawiera manifesty
  • files - zawiera pliki
  • templates - zawiera szablony
  • lib — zawiera kod Ruby

Nie jest to pełna lista katalogów i plików, ale na razie wystarczy w tym artykule.

Nazwy zasobów i nazwy plików w module

Dokumentacja tutaj.

Zasoby (klasy, definicje) w module nie mogą mieć dowolnych nazw. Ponadto istnieje bezpośrednia zgodność pomiędzy nazwą zasobu a nazwą pliku, w którym Puppet będzie szukał opisu tego zasobu. Jeśli naruszysz zasady nazewnictwa, Puppet po prostu nie znajdzie opisu zasobu i pojawi się błąd kompilacji.

Zasady są proste:

  • Wszystkie zasoby w module muszą znajdować się w przestrzeni nazw modułu. Jeśli moduł zostanie wywołany foo, wówczas wszystkie znajdujące się w nim zasoby powinny zostać nazwane foo::<anything>, Lub tylko foo.
  • Zasób z nazwą modułu musi znajdować się w pliku init.pp.
  • W przypadku innych zasobów schemat nazewnictwa plików jest następujący:
    • prefiks z nazwą modułu jest odrzucany
    • wszystkie podwójne dwukropki, jeśli występują, są zastępowane ukośnikami
    • dodano rozszerzenie .pp

Pokażę na przykładzie. Powiedzmy, że piszę moduł nginx. Zawiera następujące zasoby:

  • klasa nginx opisane w manifeście init.pp;
  • klasa nginx::service opisane w manifeście service.pp;
  • definiować nginx::server opisane w manifeście server.pp;
  • definiować nginx::server::location opisane w manifeście server/location.pp.

Szablony

Na pewno sam wiesz, czym są szablony, nie będę ich tutaj szczegółowo opisywał. Ale zostawię to na wszelki wypadek link do Wikipedii.

Jak korzystać z szablonów: Znaczenie szablonu można rozszerzyć za pomocą funkcji template, do którego przekazywana jest ścieżka do szablonu. Dla zasobów typu filet używane razem z parametrem content. Na przykład tak:

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

Zobacz ścieżkę <modulename>/<filename> oznacza plik <rootdir>/modules/<modulename>/templates/<filename>.

Ponadto istnieje funkcja inline_template — jako dane wejściowe otrzymuje tekst szablonu, a nie nazwę pliku.

W szablonach możesz używać wszystkich zmiennych Puppet w bieżącym zakresie.

Puppet obsługuje szablony w formacie ERB i EPP:

Krótko o ERB

Struktury kontrolne:

  • <%= ВЫРАЖЕНИЕ %> — wstaw wartość wyrażenia
  • <% ВЫРАЖЕНИЕ %> — obliczyć wartość wyrażenia (bez wstawiania). Zwykle tutaj znajdują się instrukcje warunkowe (if) i pętle (każda).
  • <%# КОММЕНТАРИЙ %>

Wyrażenia w ERB są zapisywane w języku Ruby (ERB to w rzeczywistości Embedded Ruby).

Aby uzyskać dostęp do zmiennych z manifestu, musisz dodać @ do nazwy zmiennej. Aby usunąć podział wiersza pojawiający się po konstrukcji sterującej, należy użyć znacznika zamykającego -%>.

Przykład wykorzystania szablonu

Powiedzmy, że piszę moduł do kontrolowania ZooKeepera. Klasa odpowiedzialna za utworzenie konfiguracji wygląda mniej więcej tak:

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

I odpowiedni szablon zoo.cfg.erb - Więc:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Fakty i zmienne wbudowane

Często konkretna część konfiguracji zależy od tego, co aktualnie dzieje się w węźle. Na przykład, w zależności od wersji Debiana, musisz zainstalować tę lub inną wersję pakietu. Możesz to wszystko monitorować ręcznie, przepisując manifesty, jeśli węzły się zmienią. Ale to nie jest poważne podejście, automatyzacja jest znacznie lepsza.

Aby uzyskać informacje o węzłach, Puppet posiada mechanizm zwany faktami. Fakty - jest to informacja o węźle, dostępna w manifestach w postaci zwykłych zmiennych w globalnej przestrzeni nazw. Na przykład nazwa hosta, wersja systemu operacyjnego, architektura procesora, lista użytkowników, lista interfejsów sieciowych i ich adresów i wiele, wiele więcej. Fakty są dostępne w manifestach i szablonach jako zwykłe zmienne.

Przykład pracy z faktami:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Formalnie fakt ma nazwę (string) i wartość (dostępne są różne typy: stringi, tablice, słowniki). Jeść zbiór wbudowanych faktów. Możesz także napisać swój własny. Opisano zbieracze faktów jak funkcje w Rubimalbo jako pliki wykonywalne. Fakty można przedstawić także w formie pliki tekstowe z danymi na węzłach.

Podczas działania agent marionetkowy najpierw kopiuje wszystkie dostępne moduły zbierające fakty z serwera pappetserver do węzła, po czym uruchamia je i wysyła zebrane fakty na serwer; Następnie serwer rozpoczyna kompilację katalogu.

Fakty w formie plików wykonywalnych

Fakty takie umieszczane są w modułach w katalogu facts.d. Oczywiście pliki muszą być wykonywalne. Po uruchomieniu muszą wyprowadzić informacje na standardowe wyjście w formacie YAML lub klucz=wartość.

Nie zapominaj, że fakty dotyczą wszystkich węzłów kontrolowanych przez serwer poppet, na którym wdrożono Twój moduł. Dlatego w skrypcie zadbaj o sprawdzenie, czy w systemie znajdują się wszystkie programy i pliki niezbędne do działania Twojego faktu.

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

Rubinowe fakty

Fakty takie umieszczane są w modułach w katalogu 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

Tekstowe fakty

Takie fakty umieszczane są na węzłach w katalogu /etc/facter/facts.d w starej marionetce lub /etc/puppetlabs/facts.d w nowej Marionetce.

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

Dotarcie do faktów

Do faktów można podejść na dwa sposoby:

  • przez słownik $facts: $facts['fqdn'];
  • używając nazwy faktu jako nazwy zmiennej: $fqdn.

Najlepiej skorzystać ze słownika $factslub jeszcze lepiej, wskaż globalną przestrzeń nazw ($::facts).

Oto odpowiednia część dokumentacji.

Wbudowane zmienne

Oprócz faktów jest też niektóre zmienne, dostępny w globalnej przestrzeni nazw.

  • zaufane fakty — zmienne pobierane z certyfikatu klienta (ponieważ certyfikat jest zwykle wydawany na serwerze popek, agent nie może po prostu pobrać i zmienić jego certyfikatu, więc zmienne są „zaufane”): nazwa certyfikatu, nazwa certyfikatu host i domena, rozszerzenia z certyfikatu.
  • fakty o serwerze —zmienne związane z informacjami o serwerze – wersja, nazwa, adres IP serwera, środowisko.
  • fakty o agentach — zmienne dodane bezpośrednio przez marionetkę-agenta, a nie przez fakt — nazwa certyfikatu, wersja agenta, wersja marionetki.
  • zmienne główne - Zmienne Pappetmaster (sic!). To mniej więcej tyle samo, co w fakty o serwerze, a także dostępne są wartości parametrów konfiguracyjnych.
  • zmienne kompilatora — zmienne kompilatora różniące się w każdym zakresie: nazwa bieżącego modułu i nazwa modułu, w którym uzyskano dostęp do bieżącego obiektu. Można ich użyć na przykład do sprawdzenia, czy Twoje prywatne zajęcia nie są wykorzystywane bezpośrednio z innych modułów.

Dodatek 1: jak to wszystko uruchomić i zdebugować?

Artykuł zawierał wiele przykładów kodu marionetkowego, ale w ogóle nie powiedział nam, jak uruchomić ten kod. No cóż, poprawiam się.

Do uruchomienia Puppet wystarczy agent, ale w większości przypadków potrzebny będzie również serwer.

Agent

Przynajmniej od wersji XNUMX pakiety marionetek-agentów z oficjalne repozytorium Puppetlabs zawierają wszystkie zależności (Ruby i odpowiadające im klejnoty), więc nie ma trudności z instalacją (mówię o dystrybucjach opartych na Debianie - nie używamy dystrybucji opartych na RPM).

W najprostszym przypadku, aby skorzystać z konfiguracji marionetkowej, wystarczy uruchomić agenta w trybie bezserwerowym: pod warunkiem, że kod marionetkowy zostanie skopiowany do węzła, uruchom 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

Lepiej oczywiście ustawić serwer i uruchomić agenty na węzłach w trybie demona – wtedy raz na pół godziny będą stosować konfigurację pobraną z serwera.

Możesz imitować model pracy push – przejdź do interesującego Cię węzła i uruchom sudo puppet agent -t. Klucz -t (--test) faktycznie zawiera kilka opcji, które można włączyć indywidualnie. Opcje te obejmują:

  • nie uruchamiaj w trybie demona (domyślnie agent uruchamia się w trybie demona);
  • wyłączany po zastosowaniu katalogu (domyślnie agent będzie kontynuował pracę i stosował konfigurację raz na pół godziny);
  • napisz szczegółowy dziennik pracy;
  • pokaż zmiany w plikach.

Agent posiada tryb pracy bez zmian - możesz z niego skorzystać, gdy nie jesteś pewien, czy napisałeś poprawną konfigurację i chcesz sprawdzić, co dokładnie agent zmieni w trakcie pracy. Ten tryb jest włączany przez parametr --noop w wierszu poleceń: sudo puppet agent -t --noop.

Dodatkowo możesz włączyć dziennik debugowania pracy - w nim marionetka pisze o wszystkich wykonywanych przez siebie akcjach: o zasobie, który aktualnie przetwarza, o parametrach tego zasobu, o tym, jakie programy uruchamia. Oczywiście, że jest to parametr --debug.

Server

W tym artykule nie będę rozważał pełnej konfiguracji pappetservera i wdrażania na nim kodu; powiem tylko, że od razu po wyjęciu z pudełka istnieje w pełni funkcjonalna wersja serwera, która nie wymaga dodatkowej konfiguracji, aby pracować z niewielką liczbą węzły (powiedzmy do stu). Większa liczba węzłów będzie wymagała dostrojenia - domyślnie marionetka uruchamia nie więcej niż czterech pracowników, aby uzyskać większą wydajność, musisz zwiększyć ich liczbę i nie zapomnieć o zwiększeniu limitów pamięci, w przeciwnym razie serwer przez większość czasu będzie zbierał śmieci.

Wdrożenie kodu - jeśli potrzebujesz go szybko i łatwo, spójrz (na r10k) [https://github.com/puppetlabs/r10k], dla małych instalacji powinno wystarczyć.

Dodatek 2: Wytyczne dotyczące kodowania

  1. Umieść całą logikę w klasach i definicjach.
  2. Trzymaj klasy i definicje w modułach, a nie w manifestach opisujących węzły.
  3. Skorzystaj z faktów.
  4. Nie rób ifów na podstawie nazw hostów.
  5. Możesz swobodnie dodawać parametry do klas i definicji - jest to lepsze niż niejawna logika ukryta w treści klasy/definicji.

Dlaczego polecam to zrobić, wyjaśnię w następnym artykule.

wniosek

Zakończmy wstępem. W następnym artykule opowiem Wam o Hiera, ENC i PuppetDB.

W ankiecie mogą brać udział tylko zarejestrowani użytkownicy. Zaloguj się, Proszę.

Tak naprawdę materiału jest znacznie więcej – mogę pisać artykuły na następujące tematy, głosować na to, o czym chcielibyście przeczytać:

  • 59,1%Zaawansowane konstrukcje marionetkowe – trochę gówna wyższego poziomu: pętle, mapowanie i inne wyrażenia lambda, kolektory zasobów, eksportowane zasoby i komunikacja między hostami za pośrednictwem Puppet, tagów, dostawców, abstrakcyjnych typów danych.13
  • 31,8%„Jestem administratorem mojej mamy”, czyli jak zaprzyjaźniliśmy się w Avito z kilkoma serwerami poppet w różnych wersjach i w zasadzie część o administrowaniu serwerem poppet.7
  • 81,8%Jak piszemy kod marionetkowy: oprzyrządowanie, dokumentacja, testowanie, CI/CD.18

Głosowało 22 użytkowników. 9 użytkowników wstrzymało się od głosu.

Źródło: www.habr.com