Ansible ile Log Yönetimi Otomasyonu

Prodüksiyon ortamlarında log yönetimi, çoğu zaman “nasıl olsa çalışıyor” diye ertelenen ama bir gün patlak veren sorunların başında gelir. Diskler dolup servisler çöker, eski loglar silinmez birikir, farklı sunuculardaki log konfigürasyonları birbirinden kopar. Ansible ile bu kaosa düzen getirmek hem mümkün hem de düşündüğünden çok daha kolay. Bu yazıda gerçek dünya senaryoları üzerinden Ansible ile kapsamlı bir log yönetimi otomasyonu kuracağız.

Neden Ansible ile Log Yönetimi?

Manuel log yönetiminin acısını yaşadıysan ne demek istediğimi anlarsın. 50 sunucuda logrotate konfigürasyonunu tek tek güncellemek, her sunucuda farklı log rotasyon sürelerinin oluşması, bazı makinelerde rsyslog’un farklı versiyonlarda olması… Bunlar gerçek sorunlar.

Ansible bu konuda şu avantajları getirir:

  • Tekrarlanabilirlik: Aynı konfigürasyonu 5 veya 500 sunucuya aynı şekilde uygularsın
  • Idempotency: Playbook’u defalarca çalıştırsan bile sonuç değişmez
  • Versiyon kontrolü: Log konfigürasyonların Git’te yaşar, kim ne değiştirdi görebilirsin
  • Hızlı rollback: Bir şeyler ters giderse önceki konfigürasyona dönmek birkaç saniye
  • Merkezi yönetim: Tüm sunuculardaki log yapısını tek yerden kontrol edersin

Proje Yapısını Kuralım

Önce mantıklı bir dizin yapısı oluşturalım. Ansible’da düzensiz playbook yığını yerine role tabanlı yapı kullanmak uzun vadede hayat kurtarır.

mkdir -p ansible-log-management/{roles,inventories,group_vars,host_vars}
cd ansible-log-management

# Role yapısını oluştur
ansible-galaxy init roles/log_management
ansible-galaxy init roles/logrotate
ansible-galaxy init roles/rsyslog
ansible-galaxy init roles/log_shipper

Dizin yapımız şöyle görünmeli:

ansible-log-management/
├── site.yml
├── inventories/
│   ├── production/
│   │   ├── hosts
│   │   └── group_vars/
│   └── staging/
│       ├── hosts
│       └── group_vars/
├── group_vars/
│   └── all.yml
└── roles/
    ├── log_management/
    ├── logrotate/
    ├── rsyslog/
    └── log_shipper/

Inventory ve Değişken Yapısı

Gerçek bir ortamda sunucularını mantıklı gruplara ayırmak kritik. Web sunucuları, veritabanı sunucuları ve uygulama sunucularının log ihtiyaçları birbirinden farklı.

# inventories/production/hosts
[webservers]
web01.example.com
web02.example.com
web03.example.com

[dbservers]
db01.example.com
db02.example.com

[appservers]
app01.example.com
app02.example.com

[all:vars]
ansible_user=ansible
ansible_become=yes
ansible_become_method=sudo

Global değişkenleri tanımlayalım:

# group_vars/all.yml
---
# Log rotasyon ayarları - tüm sunucular için defaults
log_rotate_frequency: daily
log_rotate_keep: 14
log_rotate_compress: true
log_rotate_missingok: true
log_rotate_notifempty: true

# Log dizinleri
log_base_dir: /var/log
custom_log_dir: /var/log/apps

# Rsyslog ayarları
rsyslog_remote_host: "logserver.example.com"
rsyslog_remote_port: 514
rsyslog_protocol: "tcp"

# Log seviyesi eşikleri
alert_log_size_mb: 500
critical_log_size_mb: 1000

# Logrotate binary yolu
logrotate_bin: /usr/sbin/logrotate

Web sunucuları için özel değişkenler:

# inventories/production/group_vars/webservers.yml
---
log_rotate_keep: 30
nginx_log_dir: /var/log/nginx
apache_log_dir: /var/log/apache2

nginx_logs:
  - path: "{{ nginx_log_dir }}/access.log"
    frequency: daily
    keep: 30
    postrotate: "nginx -s reopen"
  - path: "{{ nginx_log_dir }}/error.log"
    frequency: daily
    keep: 60
    postrotate: "nginx -s reopen"

Logrotate Role’ünü Yazalım

Logrotate yönetimi için template tabanlı bir yaklaşım kullanalım. Her servis için ayrı konfigürasyon dosyası oluşturacağız.

# roles/logrotate/tasks/main.yml
---
- name: Logrotate paketinin kurulu olduğunu doğrula
  ansible.builtin.package:
    name: logrotate
    state: present

- name: Global logrotate konfigürasyonunu ayarla
  ansible.builtin.template:
    src: logrotate.conf.j2
    dest: /etc/logrotate.conf
    owner: root
    group: root
    mode: '0644'
  notify: test logrotate config

- name: Uygulama log rotasyon konfigürasyonlarını oluştur
  ansible.builtin.template:
    src: app_logrotate.j2
    dest: "/etc/logrotate.d/{{ item.name }}"
    owner: root
    group: root
    mode: '0644'
  loop: "{{ logrotate_configs }}"
  when: logrotate_configs is defined
  notify: test logrotate config

- name: Custom log dizininin varlığını garantile
  ansible.builtin.file:
    path: "{{ custom_log_dir }}"
    state: directory
    owner: root
    group: adm
    mode: '0755'

- name: Logrotate cron job'ının aktif olduğunu kontrol et
  ansible.builtin.stat:
    path: /etc/cron.daily/logrotate
  register: logrotate_cron

- name: Logrotate cron job yoksa uyar
  ansible.builtin.debug:
    msg: "UYARI: Logrotate daily cron job bulunamadı!"
  when: not logrotate_cron.stat.exists

Logrotate template’i:

# roles/logrotate/templates/logrotate.conf.j2
# Ansible tarafından yönetilmektedir - manuel değişiklik yapmayın
# Son güncelleme: {{ ansible_date_time.iso8601 }}

weekly
rotate {{ log_rotate_keep }}
{% if log_rotate_compress %}
compress
delaycompress
{% endif %}
{% if log_rotate_missingok %}
missingok
{% endif %}
{% if log_rotate_notifempty %}
notifempty
{% endif %}
create
dateext
dateformat -%Y%m%d

include /etc/logrotate.d

Uygulama bazlı logrotate template:

# roles/logrotate/templates/app_logrotate.j2
# {{ item.name }} için logrotate konfigürasyonu
# Ansible yönetimindedir

{{ item.log_path }} {
    {{ item.frequency | default(log_rotate_frequency) }}
    rotate {{ item.keep | default(log_rotate_keep) }}
    {% if item.compress | default(log_rotate_compress) %}
    compress
    delaycompress
    {% endif %}
    missingok
    notifempty
    {% if item.create_mode is defined %}
    create {{ item.create_mode }} {{ item.create_owner | default('root') }} {{ item.create_group | default('root') }}
    {% endif %}
    {% if item.postrotate is defined %}
    postrotate
        {{ item.postrotate }}
    endscript
    {% endif %}
    {% if item.sharedscripts | default(false) %}
    sharedscripts
    {% endif %}
}

Handler ekleyelim:

# roles/logrotate/handlers/main.yml
---
- name: test logrotate config
  ansible.builtin.command: "{{ logrotate_bin }} --debug /etc/logrotate.conf"
  register: logrotate_test
  changed_when: false
  failed_when: logrotate_test.rc != 0

Rsyslog Role’ü

Merkezi log toplama için rsyslog konfigürasyonu kritik bir parça. Tüm sunuculardan logları merkezi bir noktaya yönlendireceğiz.

# roles/rsyslog/tasks/main.yml
---
- name: Rsyslog kurulumu
  ansible.builtin.package:
    name: rsyslog
    state: present

- name: Rsyslog TLS için gerekli paketleri kur
  ansible.builtin.package:
    name:
      - rsyslog-gnutls
      - ca-certificates
    state: present
  when: rsyslog_use_tls | default(false)

- name: Rsyslog ana konfigürasyonunu yönet
  ansible.builtin.template:
    src: rsyslog.conf.j2
    dest: /etc/rsyslog.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
  notify:
    - validate rsyslog config
    - restart rsyslog

- name: Uygulama bazlı rsyslog konfigürasyonları
  ansible.builtin.template:
    src: rsyslog_app.conf.j2
    dest: "/etc/rsyslog.d/{{ item.priority | default('50') }}-{{ item.name }}.conf"
    owner: root
    group: root
    mode: '0644'
  loop: "{{ rsyslog_app_configs | default([]) }}"
  notify:
    - validate rsyslog config
    - restart rsyslog

- name: Rsyslog servisinin çalıştığından emin ol
  ansible.builtin.service:
    name: rsyslog
    state: started
    enabled: yes

- name: Rsyslog log dizin izinlerini düzelt
  ansible.builtin.file:
    path: "{{ log_base_dir }}"
    state: directory
    owner: root
    group: adm
    mode: '0755'

Rsyslog template:

# roles/rsyslog/templates/rsyslog.conf.j2
# Ansible yönetimindedir - {{ inventory_hostname }}
# Oluşturulma: {{ ansible_date_time.iso8601 }}

$ModLoad imuxsock
$ModLoad imklog

$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat

$FileOwner root
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022

$WorkDirectory /var/spool/rsyslog

# Uzak log sunucusuna yönlendirme
{% if rsyslog_remote_host is defined %}
{% if rsyslog_protocol == 'tcp' %}
*.* @@{{ rsyslog_remote_host }}:{{ rsyslog_remote_port }}
{% else %}
*.* @{{ rsyslog_remote_host }}:{{ rsyslog_remote_port }}
{% endif %}
{% endif %}

# Standart log hedefleri
auth,authpriv.*                 /var/log/auth.log
*.*;auth,authpriv.none          -/var/log/syslog
kern.*                          -/var/log/kern.log
mail.*                          -/var/log/mail.log
mail.err                        /var/log/mail.err

$IncludeConfig /etc/rsyslog.d/*.conf

Disk Doluluk İzleme ve Otomatik Temizlik

Prodüksiyonda en sık yaşanan sorunlardan biri disk dolması. Bunu proaktif olarak yönetelim.

# roles/log_management/tasks/disk_management.yml
---
- name: Log dizini disk kullanımını kontrol et
  ansible.builtin.shell: |
    df -m {{ log_base_dir }} | awk 'NR==2 {print $5}' | tr -d '%'
  register: log_disk_usage
  changed_when: false

- name: Disk kullanım durumunu kaydet
  ansible.builtin.set_fact:
    current_disk_usage: "{{ log_disk_usage.stdout | int }}"

- name: Yüksek disk kullanımı uyarısı
  ansible.builtin.debug:
    msg: |
      UYARI: {{ inventory_hostname }} üzerinde log dizini disk kullanımı %{{ current_disk_usage }}
      Eşik değeri: %{{ disk_usage_warning_threshold | default(75) }}
  when: current_disk_usage | int >= (disk_usage_warning_threshold | default(75)) | int

- name: Kritik disk kullanımında eski logları temizle
  ansible.builtin.find:
    paths: "{{ log_base_dir }}"
    patterns: "*.log.*.gz"
    age: "{{ emergency_cleanup_age | default('30d') }}"
    recurse: yes
  register: old_compressed_logs
  when: current_disk_usage | int >= (disk_usage_critical_threshold | default(90)) | int

- name: Eski sıkıştırılmış logları kaldır
  ansible.builtin.file:
    path: "{{ item.path }}"
    state: absent
  loop: "{{ old_compressed_logs.files | default([]) }}"
  when:
    - current_disk_usage | int >= (disk_usage_critical_threshold | default(90)) | int
    - old_compressed_logs.files is defined
  register: cleanup_result

- name: Temizlik sonucu raporu
  ansible.builtin.debug:
    msg: "{{ cleanup_result.results | length }} adet eski log dosyası silindi"
  when:
    - cleanup_result is defined
    - cleanup_result.results is defined

Log Arşivleme ve S3’e Yükleme

Uzun dönem saklama için logları S3 veya uyumlu bir obje deposuna atalım.

# roles/log_management/tasks/archive_logs.yml
---
- name: AWS CLI kurulumunu kontrol et
  ansible.builtin.command: which aws
  register: aws_cli_check
  changed_when: false
  failed_when: false

- name: Log arşiv dizinini oluştur
  ansible.builtin.file:
    path: "{{ log_archive_dir | default('/var/log/archive') }}"
    state: directory
    owner: root
    group: root
    mode: '0750'

- name: Eski logları arşiv dizinine taşı
  ansible.builtin.find:
    paths: "{{ log_base_dir }}"
    patterns: "*.log.*.gz"
    age: "{{ archive_after_days | default('7d') }}"
    recurse: yes
  register: logs_to_archive

- name: Arşiv script'ini oluştur
  ansible.builtin.template:
    src: archive_logs.sh.j2
    dest: /usr/local/bin/archive_logs.sh
    owner: root
    group: root
    mode: '0750'

- name: Haftalık arşiv cron job'ı kur
  ansible.builtin.cron:
    name: "Log arşivleme - {{ inventory_hostname }}"
    minute: "0"
    hour: "2"
    weekday: "0"
    job: "/usr/local/bin/archive_logs.sh >> /var/log/archive_logs.log 2>&1"
    user: root
    state: present

Arşiv script template’i:

# roles/log_management/templates/archive_logs.sh.j2
#!/bin/bash
# Ansible tarafından oluşturulmuştur - {{ ansible_date_time.iso8601 }}
# Log arşivleme scripti - {{ inventory_hostname }}

set -euo pipefail

LOG_DIR="{{ log_base_dir }}"
ARCHIVE_DIR="{{ log_archive_dir | default('/var/log/archive') }}"
S3_BUCKET="{{ s3_log_bucket | default('') }}"
HOSTNAME="{{ inventory_hostname }}"
DATE=$(date +%Y%m%d)
RETENTION_DAYS="{{ local_archive_retention | default(30) }}"

echo "[$(date)] Arşivleme başladı"

# Arşiv oluştur
ARCHIVE_FILE="${ARCHIVE_DIR}/${HOSTNAME}_logs_${DATE}.tar.gz"

find "${LOG_DIR}" -name "*.log.*" -mtime +7 -print0 | 
    tar czf "${ARCHIVE_FILE}" --null -T - 2>/dev/null || true

if [ -f "${ARCHIVE_FILE}" ]; then
    echo "[$(date)] Arşiv oluşturuldu: ${ARCHIVE_FILE}"

    # S3'e yükle (eğer bucket tanımlıysa)
    if [ -n "${S3_BUCKET}" ]; then
        aws s3 cp "${ARCHIVE_FILE}" 
            "s3://${S3_BUCKET}/logs/${HOSTNAME}/${DATE}/" 
            --storage-class STANDARD_IA
        echo "[$(date)] S3'e yükleme tamamlandı"
    fi

    # Eski arşivleri temizle
    find "${ARCHIVE_DIR}" -name "*.tar.gz" -mtime +"${RETENTION_DAYS}" -delete
    echo "[$(date)] Eski arşivler temizlendi"
fi

echo "[$(date)] Arşivleme tamamlandı"

Ana Playbook’u Bir Araya Getirelim

# site.yml
---
- name: Tüm sunuculara temel log yönetimi uygula
  hosts: all
  become: yes
  roles:
    - log_management
    - logrotate
    - rsyslog

- name: Web sunucuları için özel log konfigürasyonu
  hosts: webservers
  become: yes
  vars:
    logrotate_configs:
      - name: nginx
        log_path: /var/log/nginx/*.log
        frequency: daily
        keep: 30
        compress: true
        postrotate: "[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)"
        sharedscripts: true
      - name: apache2
        log_path: /var/log/apache2/*.log
        frequency: daily
        keep: 14
        compress: true
        postrotate: "apache2ctl graceful"
        sharedscripts: true
  roles:
    - logrotate

- name: Veritabanı sunucuları için log ayarları
  hosts: dbservers
  become: yes
  vars:
    log_rotate_keep: 60
    logrotate_configs:
      - name: postgresql
        log_path: /var/log/postgresql/*.log
        frequency: weekly
        keep: 12
        compress: true
        create_mode: "0640"
        create_owner: postgres
        create_group: postgres
      - name: mysql
        log_path: /var/log/mysql/*.log
        frequency: daily
        keep: 30
        compress: true
        create_mode: "0640"
        create_owner: mysql
        create_group: adm
  roles:
    - logrotate

Log Konfigürasyon Doğrulama Playbook’u

Uyguladıktan sonra her şeyin düzgün çalıştığını doğrulamak için ayrı bir playbook kullanmak iyi pratik.

# verify_logs.yml
---
- name: Log yönetimi konfigürasyonunu doğrula
  hosts: all
  become: yes
  gather_facts: yes

  tasks:
    - name: Logrotate konfigürasyonunu test et
      ansible.builtin.command: "logrotate --debug /etc/logrotate.conf"
      register: logrotate_verify
      changed_when: false
      failed_when: false

    - name: Logrotate test sonuçlarını göster
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}: Logrotate {{ 'OK' if logrotate_verify.rc == 0 else 'HATA' }}"

    - name: Rsyslog konfigürasyonunu doğrula
      ansible.builtin.command: rsyslogd -N1
      register: rsyslog_verify
      changed_when: false
      failed_when: false

    - name: Kritik log dosyalarının var olduğunu kontrol et
      ansible.builtin.stat:
        path: "{{ item }}"
      loop:
        - /var/log/syslog
        - /var/log/auth.log
        - /etc/logrotate.conf
      register: critical_log_files

    - name: Eksik kritik log dosyalarını raporla
      ansible.builtin.debug:
        msg: "EKSIK: {{ item.item }}"
      loop: "{{ critical_log_files.results }}"
      when: not item.stat.exists

    - name: Disk durumu özeti
      ansible.builtin.shell: df -h {{ log_base_dir }} | tail -1
      register: disk_status
      changed_when: false

    - name: Disk durumunu göster
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} disk durumu: {{ disk_status.stdout }}"

CI/CD Pipeline ile Entegrasyon

Ansible playbook’larını GitLab CI veya GitHub Actions ile entegre etmek, log konfigürasyon değişikliklerinin otomatik deploy edilmesini sağlar.

# .gitlab-ci.yml
stages:
  - lint
  - test
  - deploy-staging
  - verify-staging
  - deploy-production

variables:
  ANSIBLE_HOST_KEY_CHECKING: "False"
  ANSIBLE_STDOUT_CALLBACK: "yaml"

ansible-lint:
  stage: lint
  image: cytopia/ansible-lint
  script:
    - ansible-lint site.yml
  only:
    - merge_requests
    - main

syntax-check:
  stage: test
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook site.yml --syntax-check -i inventories/staging/hosts
  only:
    - merge_requests
    - main

deploy-staging:
  stage: deploy-staging
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook site.yml
        -i inventories/staging/hosts
        --vault-password-file /run/secrets/vault_pass
        -e "env=staging"
  environment:
    name: staging
  only:
    - main

verify-staging:
  stage: verify-staging
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook verify_logs.yml
        -i inventories/staging/hosts
        --vault-password-file /run/secrets/vault_pass
  only:
    - main

deploy-production:
  stage: deploy-production
  image: willhallonline/ansible:latest
  script:
    - ansible-playbook site.yml
        -i inventories/production/hosts
        --vault-password-file /run/secrets/vault_pass
        -e "env=production"
  environment:
    name: production
  when: manual
  only:
    - main

Ansible Vault ile Hassas Verileri Koruma

Log sunucusu adresi, S3 kimlik bilgileri gibi hassas verileri düz metin bırakmak olmaz.

# Vault şifreli değişken dosyası oluştur
ansible-vault create group_vars/all/vault.yml

# İçerik (şifrelenmeden önce):
# vault_rsyslog_remote_host: "logs.internal.example.com"
# vault_s3_access_key: "AKIAIOSFODNN7EXAMPLE"
# vault_s3_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# vault_s3_log_bucket: "company-prod-logs"

# group_vars/all.yml içinde vault değişkenlerine referans ver:
rsyslog_remote_host: "{{ vault_rsyslog_remote_host }}"
s3_access_key: "{{ vault_s3_access_key }}"
s3_secret_key: "{{ vault_s3_secret_key }}"
s3_log_bucket: "{{ vault_s3_log_bucket }}"

# Playbook'u vault ile çalıştır
ansible-playbook site.yml -i inventories/production/hosts --ask-vault-pass

# Veya vault password dosyası ile
ansible-playbook site.yml -i inventories/production/hosts --vault-password-file ~/.vault_pass

Pratik İpuçları ve Yaygın Hatalar

Gerçek ortamlarda bu kurulumu yaparken karşılaşacağın birkaç önemli noktayı paylaşayım.

Check mode kullanımını alışkanlık edin: Değişiklikleri uygulamadan önce her zaman --check ile ne değişeceğini gör. Özellikle prodüksiyon ortamlarında bu alışkanlık seni ciddi sorunlardan kurtarır.

Backup direktifini unutma: Rsyslog ve logrotate template’lerinde backup: yes parametresi kritik. Ansible konfigürasyon dosyasını değiştirmeden önce yedek alır, geri dönüşünü kolaylaştırır.

Servis restart’larına dikkat: Log konfigürasyon değişikliklerinde rsyslog’u restart etmek yerine reload tercih et. Restart sırasında kısa bir süre log kaybı yaşanabilir.

Logrotate test alışkanlığı: Her deploy sonrası logrotate --debug /etc/logrotate.conf çalıştırarak konfigürasyonun geçerli olduğunu doğrula. Handler’a bu testi eklemek bunu otomatik hale getirir.

İzin sorunlarına karşı dikkatli ol: Farklı Linux dağıtımlarında log dosyası izinleri değişebilir. Ubuntu’da adm grubu, CentOS/RHEL’de farklı gruplar kullanılır. ansible_os_family değişkenini kullanarak dağıtım bazlı ayarlar yapabilirsin.

Idempotency testleri yap: Playbook’unu arka arkaya iki kez çalıştır. İkinci çalıştırmada hiçbir değişiklik olmamalı. Değişiklik görüyorsan bir şeyler yanlış.

Sonuç

Ansible ile log yönetimini otomatize etmek, başlangıçta biraz zaman alsa da uzun vadede ciddi bir zaman ve stres tasarrufu sağlar. Bu yazıda ele aldığımız yaklaşımla şunları elde etmiş oldun:

  • Tüm sunucularda tutarlı logrotate konfigürasyonu
  • Merkezi log toplama için otomatize rsyslog ayarları
  • Disk doluluk izleme ve proaktif temizlik mekanizması
  • Uzun dönem arşivleme için S3 entegrasyonu
  • CI/CD pipeline’a entegre edilmiş, kod review sürecinden geçen log yönetimi

En önemli nokta şu: Log yönetimini bir kez doğru kurduğunda saat 3’te “disk doldu, servis çöktü” alarmıyla uyanmazsın. Ve “bu sunucuya logrotate kurmuştum ama şunun konfigürasyonu eksik kalmış” gibi durumlar ortadan kalkar.

Yapıyı kendi ortamına uyarlarken önce staging’de test et, sonra prodüksiyona geç. Ve her değişikliği Git’e commit’le, gelecekteki sen sana teşekkür eder.

Bir yanıt yazın

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