Ansible: Zusammenhängende Konfiguration über mehrere Server
Ich nutze Prometheus1 um einige Sensordaten und Statusinformationen meiner Server zu verarbeiten. Prometheus arbeitet mit sog. Exportern2. Hierbei handelt es sich um kleine Programme, die von Prometheus abgefragt werden und so ihre Daten übermitteln. Die Exporter können über beliebige interne und externe Systeme verteilt sein. Hierzu muss Prometheus aber natürlich wissen unter welchen Adressen die jeweiligen Exporter erreichbar sind.
Ich verwalte sowohl die Prometheusinstanz selbst als auch meine Exporter mit Ansible. Immer wenn ich einen Exporter auf einem System hinzufüge, muss die zentrale Prometheus-Instanz hierüber natürlich informiert werden. In diesem Artikel geht es darum, wie man das ganz einfach bewerkstelligen kann. Die ersten Schritte mit Ansible habe ich unter 3 beschrieben.
Schauen wir uns einmal meine Prometheus Role und die zugehörigen Variablen an. Jeder Host hat einen Abschnitt prometheus_exporter
in den host_vars
, hier werden u.a. die zu installierenden Exporter aufgeführt:
zha:
name: zha
target: "['localhost:9644']"
kasa:
name: kasa
target: node-collector
mqtt:
sensors: "localhost:9641"
state: "localhost:9642"
esp: "localhost:9643"
Der eigentliche Prometheus Task kümmert sich um die Installation und Konfiguration von Prometheus selbst (natürlich nur auf einem Host). Hier ist zunächst alles wie immer, ein erwähnenswertes Detail ist noch, dass die grundlegenden Konfigurationseinstellungen von Prometheus in der Konfigurationsdatei (prometheus.yml
) mit der Zeile # END BASE CONFIG ANSIBLE MANAGED BLOCK
abgeschlossen werden. Das ist wichtig, damit die Konfigurationsabschnitte der Exporter an die korrekte Stelle geschrieben werden:
- name: Configure prometheus
ansible.builtin.blockinfile:
path: /etc/prometheus/prometheus.yml
block: "{{ lookup('file', 'files/prometheus.yml') }}"
marker: "# {mark} BASE CONFIG ANSIBLE MANAGED BLOCK"
create: true
mode: "0640"
when: "'monitor' in inventory_hostname"
notify: "Restart prometheus"
Die Zeile {{ lookup('file', 'files/prometheus.yml') }}
sorgt dafür, dass der Inhalt der block
Anweisung - die normalerweise Inline erfolgt - aus der Datei files/prometheus.yml
bezogen wird. Diese sieht bei mir wie folgt aus:
global:
scrape_interval: 60s
evaluation_interval: 60s
alerting:
alertmanagers:
- static_configs:
- targets: ["localhost:9093"]
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "wttr_cgn"
scheme: https
static_configs:
- targets: ["wttr.in"]
metrics_path: "/cgn"
params:
format: ["p1"]
scrape_interval: 60s
scrape_timeout: 15s
Neben der Grundkonfiguration sind auch schon ein paar generelle Jobs unterhalb von scrape_configs
definiert. Hier werden später die weiteren Exporter angefügt. Durch die marker
Anweisung im Task beginnt die tatsächlich geschriebene prometheus.yml
natürlich mit der Zeile # BEGIN BASE CONFIG ANSIBLE MANAGED BLOCK
und endet - wie erwähnt - auf # END BASE CONFIG ANSIBLE MANAGED BLOCK
. Dies ist auch wichtig, damit blockinfile
bei späteren Änderungen an files/prometheus.yml
den korrekten Abschnitt ändert und nicht einfach zusätzlich in die Datei einfügt.
Soweit, so gut. Neben der main.yml
im tasks
Verzeichnis meiner Prometheus Rolle gibt es noch weitere Tasks für jeden möglichen Exporter. Diese werden vom main.yml
Task importiert, wenn die entsprechenden Namen in den host_vars
des entsprechenden Hosts angegeben sind. In unserem Beispiel sind das ja zha
, kasa
und mqtt
. Im main.yml
Tasks sieht das wie folgt aus:
- name: Kasa Exporter
ansible.builtin.include_tasks: kasa.yml
when: "'kasa' in prometheus_exporter"
- name: MQTT Exporter
ansible.builtin.include_tasks: mqtt.yml
when: "'mqtt' in prometheus_exporter"
- name: ZHA Exporter
ansible.builtin.include_tasks: zha.yml
when: "'zha' in prometheus_exporter"
Diese zusätzlichen Tasks sind in der Regel sehr ähnlich aufgebaut, beispielhaft nehme ich hier daher einfach mal nur den ZHA Exporter
:
- name: Install zigbee/zha exporter
ansible.builtin.copy:
dest: /opt/zha-exporter/
src: files/zha-exporter/
mode: "0755"
- name: Create supervisor config
ansible.builtin.template:
dest: /etc/supervisor/conf.d/zha-exporter.conf
src: templates/zha.conf.j2
mode: "0640"
notify: "Restart zha exporter"
- name: Add exporter to prometheus config
ansible.builtin.blockinfile:
path: /etc/prometheus/prometheus.yml
block: |
- job_name: "{{ prometheus_exporter['zha']['name'] }}"
static_configs:
- targets: {{ prometheus_exporter['zha']['target'] }}
insertafter: "# END BASE CONFIG ANSIBLE MANAGED BLOCK"
marker: "# {mark} ZHA ANSIBLE MANAGED BLOCK"
delegate_to: monitor
notify: "Restart prometheus"
Der erste Abschnitt Install zigbee/zha exporter
kümmert sich ganz einfach nur darum, dass die Binärdatei des Exporters auf den Host kopiert wird. Im zweiten Abschnitt Create supervisor config
wird eine Konfigurationsdatei für supervisord
4 geschrieben (um dessen Installation kümmert sich der main.yml
Task).
Interessant wird es im letzten Abschnitt Add exporter to prometheus config
. Zum besseren Verständnis machen wir uns noch einmal bewusst, wie Ansible hier arbeitet. Für jeden Host für den unse Prometheus Role relevant ist, werden die Playbooks auf dem jeweiligen Node selbst ausgeführt. Die Exporter können natürlich auf dem Prometheus Server selbst laufen, tun dies aber in der Regel nicht. Sie haben meist auch keine Zugriffsmöglichkeit auf den Prometheus Server. Dennoch müssen wir der prometheus.yml
auf dem Prometheus Server bescheid geben, dass es den gerade eingerichteten Exporter gibt und wo er ihn findet. Unser Task Add exporter to prometheus config
muss daher nicht auf dem Exporter Host ausgeführt werden, sondern auf dem Prometheus Host (hier monitor
genannt). Und genau das passiert auch aufgrund der Anweisung delegate_to: monitor
.
Auch hier nutzen wir wieder blockinfile
diesmal tatsächlich mit einer Inline Definition von block
und erneut mit einem marker
. Außerdem nutzen wir die zuvor schon vom main.yml
Tasks eingefügte # END BASE CONFIG ANSIBLE MANAGED BLOCK
Zeile um unsere Exporter Konfiguration direkt hinter diese Zeile anzufügen (insertafter
). Außerdem informieren wir noch dem Restart prometheus
Handler (notify
) über die geänderte Konfiguration. Da dies im delegate_to
Kontext stattfindet, wird auch der Handler auf dem monitor
Host ausgeführt.
Mit der deletate_to
Anweisung ist es also kein Problem mehr, Konfigurationen über mehrere voneinander abhängige Server hinweg vorzunehmen. Wir können so sogar Informationen vom eigentlichen Hosts an einen anderen Hosts übergeben. In den host_vars
unseres Exporter Hosts haben wir ja für zha
die folgenden Variablen angegeben:
name: zha
target: "['localhost:9644']"
Im Tasks lesen wir diese entsprechend aus:
block: |
- job_name: "{{ prometheus_exporter['zha']['name'] }}"
static_configs:
- targets: {{ prometheus_exporter['zha']['target'] }}
Diese Variablen werden auf dem Exporter Host befüllt bevor die Anweisung an den Prometheus Host übergeben werden. So können natürlich auch Passwörter oder ähnliches übertragen werden, ohne diese mehrfach in allen host_vars
bzw. group_vars
aufführen zu müssen.
Ich finde die delegate_to
5 Funktion von Ansible daher wirklich elegant und einfach gelöst und sie sollte zum fest zum Werkzeugkasten eines jeden Ansible Nutzers gehören.