Ansible Handler Tanımlama: Değişiklik Tetiklemeli Görevler

Ansible ile çalışırken en sık yapılan hatalardan biri, her görevin ardından servisi yeniden başlatmak için ayrı bir task yazmak. Bu hem verimsiz hem de tehlikeli. Bir konfigürasyon değişmemişse neden servisi gereksiz yere restart edelim ki? İşte tam bu noktada handler kavramı devreye giriyor ve Ansible’ın en zarif özelliklerinden biri haline geliyor.

Handler Nedir ve Neden Kullanırız?

Handler’lar, yalnızca bir görev tarafından tetiklendiğinde çalışan özel Ansible görevleridir. Normal task’lardan temel farkı şu: bir handler, play boyunca kaç kez tetiklenirse tetiklensin, yalnızca bir kez çalışır ve bu çalışma play’in sonunda gerçekleşir.

Bunu gerçek hayattan bir örnekle açıklayalım. Nginx konfigürasyonunu değiştirdiniz, SSL sertifikasını güncellediniz ve bir upstream bloğu eklediniz. Bu üç görev de “nginx reload” istiyor. Handler olmadan bu üç task ardı ardına nginx’i yeniden yüklerdi. Handler ile bu işlem en sona ertelenir ve yalnızca bir kez yapılır.

Handler kullanmanın temel faydaları:

  • İdempotency korunur: Değişiklik yoksa servis restart edilmez
  • Performans artar: Gereksiz servis yeniden başlatmaları önlenir
  • Güvenilirlik sağlanır: Play sonunda tek bir temiz restart yapılır
  • Bağımlılıklar yönetilir: Birden fazla değişiklik tek bir handler ile toplanır

Temel Handler Sözdizimi

Handler tanımlamak için playbook içinde handlers bloğu kullanılır. Yapısı task’larla neredeyse aynıdır, sadece name değeri hem tanımlayıcı hem de tetikleyici olarak kullanılır.

---
- name: Nginx Kurulum ve Yapılandırma
  hosts: webservers
  become: yes

  tasks:
    - name: Nginx konfigurasyonunu kopyala
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: nginx'i yeniden baslat

  handlers:
    - name: nginx'i yeniden baslat
      ansible.builtin.service:
        name: nginx
        state: restarted

Burada kritik nokta notify direktifidir. Task başarılı olduğunda ve bir değişiklik gerçekleştiğinde (changed durumu), notify ile belirtilen handler kuyruğa eklenir. Task herhangi bir değişiklik yapmazsa (ok durumu), handler hiç çalışmaz.

Birden Fazla Handler Kullanımı

Gerçek dünya senaryolarında tek bir handler yetmez. Farklı servisler için farklı handler’lar tanımlamak gerekir.

---
- name: LAMP Stack Yapilandirmasi
  hosts: appservers
  become: yes

  tasks:
    - name: Apache konfigurasyonunu guncelle
      ansible.builtin.template:
        src: httpd.conf.j2
        dest: /etc/httpd/conf/httpd.conf
      notify: apache'yi yeniden yukle

    - name: PHP-FPM havuz konfigurasyonu
      ansible.builtin.template:
        src: www.conf.j2
        dest: /etc/php-fpm.d/www.conf
      notify: php-fpm'i yeniden baslat

    - name: MySQL konfigurasyonunu guncelle
      ansible.builtin.template:
        src: my.cnf.j2
        dest: /etc/my.cnf
      notify: mysql'i yeniden baslat

  handlers:
    - name: apache'yi yeniden yukle
      ansible.builtin.service:
        name: httpd
        state: reloaded

    - name: php-fpm'i yeniden baslat
      ansible.builtin.service:
        name: php-fpm
        state: restarted

    - name: mysql'i yeniden baslat
      ansible.builtin.service:
        name: mysqld
        state: restarted

Dikkat edin: Apache için reloaded, PHP-FPM ve MySQL için restarted kullandık. Bu fark önemlidir. reload servisi durdurmadan konfigürasyonu yeniden yükler, restart ise tamamen durdurur ve başlatır. Üretim ortamında mümkün olduğunca reload tercih etmelisiniz.

Bir Task’tan Birden Fazla Handler Tetikleme

Tek bir task birden fazla handler tetikleyebilir. Bunun için notify direktifine liste şeklinde handler isimleri verilir.

---
- name: SSL Sertifika Guncelleme
  hosts: loadbalancers
  become: yes

  tasks:
    - name: SSL sertifikasini guncelle
      ansible.builtin.copy:
        src: "files/ssl/{{ inventory_hostname }}.crt"
        dest: /etc/ssl/certs/site.crt
        mode: '0644'
      notify:
        - nginx'i yeniden yukle
        - sertifika degisikligini logla

    - name: SSL ozel anahtarini guncelle
      ansible.builtin.copy:
        src: "files/ssl/{{ inventory_hostname }}.key"
        dest: /etc/ssl/private/site.key
        mode: '0600'
      notify:
        - nginx'i yeniden yukle
        - sertifika degisikligini logla

  handlers:
    - name: nginx'i yeniden yukle
      ansible.builtin.service:
        name: nginx
        state: reloaded

    - name: sertifika degisikligini logla
      ansible.builtin.shell:
        cmd: echo "SSL sertifikasi guncellendi: $(date)" >> /var/log/ssl-changes.log

Burada hem sertifika hem de anahtar değişse bile nginx'i yeniden yukle handler’ı yalnızca bir kez çalışır. Ansible akıllıca bu duplikasyonu ortadan kaldırır.

Handler Zincirleme: Handler’dan Handler Tetikleme

Ansible 2.2 ve sonrasında handler’lar birbirini tetikleyebilir. Bu özellik karmaşık bağımlılık zincirlerini yönetmek için son derece kullanışlıdır.

---
- name: Systemd Servis Yonetimi
  hosts: all
  become: yes

  tasks:
    - name: Uygulama binary dosyasini guncelle
      ansible.builtin.copy:
        src: myapp
        dest: /usr/local/bin/myapp
        mode: '0755'
      notify: systemd daemon-reload yap

  handlers:
    - name: systemd daemon-reload yap
      ansible.builtin.systemd:
        daemon_reload: yes
      notify: uygulamayi yeniden baslat

    - name: uygulamayi yeniden baslat
      ansible.builtin.service:
        name: myapp
        state: restarted

Bu zincirde: binary değişir, daemon-reload yapılır, ardından uygulama yeniden başlatılır. Sıralama önemlidir; handler’lar tanımlandıkları sırayla değil, tetiklendikleri sırayla çalışır. Ancak zincirleme handler’larda sıra garantisi için tanım sırasına dikkat etmek gerekir.

Role İçinde Handler Kullanımı

Ansible role’lerinde handler’lar handlers/main.yml dosyasında tanımlanır. Bu yapı hem düzenli hem de yeniden kullanılabilir kod yazmanızı sağlar.

# roles/nginx/handlers/main.yml

---
- name: nginx'i yeniden baslat
  ansible.builtin.service:
    name: nginx
    state: restarted
  listen: "nginx restart"

- name: nginx'i yeniden yukle
  ansible.builtin.service:
    name: nginx
    state: reloaded
  listen: "nginx reload"

- name: nginx yapilandirmasini test et
  ansible.builtin.command: nginx -t
  changed_when: false
  listen: "nginx config test"
# roles/nginx/tasks/main.yml

---
- name: Nginx ana konfigurasyonu
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: "nginx reload"

- name: Virtual host konfigurasyonu
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ site_name }}.conf"
  notify: "nginx reload"

- name: Nginx servis dosyasini guncelle
  ansible.builtin.template:
    src: nginx.service.j2
    dest: /etc/systemd/system/nginx.service
  notify: "nginx restart"

Burada listen direktifini kullandık. Bu, birden fazla handler’ın aynı tetikleyiciye yanıt vermesini sağlayan güçlü bir özelliktir.

Listen Direktifi ile Esnek Handler Grupları

listen direktifi, handler isimlerinizi değiştirmeden farklı tetikleyiciler oluşturmanızı sağlar. Özellikle büyük projelerde çok işe yarar.

---
- name: Uygulama Stack Yeniden Baslat
  hosts: production
  become: yes

  tasks:
    - name: Uygulama konfigurasyonunu guncelle
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/myapp/app.conf
      notify: "stack yeniden baslat"

    - name: Cache konfigurasyonunu guncelle
      ansible.builtin.template:
        src: redis.conf.j2
        dest: /etc/redis/redis.conf
      notify: "stack yeniden baslat"

  handlers:
    - name: uygulamayi yeniden baslat
      ansible.builtin.service:
        name: myapp
        state: restarted
      listen: "stack yeniden baslat"

    - name: redis'i yeniden baslat
      ansible.builtin.service:
        name: redis
        state: restarted
      listen: "stack yeniden baslat"

    - name: nginx'i yeniden yukle
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: "stack yeniden baslat"

“Stack yeniden baslat” tetiklendiğinde tüm bu handler’lar sırayla çalışır. Tek bir notify ile tüm stack’i koordineli şekilde yeniden başlatmış olursunuz.

Force Handler ve Meta Modülü

Bazen play sonunu beklemeden handler’ı hemen çalıştırmak gerekebilir. Örneğin bir sonraki task, handler’ın tamamlanmasına bağlıysa bu durum ortaya çıkar.

---
- name: Veritabani Migrasyon Senaryosu
  hosts: dbservers
  become: yes

  tasks:
    - name: Yeni veritabani schema dosyasini kopyala
      ansible.builtin.copy:
        src: schema_v2.sql
        dest: /tmp/schema_v2.sql
      notify: veritabanini hazirla

    - name: Handler'lari hemen calistir
      ansible.builtin.meta: flush_handlers

    - name: Migrasyon scriptini calistir
      ansible.builtin.command: /usr/local/bin/run_migration.sh
      args:
        creates: /var/log/migration_v2.done

  handlers:
    - name: veritabanini hazirla
      ansible.builtin.command: mysql -u root < /tmp/schema_v2.sql
      args:
        creates: /var/lib/mysql/myapp/new_table.frm

meta: flush_handlers komutu o ana kadar birikmiş tüm handler’ları hemen çalıştırır. Migrasyon scripti ancak schema hazırlandıktan sonra çalışabildiği için bu pattern zorunlu hale gelir.

Hata Durumlarında Handler Davranışı

Varsayılan olarak play başarısız olursa pending handler’lar çalışmaz. Bu bazen istenen davranış olmayabilir. Örneğin konfigürasyon değişti, sonraki task hata verdi ama servisi restart etmemiz gerekiyor.

---
- name: Kritik Servis Guncelleme
  hosts: webservers
  become: yes
  force_handlers: true   # Hata olsa bile handler'lari calistir

  tasks:
    - name: Nginx konfigurasyonunu guncelle
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: nginx'i yeniden yukle

    - name: Belki basarisiz olabilecek bir task
      ansible.builtin.command: /opt/scripts/risky_operation.sh
      ignore_errors: yes

  handlers:
    - name: nginx'i yeniden yukle
      ansible.builtin.service:
        name: nginx
        state: reloaded

force_handlers: true ayarı play’de hata oluşsa bile handler’ların çalışmasını garantiler. Bu özellikle rollback senaryolarında veya konfigürasyon değişikliklerinin mutlaka uygulanması gereken durumlarda kritiktir.

Gerçek Dünya Senaryosu: Kubernetes Node Yönetimi

Büyük bir cluster’da node konfigürasyonlarını yönetirken handler’ların gücü gerçekten ortaya çıkıyor.

---
- name: Kubernetes Worker Node Yapilandirmasi
  hosts: k8s_workers
  become: yes
  serial: 1  # Bir seferde bir node

  tasks:
    - name: Kubelet konfigurasyonunu guncelle
      ansible.builtin.template:
        src: kubelet-config.yaml.j2
        dest: /etc/kubernetes/kubelet-config.yaml
      notify:
        - kubelet'i yeniden baslat
        - node durumunu dogrula

    - name: Containerd konfigurasyonunu guncelle
      ansible.builtin.template:
        src: config.toml.j2
        dest: /etc/containerd/config.toml
      notify: containerd'yi yeniden baslat

    - name: Node'u cordon et (is almayacak sekilde isaretl)
      ansible.builtin.command: kubectl cordon {{ inventory_hostname }}
      delegate_to: "{{ groups['k8s_masters'][0] }}"
      changed_when: true
      notify: node'u uncordon et

    - name: Handler'lari hemen uygula (restart oncesi cordon tamamlansin)
      ansible.builtin.meta: flush_handlers

  handlers:
    - name: containerd'yi yeniden baslat
      ansible.builtin.service:
        name: containerd
        state: restarted

    - name: kubelet'i yeniden baslat
      ansible.builtin.service:
        name: kubelet
        state: restarted

    - name: node durumunu dogrula
      ansible.builtin.command: kubectl get node {{ inventory_hostname }}
      delegate_to: "{{ groups['k8s_masters'][0] }}"
      changed_when: false
      retries: 5
      delay: 10
      register: node_status
      until: "'Ready' in node_status.stdout"

    - name: node'u uncordon et
      ansible.builtin.command: kubectl uncordon {{ inventory_hostname }}
      delegate_to: "{{ groups['k8s_masters'][0] }}"
      changed_when: true

Bu senaryoda serial: 1 ile bir seferde tek node üzerinde çalışıyoruz. Node cordon edilmeden restart yapılmaz, restart tamamlanmadan uncordon edilmez. Handler zinciri bu koordinasyonu sağlıyor.

Handler Hata Ayıklama İpuçları

Handler’larla çalışırken karşılaşılan yaygın sorunlar ve çözümleri:

Handler hiç çalışmıyor: Task’ın “changed” durumuna geçip geçmediğini kontrol edin. ansible-playbook -v ile verbose çıktı alarak hangi task’ların changed döndürdüğünü görün.

Handler beklenmedik sırada çalışıyor: Handler’lar tanım sıralarına göre değil, ilk tetiklenme sıralarına göre çalışır. Sıra önemliyse listen gruplaması veya meta: flush_handlers kullanın.

Handler birden fazla kez çalışıyor: Bu genellikle farklı host’larda paralel çalışmadan kaynaklanır. Her host için handler bir kez çalışır, bu normaldir.

Role handler’ları çalışmıyor: Role’ü import mı yoksa include mı ettiğinizi kontrol edin. import_role ve include_role arasındaki fark handler görünürlüğünü etkiler.

Handlers ile İyi Pratikler

Uzun yıllar boyunca edinilen deneyimlerle şekillenen bazı önemli kurallar:

  • Anlamlı isimler kullanın: Handler ismi ne yaptığını açıkça belirtmeli. “nginx restart” yerine “nginx konfigurasyonu degisti, yeniden yukle” gibi açıklayıcı isimler tercih edin.
  • Reload vs Restart ayrımına dikkat edin: Mümkün olan her durumda reload kullanın. Özellikle üretim ortamında nginx, apache, haproxy gibi servislerde reload downtime yaratmaz.
  • Handler’ları role’lerde merkezileştirin: Aynı handler’ı birden fazla playbook’ta tanımlamak yerine role içinde tutun.
  • İdempotency kontrolü yapın: Handler içindeki komutlar da idempotent olmalı. creates veya when direktifleriyle gereksiz çalışmaları önleyin.
  • Test ortamında force_handlers kullanmayın: Bu ayar üretimde belirli senaryolar için kullanışlı olsa da test ortamında normal akışı taklit etmek için kapalı tutun.
  • Zincirleme handler’ları belgelendirin: Handler zincirleri karmaşıklaşabilir. Comment ekleyerek hangi handler’ın neden tetiklendiğini açıklayın.

Sonuç

Handler’lar, Ansible’ın “değişiklik yoksa dokunma” felsefesinin en somut uygulamasıdır. Onlarsız bir Ansible playbook, her çalışmasında servisleri gereksiz yere restart eden, log’ları kalabalıklaştıran ve üretim ortamında gereksiz kesintilere yol açan bir araç haline gelir.

Temel kullanımdan başlayarak listen direktifi, handler zincirleme ve meta: flush_handlers gibi ileri seviye özelliklere kadar handler’lar her ölçekteki Ansible projesinin vazgeçilmez parçasıdır. Özellikle yüzlerce sunucu yöneten ortamlarda, bir konfigürasyon değişikliğinin yalnızca değişen sunucularda ve yalnızca bir kez servis restart’ına yol açması hem operasyonel güvenlik hem de sistem kararlılığı açısından büyük önem taşır.

Bir sonraki playbook’unuzu yazarken kendinize şu soruyu sorun: “Bu servis restart’ı gerçekten bir değişikliğe bağlı mı?” Eğer cevap evet ise, o task’ı doğrudan değil bir handler aracılığıyla çalıştırın. Hem kendinize hem de yönettiğiniz sistemlere iyilik yapmış olursunuz.

Yorum yapın