Home Schleifen in Ansible sind Scheiße
Post
Cancel

Schleifen in Ansible sind Scheiße

Ich weiß .. steile These, daher lasst mich ein wenig tiefer darauf eingehen!

technischer Hintergrund

Beim Aufruf eines Ansible Modules baut der Controller eine Verbindung über SSH zum Zielsystem auf.
Je nach Module, Netzwerk und Konfiguration dauert dann das abarbeiten des Modules seine Zeit.

Sobald man keinen direkten Zugriff auf das Zielsystem hat, kommen noch weitere Latenzen - wie z.B. ein dazwischen liegender Jumphost - hinzu.

  • Verbindungsaufbau
  • Schlüsselaustausch
  • Tansfer von Daten zum Zielsystem
  • Ausführen des Pythoncodes
  • Transfer vom Output zum Controller

Je nach Ansible Konfiguration kommt gegenbenfalls auch noch das schließen der Verbindung hinzu.

Das ganze funktioniert für einzelne Tasks ganz gut, für größere Rollen wird es dann schon unangenehm langwierig, aber sobald man bei Schleifen landert, wird es … lästig.

kleine Beispiele

Damit das ganze nicht zu theoretisch wird, ein paar Beispiele.

Beispiel 1 - Installation von Paketen

In vielen (vor allem älteren) Rollen findet man zum Beispiel dieses Konstrukt:

1
2
3
4
5
6
7
8
- name: install packages
  package:
    name: "{{ item }}"
    state: present
  loop:
    - vim
    - nano
    - iproute2

Ich gebs zu, als ich meine ersten Ansible Rollen schrieb, machte ich es genauso.
Ich kann aber nicht mehr nachvollziehen, ob es daran lag, ob es keine andere Möglichkeit gab, die Dokumentation einfach schlecht war, oder ich miese Beispiele bei StackOverflow gefunden hatte.

Effizienter, schneller und daher auch besser ist dieser Weg:

1
2
3
4
5
6
7
- name: install packages
  package:
    name: 
      - vim
      - nano
      - iproute2
    state: present

Das vermeiden von Schleifen und die Liste gleich dem Modul zu übergeben.

Beispiel 2 - Erstellen von Verzeichnissen

Wenn man für einen nginx pro VHost ein eigenes Verzeichnis für Logfiles erstellen möchte.
Dann parst man in der Regel durch eine riesige YAML Tapete, nimmt sich dort definierte Logfiles und iteriert über das Ergebniss. Anschließend ruft mal in einer Schleife das file Modul auf:

1
2
3
4
5
6
7
8
- name: create nginx logfile directory for vhost.
  file:
    path: "{{ item | item.logfiles.access | default('/var/log/nginx/access.log') | dirname }}"
    state: directory
    mode: 0755
  loop: "{{ vhosts }}"
  loop_control:
    label: "{{ item | item.logfiles.access | default('/var/log/nginx/access.log') | dirname }}"

Beispiel 3 - Erstellen von Usern

Wir haben eine übliche Infrastruktur und ein Operationsteam mit ca. 15 Mitarbeitenden.
Die Firmeninterne Complianceregel schreibt vor, dass jeder der Mitarbeitenden sich nur mit seinem eigenen SSH-Key dort anmelden darf und er muss einen eindeutige zuweisbaren Usernamen besitzen.

Für diejenigen, die bislang dachten das gibt es nicht (mehr): Doch, das gibt es!
Denn irgendwo in der heilen Kuberneteswelt gibt es tatsächlich auch noch Hardware bzw. virtuelle Instanzen, auf denen solch ein Cluster läuft. Und diese Infrastruktur muss eben gepflegt werden, damit der verf* Cluster auch vernünftig läuft! Und genau dafür gibt es die guten OPs, die genau dafür Sorge tragen …

Das erstellen eines Users ist banal:

  • User erstellen
  • .ssh Verzeichniss erstellen
  • SSH-Key kopieren

Hier die Beispiel aus meiner Ansible Rolle. (Die macht noch etwas mehr, daher auch nur in Ausschnitten):

User erstellen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- name: create users, with home directories
  ansible.builtin.user:
    name: "{{ item.username }}"
    uid: "{{ item.uid | default(omit, True) }}"
    password: "{{ item.password if item.password is defined else '!' }}"
    update_password: "{{ item.update_password if item.update_password is defined else users_default_update_password }}"
    groups: "{{ item.groups | default(omit) }}"
    shell: "{{ item.shell if item.shell is defined else users_default_shell }}"
    createhome: true
    comment: "{{ item.comment if item.comment is defined else '' }}"
    state: "{{ item.user_state }}"
  when:
    - item.user_state in ('present', 'lock')
  loop:
    "{{ present_users }}"
  loop_control:
    label: "username: {{ item.username }}"

Verzeichnissstruktur im $HOME Verzeichniss erstellen

Um den SSH-Key kopieren zu können, muss das .ssh Verzeichniss erst erstellt werden.

1
2
3
4
5
6
7
8
9
10
11
- name: create .ssh directory in user home
  ansible.builtin.file:
    path: "/home/{{ item.username }}/.ssh"
    state: directory
    owner: "{{ item.username }}"
    group: "{{ item.primary_group }}"
    mode: 0700
  loop:
    "{{ present_users }}"
  loop_control:
    label: "username: {{ item.username }}"

SSH-Key kopieren

Und erst jetzt können wir den Key hinkopieren.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- name: add authorized key for ssh key authentication
  ansible.posix.authorized_key:
    user: "{{ item.username }}"
    key: "{{ item.ssh_key }}"
    exclusive: "{{ item.exclusive_ssh_key if item.exclusive_ssh_key is defined else 'no' }}"
    state: "{{ item.user_state | default('present', true) }}"
  when:
    - item.ssh_key is defined
    - item.user_state in ('present', 'lock')
    - not item.ssh_key_directory is defined
  loop:
    "{{ present_users }}"
  loop_control:
    label: "username: {{ item.username }}"

(Ggf. käme noch etwas Magie für sudo hinzu, aber die lasse ich hier mal aussen vor.)
Wir haben also minimal 3 Schleifen über die YAML Konfiguration von 15 Usern.
Dabei ist es egal, ob die User / SSH-Keys bereits existieren oder nicht, die 3 Schleifendurchgänge bleiben!

Jeder Durchlauf kostet seine Zeit. (In meinem molecule Test sind das bei 20 Usern ~1 Minute und 40 Sekunden.)

Ginge es denn auch besser?

Natürlich geht es besser!

Der erste Schritt wäre es, auf Schleifen ganz zu verzichten, was aber leider nicht immer möglich ist.

Änderung der Ansible Architektur

Der effizientere, und IMHO auch nachhaltigere, Schritt wäre es die grundlegende Ansible Architektur zu überarbeiten.

Ich könnte mir schon einen imensen Geschwindigkeitszuwachs vorstellen, wenn nicht jeder Task eine eigene Verbindung zum Zielsystem benötigen würde, Man könnte - zum Beispiel - das Inventory und die benötigten Rollen auf das Zielsystem übertragen und die vor Ort abarbeiten. Anschließend muss nur noch das Ergebniss eingesammelt und dargestellt werden. Damit würde das ständige iterieren über Module, Datentransfer und dem Foo darum wegfallen. Die Abarbeitung wäre schneller und man müsste noch nicht mal allzuviel an Ansible umbauen …
Allerdings müsste man sich überlegen, wie man ein delegate_to umsetzt.

Eigene Lösungen

Für größere oder komplexere Rollen erstelle ich mir mittlerweile ein eigenes Modul.
Natürlich komme ich nicht um das iterieren der Konfiguration herum. Aber ich mache das dann halt auf dem Zielsystemda.
Am Beispiel des erstellen von Verzeichnissen habe ich ein nginx_log_directories Modul, welches auf dem Zielsystem die entsprechenden Verzeichnisse erstellt oder eben entfernt.

Ich möchte eine schnelle Automatisierung zu erhalten, eine die ich auch wirklich als Continuous Delivery laufen lassen kann, ohne Stundenlang auf ein Ergebniss warten zu müssen.

This post is licensed under CC BY 4.0 by the author.