Ansible w Enterprise Linuksie – cz. III: obowiązkowy playbook

ansible 3

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:

  1. Tworzył użytkownika Ansible
  2. Dodawał nasz klucz ssh.
  3. Pozwalał użytkownikowi Ansible na używanie sudo.
  4. Wymuszał posiadanie włączonego SELinuxa.
  5. Zabraniał logowania się na roota przez SSH.
  6. Ustawiał nazwę systemu na tożsamą z ansiblową.
  7. Rejestrował maszynę w EuroManie lub podobnym rozwiązaniu.
  8. Aktualizował system.
  9. Instalował i uruchamiał demona firewalld.
  10. Otwierał port 22.
  11. Instalował biblioteki odpowiedzialne za wsparcie dla SELinuxa.
  12. Instalował pakiety odpowiedzialne za synchronizację czasu.
  13. 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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *