21x9.org | System Administration | Home Automation | Smart Home
23.08.2023

Ansible: Best Practice

Ansible ist sehr flexibel, wenn es um Struktur und Aufbau der eigenen Playbooks geht. Das ist auf der einen Seite natürlich schön, allerdings macht es gerade den Einstieg natürlich etwas schwerer. Ich möchte daher hier mal meine persönlichen Best Practice Regeln vorstellen, von denen ich denke, dass sie meine Playbooks übersichtlicher und besser wartbar machen.

Der grundlegende Aufbau meiner Rollen/Playbooks ist immer wie folgt:

.
├── defaults
│   └── main.yml
├── files
│   └── example
├── tasks
│   ├── client.yml
│   ├── config.yml
│   ├── install.yml
│   ├── main.yml
│   ├── preflight.yml
│   └── server.yml
├── templates
│   └── example.j2
└── vars
    ├── debian-11.yml
    └── debian-12.yml

Die Rolle nehme ich immer wie folgt in mein Hauptplaybook (site.yml) auf:

  roles:
    - {role: "example", tags: "example"}

So ist sichergestellt, dass ich die Rolle jederzeit über ihren tag auch einzeln aufrufen kann, ohne hierfür ein eigenes Playbookfile zu benötigen.

In defaults/main.yml führe ich immer alle Variablen auf, die das Playbook verarbeiten kann, inkl. Beispielen ihrer Nutzung. Auch dann, wenn ich hierfür gar keine Defauls setzen möchte:

# docker:
#  zfs_pool: tank/data
#
#  instances:
#    grav:
#      volumes:
#        /srv/docker/grav/www:/var/www/html
#    gitea:
#      volumes:
#        server:
#          /srv/docker/gitea/www:/data
#          /etc/timezone: /etc/timezone:ro
#          /etc/localtime: /etc/localtime:ro
#        db:
#          /srv/docker/gitea/postgres:/var/lib/postgresql/data
#      db: gitea
#      db_user: gitea
#      db_pass: !vault |

So ist sichergestellt, dass ich - z.B. beim Erstellen eines neuen Hosts - keine wichtigen Einstellungen vergesse und mein Playbook auch nach Jahren noch schnell verstehen kann. Das ist insbesondere dann hilfreich, wenn man im Playbook bereits Konfigurationen vorausgeplant hat, die bisher noch nicht verwendet werden (z.B. die Nutzung von Let's Encrypt mittels dns-01 statt http-01). Eine solche Vorausplanung finde ich sehr sinnvoll, da man zum Zeitpunkt der Playbookerstellung wahrscheinlich oft besonders tief im Thema ist. So erspart man es sich selbst bestenfalls sich noch einmal tiefer mit dem Thema beschäftigen zu müssen.

Die Verzeichnisse files und templates nutze ich natürlich nur dann, wenn es entsprechende Dateien gibt. Beide Verzeichnisse untergliedere ich meist noch weiter (z.B. in bin, conf, etc.). Bei Templates hänge ich zudem an den eigentlichen Dateinamen immer die Endung .j2 an.

Am wichtigsten ist aber natürlich der Aufbau von tasks. Ich versuche hier stets meine Tasks-Dateien möglichst kurz zu halten und auf die verschiedenen Schritte (wie z.B. Installation und Konfiguration) aufzuteilen. Oft kommt es auch vor, dass ein Tool eine Server/Client Architektur voraussetzt. Hier könnte man natürlich stets eine example-server und eine separate example-client Rolle oder Playbook anlegen, ich mache es jedoch meist so, dass ich für den jeweilign Part eine eigene Datei (z.B. server.yml und client.yml) anlege.

Je nach Betriebssystem bzw. Betriebssystemversion weichen die Konfigurationsdateien oder z.B. auch Paketnamen leicht voneinander ab. Oft reicht es hier einfach mit Variablen zu arbeiten. Ist dies der Fall lege ich für jedes Betriebssystem bzw. jede -version eine Variablendatei in vars ab und füge meiner tasks/main.yml folgenden Tasks hinzu (sinnvollerweise an den Beginn der Datei):

- name: Gather variables for each operating system
  include_vars: "{{ item }}"
  with_first_found:
    - files:
        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower }}.yml"
        - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
        - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
        - "{{ ansible_distribution | lower }}.yml"
        - "{{ ansible_os_family | lower }}.yml"
      skip: true

Die genutzten Variablen werden von Ansible automatisch bereitgestellt, wenn gather_facts auf true gesetzt wurde. ansible_distribution enthält hierbei den Namen der Distribution als z.B. debian, pop!_os oder ubuntu. ansible_distribution_version ist z.B. 12.01 oder 22.04 wo hingegen ansible_distribtution_major_version hier dann nur 12 oder 22 enthält. ansible_os_family enthält den Namen der Distribution auf der das genutzte Betriebssystem basiert, also z.B. debian für ubuntu und pop!_os. Letzeres kann hilfreich sein um zu entscheiden, welchen Paketmanager man für die Installation von Softwarepaketen nutzt (z.B. apt oder yum).

Durch die Anweisung with_first_found müssen nicht alle möglichen Dateien vorhanden sein, Ansible nimmt einfach die erste Datei, die passt. Daher ist natürlich auch die Reihenfolge der files Liste wichtig. Ich kann also sowohl die Datei ubuntu-22.yml als auch einfach ubuntu-22.04.yml anlegen oder schlicht debian.yml, je nachdem wie fein die Unterscheidung sein muss. skip: true sorgt dafür, dass Ansible die Verarbeitung nicht abbricht, wenn keine der Dateien gefunden werden konnte. Da in diesem Fall die Variablen gar nicht gesetzt würden, sollten diese daher auch in defaults/main.yml definiert sein, dort dann eben mit den Werten, die von den meisten OS-Versionen genutzt werden.

Ein Beispiel könnten z.B. die unterschiedlichen Pfade und Namen für die Konfigurationsdatei von dropbear unter Debian 11 und 12 sein:

debian-12.yml:

dropbear_config_dir: "/etc/dropbear/initramfs/"
dropbear_config_file: "dropbear.conf"

debian-11.yml:

dropbear_config_dir: "/etc/dropbear-initramfs/"
dropbear_config_file: "config"

An sich wäre es natürlich auch ganz nett die Anweisung für das Einlesen der Variablen direkt in meine site.yml schreiben zu können, aber dort ist include_vars leider nicht erlaubt und vars_files kann nicht mit with_first_found umgehen.

Eine weitere Besonderheit ist die Datei preflight.yml, hier definiere ich Checks die vor der eigentlich Ausführung der Tasks bestanden werden müssen. Dies ist insbeondere dann hilfreich, wenn man include_vars und skip: true nutzt, denn während des Preflights kann man prüfen, ob wirklich alle benötigten Variablen gesetzt sind:

- name: Fail when dropbear config vars are not set
  fail:
    msg: "Please specify dropbear config vars"
  when:
    - dropbear_config_dir is not defined or
      dropbear_config_file is not defined

Meine main.yml baue ich nach folgendem Schema auf:

---
- name: Gather variables for each operating system
  include_vars: "{{ item }}"
  with_first_found:
    - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower }}.yml"
    - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
    - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version | lower }}.yml"
    - "{{ ansible_distribution | lower }}.yml"
    - "{{ ansible_os_family | lower }}.yml"
  tags:
    - example_install
    - example_config
    - example_server
    - example_client

- include: preflight.yml
  tags:
    - example_install
    - example_config
    - example_server
    - example_client

- include: install.yml
  become: true
  tags:
    - example_install

- include: config.yml
  become: true
  tags:
    - example_config

- include: server.yml
  when: example_server == true
  tags:
    - example_server

- include: client.yml
  when: example_client == true
  tags:
    - example_client

Ein weiterer Vorteil dieser Vorgehensweise, man kann Anweisungen wie tags, become und when nutzen, ohne sie an jedes einzelne Play in den Tasks schreiben zu müssen. Außerdem bleibt es so sehr übersichtlich, auf welchen Hosts welcher Task ausgeführt wird.

Die Dateien install.yml, config.yml, server.yml und client.yml sind dann ganz normale Tasks für die Installation, die gemeingültige Konfiguration (sofern vorhanden), sowie für Server- und Clientkonfiguration.

Wer möchte kann noch einen meta Ordner für jede Role anlegen und dort in einer main.yml einige weiterführende Informationen zur Role anlegen, z.B.:

---
galaxy_info:
  author: gpkvt
  description: Example
  license: MIT
  company: none
  min_ansible_version: 2.10
  platforms:
    - name: Debian
      versions:
        - bullseye
        - bookworm
  galaxy_tags:
  - example

dependencies: []

Diese Angaben dienen in erster Linie zur Veröffentlichung einer Role via ansible-galaxy, kann aber auch für die eigene Nutzung hilfreich sein. Insbesondere natürlich um zu erkennen, welche Betriebssysteme in welcher Version von der Role unterstützt werden.

Tags: ansible best practice roles tasks playbook lvdisplay vars meta

Mehr

  • Ansible Secrets mit Lookup Plugins
  • Ansible: Zusammenhängende Konfiguration über mehrere Server
  • Serververwaltung mit Ansible
  • Backups mit Borg und ZFS/LVM Snapshots
  • Smarthome Schaltzentrale mit Home Assistant

Tags

ansible best practice roles tasks playbook lvdisplay vars meta

Archiv

  • Mar 2025 (2)
  • May 2024 (2)
  • Oct 2023 (1)
  • Aug 2023 (5)
  • Jul 2023 (31)

  • Ältere Einträge (95)

Feeds

Atom 1.0 RSS JSON
  • Datenschutz
  • Impressum
  • Archiv