Ansible w Enterprise Linuksie – cz. III: obowiązkowy playbook
W tej części naszego cyklu przygotujemy bazowego playbooka, który będzie playbookiem pierwszego uruchomienia na maszynie. Przy okazji stworzymy też pierwszy template (szablon) oparty o język szablonów Jinja2. Skorzystamy też z faktów zbieranych przez Ansible. Wszystko krok po kroku. Zapraszam.
W tej części naszego cyklu przygotujemy bazowego playbooka, który będzie playbookiem pierwszego uruchomienia na maszynie.
Playbook ten będzie wykonywał następujące czynności:
- Tworzył użytkownika Ansible
- Dodawał nasz klucz ssh.
- Pozwalał użytkownikowi Ansible na używanie sudo.
- Wymuszał posiadanie włączonego SELinuxa.
- Zabraniał logowania się na roota przez SSH.
- Ustawiał nazwę systemu na tożsamą z ansiblową.
- Rejestrował maszynę w EuroManie lub podobnym rozwiązaniu.
- Aktualizował system.
- Instalował i uruchamiał demona firewalld.
- Otwierał port 22.
- Instalował biblioteki odpowiedzialne za wsparcie dla SELinuxa.
- Instalował pakiety odpowiedzialne za synchronizację czasu.
- Ustawiał motd (message of the day), by poinformować użytkowników logujących się na maszynie, że wszystkie zmiany konfiguracyjne powinny być wykonywane przez nasz kod w Ansible.
Przy okazji stworzymy też pierwszy template (szablon) oparty o język szablonów Jinja2. Skorzystamy też z faktów zbieranych przez Ansible. Jednak dopiero w następnej części poświecimy tym dwóm zagadnieniom więcej uwagi.
Dokumentację do wszystkich modułów możemy znaleźć na oficjalnej stronie projektu http://docs.ansible.com – można też skorzystać z linku, umieszczanego przy przywołaniu modułu.
1 Tworzenie użytkownika
Do zarządzania użytkownikami służy moduł user
. Użytkownika ansible dodajemy do grupy wheel, która następnie będzie mogła korzystać z uprawnień sudo.
# Poniższy kod dodaje użytkownika o nazwie ansible. - name: Setup Ansible User user: name: ansible comment: Ansible Management User group: wheel
2 Dodanie naszego klucza ssh
Kluczami SSH zarządza moduł authorized_key
. W poniższym przykładzie możemy skopiować klucz ssh z popularnego serwisu GitHub. Oczywiście user_name należy zamienić na nazwę naszego użytkownika.
- name: Copy our ssh key to ansible user authorized_key: user: ansible key: https://github.com/user_name.keys state: present
3 Używanie sudo
Do edytowania plików użyjemy modułu lineinfile
. Zauważmy, że od wersji 2.3 powinniśmy używać argumentu path
zamiast dest
. Moduł ten wyszukuje linie z wyrażenia regularnego. W zależności od konfiguracji może upewnić się, że linia ma zadaną zawartość, zmienić zawartość lub ją usunąć. Więcej o module znajdziemy w jego dokumentacji.
W poniższym przykładzie użyliśmy także opcji validate
, która sprawdza, czy plik jest w porządku. Jeśli komenda ta zwróci niezerowy status wyjściowy, to plik nie zostanie zmieniony i pozostanie w swojej oryginalnej formie.
- name: "Ensuring that wheel is able to use sudo without password" lineinfile: path: /etc/sudoers regexp: '^%wheel' line: '%wheel ALL=(ALL) NOPASSWD: ALL' validate: 'visudo -cf %s'
4 Włączenie SELinuxa
Odnośnie SELinuxa zdążyło narosnąć wiele legend, wiele osób nie trawi go, jest on rzeczywiście skomplikowany. Jednak wyłączenie go bez dobrego powodu lub z niedostatków wiedzy i umiejętności związanej z nim, świadczy o poważnych brakach i jest zazwyczaj poważnym błędem w zakresie bezpieczeństwa. Pomijając małą wojenkę, którą powyższe zdania mogą spowodować wśród społeczności administratorów infrastruktury Linuxowej – poniżej znajduje się fragment playbooka odpowiadający za włączenie SELinuxa. Jest on także oparty o moduł lineinfile
.
- name: "Ensuring that SELinux is enabled" lineinfile: path: /etc/selinux/config regexp: '^SELINUX=' line: 'SELINUX=enforcing' notify: restart server
Warto zauważyć, że ponowne włączenie SELinuxa (jeśli był wyłączony) wymaga z ponownego uruchominia systemu. W związku z tym faktem podamy handler (uchwyt), który zostanie wykonany w przypadku zmiany.
5 SSH – wyłączenie logowania na roota
Ponownie został użyty moduł lineinfile
. Tym razem wyłączamy dostęp do logowania się na konto roota przez SSH. Dodajemy linie zakazująca logowania się na konto roota przez SSH.
- name: "Disable root ssh login" lineinfile: path: /etc/ssh/sshd_config regexp: '^PermitRootLogin' line: 'PermitRootLogin no'
6 Ustawienie nazwy na tożsamą z Ansible
Do ustawiania nazwy systemu służy moduł hostname
. Możemy też posłużyć się zwykłym prostym poleceniem powłoki wykorzystującym komendę hostnamectl
.
- name: "Set hostname to ansible name" shell: hostnamectl set-hostname "{{ inventory_hostname }}"
W przykładzie tym pojawił się magiczny ciąg znaków {{ inventory_hostname }}, jest to zmienna, którą Ansible posiada i należy do zbieranych faktów, dostępnych dla nas przed uruchomieniem playbooka. Więcej na temat zbierania faktów dowiemy się w następnej części cyklu. Póki co warto zapamiętać, że Ansible posiada wiele zmiennych, które są dla nas dostępne – jedną z nich jest inventory_hostname i że te zmienne są zapisywane w nawiasach podwójnych {{ }}.
7 Rejestracja w EuroManie lub rozwiązaniu tożsamym
Fragment ten kopiujemy z pierwszego artykułu tego cyklu. Korzystamy z modułu rhn_register
.
- name: Register to EuroManager rhn_register: state=present username=XXX password=XXX server_url=https://xmlrpc.elupdate.euro-linux.com/XMLRPC
8 Aktualizacja systemu
Przeprowadzimy teraz proces aktualizacji systemu. W tym przypadku informujemy Ansible, że wszystkie zainstalowane pakiety mają być w wersji najnowszej. Korzystamy oczywiście z modułu yum
.
- name: Update all packages yum: name: '*' state: latest
9 – 12 Instalacja pakietów dodatkowych firewalld, chrony, obsługa SELinuxa w pythonie. Włączenie i konfiguracja firewalld oraz chronyd
By nie rozwlekać zbytnio opisu zadania, postanowiłem wprowadzić tutaj nowy element tj. with_items
. Pozwala on na wykonywanie zadanego modułu ansiblowego w pętli, w której jest podawany każdy następny element. W kroku tym upewniamy się, że mamy zainstalowany firewalld, biblioteki odpowiedzialne za wsparcie selinuxa w pythonie, oraz obsługę NTP przez chrony.
Do instalacji użyjemy modułu yum
.
- name: Install additional software yum: state=present name={{ item }} with_items: - firewalld - libselinux-python - libsemanage-python - chrony
Administratorom może się wydawać, że instalowanie pakietów, które często są domyśle, jest tzw over-killem. Jednak jak pokazuje przykład choćby z firewalld, który w wersji 7.1 był domyślnym pakietem instalacji minimalnej, by w 7.2 stracić ten status, aż w końcu w 7.3 go odzyskać, nic do końca nie jest oczywiste.
Do udostępnienia portu ssh użyjemy modułu firewalld
.
- name: Incoming ssh enabled in firewalld firewalld: service: ssh permanent: true state: enabled notify: restart firewalld
Do zarządzania usługami (serwisami) służy moduł service
.
- name: Start and enable chronyd && firewalld service: name: '{{ item }}' state: started enabled: True with_items: - firewalld - chronyd
13 Ustawienie Message Of The Day (motd)
Na samym końcu zrobimy plik szablonowy motd. W tym celu w katalogu gdzie znajduje/będzie znajdował się nasz playbook, tworzymy katalog templates mkdir templates; vim templates/motd.j2
.
Restricted area. All violations will be prosecuted. This system was setup with Ansible, it's verly likely that it's still managed by it. Any changes might be overwritten by Ansible! Host admin: {{ admin_name }} {{ admin_surname }} Have a nice day!
Tym razem skorzystamy z modułu template
. Moduł ten kopiuje plik wzorca, który po drodze jest uzupełniany o nasze zmienne, do miejsca docelowego w systemie zarządzanym.
- name: Setting message of the day template: src: templates/motd.j2 dest: /etc/motd owner: root group: root mode: 0644
Handlery, czyli zrestartujmy system lub usługi
Zgodnie z poprzednią częścią “Należy jeszcze dodać tzw. uchwyty (handlery), czyli fragmenty kodu, które są wywoływane (ang. triggered) przez taski. Różnią się one jednak tym, że są wykonywane pod koniec playbooka.” Handlery są wykonywane tylko, jeśli task (zadanie) zgłosiło zmianę. Połączeniem między taskiem, a handlerem jest opcja notify
.
handlers: - name: reload ssh service: name=sshd state=reloaded - name: restart firewalld service: name=firewalld state=restarted - name: restart server shell: sleep 5 && shutdown -r now "Host restart triggered" async: 1 poll: 0 ignore_errors: true
Jeden, by wszystkie zgromadzić i w ciemności związać
Połączony playbook wygląda następująco
--- - hosts: all user: root tasks: - name: Setup Ansible User user: name: ansible comment: Ansible Management User group: wheel - name: Copy our ssh key to ansible user authorized_key: user: ansible key: https://github.com/XXX.keys state: present - name: "Ensuring that wheel is able to use sudo without password" lineinfile: path: /etc/sudoers regexp: '^%wheel' line: '%wheel ALL=(ALL) NOPASSWD: ALL' validate: 'visudo -cf %s' - name: "Ensuring that SELinux is enabled" lineinfile: path: /etc/selinux/config regexp: '^SELINUX=' line: 'SELINUX=enforcing' notify: restart server - name: "Disable root ssh login" lineinfile: path: /etc/ssh/sshd_config regexp: '^PermitRootLogin' line: 'PermitRootLogin no' - name: "Set hostname to ansible name" shell: hostnamectl set-hostname "{{ inventory_hostname }}" - name: Register to EuroManager rhn_register: state=present username=XXX password=XXX server_url=https://xmlrpc.elupdate.euro-linux.com/XMLRPC - name: Update all packages yum: name: '*' state: latest - name: Install additional software yum: state=present name={{ item }} with_items: - firewalld - libselinux-python - libsemanage-python - chrony - name: Incoming ssh enabled in firewalld firewalld: service: ssh permanent: true state: enabled notify: restart firewalld - name: Start and enable chronyd && firewalld service: name: '{{ item }}' state: started enabled: True with_items: - firewalld - chronyd - name: Setting message of the day template: src: templates/motd.j2 dest: /etc/motd owner: root group: root mode: 0644 handlers: - name: reload ssh service: name=sshd state=reloaded - name: restart firewalld service: name=firewalld state=restarted - name: restart server shell: sleep 5 && shutdown -r now "Host restart triggered" async: 1 poll: 0 ignore_errors: true
Specjalnie dla tego zadania proponuje stworzyć tymczasowy plik inventory.
vim first_inventory
# Nie musimy tworzyć żadnej grupy, ten plik inventory powinien posiadać tylko nowe hosty. apps.local
Możemy teraz uruchomić nasz playbook (należy oczywiście dopisać użytkownika i hasło do EuroMana, oraz adres z naszym kluczem ssh), jeśli korzystamy z niewspieranej dystrybucji, która posiada już repozytoria, krok z rejestracją w EuroManie należy pominąć.
ansible-playbook first_run.yml -i ./first_inventory -e admin_name=Jan -e admin_surname=Kowalski -e host_key_checking=False -k
Opcje użyte to -i
wskazujący plik inventory, -e
dodające ekstra argumenty – admin_name dla motd.j2 i host_key_checking_False by ssh nie szukało hosta w known_host
. Opcja -k
informuje Ansible, że będziemy chcieli podać hasło do SSH.
Po uruchomieniu playbooka powinniśmy dostać następujące wyjście
SSH password: PLAY [all] ************************************************************************************ TASK [Gathering Facts] ************************************************************************ ok: [apps.local] TASK [Setup Ansible User] ********************************************************************* changed: [apps.local] TASK [Copy our ssh key to ansible user] ******************************************************* changed: [apps.local] TASK [Ensuring that wheel is able to use sudo without password] ******************************* changed: [apps.local] TASK [Ensuring that SELinux is enabled] ******************************************************* changed: [apps.local] TASK [Disable root ssh login] ***************************************************************** changed: [apps.local] TASK [Set hostname to ansible name] *********************************************************** changed: [apps.local] TASK [Register to EuroManager] **************************************************************** changed: [apps.local] TASK [Update all packages] ******************************************************************** changed: [apps.local] TASK [Install additional software] ************************************************************ changed: [apps.local] => (item=[u'firewalld', u'libselinux-python', u'libsemanage-python', u'chrony']) TASK [Incoming ssh enabled in firewalld] ****************************************************** ok: [apps.local] TASK [Start and enable chronyd && firewalld] ************************************************** changed: [apps.local] => (item=firewalld) changed: [apps.local] => (item=chronyd) TASK [Setting message of the day] ************************************************************* changed: [apps.local] RUNNING HANDLER [restart server] ************************************************************** changed: [apps.local] PLAY RECAP ************************************************************************************ apps.local : ok=14 changed=12 unreachable=0 failed=0
Zauważmy, że ponowne uruchomienie tego playbooka nie powinno się udać.
Wykonując ponownie ansible-playbook
dostaniemy na wyjściu.
TASK [Gathering Facts] ************************************************************************ fatal: [apps.local]: UNREACHABLE! => {"changed": false, "msg": "Authentication failure.", "unreachable": true}
Zakończenie
Po przeczytaniu i zrozumieniu tego artykułu czytelnik powinien być w stanie napisać obszerny playbook. Jest to playbook pierwszej konfiguracji hosta. Po nim następują playbooki używające tylko użytkownika Ansible.
W czwartej części skupimy się na temacie instrukcji w playbooku oraz rozszerzymy nasze umiejętności związane z Jinja2.