Bevor wir nun in die Welt der roles bei Ansible eintauchen, werfen wir kurz noch einen Blick auf zwei gängige Szenarien:
Wenn wir das Thema nun nüchtern und wertfrei betrachten werden wir feststellen, dass wir so eine Vielzahl von Systemen nicht auf Dauer ohne große Aufwände mit Ansible verwalten können. Um nun diese Herausforderungen elegant zu lösen bringt uns Ansible die Funktion der Rollen (roles) mit.
Rollen sind im Grunde nichts anderes als Verzeichnisse, die auf eine bestimmte Art und Weise angelegt sind. Rollen folgen vordefinierten Verzeichnis-Layout-Konventionen und erwarten, dass sich dabei die zugehörigen Komponenten in dem für sie vorgesehenen Verzeichnispfad befindet. Bei der Grundkonfiguration unseres Ansible-Hosts hatte wir bereits diese Verzeichnisstruktur beim Anlegen des Ansible: Directory Layout angelegt.
/home/ansible/ansible/roles/ └── roles # Verzeichnis für die einzelnen (unterschiedlichen) Rollen └── common # Verzeichnis "role" common mit seinen entsprechenden Definitionen - Vorlage zum Kopieren ├── defaults # Verzeichnis "defaults" │ └── main.yml # Standardvariablen mit niedrigerer Priorität für diese Rolle ├── files # Verzeichnis "files" │ └── main.yml # (Skript-)Dateien zur Verwendung als Kopier- bzw. Script-Rressource ├── handlers # Verzeichnis "handlers" │ └── main.yml # Datei mit den Definitionen zu den rollenspezifischen "handlers" ├── library # Verzeichnis für benutzerdefinierte Module einer der Rolle (role) "common" ├── lookup_plugin # Verzeichnis für weitere Arten von Plugins, wie in diesem Fall "lookup" ├── meta # Verzeichnis "meta" für Rollenspezifische Definitionen/Abhängigkeiten │ └── main.yml # Datei mit Rollenspezifischen Definitionen ├── module_utils # Verzeichnis "module_utils", die benutzerdefinierte module_utils der Rollen enthalten könnte ├── tasks # Verzeichnis "tasks" für kleinere Aufgabendateien │ └── main.yml # Datei für kleinere Aufgaben, falls diese benötigt werden würden ├── templates # Verzeichnis mit den Templates │ └── main.j2 # Template-Datei mit dem Dateisuffix/-Ende .j2 └── vars # Verzeichnis "vars", mit den zu dieser Rolle zugeordneten Variablen └── main.yml # Datei mit den Rollenspezifischen Variablen
Diese Kopiervorlage common
brauchen wir nur noch für jede entsprechende Rolle dann kopieren, so z.B. für die Rolle postfix
$ cd ~/ansible/roles $ cp -avr common/ postfix/
In folgendem Konfigurationsbeispiel wollen wir uns zu folgendem Szenario eine handelbare Lösung genauer betrachten. Wir gehen dabei von folgendem Einsatzszenario aus:
Es ist ein Webserver mit Hilfe von Ansible zu erstellen. Am Host soll sich der verantwortliche WEB-Admin ruben anmelden können und der Host soll sich des NTP-Servers im der eigenen Zone bedienen. Dies sind Standardkonfigurationsaufgaben, die auf jedem Server in der betreffenden Sicherheitszone zutreffen. Nach der Installation des Webserver-Daemon apache soll dieser konfiguriert, die initiale Homepage Hello World installiert und zum Schluss der Webserver gestartet werden.
Als erstes zerlegen wir die Aufgabenstellung in einzelne Arbeitspakete und unterteilen diese Pakete in einzelne Aufgaben, als da wären:
DOCUMENT_ROOT
des Apache-WebserversUm möglichst für die Zukunft flexibel zu sein, werden wir unser Ansible Playbook oder bessere gesagt den darunter liegenden Code, möglichst flexibel halten. Das bedeutet, dass wir sowohl für die Grundkonfiguration (User/Gruppe anlegen, wie Installation des Chrony-Daemon) wie auch für die spätere Host-/Aufgabenspezifische Installation des Webservers (Apache) eigene Rollen definieren werden. So können wir später die Rollen mühelos erweitern, wenn wir z.B. weitere Dienste wie z.B. einen PHP-Interpreter oder die zum Apachen zugehörige TLS-Konfiguration vornehmen werden.
Folgende Struktur soll definiert werden:
site.yml
db.yml
(spätere erweiterte Konfiguration)postfix.yml
(spätere erweiterte Konfiguration)web.yml
web.yml
bedient sich der Rollen:base
über die die Benutzeranlage erfolgtchrony
mit Hilfe deren die NTP-Client-Konfiguration erledigt wirdapache
Installation und Konfiguration des Apache Webservers mit Hilfe von tasks
, files
und handlers
Zunächst erstellen und befüllen wir der Playbook-Konfigurationsdatei site_generation.yml
. Diese Datei wird später alle Definitionen unserer Systemumgebung enthalten, also nicht nur die für den Apache-Webserver, sondern auch für den Datenbank-, Mail- und sonstige Server. Die spezifischen Playbooks werden dann hier nur noch inkludiert bzw. importiert.
$ vim ~/ansible/playbooks/site_generation.yml
--- # Start des systemweiten Playbooks site_generation.yml - import_playbook: web.yml # Playbook zum Konfigurieren unseres Webservers einbinden ... # Ende unseres systemweiten Playbooks
Für die Konfiguration des Webservers an sich verwenden wir dann die Konfigurationsdatei web_server.yml
; die wir in der zuvor angelegten systemweiten Konfigurationsdatei site_generation.yml
inkludiert hatten. Da die Konfiguration auf allen WEB-Hosts erfolgen soll, geben wir beim Parameter hosts
den Namen der Hostgruppe aus unserer Inventory-Definition an. Bei der Definition der Rollen geben wir die drei roles
an, die wir in unseren zuvor angestellten Überlegungen gewählt hatten.
$ vim ~/ansible/playbooks/web.yml
--- # Start des Playbooks für den Web-Server - hosts: www roles: - base # Basiskonfiguration (User anlegen) - chrony # Installation und Konfiguration NTP-Client - www # Installation und Konfiguration Apache Webserver ... # Start des Playbooks für den Web-Server
Unser Kopiervorlage common
für die Rollen kopieren wir nun für die gewählten Arbeitspakete (roles), als erstes also für die Rolle base
, bei der wir den verantwortlichen Web-Admin anlegen werden.
$ cd ~/ansible/roles $ cp -avr common/ base/
Unser Playbook-Beispiel 01 passen wir nun an und speichern dieses im Verzeichnis ~/ansible/roles/base/tasks/
unter den Namen main.yml
$ vim ~/ansible/roles/base/tasks/main.yml
--- # Grundlegende Konfiguration für alle Hosts - name: "***base*** : Gruppe für (WEB-Entwickler) '{{ createuser }}' erstellen" ansible.builtin.group: # https://docs.ansible.com/ansible/latest/modules/group_module.html name: '{{ createuser }}' gid: '{{ createguid }}' state: present - name: "***base*** : WEB-Admin Nutzerkonto für den User '{{ createuser }}' mit frn zugehörigen UID '{{ createguid }}' anlegen un der Gruppe '{{ createuser }}' zuordnen." ansible.builtin.user: # https://docs.ansible.com/ansible/latest/modules/user_module.html name: '{{ createuser }}' comment: '{{ createusername }}' uid: '{{ createguid }}' group: '{{ createuser }}' state: present - name: "***base*** : Initiales Passwort für den WEB-Admin '{{ createuser }}' hinterlegen" ansible.builtin.shell: # https://docs.ansible.com/ansible/latest/modules/shell_module.html cmd: usermod -p $(echo '{{ createpassword }}' | openssl passwd -1 -stdin) {{ createuser }}
Wie wir sehen, beinhaltet diese Datei nur noch die Definition der tasks aber keine Sitespezifischen Variablen, da wir diese in eine separate Date i auslagern werden. Wir könnten als jederzeit diese Datei auf andere Installationsumgebungen portieren oder an interessierte Adminkollegen weitergeben!
Natürlich benötigen wir nun die Definition der Rollenspezifischen Variablen, die je Rolle hier ~/ansible/roles/base/vars/main.yml
abzulegen sind.
$ vim ~/ansible/roles/base/vars/main.yml
--- # Definition der rollenspezifische Variablen createguid : '1010' # GID/UID des Benutzers00 createuser : 'ruben' # Username createusername: 'Ruben Nausch' # Vollständiger Name createpassword: 'M31nP4p4157d3r4113r83573!' # Initialpasswort (ungecrypted!)
Da wir dort ein Passwort vorhalten, werden wir Dank unserer Ansible-Vault-Konfiguration diese Datei nun verschlüsseln.
$ ansyble-vault encrypt ~/ansible/roles/base/vars/main.yml
Die nächste Rolle, die wir konfigurieren müssen, ist die für den NTP-Daemon chrony
. Auch hier kopieren wir zunächst das Default-role-template common.
$ cd ~/ansible/roles $ cp -avr common/ chrony/
Auch hier greifen wir wieder auf das bereits bekannte Playbook-Beispiel 05 zurück, passen dieses an und speichern es im Verzeichnis ~/ansible/roles/chrony/tasks/
unter den Namen main.yml
. Bei diesem Konfigurationsbeispiel besteht das Playbook aber lediglich aus vier tasks, die wir als eigenständige Dateien auslagern und hier nur inkludieren werden.
$ vim ~/ansible/roles/chrony/tasks/main.yml
--- # Hauptinstallations-/Konfigurationsdatei für den Dienst chrony - include: install.yml # Installation - include: config-backup.yml # original-Konfig sichern - include: configure.yml # Konfiguration - include: service.yml # Service starten
Die vier tasks hinterlegen wir dann anschließend in der zugehörigen YML-Datei.
$ vim ~/ansible/roles/chrony/tasks/install.yml
--- - name: "***chrony*** : Installation des Deamon '{{ daemon_name }}' (in der aktuellsten Version)" ansible.builtin.dnf: #https://docs.ansible.com/ansible/latest/modules/dnf_module.html name: '{{ daemon_name }}' state: latest
$ vim ~/ansible/roles/chrony/tasks/config-backup.yml
--- - name: "***chrony*** : 1) Überprüfen ob das Backup der Konfigurationsdatei '{{ config_file }}' bereits existiert" ansible.builtin.stat: # https://docs.ansible.com/ansible/latest/modules/stat_module.html path: /etc/chrony.conf.orig register: stat_result - name: "***chrony*** : 2) Von der bestehenden originalen Konfigurationsdatei '{{ config_file }}' ein Backup '{{ config_file }}'.orig erstellen" ansible.builtin.copy: # https://docs.ansible.com/ansible/latest/modules/copy_module.html remote_src: yes src: '{{ config_file }}' dest: /etc/chrony.conf.orig when: stat_result.stat.exists == False
$ vim ~/ansible/roles/chrony/tasks/configure.yml
--- - name: "***chrony*** : Template Konfigurationsdatei an Ort und Stelle kopieren und Variablen setzen" ansible.builtin.template: # https://docs.ansible.com/ansible/latest/modules/template_module.html src: templates/chrony-client.conf.j2 dest: "{{ config_file }}"
Das zugehörige Template für die Konfigurationsdatei legen wir im zugehörigen Verzeichnis ~/ansible/roles/chrony/templates
ab.
$ vim ~/ansible/roles/chrony/templates/chrony-client.conf.j2
# Use public servers from the pool.ntp.org project. # Please consider joining the pool (http://www.pool.ntp.org/join.html). {{ chrony_pool }} # Ignore stratum in source selection {{ chrony_stratumweight }} # Record the rate at which the system clock gains/losses time. driftfile /var/lib/chrony/drift # Allow the system clock to be stepped in the first three updates # if its offset is larger than 1 second. makestep 1.0 3 # Enable kernel synchronization of the real-time clock (RTC). rtcsync # In first three updates step the system clock instead of slew # if the adjustment is larger than 10 seconds. {{ chrony_makestep }} # Enable hardware timestamping on all interfaces that support it. #hwtimestamp * # Increase the minimum number of selectable sources required to adjust # the system clock. #minsources 2 # Allow NTP client access from local network. #allow 192.168.0.0/16 # Serve time even if not synchronized to a time source. #local stratum 10 # Specify file containing keys for NTP authentication. keyfile /etc/chrony.keys # Get TAI-UTC offset and leap seconds from the system tz database. leapsectz right/UTC # Specify directory for log files. logdir /var/log/chrony # Select which information is logged. #log measurements statistics tracking
Der task zum Starten des Daemon legen wir im Verzeichnis task
ab.
$ vim ~/ansible/roles/chrony/tasks/service.yml
--- - name: "***chrony*** : Sicherstellen dass der Daemon '{{ daemon_name }}' (beim Systemstart) gestartet wird und läuft" ansible.builtin.service: # https://docs.ansible.com/ansible/latest/modules/service_module.html name: chronyd state: started enabled: yes
Zu guter letzt benötigen wir auch hier eine zur Rolle gehörige Parameter-Datei mit den Definitionen der Variablen.
$ vim ~/ansible/roles/chrony/vars/main.yml
--- # Definition der rollenspezifische Variablen zum Dienst chrony daemon_name : chrony config_file : /etc/chrony.conf # chronyd client config-options chrony_pool : "server time.dmz.nausch.org iburst" chrony_stratumweight: "stratumweight 0" chrony_makestep : "makestep 10 3"
Alle bisher getroffenen Konfigurationseinstellungen gelten für alle Applikations-Hosts und wir müssen lediglich die beiden Rollen base
und chrony
bei den entsprechenden Playbook-Dateien includieren!
Nun folgt der Webserverspezifische Teil unseres Ansible-Musterkonfiguration eines Webserversmit der Definition der Role www
. Zunächst benötigen wir natürlich auch für diese Rolle die altbekannte Verzeichnisstruktur.
$ cd ~/ansible/roles $ cp -avr common/ www/
Angelehnt an die Rolle chrony
definieren wir auch hier als erstes das playbook mit fünf einzelnen tasks, die wir als eigenständige Dateien auslagern und hier nur includieren werden. In dieses Falle sindes fünf tasks, also einer mehr wie bei der Rolle chrony
, da wir ja hier unsere initialen Webseite nach DOCUMENT_ROOT
des Apache-Webservers noch kopieren wollen.
$ vim ~/ansible/roles/www/tasks/main.yml
--- # Hauptinstallations-/Konfigurationsdatei für den Apache-Webserver - include: install.yml # Installation - include: config-backup.yml # original-Konfig sichern - include: configure.yml # Konfiguration - include: content.yml # Initiale Webseite befüllen - include: service.yml # Service starten
Da wir das Rad ja bekanntlicher Weise nicht 2x erfinden müssen, greifen wir bei der Konfiguration der einzelnen tasks der Rolle www
auf altbewährtes, nämlich der Konfigurationsvorlage der zuvor erstellten Rolle chrony
zurück. Wir kopieren uns die Datei und verändern diese entsprechend.
$ cp ~/ansible/roles/chrony/tasks/install.yml ~/ansible/roles/www/tasks/install.yml $ vim ~/ansible/roles/www/tasks/install.yml
--- - name: "***www*** : Installation des Deamon '{{ daemon_name }}' (in der aktuellsten Version)" ansible.builtin.dnf: #https://docs.ansible.com/ansible/latest/modules/dnf_module.html name: '{{ daemon_name }}' state: latest
Ähnlich verfahren wir mit dem task zum Sichern der originalen Konfigurationsdatei config-backup.yml
sowie zum Konfigurieren des Daemon configure.yml
.
$ cp ~/ansible/roles/chrony/tasks/config-backup.yml ~/ansible/roles/www/tasks/config-backup.yml $ vim ~/ansible/roles/www/tasks/config-backup.yml
--- - name: "***www*** : 1) Überprüfen ob das Backup der Konfigurationsdatei '{{ config_file }}' bereits existiert" ansible.builtin.stat: # https://docs.ansible.com/ansible/latest/modules/stat_module.html path: '{{ backup_file }}' register: stat_result - name: "***www*** : 2) Von der bestehenden originalen Konfigurationsdatei '{{ config_file }}' ein Backup '{{ backup_file }}' erstellen" copy: # https://docs.ansible.com/ansible/latest/modules/copy_module.html remote_src: yes src: '{{ config_file }}' dest: '{{ backup_file }}' when: stat_result.stat.exists == False
$ cp ~/ansible/roles/chrony/tasks/configure.yml ~/ansible/roles/www/tasks/configure.yml $ vim ~/ansible/roles/www/tasks/configure.yml
--- - name: "***www*** : Template Konfigurationsdatei an Ort und Stelle kopieren und Variablen setzen" ansible.builtin.template: # https://docs.ansible.com/ansible/latest/modules/template_module.html src: templates/httpd-server.conf.j2 dest: "{{ config_file }}"
Auch hier benötigen wir eine Datei, in der die Variablen für den Task www vorgehalten werden.
$ vim ~/ansible/roles/www/vars/main.yml
--- # Definition der rollenspezifische Variablen zum Apache-Webserver httpd daemon_name : httpd config_file : /etc/httpd/conf/httpd.conf backup_file : /etc/httpd/conf/httpd.conf.orig # httpd-Server config-options httpd_server_admin : "ServerAdmin webmaster@nausch.org" httpd_server_http_header : "ServerTokens Prod\nServerSignature Off\nHeader always unset \"X-Powered-By\"\nHeader unset \"X-Powered-By\"" httpd_server_extendedstatus: "ExtendedStatus On" httpd_server_traceenable : "TraceEnable off"
Zu guter letzt müssen wir natürlich auch noch Sorge Tragen dass der HTTP-Daemon läuft und beim Systemstart auch geladen wird. Auch hier kopieren wir uns der Einfachheit halber das passende Gegenstück aus unserem Erfahrungsschatz.
$ cp ~/ansible/roles/chrony/tasks/service.yml ~/ansible/roles/www/tasks/service.yml $ vim ~/ansible/roles/www/tasks/service.yml
--- - name: "***www*** : Sicherstellen dass der Daemon '{{ daemon_name }}' (beim Systemstart) gestartet wird und läuft" ansible.builtin.service: # https://docs.ansible.com/ansible/latest/modules/service_module.html name: httpd state: started enabled: yes - name: "***www*** : Paketfilter anpassen und Port 80 (HTTP) öffnen" ansible.builtin.firewalld: # https://docs.ansible.com/ansible/latest/modules/firewalld_module.html service: http permanent: yes state: enabled
Wir haben also ein modular aufgebautes Installationsplaybook für unseren Webserver, mit den drei Rollen base, chrony und www und jeweils abgetrennte lokale individuelle Konfigurationsparameter, sowie das Playbook web.yml
und der übergeordneten Site-Konfiguration site.yml
.
/home/ansible/ansible/ ├── filter_plugins ├── inventories │ ├── production │ │ ├── group_vars │ │ ├── hosts.yml │ │ └── host_vars │ └── staging │ ├── group_vars │ ├── hosts.yml │ └── host_vars ├── library ├── module_utils ├── roles │ ├── base │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── library │ │ ├── lookup_plugin │ │ ├── meta │ │ │ └── main.yml │ │ ├── module_utils │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ └── main.yml │ │ └── vars │ │ └── main.yml │ ├── chrony │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── library │ │ ├── lookup_plugin │ │ ├── meta │ │ │ └── main.yml │ │ ├── module_utils │ │ ├── tasks │ │ │ ├── config-backup.yml │ │ │ ├── configure.yml │ │ │ ├── install.yml │ │ │ ├── main.yml │ │ │ └── service.yml │ │ ├── templates │ │ │ ├── chrony-client.conf.j2 │ │ │ └── main.yml │ │ └── vars │ │ └── main.yml │ ├── common │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ ├── library │ │ ├── lookup_plugin │ │ ├── meta │ │ │ └── main.yml │ │ ├── module_utils │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ └── main.yml │ │ └── vars │ │ └── main.yml │ └── www │ ├── defaults │ │ └── main.yml │ ├── files │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── library │ ├── lookup_plugin │ ├── meta │ │ └── main.yml │ ├── module_utils │ ├── tasks │ │ ├── config-backup.yml │ │ ├── configure.yml │ │ ├── content.yml │ │ ├── install.yml │ │ ├── main.yml │ │ └── service.yml │ ├── templates │ │ ├── homepage.conf.j2 │ │ ├── httpd-server.conf.j2 │ │ └── main.yml │ └── vars │ └── main.yml ├── site.yml └── web.yml
Zum Schluss rufen wir unser erstes role-based Playbook auf uns installieren und konfigurieren unseren Webserver.
$ ansible-playbook site.yml
BECOME password: PLAY [demo] ***************************************************************************************************************************************************** TASK [Gathering Facts] ****************************************************************************************************************************************** ok: [demo]
TASK [***base*** : Gruppe für (WEB-Entwickler) 'ruben' erstellen] *********************************************************************************************** changed: [demo]
TASK [***base*** : WEB-Admin Nutzerkonto für den User 'ruben' mit frn zugehörigen UID '1010' anlegen un der Gruppe 'ruben' zuordnen.] *************************** changed: [demo]
TASK [***base*** : Initiales Passwort für den WEB-Admin 'ruben' hinterlegen] ************************************************************************************ changed: [demo]
TASK [***chrony*** : Installation des Deamon 'chrony' (in der aktuellsten Version)] ***************************************************************************** ok: [demo]
TASK [***chrony*** : 1) Überprüfen ob das Backup der Konfigurationsdatei '/etc/chrony.conf' bereits existiert] ************************************************** ok: [demo]
TASK [***chrony*** : 2) Von der bestehenden originalen Konfigurationsdatei '/etc/chrony.conf' ein Backup '/etc/chrony.conf'.orig erstellen] ********************* changed: [demo]
TASK [***chrony*** : Template Konfigurationsdatei an Ort und Stelle kopieren und Variablen setzen] ************************************************************** changed: [demo]
TASK [***chrony*** : Sicherstellen dass der Daemon 'chrony' (beim Systemstart) gestartet wird und läuft] ******************************************************* ok: [demo]
TASK [***www*** : Installation des Deamon 'httpd' (in der aktuellsten Version)] ********************************************************************************* changed: [demo]
TASK [***www*** : 1) Überprüfen ob das Backup der Konfigurationsdatei '/etc/httpd/conf/httpd.conf' bereits existiert] ******************************************* ok: [demo]
TASK [***www*** : 2) Von der bestehenden originalen Konfigurationsdatei '/etc/httpd/conf/httpd.conf' ein Backup '/etc/httpd/conf/httpd.conf.orig' erstellen] **** changed: [demo]
TASK [***www*** : Template Konfigurationsdatei an Ort und Stelle kopieren und Variablen setzen] ***************************************************************** changed: [demo]
TASK [***www*** : default-Homepage an Ort und Stelle kopieren] ************************************************************************************************** changed: [demo]
TASK [***www*** : Sicherstellen dass der Daemon 'httpd' (beim Systemstart) gestartet wird und läuft] *********************************************************** changed: [demo]
TASK [***www*** : Paketfilter anpassen und Port 80 (HTTP) öffnen] *********************************************************************************************** changed: [demo]
PLAY RECAP ****************************************************************************************************************************************************** demo : ok=16 changed=11 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Abschließend kann man sich nun berechtigter Weise die Frage stellen, warum man nun mehrere Dateien erstellt haben,um den Code und die Variablen dazu, der die Pakete installiert und die Dienste verwaltet, separat zu speichern? Wir sind so sehr leicht in der Lage zum, Beispiel Dienste in mehreren Phasen bereitstellen. In einer ersten Phase können wir so Anwendungen lediglich installieren und konfigurieren und erst in der zweiten Phase dann die Dienste dann starten. Natürlich haben es wir so auch leichter Teile des Codes wieder zu verwenden, als wenn man eine große Datei verwenden würde in der alle Einzelschritte selektiv aufgeführt sind!