Załóżmy, że mamy do postawienia kilkaset wirtualnych serwerów z określonym oprogramowaniem i konfiguracją. Jeżeli chcielibyśmy wykonać to zadanie ręcznie byłoby to bardzo pracochłonne. W instalacji wirtualnych maszyn pomoże nam z pewnością Kickstart, a z konfiguracją tych maszyn może nam pomóc dowolny menadżer konfiguracji. Najbardziej znanym takim menadżerem jest z pewnością Puppet. Puppet odczytuje konfiguracje serwera zawartą w tzw. manifeście. Zazwyczaj jest to plik tekstowy z rozszerzeniem .pp.
Pierwszy manifest.
Manifest to plik zawierający konfigurację klienta napisaną w deklaratywnym języku Puppeta lub w języku Ruby DSL (domain-specific language). Główny plik z manifestem Puppeta to /etc/puppetlabs/code/environments/production/manifests/site.pp
. Manifesty Puppeta powstają wg schematu:
1 2 3 4 5 6 7 8 9 10 11 |
node 'ppagent1.example.com', 'ppagent2.example.com', ... { RESOURCE { NAME1: ATTRIBUTE1 => VALUE1, ... RESOURCE { NAME_N: ATTRIBUTE_N => VALUE_N, } } node default { } |
Wszystkie dostępne rodzaje zasobów Puppeta można wylistować po wpisaniu komendy:
1 |
# puppet resource --types |
Zasoby zadeklarowane na węźle o sprecyzowanej nazwie (np. ppagent1.example.com) zostaną utworzone na tym konkretnym węźle. Zasoby zadeklarowane na węźle domyślnym (default) zostaną utworzone na tych wszystkich agentach Puppeta, które nie zostały wprost zadeklarowane wcześniej (tutaj ppagent1.example.com). Np. do pliku site.pp
wpisujemy
1 2 3 4 5 6 7 |
node 'ppagent1.example.com' { file { '/tmp/hello': content => "Hello, world\n", } node default { } |
Uruchomienie manifestu w trybie master/agent:
1 |
# /opt/puppetlabs/bin/puppet agent --test |
lub po prostu:
1 |
# puppet agent --test |
W trybie stand-alone manifest wykonywany jest tylko na hoście na którym został uruchomiony. W tym wypadku manifest może być w utworzony w dowolnym katalogu. Uruchamiamy go komendą:
1 |
# puppet apply /path/to/site.pp |
Pakiety.
Załóżmy, że chcemy uruchomić serwer www nginx z przykładową stroną. Najpierw musimy zainstalować epel-relase a później pakiet nginx. Zawartość pliku /etc/puppetlabs/code/environments/production/manifests/site.pp
:
1 2 3 4 5 6 7 8 9 10 11 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'nginx': ensure => installed, } } node default { } |
Wyrażenie “->” oznacza kolejność wykonywania działań. Jeżeli w repozytoriach są różne wersje nginx, możemy wskazać którą konkretną wersję chcemy zainstalować:
1 2 3 |
package { 'nginx': ensure => '1.1.19', } |
Jeżeli instalujemy nginxa to nie potrzebujemy apacha, a zatem puppet powinien go odinstalować jeżeli jest już w systemie:
1 2 3 |
package { 'httpd': ensure => absent, } |
Jeżeli zależy nam na tym aby mieć zainstalowaną najnowszą wersję danego pakietu:
1 2 3 |
package { 'puppet': ensure => latest, } |
Ale nie zawsze jest to najlepsze rozwiązanie ponieważ najnowsza wersja danego pakietu może mieć inną konfigurację i system może nie działać tak jak sobie tego życzymy. Taki sposób zarządzania aktualizacjami systemu może być dobrym rozwiązaniem gdy mamy swoje repozytorium dla serwerów i kontrolujemy jakie pakiety są w tym repozytorium. A zatem na chwilę obecną manifest wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'httpd': ensure => absent, }-> package { 'nginx': ensure => installed, } } node default { } |
Usługi.
Po instalacji nginx’a należałoby go jeszcze uruchomić. W tym celu edytujemy /etc/puppetlabs/code/environments/production/manifests/site.pp
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'httpd': ensure => absent, }-> package { 'nginx': ensure => installed, } service { 'nginx': ensure => running, require => Package['nginx'], } } node default { } |
Atrybut require
określa zależności pomiędzy zasobami. W tym przypadku instalacja nginx’a możliwa jest po odinstalowaniu apacha, a uruchomienie nginxa po jego zainstalowaniu. Wyrażenie Package
w zapisie require => Package
napisane jest z dużej litery ponieważ odnosi się do nazwanej instancji zasobu z pakietem. Kod Puppeta wylistowany powyżej jest równoważny z:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'httpd': ensure => absent, }-> package { 'nginx': ensure => installed, before => Service[nginx], } service { 'nginx': ensure => running, } } node default { } |
Czyli pakiet nginx musi zostać zainstalowany przed (before) uruchomieniem go.
Na agencie wykonujemy zmodyfikowany manifest:
1 2 3 4 5 6 7 8 9 10 11 |
[root@ppagent puppet]# puppet agent -t Info: Using configured environment 'production' Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for ppagent1.netico.pl Info: Applying configuration version '1505502399' Notice: /Stage[main]/Main/Node[ppagent1.example.com]/Service[nginx]/ensure: ensure changed 'stopped' to 'running' Info: /Stage[main]/Main/Node[ppagent1.example.com]/Service[nginx]: Unscheduling refresh on Service[nginx] Notice: Applied catalog in 1.34 seconds |
Przykład zależności plików jednych od drugich w pętli:
1 2 3 4 5 6 7 8 9 |
file { '/tmp/file1': require => File['/tmp/file2'], } file { '/tmp/file2': require => File['/tmp/file3'], } file { '/tmp/file3': require => File['/tmp/file1'], } |
Dodanie opcji --graph
podczas uruchamiania manifestu wyświetli diagram zależności zasobów.
Puppet potrafi kontrolować czy dana usługa ma być uruchamiana w czasie startu systemu. Np. aby wyłączyć automatyczne uruchamianie nginxa podczas staru systemu w przypadku gdy usługa jest zarządzana przez framework high-availability Heartbeat:
1 2 3 4 |
service { 'nginx': ensure => running, enable => false, } |
Automatyczne włączenie usługi przy starcie systemu wymaga ustawienia atrybutu enable => true
.
Skrypt zarządzania daną usługą powinien obsługiwać opcję status, puppet będzie chciał ją użyć. Jeżeli skrypt nie ma takiej opcji to ją wyłączamy:
1 2 3 4 |
service { 'my-service': ensure => running, hasstatus => false, } |
Puppet sprawdzi wtedy czy dana usługa po uruchomieniu jest na liście uruchomionych procesów systemowych (np. ps). Jeżeli ta usługa nie pojawia się na liście procesów systemowych to można zdefiniować własny wzorzec szukania procesu:
1 2 3 4 5 |
service { 'my-service': ensure => running, hasstatus => false, pattern => 'ruby myservice.rb', } |
Jeżeli nie ma żadnej możliwości na odnalezienie uruchomionego procesu na liście procesów, można samemu wskazać puppetowi komendę, która zwróci określony status (0 usługa działa, inna wartość usługa nie działa):
1 2 3 4 5 |
service { 'my-service': ensure => running, hasstatus => false, status => 'grep running /var/lib/myservice/status.txt', } |
Jeżeli jest potrzeba restartu usługi lub wczytania nowych ustawień z plików konfiguracyjnych, puppet domyślnie restartuje usługę wyłączając ją (stop) i włączając (start). Ponieważ wiele usług zapewnia opcje restart lub reload a wiele demonów przechowuje w pamięci użyteczne dane o stanie usługi, to lepiej wskazać puppetowi jak ma przeładować lub zrestartować usługę:
1 2 3 4 |
service { 'ssh': ensure => running, restart => '/usr/sbin/service ssh reload', } |
Użytkownicy.
Załóżmy, że na maszynie server1 chcemy założyć konto dla nowego użytkownika user1. Zmieniamy zatem zawartość pliku site.pp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'httpd': ensure => absent, }-> package { 'nginx': ensure => installed, } service { 'nginx': ensure => running, require => Package['nginx'], } } node default { user { 'user1': ensure => present, comment => 'First and Second Name', home => '/home/user1', managehome => true, } } |
Atrybut home
ustawia ścieżkę do katalogu domowego użytkownika, katalog ten zostanie utworzony tylko wtedy, gdy zostanie ustawiony również atrybut managehome => true
. Chociaż puppet potrafi ustawiać hasła dla kont użytkowników (atrybut password
) rekomendowana jest autentykacja przez klucze SSH.
Usunięcie deklaracji wcześniej założonego konta użytkownika z manifestu nie spowoduje usunięcia tego konta. Gdy zajdzie potrzeba usunięcia konta użytkownika można to zrobić następująco.:
1 2 3 |
user { 'user1': ensure => absent, } |
Katalog domowy użytkownika user1 nie zostanie jednak usunięty.
Kontrola dostępu.
Puppet może zarządzać kluczami publicznymi SSH i autoryzować je dla kont użytkowników dzięki zasobowi ssh_authorized_key
. Jeżeli mamy już klucz publiczny to możemy go wykorzystać:
1 2 |
# cat ~/.ssh/id_rsa.pub ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA3ATqENg+GWACa2BzeqTdGnJhNoBer8x6pfWkzNzeM8Zx7/2Tf2pl7kHdbsiTXEUawqzXZQtZzt/j3Oya+PZjcRpWNRzprSmd2UxEEPTqDw9LqY5S2B8og/NyzWaIYPsKoatcgC7VgYHplcTbzEhGu8BsoEVBGYu3IRy5RkAcZik= |
Jeżeli nie to należy go wygenerować:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# ssh-keygen Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in user1. Your public key has been saved in id_rsa.pub. The key fingerprint is: 40:83:af:73:1d:ac:8e:28:12:16:6a:9e:6b:59:1e:29 root@ppmaster.netico.pl The key's randomart image is: +--[ RSA 2048]----+ | .o | | .. . | | ... | | . ..o | |. . .. oS. | |oE +o o . | |+.*..= | |o=... . | |oo. | +-----------------+ |
Teraz modyfikujemy zawartość pliku site.pp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
node 'ppagent1.example.com' { package { 'epel-release': ensure => installed, }-> package { 'httpd': ensure => absent, }-> package { 'nginx': ensure => installed, } service { 'nginx': ensure => running, require => Package['nginx'], } } node default { user { 'user1': ensure => present, comment => 'First and Second Name', home => '/home/user1', managehome => true, } ssh_authorized_key { 'user1': user => 'user1', type => 'rsa', key => 'AAAAB3NzaC1yc2EAAAABIwAAAIEA3ATqENg+GWACa2BzeqTdGnJhNoBer8x6pfWkzNzeM8Zx7/2Tf2pl7kHdbsiTXEUawqzXZQtZzt/j3Oya+PZjcRpWNRzprSmd2UxEEPTqDw9LqY5S2B8og/NyzWaIYPsKoatcgC7VgYHplcTbzEhGu8BsoEVBGYu3IRy5RkAcZik=', } } |
Uruchomienie manifestu puppeta na agencie ppagent1.example.com:
1 |
# puppet -t agent |
Klucz publiczny znajdzie się teraz na liście kluczy autoryzowanych dla użytkownika user1:
1 2 3 4 5 |
# cat /home/user1/.ssh/authorized_keys # HEADER: This file was autogenerated at 2017-09-19 15:08:58 +0200 # HEADER: by puppet. While it can still be managed manually, it # HEADER: is definitely not recommended. ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA3ATqENg+GWACa2BzeqTdGnJhNoBer8x6pfWkzNzeM8Zx7/2Tf2pl7kHdbsiTXEUawqzXZQtZzt/j3Oya+PZjcRpWNRzprSmd2UxEEPTqDw9LqY5S2B8og/NyzWaIYPsKoatcgC7VgYHplcTbzEhGu8BsoEVBGYu3IRy5RkAcZik= user1 |
My jako root z hosta ppmaster.example.com będziemy mogli się logować bez hasła na konto user1 na hosta ppagent1.example.com:
1 2 |
[root@ppmaster manifests]# ssh user1@ppagent1.netico.pl Last login: Tue Sep 19 15:09:21 2017 from ppmaster |
Jeżeli chcemy wygenerować klucz dla innego użytkownika niż aktualnie zalogowany:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# cd /home/user1/.ssh # ssh-keygen -f id_rsa Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in user1. Your public key has been saved in user1.pub. The key fingerprint is: 40:83:af:73:1d:ac:8e:28:12:16:6a:9e:6b:59:1e:29 root@ppmaster.netico.pl The key's randomart image is: +--[ RSA 2048]----+ | .o | | .. . | | ... | | . ..o | |. . .. oS. | |oE +o o . | |+.*..= | |o=... . | |oo. | +-----------------+ |
Odczyt klucza publicznego:
1 2 |
# cat /home/user1/.ssh/id_rsa.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDozMFm++tr0UFUeHZTT3vqqygsnMcdkIiDDCYQz3140lOjx3kts4lPF41gVU1V6cVoY7yJ3XVMUguEHOEwoMuvUudSurN0ufIxh5H8ZVott27g7aJZGwUdEkdPNX/U1G+GnQ0dU/RtPw+oTIaXlnMEG6T9ECmZP8ON5n3uvlpsH/9t6U19B4t2/0oPIfu5H+5TgNDbweceun5X39XZm/PqMOy7A0Ynuh/4g9iA4VIo2YUrQV7kA2lK6tWQZ1SoJnrMr21NUq8L4fP1KclGcZz2dmRbDSDJNGLPlKjZtwXK+cLGQzvBUpqFjGG8FDx5Lz9AhxhVg/MiZfXNq1H0mw/n root@example.com |
Korzystając z tak wygenerowanego klucza będziemy mogli logować się bez hasła z konta user1@ppmaster na konto user1@ppagent1.
1 2 |
[user1@ppmaster manifests]# ssh user1@ppagent1.netico.pl Last login: Tue Sep 19 15:00:20 2017 from ppmaster |
Jeżeli natomiast zajdzie potrzeba zablokowania użytkownikowi user1 logowania to można tymczasowo usunąć jego klucz SSH z Puppeta:
1 2 3 4 5 |
ssh_authorized_key { 'user1': user => 'user1', type => 'rsa', key => '', } |
Jeżeli natomiast użytkownik user1 miał ustawioną kontrolę dostępu na podstawie hasła a nie klucza ssh (co nie jest rekomendowane) to możemy mu zablokować możliwość logowania do systemu ustawiając atrybut password => '*'
:
1 2 3 4 5 6 7 8 |
user { 'user1': ensure => present, comment => 'First and Second Name', home => '/home/user1', managehome => true, password => '*', } |
Zadania.
Puppet daje możliwość wykonywania określonych komend bezpośrednio przy wykorzystaniu zasobu exec,
np:
1 2 3 4 |
exec { 'My command': command => '/bin/echo I ran this command on `/bin/date` >/tmp/ command.output.txt', } |
Wymagane jest podanie przez nas pełnej ścieżki do pliku ale możemy także dostarczyć listę takich ścieżek atrybutem path
.
1 2 3 4 5 |
exec { 'My command': command => 'echo I ran this command on `date` >/tmp/ command.output.txt', path => ['/bin', '/usr/bin'], } |
Możliwe jest także określenie domyślnej ścieżki dla wszystkich zasobów exec
:
1 2 3 |
Exec { path => ['/bin', '/usr/bin'], } |
Po takiej deklaracji możliwe jest podawania komend bez pełnej ścieżki:
1 2 3 4 |
exec { 'My command': command => 'echo I ran this command on `date` >/tmp/ command.output.txt', } |
Zasób exec
spowoduje uruchomienie jednak komendy za każdym razem gdy uruchomiony zostanie na agencie manifest Puppeta. Jeżeli natomiast chcemy daną komendę uruchomić tylko jeden raz to robimy to w sposób, który ilustruje przekład poniżej.
1 2 3 4 5 |
exec { 'Download private key for John': cwd => '/home/john/.ssh', command => '/usr/bin/wget http://example.com/files/id_rsa', creates => '/home/john/.ssh/id_rsa', } |
Atrybut cwd
zmienia bieżący katalog. Puppet sprawdza czy istnieje plik określony przez atrybut creates
. Jeżeli nie istnieje to jest uruchamiany atrybut command
. Jeżeli istnieje to Puppet nie robi nic.
Można to samo osiągnąć atrybutem unless
lub onlyif
.
1 2 3 4 5 |
exec { 'Download private key for John': cwd => '/home/john/.ssh', command => '/usr/bin/wget http://example.com/files/id_rsa', unless => '/usr/bin/find . -name 'id_rsa', } |
Komendy można także wykonywać automatycznie jeżeli zmieni się zawartość jakiegoś pliku.
1 2 3 4 5 6 |
exec { 'icinga-config-check': command => '/usr/sbin/icinga -v /etc/icinga/icinga.cfg && /usr/ sbin/service icinga restart', refreshonly => true, subscribe => File['/etc/icinga/icinga.cfg'], } |
Komendy można także wykonywać sekwencyjnie. Odpowiednikiem zapisu:
1 2 |
# /usr/sbin/icinga -v /etc/icinga/icinga.cfg && /usr/sbin/service icinga restart |
jest zapis:
1 2 3 4 5 6 7 8 9 10 11 |
exec { 'command-1': command => '/bin/echo Step 1', } exec { 'command-2': command => '/bin/echo Step 2', require => Exec['command-1'], } exec { 'command-3': command => '/bin/echo Step 3', require => Exec['command-2'], } |
Cron.
Jeżeli istnieje potrzeba wykonywania pewnych czynności okresowo to realizujemy to przy pomocy crona. Puppet może również zarządzać cronem na naszych agentach.
1 2 3 4 5 |
cron { 'Files backup': command => '/usr/bin/rsync -az /path/from/ /path/to/', hour => '04', minute => '00', } |
Jeżeli pozostałe argumenty crona takie jak month
, day
, weekday
nie zostały podane tzn. , że przyjmują one wartość domyślną *. Domyślnie zadania crona uruchamia root, ale możemy je uruchamiać także jako inny użytkownik dzięki atrybutowi user
:
1 2 3 4 5 6 |
cron { 'Files backup': command => '/usr/bin/rsync -az /path/from/ /path/to/', user => 'user1', hour => '04', minute => '00', } |
Sudo (OLD).
Jeżeli chcemy aby aby zwykli użytkownicy mieli jakieś uprawnienia superużytkownika root to wykonujemy poniższe kroki.
Tworzymy katalogi dla konfiguracji /etc/sudoers
:
1 2 |
# mkdir -p /puppet/modules/ssh/manifests # mkdir -p /puppet/modules/ssh/files |
Zakładamy plik /puppet/modules/sudoers/manifests/init.pp
o treści:
1 2 3 4 5 6 7 8 9 10 |
# Manage the sudoers file class sudoers { file { '/etc/sudoers': source => 'puppet:///puppet/modules/sudoers/sudoers', mode => '0440', owner => 'root', group => 'root', } } |
Zakładamy plik /puppet/modules/sudoers/files/sudoers
z zawartością:
1 2 3 4 |
# User privilege specification root ALL = (ALL) ALL user1 ALL = (ALL) NOPASSWD:ALL user2 ALL = (ALL) NOPASSWD: /bin/ls |
Sprawdzamy poprawność pliku:
1 2 |
# visudo -c -f /puppet/modules/sudoers/files/sudoers modules/sudoers/files/sudoers: parsed OK |
Do definicji węzła server1 dodajemy wczytywanie nowej klasy sudoers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
node 'server1' { include nginx include ssh include sudoers user { 'user1': ensure => present, comment => 'First and Second Name', home => '/home/user1', managehome => true, } ssh_authorized_key { 'user1_ssh': user => 'user1', type => 'rsa', key => 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDozMFm++tr0UFUeHZTT3vqqygsnMcdkIiDDCYQz3140lOjx3kts4lPF41gVU1V6cVoY7yJ3XVMUguEHOEwoMuvUudSurN0ufIxh5H8ZVott27g7aJZGwUdEkdPNX/U1G+GnQ0dU/RtPw+oTIaXlnMEG6T9ECmZP8ON5n3uvlpsH/9t6U19B4t2/0oPIfu5H+5TgNDbweceun5X39XZm/PqMOy7A0Ynuh/4g9iA4VIo2YUrQV7kA2lK6tWQZ1SoJnrMr21NUq8L4fP1KclGcZz2dmRbDSDJNGLPlKjZtwXK+cLGQzvBUpqFjGG8FDx5Lz9AhxhVg/MiZfXNq1H0mw/n', } } |
Tak jak po każdej zmianie tak i teraz uruchamiamy ponownie papply:
1 |
# papply |