Monitoring yang enak dipakai itu dimulai dari metrik yang konsisten di semua server. Kalau node_exporter dipasang manual satu per satu di puluhan host, ujung-ujungnya versinya beda-beda, flag-nya beda, atau ada yang lupa di-enable sehingga mati setelah reboot. Di tulisan ini saya mau berbagi role Ansible sederhana untuk memasang exporter Prometheus supaya hasilnya selalu sama.

Contohnya pakai node_exporter, tapi polanya bisa dipakai juga untuk exporter lain yang bentuknya satu binary, misalnya exporter MongoDB, MySQL, atau HAProxy.

Prinsip yang saya pegang

  • Versi dikunci di variabel, bukan “latest”.
  • Checksum dicek dulu sebelum binary dipasang.
  • Jalan pakai user khusus tanpa shell, bukan root.
  • Dikelola systemd, jadi otomatis hidup lagi setelah reboot.
  • Nggak terbuka ke publik. Port exporter cuma bisa diakses server Prometheus.

Struktur role

roles/node_exporter/
├── defaults/main.yml
├── handlers/main.yml
├── tasks/main.yml
└── templates/node_exporter.service.j2

Variabel

# roles/node_exporter/defaults/main.yml
node_exporter_version: "1.9.1"
node_exporter_arch: amd64
node_exporter_user: node_exporter
node_exporter_bin: /usr/local/bin/node_exporter
node_exporter_listen_address: "{{ ansible_default_ipv4.address }}:9100"
node_exporter_extra_flags:
  - --collector.systemd

Kalau mau upgrade, cek versi terbaru di halaman rilis resmi lalu ganti node_exporter_version secara manual. Dengan begitu upgrade selalu disengaja, bukan kejutan.

Task

# roles/node_exporter/tasks/main.yml
- name: Buat user sistem untuk node_exporter
  ansible.builtin.user:
    name: "{{ node_exporter_user }}"
    system: true
    shell: /usr/sbin/nologin
    create_home: false

- name: Unduh arsip node_exporter dengan verifikasi checksum
  ansible.builtin.get_url:
    url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-{{ node_exporter_arch }}.tar.gz"
    dest: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
    checksum: "sha256:https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/sha256sums.txt"
    mode: "0644"

- name: Ekstrak arsip
  ansible.builtin.unarchive:
    src: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
    dest: /tmp
    remote_src: true
    creates: "/tmp/node_exporter-{{ node_exporter_version }}.linux-{{ node_exporter_arch }}/node_exporter"

- name: Pasang binary node_exporter
  ansible.builtin.copy:
    src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-{{ node_exporter_arch }}/node_exporter"
    dest: "{{ node_exporter_bin }}"
    remote_src: true
    owner: root
    group: root
    mode: "0755"
  notify: Restart node_exporter

- name: Pasang unit systemd
  ansible.builtin.template:
    src: node_exporter.service.j2
    dest: /etc/systemd/system/node_exporter.service
    owner: root
    group: root
    mode: "0644"
  notify: Restart node_exporter

- name: Aktifkan dan jalankan node_exporter
  ansible.builtin.systemd_service:
    name: node_exporter
    state: started
    enabled: true
    daemon_reload: true

- name: Terapkan restart bila ada perubahan
  ansible.builtin.meta: flush_handlers

- name: Pastikan endpoint metrics merespons
  ansible.builtin.uri:
    url: "http://{{ node_exporter_listen_address }}/metrics"
    status_code: 200
  register: node_exporter_metrics
  until: node_exporter_metrics.status == 200
  retries: 5
  delay: 3

Unit systemd

# roles/node_exporter/templates/node_exporter.service.j2
[Unit]
Description=Prometheus Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
User={{ node_exporter_user }}
Group={{ node_exporter_user }}
Type=simple
ExecStart={{ node_exporter_bin }} \
  --web.listen-address={{ node_exporter_listen_address }}{% for flag in node_exporter_extra_flags %} \
  {{ flag }}{% endfor %}

Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Opsi hardening seperti ProtectSystem dan NoNewPrivileges gampang ditambahkan, dan cukup membantu membatasi dampaknya kalau suatu saat ada celah di exporter.

Handler

# roles/node_exporter/handlers/main.yml
- name: Restart node_exporter
  ansible.builtin.systemd_service:
    name: node_exporter
    state: restarted
    daemon_reload: true

Playbook

# monitoring.yml
- name: Pasang node_exporter
  hosts: all
  become: true
  roles:
    - node_exporter
ansible-playbook -i inventory/hosts.yml monitoring.yml

Target scrape dari inventory

Supaya daftar target Prometheus nggak perlu ditulis manual, saya generate dari inventory yang sama. Contoh template untuk file_sd:

# roles/prometheus_targets/templates/node.yml.j2
- targets:
{% for host in groups['all'] %}
    - "{{ hostvars[host]['ansible_default_ipv4']['address'] }}:9100"
{% endfor %}
  labels:
    job: node

Lalu di prometheus.yml:

scrape_configs:
  - job_name: node
    file_sd_configs:
      - files:
          - /etc/prometheus/targets/node.yml

Prometheus otomatis membaca ulang file file_sd, jadi kalau ada host baru, cukup tambahkan ke inventory lalu jalankan playbook. Satu catatan: template ini butuh fakta dari semua host, jadi pastikan fact gathering sudah jalan untuk semua host di play yang sama.

Keamanan

  • Buka port 9100 di firewall hanya untuk alamat server Prometheus.
  • Kalau jaringannya nggak sepenuhnya bisa dipercaya, aktifkan TLS dan basic auth lewat --web.config.file, yang sudah didukung exporter resmi Prometheus.
  • Matikan collector yang nggak dipakai. Makin sedikit metrik, makin ringan bebannya dan makin sedikit informasi yang terekspos.

Alternatif: collection resmi

Kalau nggak mau repot memelihara role sendiri, collection prometheus.prometheus di Ansible Galaxy sudah punya role untuk node_exporter dan beberapa exporter lain. Tapi bikin role sendiri tetap ada gunanya: kita jadi paham apa yang terjadi di balik layar, dan bisa dipakai untuk exporter yang belum ada di collection itu.

Exporter ini cocok dipasangkan dengan MongoDB replica set yang dipasang pakai Ansible. Metrik host dari node_exporter, metrik database dari exporter MongoDB, dan semuanya dipasang dari satu repository otomasi.