Schlagwort-Archive: Ansible

Nextcloud im Container – Teil 2: Die Ansible-Rolle

In Teil 1 dieser Artikelserie habe ich mein Ansinnen ausführlich beschrieben. Dieser Teil widmet sich der Entwicklung einer Ansible-Rolle zum Deployment des Nextcloud-Apache-Container-Images.

In den folgenden Abschnitten beschreibe ich die Einrichtung eines Python Virtual Environments, die Installation von Ansible in dem zuvor erstellten Environment und die Installation der Ansible-Collection containers.podman, bevor ich mich abschließend der eigentlichen Ansible-Rolle widme.

Python Virtual Environments für Ansible

Zur Einrichtung habe ich mich an den englischsprachigen Artikel „How to set up and use Python virtual environments for Ansible“ von Gineesh Madapparambath gehalten. Die notwendigen Schritte werden hier kurz und bündig dokumentiert.

[t14s ~]$ python3 --version
Python 3.9.7

[t14s ~]$ mkdir python-venv
[t14s ~]$ cd !$
cd python-venv

[t14s python-venv]$ python3 -m venv ansible-core2.x
[t14s python-venv]$ source ansible-core2.x/bin/activate
(ansible-core2.x) [jkastning@t14s python-venv]$ python3 -m pip install --upgrade pip
Requirement already satisfied: pip in ./ansible-core2.x/lib/python3.9/site-packages (21.0.1)
Collecting pip
  Downloading pip-21.3.1-py3-none-any.whl (1.7 MB)
     |████████████████████████████████| 1.7 MB 2.3 MB/s 
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 21.0.1
    Uninstalling pip-21.0.1:
      Successfully uninstalled pip-21.0.1
Successfully installed pip-21.3.1

(ansible-core2.x) [t14s python-venv]$ python3 -m pip install ansible-core
Collecting ansible-core
[...]

(ansible-core2.x) [t14s python-venv]$ ansible --version
ansible [core 2.11.6] 
  config file = None
  configured module search path = ['/home/tronde/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/tronde/python-venv/ansible-core2.x/lib64/python3.9/site-packages/ansible
  ansible collection location = /home/tronde/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/tronde/python-venv/ansible-core2.x/bin/ansible
  python version = 3.9.7 (default, Aug 30 2021, 00:00:00) [GCC 11.2.1 20210728 (Red Hat 11.2.1-1)]
  jinja version = 3.0.2
  libyaml = True

Damit ist die Installation von ansible-core abgeschlossen. Im folgenden Code-Block wird geprüft, ob Ansible sich grundsätzlich mit dem Zielsystem verbinden und dort einen Python-Interpreter identifizieren kann.

(ansible-core2.x) [t14s python-venv]$ ansible -i hosts --private-key ~/.ssh/ansible_id_rsa -m ping example.com
example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Installation der Ansible-Collection containers.podman

Um Podman auf dem Zielsystem konfigurieren zu können, wird die genannte Ansible-Collection benötigt, welche mit folgendem Befehl installiert werden kann. Der Code-Block zeigt zusätzlich die Ausgabe während der Installation.

(ansible-core2.x) [t14s ansible-core2.x]$ ansible-galaxy collection install containers.podman
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/download/containers-podman-1.8.2.tar.gz to /home/tronde/.ansible/tmp/ansible-local-8729oh0om8w3/tmp7tv2yrae/containers-podman-1.8.2-9rw3fd1y
Installing 'containers.podman:1.8.2' to '/home/tronde/.ansible/collections/ansible_collections/containers/podman'
containers.podman:1.8.2 was installed successfully

Ansible-Rolle: Deployment von Nextcloud und MariaDB als Pod

Nextcloud benötigt für den Betrieb eine Datenbank. Hierfür könnte man eine integrierte SQLite nutzen. Dies wird jedoch nur für kleine Umgebungen empfohlen. Während der Entstehung dieses Artikels wird MariaDB als Datenbank-Backend vom Nextlcoud-Projekt empfohlen. Daher habe ich mich entschieden, das Nextcloud-Image zusammen mit einem MariaDB-Container zu deployen. Dazu greife ich auf die beiden folgenden Container-Repositorien zurück:

Das Grundgerüst bzw. die Verzeichnisstruktur für die Ansible-Rolle wurde erstellt mit:

$ ansible-galaxy role init --offline ansible_role_deploy_nextcloud_with_mariadb_pod

Die aktuelle Version der Ansible-Rolle ist auf GitHub zu finden. Ich werde ihre Bestandteile hier im Einzelnen vorstellen.

Die Variablen in defaults/main.yml

In der Datei defaults/main.yml habe ich Standardwerte für Variablen definiert, die geeignet sind, eine funktionsfähige Nextcloud-Instanz zu initialisieren. Die Bezeichner der Variablen sind dabei der Dokumentation der verwendeten Container-Repositorien entnommen.

In Zeile 4-7 und 10 werden die Namen für Podman-Volumes definiert, welche die persistent zu speichernden Daten aufnehmen werden.

     1	---
     2	# defaults file for ansible_role_deploy_nextcloud_with_mariadb_pod
     3	# Podman volumes for Nextcloud
     4	NC_HTML: nc_html
     5	NC_APPS: nc_apps
     6	NC_CONFIG: nc_config
     7	NC_DATA: nc_data
     8	
     9	# Podman volume for MariaDB
    10	MYSQL_DATA: mysql_data

Die Zeilen 13-17 definieren Variablen für die MariaDB-Instanz, wie z.B. Namen der Datenbank, Benutzername und Passwörter für diese Datenbank und den DB-Host. Diese werden neben dem MariaDB-Container auch von dem Nextcloud-Container benötigt, um eine Verbindung zur Datenbank herstellen zu können.

    12	# MySQL/MariaDB vars
    13	MYSQL_DATABASE: nc_db
    14	MYSQL_USER: nextcloud
    15	MYSQL_PASSWORD: ToPSeCrEt2021!
    16	MYSQL_ROOT_PASSWORD: ToPSeCrEt2021!
    17	MYSQL_HOST: 127.0.0.1
    18	
    19	# Vars for MariaDB container
    20	MARIADB_CONMON_PIDFILE: /tmp/mariadb_conmon.pid
    21	MARIADB_IMAGE: docker.io/library/mariadb:10.5.7
    22	MARIADB_NAME: nc_mariadb

Zeile 20-22 definiert Variablen, die für den MariaDB-Container benötigt werden. Hier wird z.B. die Version des Container-Images (MARIADB_IMAGE) und ein Name für die Container-Instanz (MARIADB_NAME) festgelegt.

Die folgenden Zeilen widmen sich den Variablen für den Nextcloud-Container. Dort werden in den Zeilen 25 u. 26 Benutzername und Passwort für den Nextcloud-Admin definiert, gefolgt von einigen Variablen, welche bei Nutzung eines Reverse-Proxy benötigt werden und SMTP-Variablen, welche der Nextcloud den Mailversand ermöglichen.

    24	# Nextcloud vars
    25	NEXTCLOUD_ADMIN_USER: nc_admin
    26	NEXTCLOUD_ADMIN_PASSWORD: VSnfD2021!
    27	NEXTCLOUD_OVERWRITEPROTOCOL: ""
    28	NEXTCLOUD_OVERWRITECLIURL: ""
    29	NEXTCLOUD_TRUSTED_DOMAINS: ""
    30	
    31	# SMTP vars
    32	SMTP_HOST: smtp.example.com
    33	SMTP_SECURE: tls # ssl to use SSL, or tls zu use STARTTLS
    34	SMTP_PORT: 587 # (25, 465 for SSL, 587 for STARTTLS)
    35	SMTP_AUTHTYPE: LOGIN
    36	SMTP_NAME: bob@example.com
    37	SMTP_PASSWORD: MailSecret1!
    38	MAIL_FROM_ADDRESS: no-reply@example.com
    39	MAIL_DOMAIN: "@example.com"

Bei den SMTP-Variablen handelt es sich um Beispiel-Werte. Diese müssen an die konkrete Umgebung angepasst werden.

Es folgen nun noch ein paar Variablen, welche dem Pod und dem Nextcloud-Container einen Namen geben, sowie die Version des zu verwendenden Nextcloud-Container-Images festlegen.

    41	# Vars for podman-pod(1)
    42	POD_NAME: nc_pod
    43	POD_PORT: 127.0.0.1:40231:80
    44	POD_INFRA_CONMON_PIDFILE: /tmp/nc_pod_infra.pid
    45	
    46	# Vars for Nextcloud container
    47	NC_CONMON_PIDFILE: /tmp/nc_conmon.pid
    48	NC_IMAGE: docker.io/library/nextcloud:23-apache
    49	NC_NAME: nextcloud

Durch POD_PORT: 127.0.0.1:40231:80 wird definiert, dass der Port 40231 an das Loopback-Interface gebunden und mit Port 80 des Pods verknüpft wird. Mit dieser Einstellung ist die Nextcloud-Instanz nur von dem Host aus erreichbar, auf dem sie ausgebracht wurde. Möchte man sie auch von anderen Hosts aus erreichbar machen, kann man entweder den Teil mit 127.0.0.1: weglassen oder einen Reverse-Proxy wie z.B. NGINX verwenden. Ich empfehle an dieser Stelle letzteres.

Hinweis: In defauts/main.yml stehen Passwörter im Klartext. Diese sind mit der Veröffentlichung der Ansible-Rolle allgemein bekannt und sollten gegen solche ersetzt werden, die geheimgehalten werden. Dies kann z.B. geschehen, in dem man die entsprechenden Variablen in vars/main.yml oder host_vars/hostname neu definiert. Es bietet sich an, diese zusätzlich mit Ansible-Vault zu verschlüsseln.

Die Tasks in tasks/main.yml

Im vorstehenden Abschnitt wurden die Variablen definiert, welche für die nun folgenden Tasks benötigt werden. Diese sind in tasks/main.yml definiert und werden im folgenden wieder abschnittsweise erläutert.

     1	---
     2	# tasks file for ansible_role_deploy_nextcloud_with_mariadb_pod
     3	- name: Main folder, needed for updating
     4	  containers.podman.podman_volume:
     5	    state: present
     6	    name: "{{ NC_HTML }}"
     7	    recreate: no
     8	    debug: no
     9	
    10	- name: Volume for installed/modified apps
    11	  containers.podman.podman_volume:
    12	    state: present
    13	    name: "{{ NC_APPS }}"
    14	    recreate: no
    15	    debug: no
    16	
    17	- name: Volume for local configuration
    18	  containers.podman.podman_volume:
    19	    state: present
    20	    name: "{{ NC_CONFIG }}"
    21	    recreate: no
    22	    debug: no
    23	
    24	- name: Volume for the actual data of Nextcloud
    25	  containers.podman.podman_volume:
    26	    state: present
    27	    name: "{{ NC_DATA }}"
    28	    recreate: no
    29	    debug: no
    30	
    31	- name: Volume for the MySQL data files
    32	  containers.podman.podman_volume:
    33	    state: present
    34	    name: "{{ MYSQL_DATA }}"
    35	    recreate: no
    36	    debug: no

Die ersten Zeilen enthalten Tasks, durch welche die Podman-Volumes zur persistenten Datenspeicherung auf dem Zielsystem erstellt werden. Diese Tasks sind, wie für Ansible üblich, deklarativ und idempotent. Existiert ein Volume bereits, liefert der entsprechende Task ein ‚OK‘ zurück, da keine Aktionen erforderlich sind.

Die folgenden Zeilen erstellen den Podman-Pod und fügen ihm einen Nextcloud- sowie einen MariaDB-Container hinzu. Die Dokumentation der verwendeten Module findet sich in Punkt 5 und 6 im Abschnitt Quellen und weiterführende Links.

    38	- name: Create the podman-pod(1)
    39	  containers.podman.podman_pod:
    40	    debug: no
    41	    infra: yes
    42	    infra_conmon_pidfile: "{{ POD_INFRA_CONMON_PIDFILE }}"
    43	    publish: "{{ POD_PORT }}"
    44	    name: "{{ POD_NAME }}"
    45	    state: started
    46	
    47	- name: Create MariaDB container
    48	  containers.podman.podman_container:
    49	    debug: yes
    50	    conmon_pidfile: "{{ MARIADB_CONMON_PIDFILE }}"
    51	    image: "{{ MARIADB_IMAGE }}"
    52	    image_strict: yes
    53	    pod: "{{ POD_NAME }}"
    54	    recreate: yes
    55	    state: started
    56	    name: "{{ MARIADB_NAME }}"
    57	    env:
    58	      MYSQL_USER: "{{ MYSQL_USER }}"
    59	      MYSQL_PASSWORD: "{{ MYSQL_PASSWORD }}"
    60	      MYSQL_ROOT_PASSWORD: "{{ MYSQL_ROOT_PASSWORD }}"
    61	      MYSQL_DATABASE: "{{ MYSQL_DATABASE }}"
    62	    volume: "{{ MYSQL_DATA }}:/var/lib/mysql:Z"
    63	
    64	- name: Wait for DB to initilize
    65	  wait_for:
    66	    timeout: 20
    67	
    68	- name: Create Nextcloud container
    69	  containers.podman.podman_container:
    70	    debug: no 
    71	    conmon_pidfile: "{{ NC_CONMON_PIDFILE }}"
    72	    image: "{{ NC_IMAGE }}"
    73	    image_strict: yes
    74	    pod: "{{ POD_NAME }}"
    75	    recreate: yes
    76	    state: started
    77	    name: "{{ NC_NAME }}"
    78	    env:
    79	      MYSQL_DATABASE: "{{ MYSQL_DATABASE }}"
    80	      MYSQL_USER: "{{ MYSQL_USER }}"
    81	      MYSQL_PASSWORD: "{{ MYSQL_PASSWORD }}"
    82	      MYSQL_HOST: "{{ MYSQL_HOST }}"
    83	      NEXTCLOUD_ADMIN_USER: "{{ NEXTCLOUD_ADMIN_USER }}"
    84	      NEXTCLOUD_ADMIN_PASSWORD: "{{ NEXTCLOUD_ADMIN_PASSWORD }}"
    85	      NEXTCLOUD_TRUSTED_DOMAINS: "{{ NEXTCLOUD_TRUSTED_DOMAINS }}"
    86	      SMTP_HOST: "{{ SMTP_HOST }}"
    87	      SMTP_SECURE: "{{ SMTP_SECURE }}"
    88	      SMTP_PORT: "{{ SMTP_PORT }}"
    89	      SMTP_AUTHTYPE: "{{ SMTP_AUTHTYPE }}"
    90	      SMTP_NAME: "{{ SMTP_NAME }}"
    91	      SMTP_PASSWORD: "{{ SMTP_PASSWORD }}"
    92	      MAIL_FROM_ADDRESS: "{{ MAIL_FROM_ADDRESS }}"
    93	      MAIL_DOMAIN: "{{ MAIL_DOMAIN }}"
    94	      OVERWRITEPROTOCOL: "{{ NEXTCLOUD_OVERWRITEPROTOCOL }}"
    95	      OVERWRITECLIURL: "{{ NEXTCLOUD_OVERWRITECLIURL }}"
    96	    volume:
    97	      - "{{ NC_HTML }}:/var/www/html:Z"
    98	      - "{{ NC_APPS }}:/var/www/html/custom_apps:Z"
    99	      - "{{ NC_CONFIG }}:/var/www/html/config:Z"
   100	      - "{{ NC_DATA }}:/var/www/html/data:Z"

In Zeile 64-66 habe ich einen Task definiert, der einfach nur 20 Sekunden wartet. Dies wurde erforderlich, da ich Laufzeitprobleme feststellen konnte, wenn der Nextcloud-Container startet, bevor die Datenbank im MariaDB-Container initialisiert war. Dieses Konstrukt ist nicht schön und ich bin für Verbesserungsvorschläge offen.

Zwischenfazit

Die Erstellung der Ansible-Rolle hat länger gedauert, als angenommen. Dies liegt nur zum Teil in meiner spärlichen Freizeit begründet. Einen größeren Einfluss darauf hatte die Dokumentation zum Nextcloud-Repository. Diese geht davon aus, dass man ein Dockerfile bzw. Docker-Compose verwendet. So war noch etwas Internet-Recherche erforderlich, um den Pod letztendlich ans Laufen zu bringen.

Dieser Artikel beschäftigte sich mit den Tag-1-Aufgaben, an deren Ende eine Nextcloud-Instanz ausgebracht wurde, welche an einen Reverse-Proxy angebunden werden kann.

Im nächsten Artikel gehe ich auf die Konfiguration des NGINX-Reverse-Proxy ein. Hierbei habe ich einige Überraschungen erlebt, welche mich an der Reife des Projekts [2] zweifeln lassen.

  1. Nextcloud System Requirements — https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html
  2. Nextcloud (Official Image) — https://hub.docker.com/_/nextcloud
  3. MariaDB (Official Image) — https://hub.docker.com/_/mariadb
  4. GitHub Tronde/ansible_role_deploy_nextcloud_with_mariadb_pod
  5. podman_pod – Manage Podman pods
  6. podman_container – Manage podman containers

Nextcloud im Container – Teil 1: Der Plan

Dies ist der Beginn meines zweiten Container-Projekts. Nach Kanboard im Container möchte ich diesmal eine Nextcloud-Instanz als Container, zusammen mit einem Datenbank-Container, in einem Podman-Pod betreiben.

Da ein einzelner Artikel vermutlich zu lang wird, teile ich das Projekt in mehrere Artikel auf. Wie viele es genau werden, kann ich jetzt noch nicht sagen. Am Ende der Reihe werde ich hier eine Übersicht einführen und die einzelnen Teilen entsprechend miteinander verbinden.

In diesem ersten Teil geht es um meine Motivation, das eigentliche Ziel und den groben Plan.

Was Leser dieser Reihe erwartet

Ihr könnt mich durch diese Reihe begleiten und euch von meinen Erlebnissen und Erkenntnissen unterhalten lassen. Dabei dürft ihr nicht annehmen, dass es sich bei dem von mir beschriebenen Vorgehen um eine gute Praxis handelt. Hier gilt eher: Der Weg ist das Ziel.

Ihr seid herzlich eingeladen, die Artikel zu kommentieren und über das Vorgehen und Alternativen dazu zu diskutieren. Gern in der Kommentarsektion unter den jeweiligen Beiträgen oder als Artikel in euren eigenen Blogs.

Ich plane die Artikel im Wochenrhythmus, wenigstens monatlich, zu veröffentlichen. Bitte verzeiht, wenn es etwas unregelmäßig wird. Dies ist ein Hobby, dem nur begrenzt Zeit zur Verfügung steht.

Motivation

Bei Linux-Containern handelt es sich um eine Technologie, die gekommen ist, um zu bleiben. Sie hat bereits in vielen Branchen Fuß gefasst und immer mehr Projekte bieten ihre Anwendungen zusätzlich oder ausschließlich in Form von Containern an.

Als Sysadmin mittleren Alters werden mich Linux-Container sicher noch viele Jahre begleiten. Um praktische Erfahrungen mit dem Betrieb zu sammeln, möchte ich einige private Projekte in Containern betreiben.

Beruflich arbeite ich überwiegend mit RHEL. Red Hat engagiert sich stark in den Projekten Ansible und Podman, welche ich auch unter anderen Distributionen, wie z.B. Debian, einsetze. Ich möchte das Projekt als Chance nutzen, mein Wissen auch in diesen Werkzeugen zu festigen und auszubauen.

Ich spiele schon seit einiger Zeit mit dem Gedanken, wieder eine eigene Nextcloud-Instanz zu betreiben. Da auf dem zur Verfügung stehenden Server bereits eine Nextcloud-Instanz läuft und ich meine Anwendung von der bestehenden Instanz getrennt und möglichst losgelöst vom Betriebssystem betreiben möchte, habe ich mich entschieden, Nextcloud im Container zu betreiben.

Ziele

Ziel dieses Projekts sind das Deployment und der Betrieb einer Nextcloud-Instanz als Podman-Pod. Im Einzelnen sollen folgende Ziele erreicht werden:

  1. Entwicklung eines wiederverwendbaren Verfahrens zum Deployment einer Nextcloud im Container
  2. Persistente Speicherung von Konfigurations- und inhaltlichen Daten im Dateisystem des Hosts
  3. Konfiguration eines Reverse-Proxies (NGINX) für den Zugriff auf die Nextcloud-Instanz
  4. Konfiguration von Backup und Restore für Konfiguration und Inhalte der Nextcloud-Instanz
  5. Konfiguration und Test automatischer durch Ansible gesteuerter Updates

Umgebung

Für die Umsetzung des Projekts steht mir ein Virtual Private Server (VPS) mit genügend Ressourcen zur Verfügung. Dieser wird in einem Rechenzentrum in Deutschland betrieben. Auf diesem sind Debian Bullseye, NGINX, ein OpenSSH-Server, Podman 3.0.1 (rootless) und Python 3.9.2 installiert. Damit erfüllt dieses System die Voraussetzungen, um mit Ansible konfiguriert zu werden und Container ausführen zu können.

Ansible selbst läuft in meiner privaten Arbeitsumgebung auf meinem Debian-PC und einem Fedora-35-Notebook.

Methodik und verwendete Werkzeuge

Zu Beginn habe ich mich etwas in der Nextcloud-Dokumentation und den verfügbaren Nextcloud-Images belesen. Besagte Dokumentation sowie die der verwendeten Werkzeuge sind im folgenden Abschnitt verlinkt.

Um die oben formulierten Ziele zu erreichen, werde ich in einem Python Virtual Environment eine Ansible-Version installieren, mit der ich die Collection containers.podman nutzen kann. Hiermit werde ich eine Ansible-Rolle entwickeln, die ich wiederverwenden kann, um Nextcloud-Instanzen in einer rootless-Podman-Umgebung zu deployen. Die Ansible-Rolle wird anschließend auf meinem GitHub-Account veröffentlicht.

Die Konfiguration von NGINX und acme.sh für die TLS-Zertifikate erfolgt manuell.

In diesem Abschnitt liste ich Links zu Artikeln und Dokumentationen auf, welche ich im Vorfeld gelesen habe und deren Kenntnis ich für die Umsetzung als nützlich erachte. Zur besseren Übersicht gliedere ich diese in die Unterabschnitte Hintergrundwissen, Dokumentation und Eigene Artikel.

Die weiteren Artikel dieser Reihe

Hintergrundwissen

Dokumentation

Eigene Artikel

Labor-Umgebung mit Ansible in KVM erstellen

Inspiriert durch die Artikel von Ricardo Geradi [1] und Alex Callejas [3] schreibe ich diesen, um zu erklären, wie mithilfe von Ansible eine Labor-Umgebung bestehend aus einer oder mehreren virtuellen Maschinen (VMs) auf einem KVM-Hypervisor provisioniert werden kann.

Dabei handelt es sich weniger um ein Tutorial, sondern mehr um eine exemplarische Beschreibung einer möglichen Vorgehensweise, die euch als Vorlage für die eigene Umgebung dienen kann.

Ich gehe nicht darauf ein, wie KVM oder Ansible installiert werden. Hierzu verweise ich auf die Dokumentation der jeweiligen Projekte und der verwendeten Linux-Distributionen.

Motivation

Um Anwendungen zu testen, benötigt man in der Regel ein Betriebssystem, auf welchem diese ausgeführt werden können. Ein Betriebssystem läuft dieser Tage meist innerhalb einer virtuellen Maschine (VM). Um bei Tests stets gleiche Rahmenbedingungen zu haben, wird empfohlen, für jeden Test eine neue VM mit einer definierten Konfiguration zu provisionieren, die geplanten Tests durchzuführen, die Ergebnisse zu sichern und die VM zu dekommissionieren.

Möchte man Infrastrukturdienste testen, werden häufig gleich mehrere VMs benötigt. Diese werden auch als Labor-Umgebung bezeichnet.

Um nicht unnötig Zeit mit der Provisionierung der VMs zu verlieren — immerhin möchte man ja seine Anwendungen bzw. Dienste testen — bietet es sich an, diesen Prozess zu automatisieren.

Doch warum mit Ansible und nicht mit [hier Lieblings-Werkzeug eurer Wahl einsetzen]?

Viele Wege führen nach Rom. Und es gibt vermutlich ähnlich viele Werkzeuge, um eine Labor-Umgebung in KVM zu provisionieren. Ich habe mich in diesem Fall für Ansible entschieden, da:

  • Ich fast täglich damit arbeite.
  • Mit ansible-galaxy role init erstellte Rollen meiner bescheidenen Meinung nach (mbMn) eine schöne Struktur zur Organisation des Codes vorgeben.
  • Mit ansible-vault ein Werkzeug dabei ist, um Dateien mit sensiblen Informationen zu verschlüsseln und diese im weiteren Verlauf einfach zu nutzen.
  • Ich meine YAML-Dateien nächstes Jahr leichter lesen und verstehen kann als meine Shell-Skripte.
  • Ich in einem zukünftigen Artikel zeigen möchte, wie man mit Ansible eine Labor-Umgebung in einem VMware vSphere Cluster provisioniert.

Umgebung

KVM-Hypervisor: Debian 11 Bullseye

Die .qcow2-Image-Dateien für die VMs werden auf dem KVM-Hypervisor im Verzeichnis /var/lib/libvirt/images/ vorgehalten.

Getestete Ansible Versionen:

  • ansible 2.10.8 ( auf Debian 11 Bullseye)
  • ansible [core 2.12.1] (auf Fedora 35)

Die Verzeichnisstruktur für meine Ansible-Umgebung entspricht der aus dem Artikel Linux-Benutzerkonten mit Ansible verwalten, wie sie im dortigen Abschnitt Vorbereitung beschrieben ist.

Die im Laufe dieses Artikels provisionierte Labor-Umgebung wird aus einer RHEL-7 und einer RHEL-8-VM bestehen. Selbstverständlich ist es möglich, durch einfache Anpassungen weitere VMs sowie andere Linux-Distributionen zu provisionieren.

Vorarbeit

Ricardo Geradi [1] und Alex Callejas [3] beziehen in ihren Artikeln die qcow2-Images, welche sie als Vorlage (engl. Template) für weitere VMs verwenden, aus diversen Internet-Quellen. Ich bin kein Freund davon, mir Images aus dem Netz zu laden und zu nutzen, für die es keine ordentliche Dokumentation gibt, mit welchen Paketen und Einstellungen diese erzeugt wurden.

Wer kauft schon gern die Katze im Sack? Daher erstelle ich mir meine Vorlagen selbst. Dazu führe ich für jede Distribution, für die ich eine Vorlage erstellen möchte, eine manuelle Installation durch. Um die Vorlagen unter all den anderen VMs leicht identifizieren zu können, gebe ich ihnen Namen wie z.B.:

  • rhel7-template
  • rhel8-template
  • debian11-template

Dabei hinterlege ich beim User root bereits den SSH-Public-Key, den ich später mit Ansible verwenden möchte, um diese Systeme weiter zu konfigurieren. Dies tue ich zwar bisher. Es ist für die Verwendung der hier beschriebenen Rolle nicht erforderlich.

Möchte ich eine Vorlage aktualisieren, fahre ich die dazugehörige VM hoch, führe ein Paket-Update durch, fahre die VM wieder herunter und bin fertig. Dies mache ich in der Regel alle paar Monate, wenn mir das Paket-Update bei neu provisionierten VMs zu lange dauert und spätestens nach Erscheinen eines neuen Minor-Release.

Die Ansible-Rolle

Eine Ansible-Rolle wird mit dem Befehl ansible-galaxy role init role_name initialisiert. In meinem Fall sieht dies wie folgt aus:

$ ansible-galaxy role init kvm_provision_lab
- Role kvm_provision_lab was created successfully
$ tree kvm_provision_lab
kvm_provision_lab
├── defaults
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
└── vars
    └── main.yml

In obiger Ausgabe fehlen die Verzeichnisse Files und Handlers. Diese hatte ich bereits gelöscht, da sie nicht benötigt werden. Die erstellte Verzeichnisstruktur kann, je nach verwendeter Version von ansible-galaxy, leicht unterschiedlich aussehen. Benötigt werden in diesem Beispiel nur die oben dargestellten Verzeichnisse und Dateien. Streng genommen können das Verzeichnis meta und die Datei README.md ebenfalls entfernt werden, wenn man nicht vorhat, die Rolle zu veröffentlichen. Ich behalte beide bei und nutze die Dateien zur Dokumentation der Rolle.

Variablen

Es ist gute Praxis alle Variablen, die von einer Ansible-Rolle verarbeitet werden, in der Datei defaults/main.yml zu dokumentieren und mit Standardwerten zu versehen. Genannte Datei hat hier folgenden Inhalt:

$ cat -n defaults/main.yml 
     1	---
     2	libvirt_pool_dir: "/var/lib/libvirt/images"
     3	vm_root_pass: "123456"
     4	ssh_key: "/path/to/ssh-pub-key"
     5	
     6	guests:
     7	  test:
     8	    vm_ram_mb: 512
     9	    vm_vcpus: 1
    10	    vm_net: default
    11	    os_type: rhel7
    12	    file_type: qcow2
    13	    base_image_name: rhel7-template
    14	    vm_template: "rhel7-template"
    15	    second_hdd: false
    16	    second_hdd_size: ""
    17	  test2:
    18	    vm_ram_mb: 512
    19	    vm_vcpus: 1
    20	    vm_net: default
    21	    os_type: rhel8
    22	    file_type: qcow2
    23	    base_image_name: rhel8-template
    24	    vm_template: "rhel8-template"
    25	    second_hdd: true
    26	    second_hdd_size: "100M"

In Zeile 2-4 werden Variablen definiert, die unabhängig von einzelnen VMs für die gesamte Rolle gelten. Dies sind der Speicherort für Image-Dateien, das Passwort für den Root-Benutzer der VMs, sowie der Pfad zu dem SSH-Public-Key, welcher beim Root-Benutzer hinterlegt werden soll.

In Zeile 6 beginnt ein sogenanntes Ansible-Dictionary (siehe [6]) namens guests. Es enthält als Keys die Namen der VMs (hier test und test2) und ordnet diesen diverse Variablen als Werte zu (z.B. vm_ram_mb). Die hierfür gewählten Strings müssen gültige Ansible-Variablen sein (siehe [7]).

Die einzelnen Variablen kurz erklärt:

  • vm_ram_mb gibt die Größe des Gast-Arbeitsspeichers in Megabyte (MB) an.
  • vm_vcpus spezifiziert die Anzahl CPUs der VM.
  • vm_net bezeichnet das KVM-Netzwerk, mit dem die VM verbunden wird.
  • os_type wird aktuell noch nicht verwendet.
  • file_type gibt den Typ der Image-Datei an.
  • base_image_name verweist auf den Namen der zu verwendenden Vorlage, die zuvor manuell installiert wurde.
  • vm_template referenziert eine Jinja2-Template-Datei, welche wir uns im nächsten Abschnitt anschauen werden.
  • second_hdd kann auf true oder false gesetzt werden und bestimmt, ob einer VM eine zweite Festplatte hinzugefügt werden soll.
  • second_hdd_size gibt die Größe der zweiten Festplatte in Megabyte (MB) an.

Führt man diese Rolle mit einem Playbook aus, ohne eigene Variablen zu definieren, werden also zwei VMs mit den Namen test und test2 sowie den obigen Parametern erstellt.

Um die Rolle möglichst flexibel einsetzen und wiederverwenden zu können, werden die gewünschten Labor-Umgebungen in separaten Dateien definiert. Für mein RHEL-Lab habe ich die benötigten Variablen in die Datei vars/rhel_lab.yml geschrieben, welche ich mit ansible-vault create vars/rhel_lab.yml erstellt habe. So bleiben mein gewähltes Passwort sowie Pfad zu und Name von meinem SSH-Public-Key vor neugierigen Blicken geschützt. Der Inhalt der Datei entspricht vom Aufbau her jedoch dem aus obigem Code-Block der defaults/main.yml. Wie die Datei rhel_lab.yml genutzt wird, wird in Abschnitt „Das Playbook“ erläutert.

Templates

In der KVM-Terminologie wird eine VM auch als Gast-Domain (engl. guest domain) bezeichnet. Die Definition der Gast-Domain kann in Form einer XML-Datei erfolgen. In diesem Abschnitt werde ich zeigen, wie man die Konfiguration einer bestehenden VM in eine XML-Datei schreibt, um diese anschließend als Template für neue VMs zu benutzen.

Im Vorfeld habe ich die VMs rhel7-template und rhel8-template manuell installiert. Diese werde ich nun nutzen, um daraus Jinja2-Templates abzuleiten, welche ich innerhalb der Rollen-Verzeichnisstruktur im Verzeichnis templates ablege. Der folgende Codeblock zeigt den Befehl exemplarisch für das rhel7-template:

$ sudo virsh dumpxml rhel7-template >templates/rhel7-template.xml.j2

Das rhel8-template.xml.j2 wird auf die gleiche Weise erzeugt. Der Inhalt wird im Folgenden auszugsweise dargestellt:

<domain type='kvm'>
  <name>rhel8-template</name>
  <uuid>cb010068-fe32-4725-81e8-ec24ce237dcb</uuid>
  <metadata>
    <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
      <libosinfo:os id="http://redhat.com/rhel/8-unknown"/>
    </libosinfo:libosinfo>
  </metadata>
  <memory unit='KiB'>2097152</memory>
  <currentMemory unit='KiB'>2097152</currentMemory>
  <vcpu placement='static'>1</vcpu>
[...]
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='/var/lib/libvirt/images/rhel8-template.qcow2'/>
      <target dev='vda' bus='virtio'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
    </disk>
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <target dev='hdb' bus='ide'/>
      <readonly/>
      <address type='drive' controller='0' bus='0' target='0' unit='1'/>
    </disk>
[...]
    <interface type='network'>
      <mac address='52:54:00:0c:8d:05'/>
      <source network='default'/>
      <model type='virtio'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
    </interface>
[...]
  </devices>
</domain>

Die Template-Dateien sind zu bearbeiten, um aktuell statisch konfigurierte Werte durch Variablen zu ersetzen. Die zu bearbeitenden Zeilen sehen anschließend wie folgt aus:

  • <name>{{ item.key }}</name>
  • <memory unit='MiB'>{{ item.value.vm_ram_mb }}</memory>
  • <vcpu placement='static'>{{ item.value.vm_vcpus }}</vcpu>
  • <source file='{{ libvirt_pool_dir }}/{{ item.key }}.qcow2'/>
  • <source network='{{ item.value.vm_net }}'/>

Darüber hinaus sind weitere Zeilen, welche für jede VM einmalig sind, aus den Template-Dateien zu löschen:

  • <uuid>...</uuid>
  • <mac address='...'/>

In der fertigen rhel8-template.xml.j2-Datei sieht es dann wie folgt aus:

<domain type='kvm'>
  <name>{{ item.key }}</name>
  <metadata>
    <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
      <libosinfo:os id="http://redhat.com/rhel/8-unknown"/>
    </libosinfo:libosinfo>
  </metadata>
  <memory unit='MiB'>{{ item.value.vm_ram_mb }}</memory>
  <vcpu placement='static'>{{ item.value.vm_vcpus }}</vcpu>
[...]
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='{{ libvirt_pool_dir }}/{{ item.key }}.qcow2'/>
      <target dev='vda' bus='virtio'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
    </disk>
[...]
    <interface type='network'>
      <source network='{{ item.value.vm_net }}'/>
      <model type='virtio'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
    </interface>
[...]
  </devices>
</domain>

Solltet ihr zu diesem Abschnitt noch Fragen haben, weil z.B. etwas unverständlich ist, stellt diese bitte in den Kommentaren oder meldet euch per E-Mail. Ich werde den Abschnitt dann je nach Bedarf ergänzen.

Tasks

Als Nächstes stelle ich die Tasks vor, welche von dieser Rolle ausgeführt werden. Dabei beginne ich mit dem Inhalt der Datei tasks/main.yml, deren Inhalt ich nach dem folgenden Codeblock erläutern werde.

$ cat -n tasks/main.yml 
     1	---
     2	# tasks file for kvm_provision_lab
     3	- name: Ensure requirements are in place
     4	  apt:
     5	    name:
     6	      - libguestfs-tools
     7	      - python3-libvirt
     8	    state: present
     9	  become: yes
    10	
    11	- name: Get VMs list
    12	  community.libvirt.virt:
    13	    command: list_vms
    14	  register: existing_vms
    15	  changed_when: no
    16	
    17	- name: Copy base image
    18	  copy:
    19	    dest: "{{ libvirt_pool_dir }}/{{ item.key }}.{{ item.value.file_type }}"
    20	    src: "{{ libvirt_pool_dir }}/{{ item.value.base_image_name }}.{{ item.value.file_type }}"
    21	    force: no
    22	    remote_src: yes
    23	    mode: 0660
    24	    group: libvirt-qemu
    25	  register: copy_results
    26	  with_dict: "{{ guests }}"
    27	
    28	- name: Create VMs if not exist
    29	  include_tasks: create_vms.yml
    30	  when: "item.key not in existing_vms.list_vms"
    31	  with_dict: "{{ guests }}"

Der Task in Zeile 3-9 stellt sicher, dass die notwendigen Voraussetzungen erfüllt sind, um sich mit libvirt verbinden zu können. Der Paketname libguestfs-tools existiert unter CentOS Stream, Debian und RHEL. Unter Fedora heißt das Paket guestfs-tools. Der Name muss an die entsprechende Distribution angepasst werden.

In Zeile 11-15 wird das Modul community.libvirt.virt verwendet, um die Liste der bereits existierenden VMs abzurufen und in der Variablen existing_vms zu speichern. Diese wird später genutzt, um nur dann eine VM zu provisionieren, wenn nicht bereits eine VM mit dem gleichen Namen existiert. Es ist quasi ein schmutziger Trick, um der Rolle ein wenig Idempotenz einzuhauchen. Da mit diesem Task nur Informationen abgefragt werden, jedoch keinerlei Änderungen vorgenommen werden, setzt man changed_when: no.

Das Copy-Modul in Zeile 17-26 kopiert die qcow2-Image-Dateien an den vorgesehenen Zielort und setzt Zugriffsrechte entsprechend. Zeile 19 sorgt dafür, dass die Zieldatei den Namen der neuen VM beinhaltet. Da das Copy-Modul bereits idempotent arbeitet, werden die Dateien nur kopiert, wenn das Ziel nicht bereits existiert. Das Ergebnis des Kopiervorgangs wird in copy_results gespeichert.

Der letzte Task führt die Task-Datei create_vms.yml für die VMs aus, die nicht bereits existieren. Dafür sorgt die Bedingung when: "item.key not in existing_vms.list_vms", die diesem Task zu Idempotenz verhilft. Das Playbook selbst hat folgenden Inhalt:

$ cat -n tasks/create_vms.yml 
     1	---
     2	- name: Configure the image
     3	  command: |
     4	    virt-customize -a {{ libvirt_pool_dir }}/{{ item.key }}.qcow2 \
     5	    --hostname {{ item.key }} \
     6	    --root-password password:{{ vm_root_pass }} \
     7	    --ssh-inject 'root:file:{{ ssh_key }}' \
     8	    --uninstall cloud-init --selinux-relabel
     9	  when: copy_results is changed
    10	
    11	- name: Define VMs
    12	  community.libvirt.virt:
    13	    command: define
    14	    xml: "{{ lookup('template', '{{ item.value.vm_template }}.xml.j2') }}"
    15	
    16	- name: Create second disk images if needed
    17	  command: |
    18	    qemu-img create -f {{ item.value.file_type }} \
    19	    {{ libvirt_pool_dir }}/{{ item.key }}-vdb.{{ item.value.file_type }} {{ item.value.second_hdd_size }}
    20	  become: yes
    21	  when: item.value.second_hdd|bool == true
    22	
    23	- name : Attach second disk image to domain
    24	  command: |
    25	    virsh attach-disk {{ item.key }} \
    26	    --source "{{ libvirt_pool_dir }}/{{ item.key }}-vdb.{{ item.value.file_type }}" \
    27	    --target vdb \
    28	    --persistent
    29	  become: yes
    30	  when: item.value.second_hdd|bool == true
    31	
    32	- name: Ensure VMs are startet
    33	  community.libvirt.virt:
    34	    name: "{{ item.key }}"
    35	    state: running
    36	  register: vm_start_results
    37	  until: "vm_start_results is success"
    38	  retries: 15
    39	  delay: 2

Der Task in Zeile 2-9 konfiguriert den Inhalt der qcow2-Image-Datei. Die Bedingung when: copy_results is changed stellt sicher, dass dies nur passiert, wenn die Image-Datei zuvor an ihren Zielort kopiert wurde. Damit wird sichergestellt, dass nicht eine bereits vorhandene Image-Datei einer existierenden VM nochmals verändert wird. Der Task konfiguriert den Hostnamen, setzt das Root-Passwort und hinterlegt den SSH-Public-Key.

Der nächste Task ab Zeile 11 definiert/erstellt die neue VM aus den XML-Template-Dateien.

Die beiden Tasks in den Zeilen 16-30 fügen einer VM eine zweite Festplatte hinzu, wenn dies in defaults/main.yml bzw. vars/rhel_lab.yml entsprechend definiert wurde.

Der letzte Task sorgt schließlich dafür, dass die neu erstellten VMs eingeschaltet werden.

Das Playbook

Im Vergleich zu den Dateien mit den einzelnen Tasks fällt das Playbook eher kurz aus:

 cat -n kvm_provision_rhel_lab.yml 
     1	---
     2	- name: Provision RHEL lab VMs
     3	  hosts: localhost
     4	  vars_files:
     5	    - roles/kvm_provision_lab/vars/rhel_lab.yml
     6	  tasks:
     7	    - name: Run role kvm_provision_lab
     8	      include_role:
     9	        name: kvm_provision_lab

In Zeile 3 ist der KVM-Hypervisor anzugeben, auf dem die Rolle ausgeführt werden soll. Dies kann, wie in meinem Fall, der gleiche Host wie der Ansible-Control-Node sein.

In Zeile 4 und 5 wird die Datei geladen, welche die Variablen für die zu erstellende Laborumgebung enthält. Ohne diese Anweisung werden die Werte aus defaults/main.yml verwendet.

Abschließend wird die Ansible-Rolle inkludiert. Dies ist auch schon alles.

Zusammenfassung

Das Schreiben dieses Artikels hat deutlich länger gedauert als die Erstellung der eigentlichen Ansible-Rolle zur Erstellung einer Laborumgebung unter KVM.

Die einzelnen Abschnitte beschreiben das Vorgehen und die Bestandteile der Rolle im Detail. Ich hoffe, damit deren Funktionsweise deutlich gemacht zu haben.

Ich kann nun meine Labor-Umgebungen in Dateien wie rhel_lab.yml, debian_lab.yml, etc. definieren und die Rolle dazu verwenden, diese zu provisionieren. So steht mir in kurzer Zeit eine frische Testumgebung bereit. Und zwar jedes Mal aufs neue, wenn ich sie benötige.

Wenn euch dieser Artikel dabei hilft, eigene Labor-Umgebungen mithilfe von Ansible zu provisionieren freut mich dies umso mehr.

  1. Build a lab in 36 seconds with Ansible. Ricardo Gerardi (Red Hat, Sudoer). Enable Sysadmin. 2021-10-22.
  2. 8 Linux virsh subcommands for managing VMs on the command line. Ricardo Gerardi (Red Hat, Sudoer). Enable Sysadmin. 2021-09.09.
  3. Build a lab in five minutes with three simple commands. Alex Callejas (Red Hat). Enable Sysadmin. 2021-08-20.
  4. Ansible Create KVM Guests
  5. community.libvirt.virt – Manages virtual machines supported by libvirt
  6. Ansible Dictionary Variables. URL: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#dictionary-variables
  7. Creating valid variable names. URL: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#creating-valid-variable-names

Quick Ansible: Enforce SSHD configuration options

You need to enforce certain configuration options in sshd_config, while leaving the rest of the config to your colleagues? Your colleagues need to be able to change these parameters too, temporarily, but they should be reset after a certain time? And you wanna do it with Ansible? Read on.

---
- hosts: all
  tasks:
  - name: sshd configuration file update
    blockinfile:
      path: /etc/ssh/sshd_config
      insertbefore: BOF # Beginning of the file
      marker: "# {mark} ANSIBLE MANAGED BLOCK BY LINUX-ADMIN"
      block: |
        PermitRootLogin no
        PubkeyAuthentication yes
        AuthorizedKeysFile .ssh/authorized_keys
        PasswordAuthentication no
      backup: yes
      validate: /usr/sbin/sshd -T -f %s

  - name: Restart SSHD
    service:
      name: sshd
      state: restarted

I’ll show you a playbook that sets the options PermitRootLogin, PubkeyAuthentication, AuthorizedKeysFile and PasswordAuthentication using the Ansible module blockinfile.

What happens here is that at the beginning of the file sshd_config a block is getting inserted containing the shown key-argument pairs. Inserting this block at the beginning of the file ensures that these lines are used, because first occurrence wins (see sshd_config(5)).

This playbook ensures the desired configuration that the user root is not permitted to login via ssh, public key authentication is enabled, the .ssh/authorized_keys file from user’s HOME directory should be used, and password authentication is disabled. Before /etc/ssh/sshd_config gets changed a backup is created and the new file is going to be validated prior to saving it.

The second task restarts the sshd service to make sure the desired config is going to be used.

Of course, any user with sudo or root access could edit the sshd_config file and restart the service to change the desired settings; and it might be OK to do so temporarily. To make sure any changes to the file will be reset to the desired config you could just run the playbook every 30 minutes per cron.

This was a really quick example of how to use ansible to set or change configuration settings. I hope you enjoyed it.

Ansible: Wiederherstellung meines Blogs auf Buster und Bullseye in 2021

Dies ist ein Update zu den Beiträgen:

  1. Konzept zum Deployment meines Blogs mit Ansible und
  2. Erfahrungsbericht zum Deployment meines Blogs mit Ansible.

Umgebung

Aktuell nutze ich als Ansible Control Node (ACN) ein Debian Buster mit Ansible 2.7.7. Die Backups liegen wie gehabt auf einem Speicher im lokalen Heimnetzwerk.

Als Zielsysteme für die Wiederherstellung dienen ein Debian 10 (Buster) und Debian Testing (das kommende Bullseye). Bei beiden Zielsystemen handelt es sich um minimale Installation in zwei VMs auf einem KVM-Hypervisor.

Ablauf

Zuerst habe ich meinen Blog aus dem Backup auf der Debian 10 VM wiederhergestellt. Dabei gab es tatsächlich ein Problem. Das VHOST-Template für den Webserver entsprach nicht mehr der Version, die auf dem Produktivsystem zum Einsatz kommt. Ich hatte schlicht vergessen, die Änderung nachzupflegen. Der Fehler wurde schnell identifiziert und behoben. Anschließend verlief der Wiederherstellungsprozess reibungslos.

Beim zweiten Versuch erfolgte die Wiederherstellung auf einer VM mit Debian Testing (Bullseye). Dieser Test ist für mich wichtig, um zu sehen, ob ich meinen Blog auch auf der kommenden stabilen Debian-Version ausrollen kann. Hier waren etwas mehr Anpassungen an der Rolle „deploy-my-blog“ notwendig, um dieses Blog erfolgreich wiederherstellen zu können. So haben sich einige Paketnamen geändert:

Alter NameNeuer Name
python-aptpython3-apt
python-mysqldbpython3-mysqldb
Gegenüberstellung der alten und neuen Paketnamen

Doch auch an der VM selbst war ein manueller Eingriff notwendig, bevor sich mein ACN überhaupt mit dem Node verbinden konnte. Ansible konnte auf dem Node keinen Python-Interpreter finden. Ich schiebe die Schuld der Version 2.7.7 in die Schuhe. Nachdem ich einen Symlink von /usr/bin/python auf /usr/bin/python3 erstellt hatte, klappte der Zugriff.

Der letzte Stolperstein war php-fpm. Kommt unter Buster Version 7.3 zum Einsatz so ist dies unter Bullseye 7.4. Da die Versionsnummer in meiner Ansible-Rolle hart verdrahtet ist, war auch hier eine Anpassung notwendig. Anschließend gelang auch hier die Wiederherstellung.

Fazit

Grundsätzlich klappte die Wiederherstellung wie gewohnt. Den Fehler mit der VHOST-Datei könnte ich zukünftig vermeiden, indem ich diese einfach mit aus dem Backup wiederherstelle, statt ein Template zu nutzen.

Das bei der Wiederherstellung auf einer neueren Betriebssystemversion Anpassungen erforderlich sind, hatte ich erwartet. Diese halten sich meiner Meinung nach in Grenzen und sind akzeptabel.

Die längste Zeit beanspruchte das Kopieren der Backups auf die Zielsysteme. Die eigentliche Wiederherstellung war mit den Stolpersteinen in 10-15 Minuten. Mit fehlerbereinigter Rolle sogar nur noch ca. 5 Minuten. Eine manuelle Wiedereinrichtung hat mich früher eher zwischen 30-60 Minuten gekostet. Von daher bin ich sehr zufrieden.

Software-Pakete mit Ansible installieren

In diesem sehr kurzen Beitrag möchte ich euch zeigen, wie ihr mit Ansible einen definierten Satz an Software-Paketen auf euren Linux-Hosts installieren könnt. Dies ist z.B. dann nützlich, wenn ihr bei der Provisionierung neuer Systeme sicherstellen möchtet, dass ein bestimmter Satz an Paketen vorhanden ist.

Natürlich kann man diese Aufgabe auch auf verschiedenen anderen Wegen lösen. Dies ist halt der Ansible-Weg. ;-)

---
- hosts: all
  tasks:
  - name: Install package baseline
    yum:
      name:
        - bind-utils
        - net-tools
        - chrony
        - ed
        - yum-utils
        - vim-enhanced
        - man-pages
        - strace
        - lsof
        - tcpdump
        - setroubleshoot-server
        - setroubleshoot
        - bash-completion
      state: latest
      update_cache: yes

Das obige Playbook zeigt ein einfaches Beispiel, wie auf einem RHEL/CentOS 7 System eine Liste von Paketen installiert werden kann. Dazu wird das Ansible-Modul yum verwendet. Der Parameter ’state‘ gibt an, dass immer die jeweils akutellste Version eines Pakets installiert werden soll. Mit ‚update_cache‘ wird definiert, dass der lokale Cache aktualisiert wird, bevor ein Paket zur Installation ausgewählt wird.

Für auf Debian/Ubuntu basierende Systeme gibt es das Modul apt. Für Fedora und RHEL/CentOS ab Version 8 nutzt man das Modul dnf.

Darüber hinaus gibt es noch das generische Modul package. Bei diesem ist jedoch zu beachten, dass die unterschiedlichen Paketmanager wie YUM und APT zum Teil unterschiedliche Paketnamen verwenden. Da ‚package‘ diese nicht automatisch übersetzt ist eine Verwendung über verschiedene Distributionen hinweg nicht ohne weiteres möglich. Daher empfinde ich dieses Modul als nutzlos.

Konfiguration der Host-Firewall mit Ansible

In diesem kurzen Beitrag möchte ich euch zeigen, wie man mit Hilfe von Ansible die Host-Firewall (firewalld) konfigurieren kann. Dies ist z.B. dann nützlich, wenn man die identische Konfiguration auf mehreren Hosts ausbringen möchte.

Bevor es an die Freigabe spezifischer Ports und Services in der Host-Firewall geht, wird zuerst sichergestellt, dass der Dienst firewalld installiert, aktiviert und gestartet ist. Dazu dienen die Ansible-Module yum und service. Folgendes Playbook zeigt beispielhaft, wie diese genutzt werden können, um vorstehende Anforderungen zu erfüllen:

---
# Install, activate and start firewalld

- hosts: foo.example.com
  tasks:
  - name: Make sure firewalld is installed
    yum:
      name: firewalld
      state: latest

  - name: Activate and start firewalld service
    service:
      name: firewalld
      enabled: yes
      status: started

Bei Verwendung einer auf Debian basierenden Distribution ist statt yum das Modul apt zu verwenden.

Um jetzt z.B. die Services HTTP und HTTPS in der lokalen Host-Firewall freizugeben, kann obiges Playbook um die folgenden Abschnitte, unter Verwendung des Ansible-Moduls firewalld, ergänzt werden:

[...]
  - name: Enable immediate and permanent access to HTTP
    firewalld:
      service: http
      permanent: yes
      immediate: yes
      state: enabled

  - name: Enable immediate and permanent access to HTTPS
    firewalld:
      service: https
      permanent: yes
      immediate: yes
      state: enabled

Aktuell ist es leider noch nicht möglich dem Parameter ’service‘ eine Liste zu übergeben. Auf GitHub existiert jedoch bereits ein RFE für diese Funktion.

Die Rolle von Ansible in unserem Linux-Betriebskonzept

Seit 2016 betreiben wir im BITS Red Hat Enterprise Linux als drittes Server-Betriebssystem neben Solaris und Microsoft Windows Server. Nachdem wir nun erste Betriebserfahrung mit der für uns neuen Distribution gesammelt haben, möchte ich in diesem Jahr unser Betriebskonzept überarbeiten, um die Erkenntnisse aus dem bisherigen Betrieb einfließen zu lassen und (hoffentlich) Verbesserungen für den zukünftigen Betrieb herbei zu führen.

Ansible wird in diesem Konzept eine wesentliche Rolle einnehmen. Als Konfigurations-Management-Werkzeug wird es für Teile des Betriebssystems und für das zentrale RHEL-Patchmanagement zum Einsatz kommen.

Die zu bewältigenden Konfigurationsaufgaben unterscheiden wir dabei in:

  • Parameter die im Bereitstellungsprozess initialisiert werden und anschließend in der Hoheit des jeweiligen Systembetreibers liegen sowie
  • Parameter welche dauerhaft durch das Konfigurations-Management verwaltet werden; diese gliedern sich wiederum in die
    • Zeilenbasierte Verwaltung von Konfigurationsdateien und
    • Dateibasierte Verwaltung von Konfigurationsdateien

In diesem Beitrag führe ich die einzelnen Parameter und Optionen auf, welche zukünftig von Ansible verwaltet werden. Von diesen wird zukünftig auf weitere Beiträge verlinkt, welche die konkrete Umsetzung mit Ansible beschreiben und dokumentieren. Es mag sich also lohnen von Zeit zu Zeit wieder hier vorbei zu schauen.

Initialisierte Parameter

Hierunter sind Parameter und Optionen zu verstehen, die zu Beginn im Bereitstellungsprozess konfiguriert werden, um sicherzustellen, dass sich das neue System optimal in unsere Infrastruktur einfügt. Diese werden initial und einmalig gesetzt und gehen nach der Übergabe des Systems in die Hoheit des jeweiligen Systembetreibers über. Der Systembetreiber ist bei uns die Person, welche den spezifischen Server administriert und Dienste auf diesem betreibt.

Zu den initial konfigurierten Parametern zählen im Einzelnen:

Dauerhaft verwaltete Parameter

Diese Parameter/Optionen werden bei lokalen Änderungen auf dem System durch Ansible überschrieben. Dabei handelt es sich um

  • Die Stage-Zuordnung über das Ansible-Inventory
  • Das Anlegen und verwalten eines Benutzers zur Anbindung an das BMC-Monitoring
  • Die Verwaltung spezifischer lokaler Benutzerkonten
  • Die Steuerung des SSH-Servers, der authorized_keys Dateien und die Verwaltung der öffentlichen SSH-Schlüssel der Benutzer
  • Steuerung des RHEL-Patchmanagements

Konzept zum Deployment meines Blogs mit Ansible

An dieser Stelle möchte ich ein Konzept erarbeiten, um meinen Blog mit Hilfe von Ansible[1. Ansible – IT-Automation für Jedermann – My-IT-Brain] [2. Ansible – Wikipedia] auf einem Ubuntu-Server ausbringen zu können.

Dabei hoffe ich auch ein wenig auf eure Unterstützung. Habt ihr Anregungen, konkrete Verbesserungsvorschläge oder auch Fragen, freue ich mich über eure Kommentare. Denn ich selbst bin kein Ansible-Experte und führe dieses Projekt zum Selbststudium und zu Übungszwecken durch.

Die Testumgebung

Die Testumgebung besteht aus der Ansible Control Machine, basierend auf RHEL 7[3. Red Hat Enterprise Linux – Wikipedia] und dem Zielsystem, basierend auf Ubuntu Bionic Beaver[4. Bionic Beaver – wiki.ubuntuusers.de]. Zum Zeitpunkt der Erstellung dieses Artikels nutze ich Ansible in Version 2.4.2.

Zwar wurde bereits die Ansible-Version 2.5 veröffentlicht, in den RHEL-7-Paketquellen ist jedoch noch die Version 2.4.2 enthalten. Ich verwende diese Version auch auf der Arbeit und erhoffe mir so, Erkenntnisse aus diesem Projekt in meine dienstliche Tätigkeit einfließen lassen zu können.

Auf dem Zielsystem existiert ein Benutzer, welcher über volle sudo-Berechtigung verfügt. Dieser Benutzer muss sich mit seinem Passwort authentisieren, wenn er sudo verwendet.

Auf der Ansible Control Machine wird ein SSH-Schlüsselpaar[5. Authentifizierung über Public-Keys – wiki.ubuntuusers.de] generiert, dessen privater Schlüssel nicht mit einer Passphrase geschützt ist. Der öffentliche Schlüssel wird auf dem Zielsystem abgelegt. Damit wird sichergestellt, dass die Ansible Control Machine SSH-Zugriff auf das Zielsystem hat.

Die Konfiguration von Ansible (/etc/ansible/ansible.cfg) wird so angepasst, dass standardmäßig das oben erzeugte SSH-Private-Key-File beim Verbindungsaufbau genutzt wird.

Die Installation der benötigten Betriebssysteme und Ansible sind nicht Gegenstand dieses Artikels.

Geplante Vorgehensweise

Um eine Vorgehensweise zu erarbeiten, orientiere ich mich an dem Artikel Testinstanz für einen WordPress-Blog erstellen. In diesem habe ich bereits beschrieben, wie ein Blog auf einem weiteren System als Testsystem aufgesetzt werden kann. Die Vorgehensweise ist ähnlich, mit dem Unterschied, dass sämtliche Schritte nun durch Ansible orchestriert werden sollen.

Danach ergibt sich folgende (vorläufige) Reihenfolge:

  1. Vom Produktivsystem benötigte Dateien holen
  2. Sicherstellen, das alle benötigten Pakete auf dem Zielsystem installiert sind
  3. VirtualHost erstellen
  4. DocumentRoot aus Backup-Datei erstellen
  5. Datei-Attribute und Zugriffsrechte korrekt setzen
  6. Eine .httpasswd-Datei erzeugen/ausbringen
  7. Datenbank aus Backup wiederherstellen
  8. Datenbankbenutzer erstellen
  9. Troubleshooting

Mit hoher Wahrscheinlichkeit ist die obige Liste noch nicht vollständig. Daher werde ich diese im Laufe des Projekts anpassen, sobald weitere Erkenntnisse vorliegen.

Als Quelldaten für den Blog liegen das DocumentRoot meines Blogs als Tarball[6. tar – Wikipedia] vor. Von der Datenbank wurde ein logisches Backup[7. Logisches Backup – wiki.ubuntuusers.de] erstellt. Auf diese Datensicherung wird zurückgegriffen, um den Blog auf einem neuen Ubuntu-Server auszubringen bzw. wiederherzustellen.

Der erste Versuch

Im ersten Anlauf habe ich eine Ansible-Rolle[8. Directory Layout — Ansible Best Practices] mit folgendem Aufbau erstellt:

/etc/ansible/roles/
└── deploy-my-blog
    ├── files
    │   ├── backup-documentroot.tar.bz2
    │   ├── dh_params.pem
    │   ├── db_dump.sql.bz2
    │   ├── my-it-brain.vhost
    │   └── php-fpm-pool.conf
    ├── handlers
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    └── vars
        └── main.yml

5 directories, 8 files

Unterhalb von files wurden Dateien abgelegt, welche vom aktuellen Produktivsystem stammen und für das Deployment auf einem neuen Server benötigt werden.

Im folgenden werde ich zuerst auf die eingesetzten Module eingehen und anschließend das Playbook zum Deployment des Blogs vorstellen.

Verwendete Module

ModulStatusVerwendung
aptstableinterfaceVerwaltet APT-Pakete
unarchivepreviewEntpackt Archiv-Dateien und kopiert sie ggf. vorher auf das Zielsystem
copystableinterfaceKopiert Dateien auf entfernte Rechner
filestableinterfaceErstellt Dateien, Verzeichnisse, symbolische Links und setzt deren Attribute
mysql_dbpreviewHinzufügen von MySQL-Datenbanken (aus Backup)
mysql_userpreviewHinzufügen (und Entfernen) von Benutzern in MySQL-Datenbanken

Die in der Spalte Status angegebenen Werte stableinterface und preview lassen Rückschlüsse auf die Stabilität der Schnittstellen eines Moduls zu.

So garantieren die Entwickler bei einem stableinterface, dass es in Zukunft keine Änderungen geben wird, die nicht abwärtskompatibel sind. Auch wenn sich keine explizite Angabe findet, wie lange diese Garantie gilt, kann man davon ausgehen, dass man seine Playbooks bei Verwendung dieser Module nicht so schnell aufgrund von Updates anpassen muss. Für Module mit dem Status preview wird genau dies hingegen nicht garantiert. Hier kann es von Version zu Version Änderungen geben, welche Anpassungen an Playbooks erforderlich machen, welche diese Module verwenden. Dadurch entsteht ggf. ein erhöhter Testaufwand, bevor man seine Ansible-Installation aktualisieren kann.

Das Playbook

Den Inhalt der Datei /etc/ansible/roles/deploy-my-blog/tasks/main.yml habe ich unter dem Namen deploy-blog-playbook.yml als Gist veröffentlicht. Um es in diesem Artikel anzeigen zu können, müsst ihr JavaScript in eurem Webbrowser aktiveren.

Die Zeilen 2-11 sorgen dafür, dass die benötigten Pakete auf dem Zielsystem installiert werden. Da es sich dabei um einen Ubuntu-Server handelt, verwende ich das Modul apt.

Die Zeilen 13-16 stellen das DocumentRoot und die SSL-Zertifikate aus dem Dateibackup auf dem Zielsystem wieder her.

In meiner aktuellen Konfiguration verwende ich PHP-FPM mit einem eigenen Pool, welcher in meinem VirtualHost referenziert wird. Um diese Konstellation auch auf dem neuen System nutzen zu können, stelle ich entsprechende Konfiguration mit den Zeilen 18-47 her. Vermutlich können die Zeilen 39-47 entfallen, da PHP-FPM das Logfile automatisch erstellt. Weitere Tests müssen dies noch zeigen. Ich habe diese Zeilen während des Troubleshootings eingefügt. Ursache für den Fehler wird vermutlich das fehlende Log-Verzeichnis gewesen sein, welches ich in den Zeilen 29-37 erzeuge.

Der NGINX auf meinem Produktivsystem verwendet die Datei dh_params.pem, welche ich in den Zeilen 49-57 auch auf den neuen Host kopiere. Anschließend wird in den Zeilen 59-66 die vHost-Datei auf das Zielsystem kopiert und durch die Zeilen 68-77 verlinkt und damit aktiviert.

Die letzten Zeilen kümmern sich um die Einrichtung der Datenbank. Zuerst wird die Dump-Datei auf das Zielsystem kopiert und dort anschließend in die neue Datenbank db_my_it_brain eingespielt. Zum Schluss wird noch der benötigte Datenbank-Benutzer erstellt.

In Zeile 94 und 95 habe ich Variablen eingesetzt, welche in vars/main.yml definiert wurden. Dort habe ich auch eine Variable für den Namen der Datenbank definiert, welche ich gern in Zeile 96 genutzt hätte. Leider ist es mir nicht gelungen, da sowohl priv: '"{{ DBNAME }}".*:ALL' als auch priv: "{{ DBNAME }}".*:ALL zu Fehlern führten. Falls hier jemand eine Idee hat, freue ich mich, wenn ihr mir die korrekte Syntax mitteilt.

Erstes Fazit

Das ging leichter als gedacht. Die benötigten Module waren schnell gefunden und ihre Anwendung aufgrund der hinreichend guten Dokumentation nicht schwierig.

Da die aktuell genutzten Variablen sensible Informationen beinhalten, möchte ich diese zukünftig mit Ansible Vault schützen.

Stand heute bin ich jedoch erstmal in der Lage meinen Blog aus der Datensicherung heraus erneut deployen zu können.

Ansible: Playbook nur auf Nodes laufen lassen, die gewissen Kriterien genügen

Manchmal möchte man ein Playbook bzw. Plays nur auf Hosts laufen lassen, welche gewissen Kriterien entsprechen. In diesem Beitrag möchte ich zwei Beispiele geben, wie sich dies umsetzen lässt.

Playbook nur auf Hosts mit Red Hat Betriebssystem ausführen

Das Modul group_by [1] wird genutzt, um während des Laufs eines Playbooks dynamisch Gruppen von Hosts zu erstellen, auf welche man später weitere Plays anwendet.

---
- hosts: all

  tasks:
    - name: Group by OS

      group_by: key=os_{{ ansible_distribution }}
      changed_when: False

- hosts: os_RedHat
  roles:
    - common

Zu Beginn eines Playbook-Laufs wird das Modul setup [2] ausgeführt, um Variablen mit nützlichen Informationen über die Nodes zu belegen. Die Variable ansible_distribution enthält dabei ein Schlüsselwort für eine bestimmte Distribution.

In obigen Beispiel wird dabei die Gruppe os_RedHat erstellt. Im Folgenden werden dann alle Nodes dieser Gruppe der Rolle common zugeordnet.

Dieses Verfahren lässt sich natürlich auch für weitere Variablen anwenden. Möchte man sehen, welche Variablen belegt werden und genutzt werden können, kann sich die Rückgabe von setup in der Standardausgabe ansehen, wenn man das Modul manuell auf einigen Nodes ausführt.

Plays nur auf bestimmten Nodes ausführen

Ein weiteres Beispiel habe ich auf ITrig [4] gefunden:

---
- name: install open-vm-tools
  hosts: vmwareclients
  gather_facts: True
  become: true
  become_user: root
  tasks:
- name: debian install open-vm-tools
  apt: name=open-vm-tools state=present
  when: ansible_os_family == "Debian" and ansible_virtualization_type == "VMware"

- name: centos install open-vm-tools
  yum: name=open-vm-tools state=present
  when: ansible_os_family == "RedHat" or ansible_distribution == 'CentOS' and ansible_virtualization_type == "VMware"

Hier werden Ansible Conditionals [3] verwendet, um zu bestimmen, auf welchen Nodes ein Play ausgeführt wird. Auch hier werden wieder Variablen ausgewertet, welche durch das Modul setup [2] belegt wurden.

In obigen Beispiel wird dies genutzt, um die open-vm-tools mit einem Playbook sowohl auf Debian und RedHat/CentOS Systemen zu installieren.

Quellen

  1. group_by – Create Ansible groups based on facts {en}
  2. setup – Gathers facts about remote hosts {en}
  3. Conditionals {en}
  4. Ansible Playbooks auf Servern mit SSH Key Authentifizierung verwenden {de}