Quick Start KVM libvirt VMs with Terraform and Ansible – Part 2

Im ersten Teil meines Quickstart Guides habe ich gezeigt, wie man per Terraform Script neue VMs bzw. Guests unter KVM libvirt sehr flexibel und schnell an den Start bringt.

Sobald die virtuellen Maschinen erst mal grundsätzlich laufen, kann man mit Ansible die weitere Konfiguration vornehmen und zusätzliche Software installieren. Ansible ist ein Open-Source Konfigurationsmanagement und Administrations Tool, um immer wiederkehrende Arbeiten an Systemen zu automatisieren. Es ist quasi des Admins bester Freund, um ihm lästige Wartungsarbeiten abzunehmen. Ansible ist eine leicht erlernbare und lesbare Skriptbeschreibungssprache im YAML Format. Es braucht dabei selbst sehr wenig Ressourcen und ist sehr flexibel. Ansible arbeitet agentless. Das bedeutet, dass kein Ansible Agent auf den Zielsystemen installiert werden muss (so wie z.B. für Puppet). Ansible benutzt stattdessen beispielsweise auf Linux-Systemen die SSH Verbindung mit private/public Keys für eine sichere Kommunikation mit den Zielsystemen.

Ansible verwendet sogenannte Playbooks (YAML Format und Syntax) in denen die Plays als Tasks beschrieben sind, die auf den Zielsystemen abgearbeitet werden sollen. Daneben gibt es ein Inventory-File, in dem unter anderem die Hostnamen hinterlegt sind. Wiederkehrende Aufgaben können auch in Module ausgelagert werden. Ansible bietet hier schon bereits selbst viel an, um die Standardaufgaben zu erledigen, es können aber auch eigene Module entwickelt werden. Ansible Playbooks sind außerdem idempotent. D.h. ein Playbook kann mehrmals ausgeführt werden und liefert immer das gleiche Ergebnis zurück. Ein Playbook beschreibt damit den Zielzustand eines Systems.

Aber fangen wir endlich an …

Vorbereitungen und Installation

Die Installation von Ansible unter Linux geht einfach vonstatten, siehe auch hier: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#installing-ansible-on-rhel-centos-or-fedora


juergen  ~  sudo dnf install ansible

Letzte Prüfung auf abgelaufene Metadaten: vor 0:37:27 am Mi 18 Nov 2020 15:23:03 CET.
Abhängigkeiten sind aufgelöst.
======================================================================================================================================================
 Package                                      Architecture                  Version                              Repository                      Size
======================================================================================================================================================
Installieren:
 ansible                                      noarch                        2.9.14-1.fc32                        updates                         15 M
Abhängigkeiten werden installiert:
 libsodium                                    x86_64                        1.0.18-3.fc32                        fedora                         169 k
 python3-babel                                noarch                        2.8.0-2.fc32                         fedora                         5.7 M
 python3-bcrypt                               x86_64                        3.1.7-4.fc32                         fedora                          44 k
 python3-fluidity-sm                          noarch                        0.2.0-18.fc32                        fedora                          19 k
 python3-jinja2                               noarch                        2.11.2-1.fc32                        updates                        490 k
 python3-jmespath                             noarch                        0.9.4-4.fc32                         fedora                          46 k
 python3-lexicon                              noarch                        1.0.0-10.fc32                        fedora                          18 k
 python3-ntlm-auth                            noarch                        1.1.0-9.fc32                         fedora                          50 k
 python3-pynacl                               x86_64                        1.3.0-6.fc32                         fedora                         102 k
 python3-pyyaml                               x86_64                        5.3.1-1.fc32                         fedora                         202 k
 python3-requests_ntlm                        noarch                        1.1.0-10.fc32                        fedora                          18 k
 python3-xmltodict                            noarch                        0.12.0-7.fc32                        fedora                          23 k
Schwache Abhängigkeiten werden installiert:
 python3-invoke                               noarch                        1.4.1-1.fc32                         fedora                         149 k
 python3-paramiko                             noarch                        2.7.1-2.fc32                         fedora                         287 k
 python3-pyasn1                               noarch                        0.4.8-1.fc32                         fedora                         133 k
 python3-winrm                                noarch                        0.3.0-9.fc32                         fedora                          59 k

Transaktionsübersicht
======================================================================================================================================================
Installieren  17 Pakete

Gesamte Downloadgröße: 23 M
Installationsgröße: 131 M

Mit ansible –help kann überprüft werden, ob die Installation geklappt hat.

Ansible benötigt zur Kommunikation mit den Zielsystemen unter Linux eine SSH Verbindung, die per SSH-Keypair aufgebaut wird. Dafür erstellen wir uns zuerst einen eigenen Benutzer.

 juergen  ~  sudo useradd -m ansible

 juergen  ~  sudo passwd ansible
ändere Passwort für Benutzer ansible.
Geben Sie ein neues Passwort ein: 
Geben Sie das neue Passwort erneut ein: 
passwd: alle Authentifizierungsmerkmale erfolgreich aktualisiert.

Dann erstellen wir uns ein SSH-Keypair für den Ansible User.

 juergen  ~  su - ansible
Passwort: 
[ansible@localhost ~]$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ansible/.ssh/id_rsa): 
Created directory '/home/ansible/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/ansible/.ssh/id_rsa
Your public key has been saved in /home/ansible/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:2          ansible@localhost.localdomain
The key's randomart image is:
+---[RSA 3072]----+
|      .          |
|      o          |
+----[SHA256]-----+
[ansible@localhost ~]$ ll .ssh
insgesamt 8
-rw-------. 1 ansible ansible 2622 18. Nov 16:13 id_rsa
-rw-r--r--. 1 ansible ansible  583 18. Nov 16:13 id_rsa.pub
[ansible@localhost ~]$ 

Falls wir unsere VMs, wie im ersten Teil meiner Installationsanleitung beschrieben, per Terraform aufsetzen, ist es sinnvoll den Public Key gleich darüber bereitzustellen. Den Inhalt aus der Datei id_rsa.pub kopieren wir dazu in die Terraform cloudinit.cfg Datei, wie in Teil 1 beschrieben und verteilen somit den Public Key auf alle neuen Zielsysteme. Man kann das zwar auch jederzeit nachträglich manuell tun. Dazu muss man aber auf jedem System zuerst einmal einen Benutzer ansible anlegen und dann vom Ansible Server aus als Benutzer ansible per ssh-copy-id Befehl den Public-Key an alle Zielserver verteilen.

Ansible Projektdateien

Zunächst benötigen wir einen Projektordner, in meinem Beispiel „newsrv“, in dem wir die Ansible Dateien erstellen werden. Ich werde nachfolgend die benötigten Dateien im Detail erläutern und möchte damit auch anregen eigene Erweiterungen zu machen.

ansible.cfg

Dann erstellen wir eine Konfigurationsdatei Namens ansible.cfg für allgemeine Ansible Settings unseres Projektes. Daneben kann man seine Einstellungen auch in der globalen Konfigurationsdatei unter /etc/ansible vornehmen, die dann für alle Projekte gleichermaßen gilt. In meiner Beispiel-Config ist zu erwähnen, dass ich host_key_checking=false gesetzt habe. Das verringert zwar die Sicherheit der SSH Verbindung, ist in meinem Fall aber hilfreich, wenn man die VM öfter mal neu aufsetzt, da sich dann der Hostkey logischerweise jedesmal ändert.

[defaults]
hash_behaviour=merge
host_key_checking=false

[ssh_connection]
pipelining = True
timeout=30

Dann müssen wir noch den private SSH Key des ansible Users, den wir oben erstellt haben, in den Projektordner kopieren. Damit authentifiziert sich nämlich der ausführende ansible user gegenüber den Zielsystemen.

 juergen  ~  Scripts  ansible  newsrv  sudo cp /home/ansible/.ssh/id_rsa .

 juergen  ~  Scripts  ansible  newsrv  sudo chown juergen:juergen id_rsa

hosts.yml

Als nächstes erstellen wir uns eine hosts.yml Datei, die den oder die Hostnamen der Zielserver enthält und einige globale Variablen.

all:
  vars:
    ansible_connection: ssh
    ansible_user: ansible
    ansible_become: true
    ansible_ssh_private_key_file: id_rsa
    ansible_python_interpreter: /usr/bin/python3

  hosts:
    mysrv.mydomain.vm:

install.yml

Dann brauchen wir noch ein Playbook das die eigentlichen Tasks ausführt. Ich werde die Datei hier wieder schrittweise erläutern. Mein Playbook enthält auf der einen Seite Einstellungen und Konfigurationen, die ich grundsätzlich für alle VMs vornehmen möchte. Und dann befinden sich im Playbook auch noch ein paar Beispiele für Software Installationen. Dies soll anregen eigene Tasks und Playbooks zu erstellen. Denn wenn man mal das Prinzip verstanden hat ist das auch gar nicht schwer das Playbook nach seinen Vorstellungen zu erweitern.

Hinweis: Eine Yaml-Datei wird immer mit drei Bindestrichen — eingeleitet und es muss auch strikt auf die richten Einrückungen geachtet werden.

Als erstes bekommt das Playbook einen Namen, in meinem Beispiel wenig kreativ: „Install some packages“. hosts: all zeigt an, dass das Playbook für alle definierten Systeme angewendet werden soll und tasks: leitet schließlich die nachfolgenden Augaben-Blöcke ein.

---
- name: Install some packages
  hosts: all
  tasks:

Im ersten Task-Block wird IPv6 komplett deaktiviert, da es in meiner VM-Umgebung überflüssig ist.

# Disable ipv6
   - name: Disable IPv6 with sysctl
     sysctl: name={{ item }} value=1 state=present reload=yes
     with_items:
       - net.ipv6.conf.all.disable_ipv6
       - net.ipv6.conf.default.disable_ipv6
       - net.ipv6.conf.lo.disable_ipv6
   
   - name: RedHat | placeholder true for ipv6 in modprobe
     lineinfile: "dest=/etc/modprobe.conf line='install ipv6 /bin/true' create=yes"
     when: ansible_os_family == 'RedHat'
   
   - name: RedHat | disable ipv6 in sysconfig/network
     lineinfile:
       dest: /etc/sysconfig/network
       regexp: "^{{ item.regexp }}"
       line: "{{ item.line }}"
       backup: yes
       create: yes
     with_items:
       - { regexp: 'NETWORKING_IPV6=.*', line: 'NETWORKING_IPV6=NO' }
       - { regexp: 'IPV6INIT=.*', line: 'IPV6INIT=no' }
     notify:
       - restart NetworkManager.service
     when: ansible_os_family == 'RedHat'

Dann werden alle Pakete auf den neuesten Stand gebracht. Will man unterschiedliche Linux Derivate mit dem gleichen Playbook bedienen, dann muss man eine Abfrage der OS-Family mit einbauen, denn die Betriebssystemfamilien haben bekanntermaßen unterschiedliche Paketmanager (Debian: apt, SuSe: zypper …)

Wenn man außerdem noch sogenannte „tags“ mit in sein Playbook einbaut, dann kann man später auch nur bestimmte Teile seines Playbooks gezielt ausführen. Das bietet sich zum Beispiel beim Upgrade-Task an, weil dieser Vorgang regelmäßig durchgeführt wird.

Der Debug-Task gibt den Inhalt der Variable „result.changed“ an der Konsole aus. Das ist hilfreich, wenn man den Übergabewert nicht von vornherein kennt und somit ausgeben will. Weiter unten im Playbook wird der Status dieser Variable dann relevant für die Entscheidung, ob am Schluss ein Reboot des Servers durchgeführt werden soll.

# Upgrade all packages
   - name: upgrade all packages on CentOS/Fedora
     yum:
       name: "*"
       state: latest
     when: ansible_os_family == 'RedHat'
     register: result
     tags:
       - upgrade
   - debug:
       var: result.changed
     tags:
       - upgrade

Anschließend werden einige Pakete installiert, die ich gern auf allen meinen Systemen hätte und die richtige Zeitzone wird gesetzt.

# Some usefull packages for CentOS
   - name: install bind-utils for CentOS
     package:
       name: bind-utils
       state: present
     when: ansible_os_family == 'RedHat'
   - name: install cronie to create cronjobs for CentOS
     package:
       name: cronie
       state: present
     when: ansible_os_family == 'RedHat'


# Install for all distributions Debian/Redhat
   - name: install cockpit to monitor server
     package:
       name: cockpit
       state: present
   - name: install bash-completion 
     package:
       name: bash-completion
       state: present

# Set timezone
   - name: set timezone
     timezone:
       name: Europe/Berlin

Als Beispiel Applikation wird hier die CentOS Software Gruppe „Server mit GUI“ und Firefox als Browser installiert.

# Install Server with GUI
   - name: install server with GUI for CentOS
     package:
       name: '@graphical-server-environment'
       state: present
     when: ansible_os_family == 'RedHat'

# Install Webbrowser
   - name: install firefox
     package:
       name: firefox
       state: present
     when: ansible_os_family == 'RedHat'

Außerdem wäre es noch nett, wenn Autologin für den Server aktiviert wäre. Zum Abschluss wird noch ein Reboot durchgeführt falls die Variable des Upgrade Tasks „true“ zurückgibt.

# Activate autologin to console for a user
   - name: Creates getty tty1_service_de directory
     file:
       path: "/etc/systemd/system/getty@tty1.service.d"
       state: directory
       owner: root
       group: root
       mode: 0775
     tags: 
       - autologin
   - name: Activate console autologin for a user
     copy:
       dest: "/etc/systemd/system/getty@tty1.service.d/override.conf"
       owner: root
       group: root
       mode: 0644
       content: |
         # Provided by Ansible
         [Service]
         ExecStart=
         ExecStart=-/sbin/agetty --noissue --autologin juergen %I $TERM
         Type=idle
       backup: yes
     tags: 
       - autologin



# Reboot when all things done
   - reboot:
       msg: "Reboot to finish setup."
       reboot_timeout: 60
     when: result.changed == "true"

Wichtige Ansible Befehle

Nachdem wir nun alle Ansible Dateien in unserem Projektordner zusammen haben, können wir mit dem folgenden Befehl z.B. alle Hosts in diesem Projekt anzeigen lassen:

 juergen  ~  Scripts  ansible  newsrv  ansible --list-hosts all -i hosts.yml
  hosts (1):
    mysrv.mydomain.vm

Als nächstes sollten wir die Verbindung zu dem oder die Zielserver durch einen Ansible Ping testen:

 juergen  ~  Scripts  ansible  newsrv  ansible -m ping all -i hosts.yml
mysrv.mydomain.vm | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Wenn das erfolgreich war, dann ist es Zeit unser Playbook laufen zu lassen.

 juergen  ~  Scripts  ansible  newsrv  ansible-playbook -i hosts.yml install.yml

Wenn nicht gleich alles auf Anhieb funktioniert, nicht gleich aufgeben. Oft sind es sehr banale Fehler, die die Ausführung verhindern. Zum Beispiel ist es mir zu Beginn oft passiert, dass ich Fehler bei den Einrückungen im YAML Code gemacht habe, weil ich etwa den Code aus dem Internet direkt kopiert habe. Am Anfang ist es auch möglich, dass der SSH Verbindungsaufbau per Key-Pair nicht richtig funktioniert. Meistens hilft googlen weiter.

So sieht der Ansibe Output während des Playbook-Runs aus.

Mit den Tags im Playbook lassen sich dann auch einfach nur Teile des Playbooks, wie z.B. der Upgrade Task, ausführen. So kann man gezielte Aufgaben erledigen, ohne immer gleich das ganze Playbook durchlaufen lassen zu müssen.

 juergen  ~  Scripts  ansible  newsrv  ansible-playbook -i hosts.yml install.yml --tags upgrade

PLAY [Install some packages] ***************************************************************************************

TASK [Gathering Facts] *********************************************************************************************
ok: [mysrv.mydomain.vm]

TASK [upgrade all packages on CentOS/Fedora] ***********************************************************************
ok: [mysrv.mydomain.vm]

TASK [debug] *******************************************************************************************************
ok: [mysrv.mydomain.vm] => {
    "result.changed": false
}

PLAY RECAP *********************************************************************************************************
mysrv.mydomain.vm          : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Damit sind wir am Ende des zweiten Teils angelangt. Ich hoffe ich konnte euch inspirieren zukünftig eure KVM/libvirt Infrastruktur per Terraform und Ansible zu verwalten. Würde mich freuen, wenn ihr mir euer Feedback dazu gebt.