Icinga2 mit Ansible deployen

Nachdem ich Icinga2 nun für alle meinen neuen VMs benutze, gehe ich auf jedem Host immer wieder die selben Schritte durch, um die initiale Konfiguration anzulegen. Ich wünschte mir recht schnell ein Skript, das mir möglichst viel Arbeit abnimmt. Da ich in letzter Zeit über verschiedenen Leuten von Ansible gehört habe, hatte ich einen Anlass mir ein erstes Playbook zu schreiben, um Icinga ziemlich einfach auf einem Debian oder Ubuntu-Host zu deployen.

Auf meiner Suche nach einer Vorlage habe ich im Monitoring Portal einen englischsprachigen Beitrag gefunden, aus dem ich mir das Setup der PKI entnommen habe: Using ansible to generate the Icinga Client Certificates. Diesen Teil finde ich am schwersten und ich spare dort im folgenden auch eine genauere Beschreibung aus.

Der erste Schritt im Playbook ist das erzeugen des Tickets auf dem Master, der später für das Setup der PKI benötigt wird:

- hosts: MASTERFQDN
  tasks:
   - name: generate ticket on the icinga master and save it as a variable
     shell: /usr/sbin/icinga2 pki ticket --cn {{ hostitem }}
     register: ticket

Interessant für den Ansible Einsteiger: register: ticket legt aus dem Ergebnis des icinga2 Aufrufs auf dem Icinga Master eine Variable an, die später im Playbook auf dem Icinga2 Satellit zum Einsatz kommt.

Als nächstes wird der PGP Key für das Icinga Repository abgelegt, damit im folgenden Schritt das icinga2 Paket direkt aus dem Icinga Repository verwendet werden kann. So erhält man eine aktuelle Icinga Version. 🙂

  - name: "Deploy icinga.key"
      apt_key:
        url: "https://packages.icinga.com/icinga.key"
        state: present
    - name: "Install Icinga Ubuntu repository"
      apt_repository:
        repo: deb http://packages.icinga.com/{{ hostvars[hostitem]['icinga_distri'] }} icinga-{{ hostvars[hostitem]['icinga_release'] }} main
        state: present
        filename: 'icinga'
    - name: Update repositories cache and install "icinga2" package
      apt:
        name: icinga2
        update_cache: yes

Das PKI Setup sorgt nun dafür, das der Satellit mit dem Master verschlüsselt reden kann und Master und Satellit wissen, dass sie sich vertrauen können:

    - name: create pki folder
      file: path=/etc/icinga2/pki state=directory mode=0700 owner=nagios group=nagios
    - name: create cert
      shell: icinga2 pki new-cert --cn {{ hostitem }} --key /etc/icinga2/pki/{{ hostitem }}.key --cert /etc/icinga2/pki/{{ hostitem }}.crt
    - name: save the masters cert as trustedcert
      shell: icinga2 pki save-cert --key /etc/icinga2/pki/{{ hostitem }}.key --cert /etc/icinga2/pki/{{ hostitem }}.crt --trustedcert /etc/icinga2/pki/trusted-master.crt --host {{ master_hostname }}
    - name: request the certificate from the icinga server
      shell: icinga2 pki request --host {{ master_hostname }} --port 5665 --ticket {{ hostvars[master_hostname]['ticket']['stdout'] }} --key /etc/icinga2/pki/{{ hostitem }}.key --cert /etc/icinga2/pki/{{ hostitem }}.crt --trustedcert /etc/icinga2/pki/trusted-master.crt --ca /etc/icinga2/pki/ca.key
    - name: node setup
      shell: icinga2 node setup --ticket {{ hostvars[master_hostname]['ticket']['stdout'] }} --endpoint {{ master_hostname }} --zone {{ hostitem }} --master_host {{ master_hostname }} --trustedcert /etc/icinga2/pki/trusted-master.crt --cn {{ hostitem }}

Nun wird in der Icinga2 Konfiguration erstmal die vorhandene Konfiguration ausgeblendet, da ich Top-Down Konfiguration durch den Master verwende:

    - name: Disable icinga2.conf conf.d
      replace:
        destfile: /etc/icinga2/icinga2.conf
        regexp: '^include_recursive "conf.d"$'
        replace: '//include_recursive "conf.d"'

Die zones.conf setze ich aus einem Template:

    - name: Setup zones.conf
      template:
        src: templates/zones.conf.j2
        dest: /etc/icinga2/zones.conf

Das passende Template für die Top-Down Konfiguration, die ich verwendet, sieht dabei so aus:

object Zone "global-templates" {
        global = true
}
object Endpoint "{{ master_hostname }}" {
        host = "{{ master_hostname }}"
}
object Zone "{{ master_hostname }}" {
        endpoints = [ "{{ master_hostname }}" ]
}
object Endpoint "{{ hostitem }}" {
        host = "{{ hostitem }}"
}
object Zone "{{ hostitem }}" {
        endpoints = [ "{{ hostitem }}" ]
        parent = "{{ master_hostname }}"
}

Der Satellit muss die Konfiguration und Kommandos akzeptieren:

    - name: Accept configuration from master
      replace:
        destfile: /etc/icinga2/features-enabled/api.conf
        regexp: '^(\s+)accept_config = false$'
        replace: '\1accept_config = true'
    - name: Accept commands from master
      replace:
        destfile: /etc/icinga2/features-enabled/api.conf
        regexp: '^(\s*)accept_commands = false$'
        replace: '\1accept_commands = true'

Ist die Konfiguration geschafft kann Icinga2 diese neu einlesen:

    - name: Reload Icinga configuration
      shell: /etc/init.d/icinga2 reload

Weiter geht es dann auf dem Master, hier muss der Satellit ebenfalls der Zonen Konfiguration hinzugefügt werden. In der zones.conf des Masters wird ein von Ansible markierter Block eingefügt, damit spätere Änderungen an diesem Host mit einem erneuten Playbook Durchlauf eingespielt werden können.

- hosts: MASTERFQDN
  tasks:
    - name: Append zones.conf
      blockinfile:
        destfile: /etc/icinga2/zones.conf
        marker: "// {mark} ANSIBLE MANAGED BLOCK {{ hostitem }}"
        block: |
         object Endpoint "{{ hostitem }}" {
           host = "{{ hostitem }}"
         }
         object Zone "{{ hostitem }}" {
           endpoints = [ "{{ hostitem }}" ]
           parent = "MASTERFQDN"
         }
    - name: Create zones.d {{ hostitem }} directory
      file:
        path: /etc/icinga2/zones.d/{{ hostitem }}
        state: directory
        mode: 0755
        owner: nagios
        group: nagios

Die initiale Host und Service Konfiguration erfolgt per Template. Das force: no verhindert ein Überschreiben von vorgenommenen Änderungen. Bei Top-Down Konfiguration wird natürlich irgendwann mal was an den Files entsprechend den Anforderungen angepasst. Diese Änderungen möchte ich nicht überschreiben, ein force: no verhindert dies.

    - name: Create host.conf
      template:
        src: templates/icinga-host.conf.j2
        dest: /etc/icinga2/zones.d/{{ hostitem }}/host.conf
        force: no
    - name: Create services.conf
      template:
        src: templates/icinga-services.conf.j2
        dest: /etc/icinga2/zones.d/{{ hostitem }}/services.conf
        force: no

Das templates/icinga-host.conf.j2 sieht so aus:

object Host "{{ hostitem }}" {
        import "generic-host"
        check_command = "hostalive"
        address = "{{ hostvars[hostitem]['icinga_addr'] }}"
        address6 = "{{ hostvars[hostitem]['icinga_addr6'] }}"
        vars.os = "Linux"
        zone = "MASTERFQDN"
}

Es unterstützt nur Dual-Stack Hosts. IPv4- oder IPv6-only werde ich versuchen später einzubauen. Die Services aus templates/icinga-services.conf.j2 entsprechen meinem Default-Setup:

// This file has been created by Ansible
object Service "disk" {
        import "generic-service
        check_command = "disk"
        host_name = "{{ hostitem }}"
}
object Service "load" {
        import "generic-service"
        check_command = "load"
        host_name = "{{ hostitem }}"
}
object Service "procs" {
        import "generic-service"
        check_command = "procs"
        host_name = "{{ hostitem }}"
}
object Service "swap" {
        import "generic-service"
        check_command = "swap"
        host_name = "{{ hostitem }}"
}
object Service "users" {
        import "generic-service"
        check_command = "users"
        host_name = "{{ hostitem }}"
}
object Service "apt" {
        import "generic-service"
        check_command = "apt"
        host_name = "{{ hostitem }}"
}
object Service "icinga" {
        import "generic-service"
        check_command = "icinga"
        host_name = "{{ hostitem }}"
}

Die Services können später generischer vorkonfiguriert werden, wenn ich meine Ansible Hosts mit den entsprechenden Rollen versehen habe. Danach kann der Master die Konfiguration neu laden.

    - name: Reload Icinga master configuration
      shell: /etc/init.d/icinga2 reload

Für jeden Host legt man noch einen Eintrag im Ansible hosts File an, der die im Playbook verwedeten Parameter setzt:

[icinga2test]
HOSTNAME ansible_user=root icinga_distri=ubuntu icinga_release=trusty icinga_addr=IPv4 icinga_addr6=IPv6

icinga_distri kann auf debian oder ubuntu gsetzt werden, das icinga_release ist jeweils der Name der Distribution in den Paketquellen, also jessie, trusty, xenial usw. Es ist alles möglich, was im Icinga Repository gepflegt wird. Es sollte natürlich zur auf der VM installierten Distribution passen.

Ist soweit alles eingerichtet, kann das Playbook wie folgt verwendet werden:

ansible-playbook deploy-icinga-agent.yml -e hostitem=HOSTNAME

Die erneute Ausführung ist wie erwähnt möglich, auf dem Master geänderte Host oder Service-Konfiguration wird nicht überschrieben.

Im folgenden kann man alle genannten Files anschauen und bei Bedarf herunterladen. Bei der Verwendung muss noch darauf geachtet werden jeweils MASTERFQDN durch den Icinga2 Master zu ersetzen.