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.
