Ansible’da Hata Yönetimi: ignore_errors ve block/rescue Yapısı

Ansible ile otomasyon yazarken her şeyin her zaman yolunda gitmeyeceğini kabul etmek, olgun bir sysadmin tutumunun ilk adımıdır. Disk dolabilir, servis başlamayabilir, ağ geçici olarak kesintiye uğrayabilir ya da bir paket deposu yanıt vermeyebilir. İşte bu noktada Ansible’ın hata yönetimi mekanizmaları devreye girer ve playbook’larınızın “kırılgan” olmaktan çıkıp “dayanıklı” hale gelmesini sağlar. Bu yazıda ignore_errors, block/rescue/always yapısı ve bunlarla birlikte kullanılan diğer direktifleri gerçek dünya senaryolarıyla ele alacağız.

Ansible’da Hata Yönetiminin Temelleri

Ansible varsayılan davranışı olarak bir görev başarısız olduğunda o host üzerindeki playbook çalışmasını durdurur. Bu çoğu zaman istediğimiz davranıştır, çünkü bağımlı adımların hatalı bir temel üzerine inşa edilmesini önler. Ancak bazı durumlarda bir görevin başarısız olması sonraki adımları engellememelidir ya da başarısızlık durumunda alternatif bir akış izlenmesi gerekir.

Ansible’ın sunduğu temel hata yönetimi araçları şunlardır:

  • ignore_errors: Görevi başarısız olsa bile devam et
  • block/rescue/always: Try/catch benzeri yapılandırılmış hata yönetimi
  • failed_when: Başarısızlık koşulunu özelleştir
  • changed_when: Değişiklik algısını özelleştir
  • any_errors_fatal: Herhangi bir hostta hata olursa tüm play’i durdur

ignore_errors: Basit Ama Tehlikeli

ignore_errors: yes direktifi, bir görev başarısız olduğunda Ansible’ın bu hatayı kayıt altına alıp devam etmesini sağlar. Kullanımı son derece basittir.

---
- name: Eski log dosyalarini temizle
  hosts: webservers
  tasks:
    - name: 30 gunden eski loglari sil
      ansible.builtin.shell: find /var/log/myapp -name "*.log" -mtime +30 -delete
      ignore_errors: yes

    - name: Uygulama servisini yeniden baslat
      ansible.builtin.systemd:
        name: myapp
        state: restarted

Bu örnekte log temizleme görevi başarısız olsa bile (örneğin dizin yoksa) servis yeniden başlatma görevi çalışmaya devam eder. Kulağa makul geliyor, değil mi? Ama burada tehlike şudur: ignore_errors gerçek hatayı gizler ve sonraki görevler bozuk bir durumda çalışmaya devam edebilir.

ignore_errors’ın Uygun Kullanım Alanları

ignore_errors‘ı her yere serpiştirmek anti-pattern’dir. Şu durumlarda kullanımı makuldür:

  • Idempotent olmayan temizleme işlemleri: Zaten silinmiş bir şeyi silmeye çalışmak
  • Bilgi toplama görevleri: Bir dosyanın veya servisin varlığını kontrol etmek
  • Opsiyonel özellik kurulumları: Paket bulunamazsa devam et ama log’a yaz
---
- name: Sistem bilgisi topla
  hosts: all
  tasks:
    - name: docker versiyonunu kontrol et
      ansible.builtin.command: docker --version
      ignore_errors: yes
      register: docker_version_result

    - name: Docker yuklu mu bilgisini goster
      ansible.builtin.debug:
        msg: "Docker durumu: {{ 'Yuklu' if docker_version_result.rc == 0 else 'Yuklu degil' }}"

failed_when ile Hata Koşulunu Özelleştirmek

Bazen bir komutun çıkış kodu 0 olmasa da bunu hata saymak istemeyiz. Ya da tam tersi, çıkış kodu 0 olsa bile sonuç içeriğine göre hataya düşürmek isteyebiliriz. İşte failed_when burada devreye girer.

---
- name: Servis durumunu kontrol et
  hosts: appservers
  tasks:
    - name: Nginx pid dosyasini kontrol et
      ansible.builtin.command: cat /var/run/nginx.pid
      register: nginx_pid
      failed_when: 
        - nginx_pid.rc != 0
        - '"No such file" not in nginx_pid.stderr'

    - name: Ozel uygulama ciktisini degerlendir
      ansible.builtin.shell: /usr/local/bin/health-check.sh
      register: health_result
      failed_when: >
        health_result.rc != 0 or
        'CRITICAL' in health_result.stdout

Bu yaklaşım ignore_errors‘dan çok daha temizdir çünkü neyi hata sayıp saymadığınızı açıkça tanımlarsınız.

block/rescue/always: Yapılandırılmış Hata Yönetimi

block/rescue/always yapısı, programlama dillerindeki try/catch/finally mekanizmasının Ansible karşılığıdır. Bu yapı ile görevleri mantıksal bloklara ayırabilir, hata durumunda kurtarma işlemleri yapabilir ve her durumda (başarı veya başarısızlık) çalışmasını istediğiniz görevleri tanımlayabilirsiniz.

Temel yapı şöyledir:

---
- name: Yapilandirmali hata yonetimi ornegi
  hosts: webservers
  tasks:
    - name: Uygulama guncelleme blogu
      block:
        - name: Uygulamayi guncelle
          ansible.builtin.shell: /usr/local/bin/update-app.sh
          
        - name: Servisi yeniden baslat
          ansible.builtin.systemd:
            name: myapp
            state: restarted
            
      rescue:
        - name: Guncelleme basarisiz oldu, geri al
          ansible.builtin.shell: /usr/local/bin/rollback-app.sh
          
        - name: Hata bildirimini gonder
          ansible.builtin.mail:
            to: [email protected]
            subject: "HATA: {{ inventory_hostname }} uygulama guncelleme basarisiz"
            body: "Otomatik geri alma yapildi. Lutfen kontrol edin."
            
      always:
        - name: Guncelleme log kaydini olustur
          ansible.builtin.lineinfile:
            path: /var/log/update-history.log
            line: "{{ ansible_date_time.iso8601 }} - Guncelleme girişimi: {{ inventory_hostname }}"
            create: yes

Bu yapıda block içindeki herhangi bir görev başarısız olursa rescue bloğu devreye girer. always bloğu ise başarı ya da başarısızlık fark etmeksizin her zaman çalışır.

Gerçek Dünya Senaryosu 1: Veritabanı Migration İşlemi

Veritabanı migration’ları en riskli otomasyon işlemleri arasındadır. Bir şeyler ters giderse hem veriyi korumak hem de sistemi çalışır durumda tutmak gerekir.

---
- name: Veritabani migration ile uygulama guncelleme
  hosts: app_primary
  vars:
    app_name: "eticaret"
    backup_dir: "/var/backups/db"
    deploy_timestamp: "{{ ansible_date_time.epoch }}"
    
  tasks:
    - name: DB migration ve uygulama guncelleme
      block:
        - name: Bakim moduna gec
          ansible.builtin.uri:
            url: "http://localhost/api/maintenance/on"
            method: POST
            headers:
              Authorization: "Bearer {{ vault_api_token }}"
          
        - name: Veritabani yedeği al
          ansible.builtin.shell: |
            pg_dump -U {{ db_user }} {{ db_name }} | gzip > {{ backup_dir }}/{{ app_name }}_{{ deploy_timestamp }}.sql.gz
          environment:
            PGPASSWORD: "{{ vault_db_password }}"
            
        - name: Yeni uygulama versiyonunu deploy et
          ansible.builtin.git:
            repo: "[email protected]:sirket/{{ app_name }}.git"
            dest: "/opt/{{ app_name }}"
            version: "{{ deploy_version }}"
            
        - name: Migration calistir
          ansible.builtin.shell: |
            cd /opt/{{ app_name }}
            python manage.py migrate --noinput
          environment:
            DATABASE_URL: "{{ vault_database_url }}"
          register: migration_result
          
        - name: Uygulamayi yeniden baslat
          ansible.builtin.systemd:
            name: "{{ app_name }}"
            state: restarted
            
        - name: Saglik kontrolu yap
          ansible.builtin.uri:
            url: "http://localhost/api/health"
            status_code: 200
          retries: 5
          delay: 10
          
      rescue:
        - name: Hata log kaydi
          ansible.builtin.debug:
            msg: "KRITIK: {{ app_name }} deploy basarisiz! Geri alma basliyor..."
            
        - name: Onceki versiyona don
          ansible.builtin.git:
            repo: "[email protected]:sirket/{{ app_name }}.git"
            dest: "/opt/{{ app_name }}"
            version: "{{ previous_version }}"
            
        - name: DB yedeğini geri yukle
          ansible.builtin.shell: |
            gunzip -c {{ backup_dir }}/{{ app_name }}_{{ deploy_timestamp }}.sql.gz | 
            psql -U {{ db_user }} {{ db_name }}
          environment:
            PGPASSWORD: "{{ vault_db_password }}"
          when: migration_result is defined
          
        - name: Onceki versiyon ile servisi baslat
          ansible.builtin.systemd:
            name: "{{ app_name }}"
            state: restarted
            
        - name: Slack bildirimi gonder
          ansible.builtin.uri:
            url: "{{ vault_slack_webhook }}"
            method: POST
            body_format: json
            body:
              text: ":red_circle: *{{ app_name }}* deploy basarisiz! Otomatik rollback yapildi. Sunucu: {{ inventory_hostname }}"
              
      always:
        - name: Bakim modundan cik
          ansible.builtin.uri:
            url: "http://localhost/api/maintenance/off"
            method: POST
            headers:
              Authorization: "Bearer {{ vault_api_token }}"
          ignore_errors: yes
          
        - name: Deploy raporu kaydet
          ansible.builtin.lineinfile:
            path: /var/log/deploy-audit.log
            line: "{{ ansible_date_time.iso8601 }} | {{ app_name }} | {{ deploy_version }} | {{ 'BASARILI' if ansible_failed_task is not defined else 'BASARISIZ' }}"
            create: yes

Bu senaryoda dikkat edilecek birkaç önemli nokta var. always bloğundaki bakim modunu kapatma görevi ignore_errors: yes ile işaretlenmiştir, çünkü rescue sırasında da bir hata oluşursa en azından maintenance modunun kapandığından emin olmak istiyoruz.

Gerçek Dünya Senaryosu 2: Çok Adımlı Sertifika Yenileme

SSL sertifika yenileme işlemleri de hata yönetimi açısından kritik senaryolardandır.

---
- name: SSL sertifika yenileme
  hosts: loadbalancers
  vars:
    cert_dir: "/etc/ssl/certs"
    domain: "sirket.com"
    
  tasks:
    - name: Mevcut sertifika bilgisini kaydet
      ansible.builtin.command: >
        openssl x509 -in {{ cert_dir }}/{{ domain }}.crt 
        -noout -enddate
      register: current_cert_info
      ignore_errors: yes
      
    - name: Sertifika yenileme islemi
      block:
        - name: Yeni sertifika talep et
          ansible.builtin.command: >
            certbot certonly --standalone
            -d {{ domain }} -d www.{{ domain }}
            --non-interactive --agree-tos
            --email ssl-admin@{{ domain }}
          register: certbot_result
          
        - name: Sertifika dosyalarini kopyala
          ansible.builtin.copy:
            src: "/etc/letsencrypt/live/{{ domain }}/{{ item.src }}"
            dest: "{{ cert_dir }}/{{ item.dest }}"
            remote_src: yes
            mode: "{{ item.mode }}"
          loop:
            - { src: "fullchain.pem", dest: "{{ domain }}.crt", mode: "0644" }
            - { src: "privkey.pem", dest: "{{ domain }}.key", mode: "0600" }
            
        - name: Nginx yapilandirmasini test et
          ansible.builtin.command: nginx -t
          register: nginx_test
          
        - name: Nginx'i yeniden yukle
          ansible.builtin.systemd:
            name: nginx
            state: reloaded
            
      rescue:
        - name: Yedek sertifikaya don
          ansible.builtin.command: >
            cp {{ cert_dir }}/{{ domain }}.crt.backup 
            {{ cert_dir }}/{{ domain }}.crt
          ignore_errors: yes
          
        - name: Nginx'i eski yapilandirmayla baslatmaya calis
          ansible.builtin.systemd:
            name: nginx
            state: restarted
          ignore_errors: yes
          
        - name: Kritik hata bildirimi
          ansible.builtin.debug:
            msg: |
              KRITIK HATA: {{ domain }} sertifika yenileme basarisiz!
              Hata detayi: {{ ansible_failed_result.msg | default('Bilinmiyor') }}
              Acil mudahale gerekiyor!
              
      always:
        - name: Islem sonucunu logla
          ansible.builtin.lineinfile:
            path: /var/log/cert-renewal.log
            line: >
              {{ ansible_date_time.iso8601 }} | {{ domain }} | 
              {{ certbot_result.rc | default('N/A') }} | 
              {{ current_cert_info.stdout | default('Mevcut cert yok') }}
            create: yes

ansible_failed_task ve ansible_failed_result Değişkenleri

rescue bloğu içinde iki özel değişkene erişebilirsiniz:

  • ansible_failed_task: Başarısız olan görev hakkında metadata içerir
  • ansible_failed_result: Başarısız görevin çıkış bilgilerini içerir

Bu değişkenleri kullanarak daha anlamlı hata mesajları oluşturabilirsiniz.

---
- name: Detayli hata raporlama ornegi
  hosts: all
  tasks:
    - name: Kritik sistem gorevleri
      block:
        - name: Disk kullanim kontrolu
          ansible.builtin.shell: df -h / | awk 'NR==2 {print $5}' | tr -d '%'
          register: disk_usage
          failed_when: disk_usage.stdout | int > 90
          
        - name: Kritik servisleri kontrol et
          ansible.builtin.systemd:
            name: "{{ item }}"
            state: started
          loop:
            - postgresql
            - nginx
            - redis
            
      rescue:
        - name: Basarisiz gorev detaylarini goster
          ansible.builtin.debug:
            msg: |
              Basarisiz gorev: {{ ansible_failed_task.name }}
              Modul: {{ ansible_failed_task.action }}
              Hata mesaji: {{ ansible_failed_result.msg | default('Mesaj yok') }}
              Stdout: {{ ansible_failed_result.stdout | default('') }}
              Stderr: {{ ansible_failed_result.stderr | default('') }}
              
        - name: Otomatik tanimlama raporu olustur
          ansible.builtin.template:
            src: incident-report.j2
            dest: "/tmp/incident-{{ ansible_date_time.epoch }}.txt"

İç İçe block/rescue Yapıları

Karmaşık senaryolarda iç içe bloklar kullanabilirsiniz. Bu yaklaşım, farklı hata seviyelerini ayrı ayrı ele almanızı sağlar.

---
- name: Ic ice block yapisi ornegi
  hosts: dbservers
  tasks:
    - name: Ana veritabani islemleri
      block:
        - name: Veritabani baglantisini dogrula
          ansible.builtin.command: pg_isready -h localhost -p 5432
          
        - name: Replica sync islemi
          block:
            - name: Primary-replica lag kontrolu
              ansible.builtin.shell: |
                psql -U postgres -c "SELECT now() - pg_last_xact_replay_timestamp() AS replication_delay;"
              register: replication_lag
              
            - name: Replica'yi guncelle
              ansible.builtin.shell: pg_basebackup -h primary-db -U replicator -D /var/lib/pgsql/data
              when: replication_lag.stdout is search("days")
              
          rescue:
            - name: Replica hatasi - sadece log kaydi yap
              ansible.builtin.debug:
                msg: "Replica sync hatasi, ana islemler devam ediyor"
                
            - name: Replication hata sayacini artir
              ansible.builtin.shell: echo "1" >> /tmp/replication_errors.count
              
        - name: Rutin bakim sorgularini calistir
          ansible.builtin.shell: psql -U postgres -c "VACUUM ANALYZE;"
          
      rescue:
        - name: Kritik DB hatasi - tum servisleri durdur
          ansible.builtin.systemd:
            name: "{{ item }}"
            state: stopped
          loop:
            - myapp
            - myapp-worker
          ignore_errors: yes
          
        - name: DBA ekibini uyar
          ansible.builtin.mail:
            to: [email protected]
            subject: "KRITIK: {{ inventory_hostname }} veritabani sorunu"
            body: "Ansible otomasyonu kritik veritabani hatasi algiladi. Acil mudahale gerekiyor."

any_errors_fatal ve max_fail_percentage

Birden fazla host üzerinde çalışırken bazen tek bir hosttaki hata tüm play’i durdurmalıdır. Örneğin bir cluster’da quorum kaybetme riskini göze alamazsınız.

---
- name: Cluster guncelleme - hata toleransi sinirli
  hosts: elasticsearch_cluster
  serial: 1
  any_errors_fatal: true
  
  tasks:
    - name: Node'u cluster'dan cikarmadan once kontrol
      block:
        - name: Cluster saglik durumunu kontrol et
          ansible.builtin.uri:
            url: "http://{{ hostvars[groups['elasticsearch_cluster'][0]]['ansible_host'] }}:9200/_cluster/health"
            return_content: yes
          register: cluster_health
          failed_when: >
            (cluster_health.json.status == 'red') or
            (cluster_health.json.number_of_nodes | int < 3)
            
        - name: Node'u devre disi birak
          ansible.builtin.uri:
            url: "http://localhost:9200/_cluster/settings"
            method: PUT
            body_format: json
            body:
              transient:
                cluster.routing.allocation.exclude._ip: "{{ ansible_host }}"
                
        - name: Shard migration tamamlanana kadar bekle
          ansible.builtin.uri:
            url: "http://localhost:9200/_cluster/health?wait_for_no_relocating_shards=true&timeout=5m"
            return_content: yes
          register: shard_wait
          until: shard_wait.json.relocating_shards == 0
          retries: 30
          delay: 10
          
        - name: Elasticsearch'u guncelle ve yeniden baslat
          ansible.builtin.yum:
            name: elasticsearch
            state: latest
          notify: restart elasticsearch
          
      rescue:
        - name: Node'u cluster'a geri ekle
          ansible.builtin.uri:
            url: "http://localhost:9200/_cluster/settings"
            method: PUT
            body_format: json
            body:
              transient:
                cluster.routing.allocation.exclude._ip: ""
          ignore_errors: yes
          
        - name: Guncelleme basarisiz, islemi durdur
          ansible.builtin.fail:
            msg: "Elasticsearch cluster guncelleme basarisiz! Manuel mudahale gerekiyor."

Yaygın Hatalar ve Best Practice’ler

Hata yönetimi yazarken karşılaşılan en yaygın sorunları ve bunlardan kaçınma yollarını şöyle özetleyebilirim:

  • ignore_errors’i kötüye kullanmak: Her başarısız görevin üstüne ignore_errors: yes koymak debug sürecini kabusa döndürür. Bunun yerine failed_when ile hata koşulunu netleştirin.
  • rescue bloğu içinde hata üretmek: rescue bloğu da başarısız olabilir. Kritik kurtarma görevlerine ignore_errors: yes eklemeyi ihmal etmeyin.
  • always bloğunu unutmak: Kaynak temizleme, kilit açma, maintenance modundan çıkma gibi işlemleri her zaman always bloğuna koyun.
  • Hata mesajlarını loglamayı ihmal etmek: ansible_failed_task ve ansible_failed_result değişkenlerini kullanarak anlamlı log kayıtları oluşturun.
  • Rollback’i test etmemek: Rollback prosedürü de bir koddur ve test edilmesi gerekir. Disaster recovery tatbikatı yapmadan üretim ortamına güvenmeyin.
  • Idempotency’yi bozmak: Rescue bloğundaki geri alma işlemleri de idempotent olmalıdır. Aynı playbook ikinci kez çalıştığında sorun çıkmamalıdır.

Sonuç

Ansible’da hata yönetimi, başlangıçta karmaşık görünse de doğru kavrandığında playbook’larınızın kalitesini dramatik biçimde artırır. ignore_errors basit ve hızlı bir çözüm sunar ama dikkatli kullanılmalıdır. block/rescue/always yapısı ise production ortamlarında gerçek anlamda güvenilir otomasyon için vazgeçilmezdir.

Pratik bir öneri olarak, yeni bir playbook yazarken önce “Bu görev başarısız olursa ne olur?” sorusunu her adım için kendinize sorun. Eğer cevap “sistemi kötü bir durumda bırakır” ise bir block/rescue yapısına ihtiyacınız var demektir. Eğer cevap “önemli değil, devam edebiliriz” ise ignore_errors ya da failed_when: false yeterli olacaktır.

Hata yönetimini sonradan eklenecek bir şey olarak değil, playbook tasarımının ayrılmaz bir parçası olarak düşünün. Sabah 3’te alınacak bir uyarıya yanıt verirken, önceden yazılmış iyi bir rollback mekanizması sizi hem zamandan hem de stres hormonu fazlasından kurtaracaktır.

Yorum yapın