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: yeskoymak debug sürecini kabusa döndürür. Bunun yerinefailed_whenile hata koşulunu netleştirin.
- rescue bloğu içinde hata üretmek:
rescuebloğu da başarısız olabilir. Kritik kurtarma görevlerineignore_errors: yeseklemeyi ihmal etmeyin.
- always bloğunu unutmak: Kaynak temizleme, kilit açma, maintenance modundan çıkma gibi işlemleri her zaman
alwaysbloğuna koyun.
- Hata mesajlarını loglamayı ihmal etmek:
ansible_failed_taskveansible_failed_resultdeğ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.