Ansible ile Monitoring Stack Kurulumu: Prometheus, Grafana ve Alertmanager

Bir production ortamında monitoring stack’in nasıl kurulduğunu manuel olarak yaşadıysanız, ne kadar sinir bozucu olduğunu bilirsiniz. Prometheus kuruyorsunuz, Grafana ekleyeceksiniz, Alertmanager’ı unutmamak lazım, Node Exporter her sunucuya tek tek… Ve bütün bunları bir gün sonra staging ortamı için tekrar yapıyorsunuz. İşte tam bu noktada Ansible devreye giriyor. Bu yazıda sıfırdan tam bir monitoring stack’i Ansible ile nasıl otomatize edeceğimizi adım adım göreceğiz.

Mimariye Genel Bakış

Kuracağımız stack şunlardan oluşacak:

  • Prometheus: Metrik toplama ve storage
  • Grafana: Görselleştirme
  • Alertmanager: Alert yönetimi ve bildirimler
  • Node Exporter: Sistem metrikleri (her hedef sunucuda)
  • Blackbox Exporter: HTTP/TCP endpoint kontrolü

Ansible tarafında ise role bazlı bir yapı kuracağız. Her bileşen kendi role’ünde yaşayacak, inventory grupları aracılığıyla hangi sunucuya ne kurulacağını belirleyeceğiz.

Senaryo şu: 3 uygulama sunucumuz var (app01, app02, app03), 1 adet monitoring sunucumuz var (mon01). Node Exporter her app sunucusuna gidecek, monitoring stack’in tamamı mon01’e kurulacak.

Proje Yapısı

Önce dizin yapısını oluşturalım:

mkdir -p monitoring-ansible/{roles,group_vars,host_vars}
cd monitoring-ansible

# Role dizinlerini oluştur
for role in prometheus grafana alertmanager node_exporter blackbox_exporter; do
  mkdir -p roles/$role/{tasks,handlers,templates,defaults,files}
done

touch site.yml inventory.ini

Bu yapıyla çalışmak hem okunabilirliği artırıyor hem de ileride yeni bileşenler eklemeyi kolaylaştırıyor.

Inventory Dosyası

# inventory.ini
[monitoring]
mon01 ansible_host=192.168.1.10

[app_servers]
app01 ansible_host=192.168.1.21
app02 ansible_host=192.168.1.22
app03 ansible_host=192.168.1.23

[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_private_key_file=~/.ssh/id_rsa

[monitoring:vars]
prometheus_port=9090
grafana_port=3000
alertmanager_port=9093

[app_servers:vars]
node_exporter_port=9100

Group Variables

Her grup için değişkenleri ayrı dosyalarda tutmak büyük projelerde hayat kurtarır:

# group_vars/all.yml
---
monitoring_server: "192.168.1.10"
prometheus_version: "2.48.0"
grafana_version: "10.2.2"
alertmanager_version: "0.26.0"
node_exporter_version: "1.7.0"
blackbox_exporter_version: "0.24.0"

# Kullanıcı ve group ayarları
prometheus_user: prometheus
prometheus_group: prometheus
grafana_user: grafana

# Dizinler
prometheus_data_dir: /var/lib/prometheus
prometheus_config_dir: /etc/prometheus
grafana_data_dir: /var/lib/grafana

# Slack webhook (alerting için)
slack_webhook_url: "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ"
alert_channel: "#ops-alerts"

Ana Playbook

# site.yml
---
- name: Node Exporter kurulumu - tüm app sunucuları
  hosts: app_servers
  become: true
  roles:
    - node_exporter

- name: Monitoring stack kurulumu
  hosts: monitoring
  become: true
  roles:
    - prometheus
    - alertmanager
    - blackbox_exporter
    - grafana

- name: Firewall kurallarını güncelle
  hosts: all
  become: true
  tasks:
    - name: UFW - monitoring sunucusundan node exporter erişimine izin ver
      ufw:
        rule: allow
        src: "{{ monitoring_server }}"
        port: "{{ node_exporter_port | default('9100') }}"
        proto: tcp
      when: "'app_servers' in group_names"

Node Exporter Role’ü

En basit ama en kritik parça. Her uygulama sunucusuna kurulacak:

# roles/node_exporter/defaults/main.yml
---
node_exporter_version: "1.7.0"
node_exporter_port: 9100
node_exporter_user: node_exporter
node_exporter_install_dir: /usr/local/bin
node_exporter_textfile_dir: /var/lib/node_exporter/textfile_collector

# Hangi collector'ların aktif olacağı
node_exporter_enabled_collectors:
  - systemd
  - processes
  - interrupts
  - tcpstat

node_exporter_disabled_collectors:
  - mdadm
  - infiniband
  - ipvs
# roles/node_exporter/tasks/main.yml
---
- name: node_exporter grubu oluştur
  group:
    name: "{{ node_exporter_user }}"
    system: true
    state: present

- name: node_exporter kullanıcısı oluştur
  user:
    name: "{{ node_exporter_user }}"
    group: "{{ node_exporter_user }}"
    shell: /sbin/nologin
    system: true
    createhome: false

- name: textfile collector dizini oluştur
  file:
    path: "{{ node_exporter_textfile_dir }}"
    state: directory
    owner: "{{ node_exporter_user }}"
    group: "{{ node_exporter_user }}"
    mode: "0755"

- name: Node Exporter binary indir
  get_url:
    url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz"
    dest: /tmp/node_exporter.tar.gz
    checksum: "sha256:https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/sha256sums.txt"
  register: node_exporter_download

- name: Binary'yi çıkart
  unarchive:
    src: /tmp/node_exporter.tar.gz
    dest: /tmp
    remote_src: true
  when: node_exporter_download.changed

- name: Binary'yi kopyala
  copy:
    src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
    dest: "{{ node_exporter_install_dir }}/node_exporter"
    owner: root
    group: root
    mode: "0755"
    remote_src: true
  notify: node_exporter restart

- name: Systemd service dosyası oluştur
  template:
    src: node_exporter.service.j2
    dest: /etc/systemd/system/node_exporter.service
    owner: root
    group: root
    mode: "0644"
  notify:
    - systemd reload
    - node_exporter restart

- name: Node Exporter'ı başlat ve enable et
  systemd:
    name: node_exporter
    state: started
    enabled: true
    daemon_reload: true

Servis template’ini de ekleyelim:

# roles/node_exporter/templates/node_exporter.service.j2
[Unit]
Description=Node Exporter
Documentation=https://prometheus.io/docs/guides/node-exporter/
Wants=network-online.target
After=network-online.target

[Service]
User={{ node_exporter_user }}
Group={{ node_exporter_user }}
Type=simple
Restart=on-failure
ExecStart={{ node_exporter_install_dir }}/node_exporter 
  --web.listen-address=:{{ node_exporter_port }} 
  --collector.textfile.directory={{ node_exporter_textfile_dir }} 
{% for collector in node_exporter_enabled_collectors %}
  --collector.{{ collector }} 
{% endfor %}
{% for collector in node_exporter_disabled_collectors %}
  --no-collector.{{ collector }} 
{% endfor %}

[Install]
WantedBy=multi-user.target

Prometheus Role’ü

Prometheus kurulumu biraz daha karmaşık çünkü konfigürasyon dosyasını dinamik olarak oluşturmamız gerekiyor. Inventory’deki sunucuların IP’lerini otomatik olarak prometheus.yml’ye ekleyeceğiz:

# roles/prometheus/tasks/main.yml
---
- name: Prometheus grubu oluştur
  group:
    name: "{{ prometheus_group }}"
    system: true

- name: Prometheus kullanıcısı oluştur
  user:
    name: "{{ prometheus_user }}"
    group: "{{ prometheus_group }}"
    shell: /sbin/nologin
    system: true
    createhome: false

- name: Gerekli dizinleri oluştur
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_group }}"
    mode: "0755"
  loop:
    - "{{ prometheus_config_dir }}"
    - "{{ prometheus_config_dir }}/rules"
    - "{{ prometheus_data_dir }}"

- name: Prometheus binary indir
  get_url:
    url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz"
    dest: /tmp/prometheus.tar.gz

- name: Archive'ı çıkart
  unarchive:
    src: /tmp/prometheus.tar.gz
    dest: /tmp
    remote_src: true

- name: Prometheus ve promtool binary kopyala
  copy:
    src: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64/{{ item }}"
    dest: "/usr/local/bin/{{ item }}"
    owner: root
    group: root
    mode: "0755"
    remote_src: true
  loop:
    - prometheus
    - promtool
  notify: prometheus restart

- name: Console dosyalarını kopyala
  copy:
    src: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64/{{ item }}"
    dest: "{{ prometheus_config_dir }}/{{ item }}"
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_group }}"
    remote_src: true
  loop:
    - consoles
    - console_libraries

- name: Prometheus konfigürasyonu oluştur
  template:
    src: prometheus.yml.j2
    dest: "{{ prometheus_config_dir }}/prometheus.yml"
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_group }}"
    mode: "0644"
    validate: /usr/local/bin/promtool check config %s
  notify: prometheus restart

- name: Alert kurallarını kopyala
  template:
    src: alerts.yml.j2
    dest: "{{ prometheus_config_dir }}/rules/alerts.yml"
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_group }}"
    mode: "0644"
    validate: /usr/local/bin/promtool check rules %s
  notify: prometheus restart

- name: Systemd service oluştur
  template:
    src: prometheus.service.j2
    dest: /etc/systemd/system/prometheus.service
  notify:
    - systemd reload
    - prometheus restart

- name: Prometheus başlat
  systemd:
    name: prometheus
    state: started
    enabled: true
    daemon_reload: true

Prometheus konfigürasyon template’i en kritik kısım. Inventory’deki sunucuları otomatik olarak alıyoruz:

# roles/prometheus/templates/prometheus.yml.j2
global:
  scrape_interval: 15s
  evaluation_interval: 15s
  external_labels:
    environment: "{{ ansible_env.ENVIRONMENT | default('production') }}"
    region: "tr-ist-1"

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - localhost:9093

rule_files:
  - "rules/*.yml"

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node_exporter'
    static_configs:
      - targets:
{% for host in groups['app_servers'] %}
          - '{{ hostvars[host]["ansible_host"] }}:{{ node_exporter_port | default(9100) }}'
{% endfor %}
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+).*'
        replacement: '$1'

  - job_name: 'alertmanager'
    static_configs:
      - targets: ['localhost:9093']

  - job_name: 'blackbox'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://yourapp.com/health
          - https://yourapp.com/api/v1/status
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9115

Grafana Role’ü

Grafana için özel olan kısım, provisioning ile dashboard ve datasource’u otomatik yüklemek:

# roles/grafana/tasks/main.yml
---
- name: Grafana GPG key ekle
  apt_key:
    url: https://apt.grafana.com/gpg.key
    state: present

- name: Grafana repository ekle
  apt_repository:
    repo: "deb https://apt.grafana.com stable main"
    state: present
    filename: grafana

- name: Grafana kur
  apt:
    name: "grafana={{ grafana_version }}"
    state: present
    update_cache: true

- name: Grafana konfigürasyonu
  template:
    src: grafana.ini.j2
    dest: /etc/grafana/grafana.ini
    owner: root
    group: grafana
    mode: "0640"
  notify: grafana restart

- name: Provisioning dizinleri
  file:
    path: "{{ item }}"
    state: directory
    owner: grafana
    group: grafana
    mode: "0755"
  loop:
    - /etc/grafana/provisioning/datasources
    - /etc/grafana/provisioning/dashboards
    - /var/lib/grafana/dashboards

- name: Prometheus datasource provisioning
  template:
    src: datasource.yml.j2
    dest: /etc/grafana/provisioning/datasources/prometheus.yml
    owner: grafana
    group: grafana
    mode: "0640"
  notify: grafana restart

- name: Dashboard provisioning config
  template:
    src: dashboard_provider.yml.j2
    dest: /etc/grafana/provisioning/dashboards/default.yml
    owner: grafana
    group: grafana
  notify: grafana restart

- name: Node Exporter dashboard kopyala
  copy:
    src: node_exporter_dashboard.json
    dest: /var/lib/grafana/dashboards/node_exporter.json
    owner: grafana
    group: grafana

- name: Grafana başlat
  systemd:
    name: grafana-server
    state: started
    enabled: true

Alertmanager Konfigürasyonu

Alertmanager için Slack entegrasyonu ile gerçek dünya konfigürasyonu:

# roles/alertmanager/templates/alertmanager.yml.j2
global:
  resolve_timeout: 5m
  slack_api_url: "{{ slack_webhook_url }}"

route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'slack-notifications'
  routes:
    - match:
        severity: critical
      receiver: 'slack-critical'
      repeat_interval: 1h
    - match:
        severity: warning
      receiver: 'slack-notifications'

receivers:
  - name: 'slack-notifications'
    slack_configs:
      - channel: "{{ alert_channel }}"
        title: '{{ "{{" }} template "slack.title" . {{ "}}" }}'
        text: '{{ "{{" }} template "slack.text" . {{ "}}" }}'
        send_resolved: true

  - name: 'slack-critical'
    slack_configs:
      - channel: "#ops-critical"
        title: ':fire: CRITICAL: {{ "{{" }} .GroupLabels.alertname {{ "}}" }}'
        text: '{{ "{{" }} range .Alerts {{ "}}" }}*{{ "{{" }} .Annotations.summary {{ "}}" }}*n{{ "{{" }} .Annotations.description {{ "}}" }}n{{ "{{" }} end {{ "}}" }}'
        send_resolved: true

inhibit_rules:
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'instance']

Playbook’u Çalıştırmak

Tag kullanımıyla kademeli deploy yapabilirsiniz:

# Önce dry-run
ansible-playbook -i inventory.ini site.yml --check --diff

# Sadece node_exporter'ı deploy et
ansible-playbook -i inventory.ini site.yml --tags node_exporter

# Sadece belirli bir host için
ansible-playbook -i inventory.ini site.yml --limit app01

# Prometheus config'i güncelle
ansible-playbook -i inventory.ini site.yml --tags prometheus_config

# Tüm stack'i deploy et
ansible-playbook -i inventory.ini site.yml -v

# Belirli bir role'ü skip et
ansible-playbook -i inventory.ini site.yml --skip-tags grafana

Ansible Vault ile hassas verileri korumayı da unutmayın:

# Slack webhook'u şifrele
ansible-vault encrypt_string 'https://hooks.slack.com/services/XXX' 
  --name 'slack_webhook_url' >> group_vars/all.yml

# Vault şifresiyle çalıştır
ansible-playbook -i inventory.ini site.yml --ask-vault-pass

# Vault password dosyasıyla
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_pass

Idempotency Kontrolü ve Test

Deploy sonrası her şeyin çalıştığını test eden basit bir verification playbook yazalım:

# verify.yml
---
- name: Monitoring stack doğrulama
  hosts: monitoring
  tasks:
    - name: Prometheus API kontrolü
      uri:
        url: "http://localhost:{{ prometheus_port }}/api/v1/targets"
        method: GET
        status_code: 200
      register: prometheus_targets

    - name: Aktif target sayısını göster
      debug:
        msg: "Aktif target sayısı: {{ prometheus_targets.json.data.activeTargets | length }}"

    - name: Grafana sağlık kontrolü
      uri:
        url: "http://localhost:{{ grafana_port }}/api/health"
        method: GET
        status_code: 200

    - name: Alertmanager sağlık kontrolü
      uri:
        url: "http://localhost:{{ alertmanager_port }}/-/healthy"
        method: GET
        status_code: 200

- name: Node Exporter doğrulama
  hosts: app_servers
  tasks:
    - name: Node Exporter metrics endpoint kontrolü
      uri:
        url: "http://localhost:{{ node_exporter_port }}/metrics"
        method: GET
        status_code: 200
      register: metrics_check

    - name: Sonuç
      debug:
        msg: "Node Exporter {{ inventory_hostname }} üzerinde sağlıklı çalışıyor"
      when: metrics_check.status == 200

Gerçek Dünya Notları

Birkaç yıldır production’da bu tür stack’leri Ansible ile kuruyorum ve öğrendiğim bazı şeyler var.

Binary checksum doğrulaması: get_url modülünde checksum parametresini her zaman kullanın. Download bozuk gelirse ya da MITM saldırısı olursa fark edersiniz.

promtool ile config validation: Template oluşturulduktan sonra validate parametresiyle promtool’u çalıştırıyoruz. Hatalı bir config ile Prometheus’u restart etmek istemezsiniz, özellikle gece 2’de.

Prometheus data dizini: Data dizinini asla task ile silmeyin. --check modunda bile buna dikkat edin. Metriklerinizi kaybetmek istemezsiniz.

Grafana version pinning: APT ile kurarken versiyonu mutlaka sabitleyin. grafana yazmak yerine grafana=10.2.2 kullanın. Grafana bazen breaking change ile güncelleme yapıyor.

Handler sırası: meta: flush_handlers kullanmayı unutmayın, özellikle bir task handler’ı tetikledi ve sonraki task o servise bağlanıyorsa.

Rolling update: App sunucularına Node Exporter deploy ederken serial: 1 veya serial: "30%" kullanarak teker teker güncelleyin. Hepsi aynı anda restart olursa monitoring kör kalır kısa süreliğine.

Inventory büyüdükçe ve farklı ortamlar (staging, production) ekledikçe bu yapı daha da değerini ortaya koyuyor. Staging’de test ettiğiniz aynı playbook production’a gidiyor, sürpriz yok.

Sonuç

Ansible ile monitoring stack kurmak başta biraz fazla setup gerektiriyor gibi görünüyor, ama ilk seferden sonra ne kadar zaman kazandırdığını görünce “neden daha önce yapmadım” diyorsunuz. Yeni bir sunucu eklediğinizde inventory’ye bir satır, ansible-playbook komutu ve bitti. Prometheus otomatik olarak yeni target’ı görüyor, Grafana dashboard’ları güncellenmiş oluyor.

Bu yazıdaki yapıyı kendi ihtiyaçlarınıza göre genişletebilirsiniz. Loki + Promtail ekleyerek log aggregation, Tempo ile distributed tracing, ya da Thanos ile uzun süreli metrik storage eklemek tamamen aynı role bazlı yapıyla yapılabilir. Temel prensip aynı kalıyor: her bileşen kendi role’ünde, değişkenler group_vars’ta, hassas bilgiler Vault’ta.

Projeyi GitHub’a alın, CI/CD pipeline’ınıza bağlayın ve monitoring stack’iniz artık kod olsun. Bir şey bozulduğunda “sunucuya ne yapmıştım” diye hatırlamaya çalışmak yerine git history’e bakarsınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir