ADMIN

2023

03

2023-02-28T12:00:00

Hybrid Cloud

SCHWERPUNKT

076

Hybrid Cloud

Cloud

Ansible

Container-Management mit Ansible

Automatisch verpackt

von Andreas Stolzenberger

Veröffentlicht in Ausgabe 03/2023 - SCHWERPUNKT

Ansible steuert nicht nur virtuelle Maschinen in Cloudumgebungen wie AWS oder GCP. Auch containerisierte Setups lassen sich mit dem Automatisierungstool sehr simpel verwalten. Dabei muss nicht immer Kubernetes auf der Zielplattform laufen. Mit Ansible, Nginx und Podman lässt sich eine ähnliche Funktionalität für kleinere Umgebungen umsetzen. Unser Workshop zeigt, wie das geht.

Im IT-Administrator Sonderheft "IT-Automatisierung" vom Oktober 2021 hatten wir zum letzten Mal ausführlich über das Thema Cloud-Rollouts mit Ansible berichtet. Im damaligen Artikel ging es vor allem darum, wie Administratoren virtuelle Maschinen in Clouds erstellen und dort ihre Applikationen einrichten.
In der Zwischenzeit hat sich einiges geändert, denn immer öfter setzen Unternehmen statt VMs containerisierte Applikationen ein. Das kann Management, Wartung und Updates der Anwendungen und Infrastrukturen erheblich vereinfachen. Die Voraussetzung dafür bleibt jedoch, dass sich die betreffende Applikation für den containerisierten Einsatz eignet. Die Plattform der Wahl ist dabei natürlich Kubernetes und im abschließenden Teil dieses Artikels stellen wir Ihnen auch vor, wie Sie Kubernetes-Applikationen mit Ansible verwalten.
Es gibt jedoch Bereiche, für die Kubernetes zu groß und kompliziert erscheint. In Filialinstallationen, Außenstellen und am Network-Edge braucht es vielleicht kein Kubernetes und ein simples Container-Setup mit Podman genügt. Eine Kubernetes-ähnliche Funktionalität für kleine Installationen lässt sich mit unserem "LANP"-Stack recht simpel umsetzen.
Im IT-Administrator Sonderheft "IT-Automatisierung" vom Oktober 2021 hatten wir zum letzten Mal ausführlich über das Thema Cloud-Rollouts mit Ansible berichtet. Im damaligen Artikel ging es vor allem darum, wie Administratoren virtuelle Maschinen in Clouds erstellen und dort ihre Applikationen einrichten.
In der Zwischenzeit hat sich einiges geändert, denn immer öfter setzen Unternehmen statt VMs containerisierte Applikationen ein. Das kann Management, Wartung und Updates der Anwendungen und Infrastrukturen erheblich vereinfachen. Die Voraussetzung dafür bleibt jedoch, dass sich die betreffende Applikation für den containerisierten Einsatz eignet. Die Plattform der Wahl ist dabei natürlich Kubernetes und im abschließenden Teil dieses Artikels stellen wir Ihnen auch vor, wie Sie Kubernetes-Applikationen mit Ansible verwalten.
Es gibt jedoch Bereiche, für die Kubernetes zu groß und kompliziert erscheint. In Filialinstallationen, Außenstellen und am Network-Edge braucht es vielleicht kein Kubernetes und ein simples Container-Setup mit Podman genügt. Eine Kubernetes-ähnliche Funktionalität für kleine Installationen lässt sich mit unserem "LANP"-Stack recht simpel umsetzen.
LANP statt Kubernetes
LAMP ist ein gängiger Begriff für den Web-Application-Stack Linux, Apache, MySQL und PHP. Die Abkürzung "LANP" für Linux, Ansible, Nginx und Podman haben wir hingegen für diesen Artikel erfunden. Das Konzept ist jedoch simpel und schlüssig: Mit Kubernetes rollen Sie Applikationen in Containern, gruppiert in Namespaces, aus und leiten den HTTP/ HTTPS-Traffic via Router vom Host in den Pod. Das Gleiche macht LANP, nur ohne Kubernetes. Der Linux-Host betreibt Podman für die Container und Nginx als Reverse-Proxy. Der Host verfügt über eine IP-Adresse und einen FQDN.
Je nachdem, wie flexibel das DNS-Setup dieser Umgebung arbeitet, leitet der Nginx-Proxy dann entweder C-Domains im Stil von "http(s)://app.fqdn" oder Subdirec­tories nach dem Muster von "http(s):// fqdn/app" in die Container weiter. Dabei übernimmt Nginx falls nötig auch die SSL-Terminierung. Somit benötigen Sie lediglich ein SSL-Zertifikat für den Host und Nginx schickt den Traffic zu den Containern intern per http weiter. Die "Ingress"-Route über ein virtuelles Unterverzeichnis lässt sich theoretisch leichter umsetzen, bedingt allerdings, dass die Applikation im Container diese Form des URL-Rewrite beherrscht. Im Zweifelsfall müssen Sie die Konfiguration der Webapplikation im Container anpassen. Zuverlässiger funktioniert daher die Wildcard-Route über die C-Domain.
Der logische Ablauf eines Application-Rollouts via Ansible auf dieser Plattform sieht so aus: Das Playbook erstellt zunächst einen Podman "POD", in dem es alle Container der gewünschten Applikation zusammenfasst. Dieser Pod stellt nur den HTTP-Port der Applikation via Port-Mapping nach außen zur Verfügung. Interne Ports wie beispielsweise der eines Datenbank-Containers bleiben außerhalb des Pods unsichtbar. So könnten Sie problemlos mehrere Pods mit dem jeweils eigenen MariaDB-Container betreiben, ohne dass einer davon den Port 3306 des Hosts blockiert.
Innerhalb des Pods rollt Ansible dann die benötigten Container mit der passenden Konfiguration aus. Abschließend erstellt es die zur Applikation passende Reverse-Proxy-Konfiguration in "/etc/nginx/ conf.d/" auf dem Podman/Nginx-Host und aktiviert Sie.
In den vergangenen Ausgaben hatten wir bei Artikeln mit Podman immer mit Bridge-Networks und eigenen IP-Adressen für Container gearbeitet. Das wäre hier ebenfalls möglich, nur dass dabei der Pod die IP-Adresse bekommt und nicht ein individueller Container. Für diesen Workshop verwenden wir diesmal das Setup über Port-Mappings ohne ein Bridge-Network.
Nötige Vorbereitungen
Installieren Sie eine Linux-Maschine/VM mit einer Distribution Ihrer Wahl und richten Sie darauf Podman und Nginx ein. Im Workshop verwenden wir RHEL, aber das Setup funktioniert auch mit allen anderen Distributionen. Lediglich die Konfiguration des Nginx-Servers weicht bei EL- und Debian-basierten Distributionen ab. Hier müssen Sie die Templates und Verzeichnisse gegebenenfalls anpassen. Vergessen Sie nicht, bei einem System mit aktivem selinux das Kommando
setsebool httpd_can_network_connect=true
abzusetzen, denn sonst darf Nginx keinen Traffic an Non-Standard-Ports leiten.
Ein Raspberry Pi sollte für einfache Applikationen bereits genügen. Stellen Sie sicher, dass die Maschine einen gültigen Namen im LAN hat und Ihr DNS diesen auch korrekt auflöst. Je nachdem, ob Sie den Proxy-Dienst mit C-Domains oder Unterverzeichnissen betreiben wollen, sollte Ihr DNS-Server Wildcards des Podman-Hosts auflösen. Für den Workshop setzen wir eine RHEL-8-VM namens "pod.mynet.ip" mit C-Domain-Routing ein. In dem DNS-Server (dnsmasq) des Netzwerks steht daher der passende Eintrag:
address=/.pod.mynet.ip/192.168.2.41
So erhalten DNS-Anfragen zu Namen wie "app1.pod.mynet.ip" oder "wp01.pod.mynet.ip" immer die Antwort "192.168.2.41". Parallel dazu kommt ein Raspberry Pi 4 mit 8 GByte RAM und RHEL 9 zum Einsatz (pi8.mynet.ip), auf dem Nginx via Subdirectory-Routing arbeitet. Den Ansible-Code führen wir auf einer Fedora-36-Workstation oder einer WSL-Umgebung mit Fedora 36 aus. Dabei findet Ansible 2.13 Verwendung. Zusätzlich zur Basisinstallation brauchen Sie die Collection "containers.podman":
ansible-galaxy collection install containers.podman
Alternativ starten Sie die Playbooks aus einer AWX-Umgebung mit dem passenden Execution Environment.
Für unser Beispiel verwenden wir ein übliches Wordpress-Setup bestehend aus einem MariaDB-Container für die Datenbank und dem Applikationscontainer mit Apache, PHP und der Wordpress-Anwendung.
Bild 1: Dank der Isolation in Pods laufen mehrere identische Applikationen parallel auf dem gleichen Host. Der Reverse-Proxy regelt den Zugriff via Name- oder Subdirectory-based-Routing.
Das Was und Wie trennen
Um Ansible Playbooks möglichst flexibel für verschiedene Szenarien einsetzen zu können, trennen wir die Automatisierungslogik von den Applikationsparametern. Das Beispiel ist dabei noch nicht vollständig generalisiert, lässt sich so aber schon für mehrere Szenarien nutzen. Alle benötigten Parameter packen Sie zuerst in eine Konfigurationsdatei "config.yml", die das eigentliche Playbook via "vars_file" einbindet. Das Einrichten beginnt mit der Host-Konfiguration des Setups, also der URL des Podman-Servers, dem Basisverzeichnis des persistenten Storage für die Container und dem Namen, den der Applikations-Pod bekommt:
domain_name: pod.mynet.ip
pod_dir: /var/pods
pod_name: wp01_pod
Darauf folgen die Parameter des Datenbank-Pods, die sich weitgehend selbst erklären. Über die Variablen "db_local_dir" und "db_pod_dir" blendet Podman später das lokale Host-Verzeichnis in den Container, sodass die Datenbank selbst bei einem Stopp oder Update des MariaDB-Ports nicht verlorengeht:
db_name: db01
db_user: wpuser
db_pwd: wp01pwd
db_root_pwd: DBrootPWD
db_image: docker.io/mariadb:latest
db_local_dir: db01
db_pod_dir: /var/lib/mysql
Ganz ähnlich sehen die Parameter für den Applikations-Pod aus. Podman wird später nur den "app_port" der Außenwelt und damit dem Reverse-Proxy zur Verfügung stellen. Lassen sie mehrere Applikations-Pods laufen, brauchen diese lediglich andere Port-Nummern:
app_name: wp01
app_port: 18000
app_image: docker.io/wordpress
app_local_dir: wp01
app_pod_dir: /var/www/html
Für den Reverse-Proxy im Router-Modus setzen wir ein simples Nginx-Konfigurationsfile als Jinja-2-Template ein, das Ansible später einfach in "/etc/nginx/conf.d/" einfügt, siehe Listing-Kasten 1. Die Konfigurationsdatei richtet einen Reverse-Proxy vom Namen der Applikation auf dessen Applikationsport am Podman-Host ein.
Listing 1: Jinja-2-Template für Proxy im Router-Modus
server {       Listen 80;       server_name {{ app_name }}.{{ domain_name }};       access_log /var/log/nginx/{{ app_name }}.access.log;       error_log /var/log/nginx/{{ app_name }}.error.log;       client_max_body_size 65536M;       location / {            proxy_pass http://127.0.0.1:{{ app_port }};            proxy_http_version 1.1;            proxy_set_header Upgrade $http_upgrade;            proxy_set_header Connection 'upgrade';            proxy_set_header Host $host;            proxy_cache_bypass $http_upgrade;       } }
Nutzen Sie Nginx zum Terminieren der SSL-Sicherheit, geben Sie bei "Listen" entsprechend den Port 443 mit SSL an und fügen die nötigen SSL-Parameter und Angaben zum Zertifikat vor dem "location"-Abschnitt ein.
Bild 2: Die Wordpress-Umgebung läuft wie gewohnt auch im abgeschotteten Pod. Die Applikation übernimmt dabei automatisch die externe URL via Reverse-Proxy.
Der Subdirectory-Modus hingegen integriert die Proxy-Konfiguration nicht über eine eigene "Server"-Konfigurationsdatei, sondern lediglich mit einer eigenen "Location"-Konfiguration, die Ansible in "/etc/nginx/default.d" verstaut. Von dort aus integriert sie "nginx.conf" innerhalb des regulären Server-Statements, siehe Listing-Kasten 2. Die dort angegebenen Rewrite- und Proxy-Regeln haben wir der Wordpress-Dokumentation entnommen. Diese funktionieren nicht unbedingt für andere Webapplikationen. Das eigentliche Ansible-Playbook, um die Container auszurollen und den Proxy zu konfigurieren, beginnt zunächst mit den Standardparametern:
- name: Roll Out Wordpress
    hosts: pod
    become: true
    gather_facts: false
 
vars_files:
          - config.yml
Listing 2: Jinja-2-Template für Proxy im Subdirectory-Modus
location /{{ app_name }}/ {       rewrite ^([^\?#]*/)([^\?#\./]+)([\?#].*)?$ $1$2/$3 permanent;       proxy_pass http://127.0.0.1:{{ app_port }}/;       proxy_read_timeout 90;       proxy_connect_timeout 90;       proxy_redirect off;       proxy_set_header X-Real-IP $remote_addr;       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;       proxy_set_header X-Forwarded-Proto $scheme;       proxy_set_header Host $host;       proxy_set_header X-NginX-Proxy true;       proxy_set_header Connection ""; }
Daten des Remotesystems, in diesem Kontext auch Facts genannt, benötigen Sie nicht. Die Konfiguration entnimmt Ansible aus dem zuvor deklarierten File. Zuerst erstellt Ansible nun den Pod und leitet nur den externen Port 18000 zum internen Port 80 um. Da in der Regel alle Webapplikationen auf Port 80 hören, haben wir ihn im Beispiel hart codiert:
tasks:
      - name: Create Application Pod
         containers.podman.podman_pod:
              name: "{{ pod_name }}"
              state: started
              ports:
                   - "{{ app_port }}:80"
Verwenden Sie Applikationen mit anderen Ports (Beispiel Kibana auf Port 5601), müssen Sie diese Angabe noch als Variable in "config.yml" deklarieren, etwa als "app_internal_ port". Danach erstellt der Eintrag aus Listing 3 die Unterverzeichnisse auf dem Podman-Host, in den später die Applikationen ihre Dateien ablegen. Der Code aus Listing 4 rollt den Datenbank-Container aus, gefolgt von den Passagen aus Listing 5, die den Applikations-Container erstellen. In dem Fall ist die Verbindung zum Datenbank-Container einfach als "127.0. 0.1:3306" festgelegt, da der Datenbankport innerhalb des Pods offen steht, aber von außen nicht zu sehen ist.
Listing 3: Unterverzeichnisse erstellen
- name: Create Container Directory DB    ansible.builtin.file:       path: "{{ pod_dir }}/{{ db_local_dir }}"       state: directory - name: Create Container Directory WP    ansible.builtin.file:       path: "{{ pod_dir }}/{{ app_local_dir }}"       state: directory
Listing 4: Datenbank-Container ausrollen
- name: Create MySQL Database for WP    containers.podman.podman_container:       name: "{{ db_name }}"       image: "{{ db_image }}"       state: started       pod: "{{ pod_name }}"       volumes:          - "{{ pod_dir }}/{{ db_local_dir }}: {{ db_pod_dir }}:Z"       env:          MARIADB_ROOT_PASSWORD: "{{ db_root_pwd }}"          MARIADB_DATABASE: "{{ db_name }}"          MARIADB_USER: "{{ db_user }}"          MARIADB_PASSWORD: "{{ db_pwd }}"
Listing 5: Applikations-Container erstellen
- name: Create and run WP Container    containers.podman.podman_container:       pod: "{{ pod_name }}"       name: "{{ app_name }}"       image: "{{ app_image }}"       state: started       volumes:          - "{{ pod_dir }}/{{ app_local_dir }}: {{ app_pod_dir }}:Z"       env:       WORDPRESS_DB_HOST: "127.0.0.1:3306"       WORDPRESS_DB_NAME: "{{ db_name }}"       WORDPRESS_DB_USER: "{{ db_user }}"       WORDPRESS_DB_PASSWORD: "{{ db_pwd }}"
Zum Schluss folgt die Reverse-Proxy-Konfiguration mit
- name: Set Reverse Proxy
    ansible.builtin.template:
              src: proxy.j2
              dest: /etc/nginx/conf.d/{{ app_name }}.conf
für den Proxy im C-Domain-Routing-Modus. Für das Subdirectory-Routing lautet der entsprechende Eintrag
dest: /etc/nginx/default.d/{{ app_name }}.conf
In beiden Fällen geht der Code weiter mit
owner: root
group: root
mode: '0644'
 
- name: Reload Reverse Proxy
    ansible.builtin.service:
              name: nginx
              state: reloaded
Und das war es dann auch schon. Mit dieser Playbook-Vorlage und individuellen "config.yml"-Dateien rollen Sie eigentlich problemlos die meisten Applikationen mit einem App- und einem DB-Container aus. Die nächste Evolution dieses Playbooks verschiebt die "env"-Variablen aus dem Playbook in die Konfigurationsdatei.
Die übernächste Stufe deklariert die Variablen in der Datei "config.yml" dann als hierarchisches "Dictionary" und braucht somit nur noch einen "Create Container"-Task im Playbook, der über das Dictionary looped – dann mit einer beliebigen Anzahl von Containern. Dazu erstellen Sie ein Cleanup-Playbook, das die komplette Installation mit Containern, Pod und Reverse-Proxy wieder löscht, dabei aber die Daten der Applikation erhält. Somit können Sie mit Cleanup und dem erneuten Rollup die Anwendung auch ganz einfach updaten, gesetzt den Fall, Ihre Container-Images verweisen auf den ":latest"-Tag.
Ansible-Rollouts auf Kubernetes
Mit Ansible rollen Sie auch auf Kubernetes im Handumdrehen ihre Applikationen aus. Die Collection "kubernetes.core" (ansible-galaxy collection install kubernetes.core) bringt dazu alle nötigen Module mit. Das Standardmodul "k8s" packt dabei einfach die Kubernetes-YML-Deklaration einer Ressource direkt in den Ansible-Code. Sie können also ihre bestehenden Kubernetes-YML-Dateien nahezu unverändert in Ihre Playbooks übernehmen.
Im Beispiel rollen wir Ansible AWX auf einer Kubernetes-Installation aus. Das Playbook setzt hierbei voraus, dass Sie an Ihrem Kubernetes-Cluster angemeldet sind. Die Art und Weise des Log-ins hängt dabei vom gewählten Auth-Dienst Ihrer Kubernetes-Version ab. Im Workshop verwenden wir eine Microshift-Installation und sind via "kubeconfig" (export KUBECONFIG=/ <Pfad>/kubeconfig) authentisiert.
Zudem haben Sie auf der Kubernetes-Installation bereits den AWX-Operator eingerichtet und gestartet: Der Prozess ist unter [1] beschrieben und bedarf nur weniger Schritte. Das Playbook beginnt wie gewohnt mit
- hosts: localhost
    connection: local
    gather_facts: False
und deklariert dann die Variablen. Weil es nur vier Stück sind, haben wir sie für das Beispiel nicht in eine separate YML-Datei ausgelagert:
vars:
    awx_name: awx01
    awx_ns: awx
    awx_port: 30080
    base_url: kube.mynet.ip
Die AWX-Installation "awx01" ist später über die URL "http://kube.mynet.ip: 30080" zu erreichen. Nutzen Sie Open- oder Microshift können Sie darüber hinaus eine Route erstellen, sodass der Zugriff auch via "http://awx01.kube.mykier.ip" erfolgen kann (Listing-Kasten 6). Der Operator wird dementsprechend einen Pod mit PostgreSQL erstellen, ihm ein persistentes Volume zuweisen und einen weiteren Pod mit den vier AWX-Containern bauen. Der "Service" leitet den Nodeport in die Applikation. Auf einer Open- beziehungsweise Microshift-Installation können Sie zudem die Route erstellen (Listing-Kasten 7).
Listing 6: AWX installieren
- name: Install AWX    kubernetes.core.k8s:       state: present       definition:          apiVersion: awx.ansible.com/v1beta1          kind: AWX          metadata:               name: "{{ awx_name }}"               namespace: "{{ awx_ns }}"          image: "{{ app_image }}"               image: service_type: nodeport               nodeport_port: "{{ awx_port }}"
Listing 7: AWX-Route erstellen
- name: AWX-route    kubernetes.core.k8s:       state: present       definition:          kind: Route          apiVersion: route.openshift.io/v1          metadata:               name: "{{ awx_name }}-route"               namespace: "{{ awx_ns }}"          spec:               host: "{{ awx_name }}.{{ base_url }}"               to:                    kind: Service                    name: "{{ awx_name }}-service"               port:                    targetPort: http               wildcardPolicy: None
Der Operator generiert ein zufälliges Password für den Admin-Account und speichert es in einem Secret ab. Das kann Ansible über das "k8s_info"-Modul auslesen und später verwenden. Im Beispiel geben wir das Passwort nur auf der CLI aus, damit sich der Nutzer anmelden kann (Listing-Kasten 8). Sie könnten aber auch direkt nach dem Rollout der AWX-Instanz diese automatisch über die Controller-Configuration-Rollen [2] aus bestehenden YML-Dateien (Export einer anderen AWX, Tower- oder Controller-Instanz) einrichten.
Listing 8: Admin-Passwort abrufen
- name: Get Secret    kubernetes.core.k8s_info:          apiVersion: v1          kind: Secret          name: "{{ awx_name }}-admin-pa,ssword"          namespace: "{{ awx_ns }}"    register: awx_secret - name: AWX Password    debug:          msg: "Password: {{ awx_secret.resources[0].data.password | b64decode }}"
Fazit
Cloud-Rollouts via Ansible funktionieren mit containerisierten Umgebungen eigentlich noch besser als mit herkömmlichen VM-Umgebungen. Die Playbooks sind einfacher und laufen viel schneller. Mit Kubernetes oder Podman überspringen sie die langwierigen Schritte für ein VM-Setup mit anschließender OS-Konfiguration, gefolgt vom Applikationssetup. Was Kubernetes für große Umgebungen leistet, kann in kleinen Umgebungen oder beim Edge-Betrieb Podman mit Hilfe von Nginx übernehmen. Ansible als Automatisierungstool arbeitet dabei mit allen Plattformen zusammen.
(ln)
Link-Codes
[2] Controller-Configuration-Rollen: https://github.com/redhat-cop/controller_configuration/