Sürdürülebilir Otomasyon: Ansible Rolleri, Vault ve Hata Yönetimi

Ansible ile yapılandırma yönetimi yaparken işler büyüdükçe kaçınılmaz bir an gelir: tek bir playbook dosyası yüzlerce satıra ulaşmış, şablonlar değişkenlerle iç içe geçmiş, handler'lar nerede başlayıp nerede bittiği belirsiz bir kütleye dönüşmüştür. Bu noktada "çalışıyor" demek yeterli değildir. Sürdürülebilir, tekrar kullanılabilir ve güvenli bir yapıya ihtiyaç duyulur. Bu yazıda Ansible'ın bu ihtiyaca verdiği yanıt olan rolleri, hassas verileri korumak için Vault'u, hata davranışını özelleştirmek için changed_when ve failed_when direktiflerini ve hataları kontrol altına almak için block/rescue/always yapısını ele alıyoruz.


Ansible Rolleri

Neden Rol Kullanılır?

Tek bir playbook dosyasında büyüyen bir projeyi yönetmek zamanla somut problemlere dönüşür. Aynı nginx kurulum adımları hem web hem proxy sunucuları için tekrarlanır. Bir değişkeni değiştirmek için dosyayı baştan sona taramak gerekir. Takımın başka bir üyesi hangi kısmın ne yaptığını anlamak için uzun zaman harcar. Test etmek istediğinizde tüm playbook'u çalıştırmanız gerekir.

Roller bu sorunları yapısal olarak çözer. Her rol, belirli bir servis veya işlev için gerekli tüm bileşenleri (görevler, değişkenler, şablonlar, handler'lar) kendi dizininde barındırır. Yapılandırmalar modülerleşir, kod tekrarı önlenir ve aynı rol farklı projelerde yeniden kullanılabilir.

Rolleri kullanmadan yazılmış geleneksel bir playbook şöyle görünebilir:

- name: Install and start Apache
  hosts: webservers
  become: true
  vars:
    apache_port: 80

  tasks:
    - name: Install Apache
      ansible.builtin.apt:
        name: apache2
        state: present

    - name: Deploy config file
      ansible.builtin.template:
        src: apache.conf.j2
        dest: /etc/apache2/sites-available/000-default.conf

    - name: Ensure Apache is running
      ansible.builtin.service:
        name: apache2
        state: started
        enabled: true

Bu yapı küçük senaryolar için işe yarar. Ancak şablonların, değişkenlerin ve handler'ların aynı dosyada büyümesiyle bakım giderek zorlaşır. Aynı örneği role dönüştürdüğümüzde ana playbook şu kadar sade bir hale gelir:

- name: Deploy Apache with Role
  hosts: webservers
  become: true
  roles:
    - apache

Dizin Yapısı

ansible-galaxy init apache komutu bir rolün standart iskeletini oluşturur:

roles/
└── apache/
    ├── tasks/
    │   └── main.yml
    ├── handlers/
    │   └── main.yml
    ├── templates/
    │   └── apache.conf.j2
    ├── files/
    │   └── index.html
    ├── defaults/
    │   └── main.yml
    ├── vars/
    │   └── main.yml
    └── meta/
        └── main.yml

Her klasörün belirli bir işlevi vardır:

Klasör Amaç
tasks/ Görevler burada tanımlanır. Ana giriş noktası main.yml'dir.
handlers/ notify ile tetiklenen servis işlemleri burada tanımlanır.
templates/ Jinja2 ile dinamik hale getirilmiş şablon dosyaları yer alır.
files/ Statik dosyalar doğrudan kopyalanmak üzere bu dizine konur.
defaults/ Kullanıcının override edebileceği varsayılan değişkenler tanımlanır.
vars/ Override edilmemesi gereken sabit değişkenler tanımlanır.
meta/ Rol bağımlılıkları ve meta bilgiler burada tutulur.

Ansible bu dizin yapısını otomatik olarak tanır. tasks/main.yml zorunlu olarak yüklenir; diğer dizinler yalnızca ilgili bir görev tarafından çağrıldığında devreye girer. Bu standartlaşma sayesinde bir rolü sadece adıyla çağırmak yeterlidir, yapıyı ayrıca tanımlamaya gerek yoktur.

Görevleri Dosyalara Bölmek: import_tasks

Gerçek projelerde tüm görevleri tek bir tasks/main.yml dosyasına yazmak, zamanla aynı tek-playbook sorununu rol içinde yeniden yaratır. Standart yaklaşım görevleri işlevlerine göre ayrı dosyalara bölmek ve main.yml'den bu dosyaları çağırmaktır.

Örnek bir tasks/ dizini şöyle görünebilir:

tasks/
├── main.yml
├── install.yml
├── configure.yml
└── service.yml

tasks/main.yml bu dosyaları sırasıyla çağırır:

# tasks/main.yml
---
- name: Kurulum görevlerini dahil et
  ansible.builtin.import_tasks: install.yml

- name: Konfigürasyon görevlerini dahil et
  ansible.builtin.import_tasks: configure.yml

- name: Servis yönetimi görevlerini dahil et
  ansible.builtin.import_tasks: service.yml

Her dosya kendi işine odaklanır:

# tasks/install.yml
---
- name: Paket listesini güncelle
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600

- name: Apache'yi kur
  ansible.builtin.apt:
    name: apache2
    state: present
# tasks/configure.yml
---
- name: Ana konfigürasyon dosyasını deploy et
  ansible.builtin.template:
    src: apache.conf.j2
    dest: /etc/apache2/apache2.conf
    owner: root
    group: root
    mode: "0644"
  notify: apache'yi yeniden başlat

- name: Siteyi etkinleştir
  ansible.builtin.file:
    src: /etc/apache2/sites-available/000-default.conf
    dest: /etc/apache2/sites-enabled/000-default.conf
    state: link
  notify: apache'yi yeniden başlat
# tasks/service.yml
---
- name: Apache servisini başlat ve boot'ta otomatik başlamasını sağla
  ansible.builtin.service:
    name: apache2
    state: started
    enabled: true

Bu yapının somut faydası şudur: bir konfigürasyon sorununu araştırırken yalnızca configure.yml'e bakmanız yeterlidir. Kurulum adımlarını değiştirmek istediğinizde install.yml dışındaki hiçbir dosyaya dokunmazsınız. import_tasks parse zamanında yüklenir, yani Ansible çalışmadan önce tüm yapıyı bilir; bu da tag'lerle birlikte düzgün çalışmasını sağlar.

defaults/ ve vars/ Farkı

Bu iki dizin sık karıştırılır. Aradaki fark öncelik seviyesidir.

defaults/main.yml içindeki değişkenler, Ansible'ın tanıdığı en düşük önceliğe sahip değişken kaynağıdır. Inventory'den, group_vars'tan, host_vars'tan, playbook vars: bloğundan veya komut satırından geçilen aynı isimli herhangi bir değişken tarafından ezilebilir. Bu kasıtlı bir tasarımdır: rolü kullanan kişiye özelleştirme esnekliği tanır.

vars/main.yml ise çok daha yüksek bir önceliğe sahiptir. Yalnızca extra-vars (-e) tarafından geçersiz kılınabilir. Rolün doğru çalışması için değişmemesi gereken değerler buraya yazılır.

Özellik defaults/main.yml vars/main.yml
Override edilebilir mi? Evet, kolayca Hayır, yalnızca -e ile
Öncelik seviyesi En düşük Yüksek
Kullanım amacı Kullanıcıya esneklik sağlamak Sabit referans değerleri

Somutlaştıralım. apache_port değişkenini düşünün. Bu değerin varsayılanı 80 olabilir ama farklı ortamlarda 8080 veya 443 olarak kullanılması gerekebilir. Bu değişken defaults/main.yml'e aittir. Öte yandan rolün hangi kullanıcı adı altında çalışacağı gibi iç bir değer, dışarıdan değiştirilmesini istemediğiniz bir şeydir; bu vars/main.yml'e aittir.

Bağımlı Roller: meta/main.yml

Roller birbiriyle bağımlı çalışabilir. apache rolü çalışmadan önce sistemin temel güvenlik ayarlarının yapılmış olması gerekiyorsa bunu her playbook'ta elle sıralamak yerine rolün kendi tanımına eklemek daha doğrudur. meta/main.yml bu amaç için kullanılır:

# roles/apache/meta/main.yml
---
dependencies:
  - role: common
  - role: firewall
    vars:
      allowed_ports:
        - 80
        - 443

apache rolü çağrıldığında Ansible önce common, sonra firewall rolünü çalıştırır; firewall rolüne de allowed_ports değişkenini aktarır. Ardından apache rolüne geçer. Bağımlılık zinciri playbook'ta değil, rolün kendi tanımında yaşar. Bir rolü kullanan kişi bu bağımlılıkları ayrıca yönetmek zorunda kalmaz.

Döngüsel bağımlılık (A rolü B'ye, B rolü A'ya bağımlı) oluşturulursa Ansible bunu tespit eder ve hata verir.

Hazır Rol Kullanımı: Ansible Galaxy

Her şeyi sıfırdan yazmak gerekmez. Ansible Galaxy, topluluk ve Red Hat tarafından geliştirilen rolleri barındıran merkezi bir depodur. Nginx, PostgreSQL, Docker gibi yaygın servislerin iyi test edilmiş rolleri burada bulunur.

Tek bir rolü indirmek için:

ansible-galaxy role install geerlingguy.nginx

Birden fazla rol kullanıldığında requirements.yml dosyasıyla tüm bağımlılıkları tek bir yerden yönetmek daha temiz bir yaklaşımdır:

# requirements.yml
---
roles:
  - name: geerlingguy.nginx
    version: "3.2.0"
  - name: geerlingguy.postgresql
    version: "3.4.2"

Tümünü tek komutla indirmek için:

ansible-galaxy install -r requirements.yml

requirements.yml dosyasını versiyon kontrolüne eklemek önemlidir. Bu dosya olmadan ekibinizdeki farklı makinelerde farklı rol sürümleri indirilmiş olabilir ve bu tür tutarsızlıklar üretim ortamında beklenmedik davranışlara yol açar.

$ quiz

defaults/main.yml ve vars/main.yml arasındaki temel fark nedir?


Rollerin Çalışma Prensibi

Bir rolün nasıl kullanılacağını bilmek yeterli değildir. Arka planda ne olduğunu anlamak, özellikle değişken çakışmaları ve beklenmedik davranışlarla karşılaşıldığında kritik fark yaratır.

Yükleme Sırası

Bir rol çağrıldığında Ansible şu sırayla ilgili dizinleri işler:

  1. defaults/ — Varsayılan değişkenler yüklenir. En düşük önceliğe sahiptirler.
  2. vars/ — Sabit değişkenler yüklenir. Daha yüksek önceliğe sahiptirler.
  3. meta/ — Bağımlılıklar kontrol edilir. Tanımlı bağımlı roller önce çalıştırılır.
  4. tasks/main.yml — Görevler sırayla çalıştırılmaya başlar.
  5. handlers/notify çağrıları sonucunda tetiklenmeye hazır hale gelir. Tüm görevler tamamlandıktan sonra çalışır.
  6. templates/ ve files/template veya copy gibi modüller kullanıldığında bu dizinler referans alınır.

Şunu vurgulayalım: templates/ ve files/ dizinleri Ansible tarafından otomatik olarak "yüklenmez". Bir görev ansible.builtin.template modülünü çağırdığında Ansible şablon dosyasını templates/ dizininde arar. Bu dizin bir depo görevi görür, görev çağrılmadıkça hiçbir şey olmaz.

Değişken Önceliği

Bir değişken birden fazla yerden tanımlanabilir. Hangisinin kazanacağını bilmek, beklenmedik davranışları anlamak için gereklidir. Rol bağlamında en sık karşılaşılan öncelik sırası şöyledir (yukarıdan aşağıya öncelik artar):

Öncelik Kaynak
En düşük defaults/main.yml (rol içi)
group_vars/
host_vars/
vars/main.yml (rol içi)
Playbook vars: bloğu
En yüksek extra-vars (-e)

Bunu somutlaştıralım. defaults/main.yml içinde apache_port: 80 tanımlı olsun. Şu senaryoları düşünün:

group_vars/webservers.yml içinde apache_port: 8080 yazıyorsa Ansible 8080'i kullanır; group_vars her zaman defaults'ı ezer. Playbook'taki vars: bloğunda apache_port: 443 yazıyorsa Ansible 443'ü kullanır; playbook vars: bloğu group_vars'ı ezer. Komut satırından -e "apache_port=9090" geçildiyse Ansible 9090'ı kullanır; extra-vars her şeyi ezer.

Bir değişken hangi değeri taşıdığı anlaşılamadığında öncelik sırası ilk bakılacak yerdir.

Rol Arama Sırası

Ansible bir rolü çağrıldığında şu dizinleri sırayla arar:

  1. Playbook'un bulunduğu dizindeki roles/ klasörü
  2. ansible.cfg içinde tanımlı roles_path
  3. ~/.ansible/roles/
  4. /etc/ansible/roles/

Yerel roller her zaman önce aranır. Aynı isimde hem yerel hem global bir rol varsa yerel olan kullanılır.

import_role ve include_role

Roller yalnızca playbook seviyesinde roles: bloğuyla değil, görevler içinde de çağrılabilir. Bu iki yöntemin farkı ne zaman yüklendiğiyle ilgilidir.

import_role, playbook parse edildiğinde yani Ansible herhangi bir görevi çalıştırmaya başlamadan önce yüklenir. Statik bir yapıdır. when gibi koşullarla birlikte kullanılamaz; koşul değerlendirilmeden önce rol zaten yüklenmiş olur.

- name: Apache rolünü dahil et
  ansible.builtin.import_role:
    name: apache

include_role ise çalışma zamanında, Ansible o göreve ulaştığında yüklenir. Dinamik yapısı sayesinde koşullarla birlikte kullanılabilir:

- name: Yalnızca Debian sistemlerde Apache rolünü dahil et
  ansible.builtin.include_role:
    name: apache
  when: ansible_facts['os_family'] == "Debian"

Pratik kural şudur: rolün her durumda çalışması gerekiyorsa import_role, bir koşula bağlıysa include_role tercih edilmelidir. import_role aynı zamanda tag'lerle daha iyi çalışır çünkü Ansible hangi görevlerin dahil olduğunu önceden bilir ve --tags parametresi beklediğiniz gibi davranır.

$ quiz

import_role ile include_role arasındaki temel fark nedir?


Vault ile Hassas Veri Yönetimi

Sorun: Açık Metin Tehlikesi

Ansible ile çalışan ortamlarda şifrelerin ve API anahtarlarının açık metin olarak saklandığını görmek hâlâ yaygındır. Veritabanı parolası vars/main.yml içinde, üretim sunucusunun API anahtarı group_vars/production.yml içinde yazar. Bu dosyalar Git'e commit edilir ve artık o repoya erişim yetkisi olan herkes bu bilgilere ulaşabilir hale gelir.

Ansible Vault bu soruna yapısal bir çözüm sunar. YAML dosyalarını AES256 algoritmasıyla şifreler; Vault parolasını bilmeyen biri ne playbook'u çalıştırabilir ne de dosya içeriğini okuyabilir. Vault, Ansible'a ayrıca kurulması gereken bir araç değildir; Ansible kurulduğunda birlikte gelir.

Temel İşlemler

Yeni bir Vault dosyası oluşturmak için:

ansible-vault create vault/secrets.yml

Komut çalıştırıldığında bir parola belirlemeniz istenir. Ardından $EDITOR olarak tanımlı editör açılır ve değişkenlerinizi tanımlayabilirsiniz:

db_password: "G!zl!P@rola2024"
api_token: "eyJhbGciOiJIUzI1NiIsInR5..."
smtp_password: "posta_sifrem"

Dosya kaydedildiğinde içerik şifreli hale gelir. Dosyayı cat ile okusanız şöyle görünür:

$ANSIBLE_VAULT;1.1;AES256
323234343334373735336133323137396665623766376662353562653865623265
363436353864306531623062613637313934656336346663643834386137653965
...

Bu format Git'e commit edilmesi güvenli olan bir içeriktir.

Var olan bir dosyayı şifrelemek için:

ansible-vault encrypt group_vars/production.yml

Şifreli bir dosyayı düzenlemek için:

ansible-vault edit vault/secrets.yml

Bu komut dosyayı geçici olarak bellekte deşifre eder, $EDITOR içinde düzenlemenizi sağlar ve kaydederken yeniden şifreler. Deşifre edilmiş içerik hiçbir zaman diske yazılmaz.

Şifreli bir dosyanın içeriğini terminalde görmek için:

ansible-vault view vault/secrets.yml

Mevcut Vault parolasını değiştirmek için:

ansible-vault rekey vault/secrets.yml

Playbook'ta Kullanım

Şifrelenmiş bir dosyayı playbook içinde kullanmak için vars_files içinde belirtmek yeterlidir:

- name: Veritabanını yapılandır
  hosts: dbservers
  become: true
  vars_files:
    - vault/secrets.yml

  tasks:
    - name: Veritabanı konfigürasyonunu deploy et
      ansible.builtin.template:
        src: templates/db.conf.j2
        dest: /etc/myapp/db.conf
        owner: root
        group: root
        mode: "0640"

db.conf.j2 şablonu içinde {{ db_password }} gibi değişkenler normal şekilde kullanılabilir.

Playbook çalıştırılırken Vault parolası iki yöntemle sağlanabilir:

# Parola interaktif olarak sorulur; terminalde görünmez
ansible-playbook playbook.yml --ask-vault-pass

# Parola dosyadan okunur; CI/CD ortamları için uygundur
ansible-playbook playbook.yml --vault-password-file ~/.vault_pass.txt

~/.vault_pass.txt dosyasının izinlerini chmod 600 ile kısıtlamak zorunludur. Bu dosya .gitignore'a mutlaka eklenmelidir; Vault parolasının kendisi hiçbir zaman versiyon kontrolüne girmemelidir.

Tek Değer Şifreleme: encrypt_string

Tüm bir dosyayı şifrelemek yerine, tek bir değeri şifreleyip doğrudan playbook veya vars: bloğu içine gömmek de mümkündür. Bu yaklaşım, şifrelenecek değer sayısı az olduğunda ya da mevcut bir dosyanın yapısını bozmak istemediğinizde işe yarar.

ansible-vault encrypt_string 'G!zl!P@rola2024' --name 'db_password'

Çıktı şöyle görünür:

db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386134653765386564653736353265323639323535363833386664363335
          ...

Bu çıktıyı doğrudan vars: bloğuna veya group_vars dosyasına yapıştırabilirsiniz:

- name: Veritabanını yapılandır
  hosts: dbservers
  vars:
    db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386134653765386564653736353265323639323535363833386664363335
          ...

Tüm dosyayı şifrelemek ile encrypt_string kullanmak arasında seçim yaparken şunu göz önünde bulundurun: tüm dosya şifrelendiğinde git diff değişiklikleri anlamlı şekilde gösteremez. encrypt_string kullanıldığında ise dosyanın geri kalanı okunabilir kalır ve yalnızca hassas değer şifreli görünür.

no_log: true ile Çıktı Koruması

Vault ile şifrelenmiş bir değişken kullansanız da bu değer bazı durumlarda Ansible çıktısına düşebilir. Özellikle bir komut başarısız olduğunda hata mesajının içinde değişken değerleri açık metin olarak görünebilir. Bunu engellemek için hassas değişken kullanan görevlere no_log: true eklenmesi gerekir:

- name: Veritabanı kullanıcı parolasını güncelle
  ansible.builtin.command: >
    mysql -u root -e
    "ALTER USER 'uygulama'@'localhost' IDENTIFIED BY '{{ db_password }}';"
  no_log: true

no_log: true olan bir görev çalıştırıldığında Ansible çıktıda yalnızca şunu gösterir:

TASK [Veritabanı kullanıcı parolasını güncelle] ***
ok: [db1]

Parametreler, komut içeriği ve değişken değerlerinden hiçbiri çıktıya yansımaz. Vault ile yönetilen değişkenleri kullanan tüm görevlerde bu direktifi kullanmak bir en iyi pratik olarak benimsenmelidir.

Çoklu Vault Desteği

Aynı playbook içinde farklı ortamlara ait farklı Vault parolaları kullanmak gerektiğinde Vault ID mekanizması devreye girer. Örneğin geliştirme ve üretim ortamları için ayrı parolalar tanımlanabilir; bu sayede geliştirici ekibinin üretim vault dosyasına erişimi olmaz.

# Şifreleme aşamasında vault ID belirtilir
ansible-vault encrypt --vault-id dev@prompt group_vars/dev.yml
ansible-vault encrypt --vault-id prod@prompt group_vars/prod.yml

# Çalıştırma aşamasında her iki parola da sağlanır
ansible-playbook site.yml \
  --vault-id dev@prompt \
  --vault-id prod@prompt

CI/CD pipeline'larında @prompt yerine parola dosyasının yolu verilir:

ansible-playbook site.yml \
  --vault-id dev@/run/secrets/dev_vault_pass \
  --vault-id prod@/run/secrets/prod_vault_pass

Vault Nasıl Çalışır?

Şifreleme aşamasında Ansible, girdiğiniz paroladan PBKDF2 ile bir anahtar türetir ve bu anahtarı kullanarak dosya içeriğini AES256 algoritmasıyla şifreler. Oluşan şifreli blok $ANSIBLE_VAULT;1.1;AES256 başlığıyla işaretlenir ve onaltılık (hex) formatında saklanır.

Çalışma zamanında Ansible playbook'u parse ederken şifreli dosyaları tespit eder. Sağlanan parola kullanılarak ilgili dosyalar bellekte deşifre edilir ve değişkenler normal değişkenler gibi işlenir. Deşifre edilen içerik hiçbir zaman diske yazılmaz; yalnızca süreç belleğinde yaşar ve playbook tamamlandığında silinir.

Dikkat Edilmesi Gerekenler

Vault şifreli dosyaları Git'e commit etmek güvenlidir; zaten bu amaçla tasarlanmıştır. Asla yapılmaması gereken şey Vault parolasının kendisini repo'ya eklemektir.

Vault parolası kaybolursa şifreli dosyaların içeriğine erişmek mümkün değildir. AES256 nedeniyle brute-force saldırısı pratik değildir. Bu yüzden Vault parolalarını güvenli bir parola yöneticisinde saklamak ve CI/CD ortamlarında HashiCorp Vault veya AWS Secrets Manager gibi profesyonel secret yönetim araçlarıyla entegre etmek uzun vadede doğru yaklaşımdır.

$ quiz

no_log: true neden kullanılır?

$ quiz

ansible-vault encrypt_string ile tüm dosya şifreleme arasındaki pratik fark nedir?


changed_when ve failed_when

block/rescue/always yapısına geçmeden önce bu iki direktifi anlamak gerekir. Hata yönetiminin önemli bir kısmı rescue bloğuna düşmeden önce bu katmanda çözülür.

changed_when

Önceki yazılarda idempotency kavramını ele almıştık: Ansible amaca özel modüller (ansible.builtin.package, ansible.builtin.service vb.) kullandığında sistemin mevcut durumunu kontrol eder ve yalnızca gerçekten bir değişiklik yaptığında changed döner. Ancak ansible.builtin.command ve ansible.builtin.shell modülleri bu kurala uymaz; Ansible bu komutların ne yaptığını bilemez ve her çalıştırmada changed döner.

Bu davranışın iki somut zararı vardır. Birincisi, handler'lar gereksiz yere tetiklenir; nginx konfigürasyonunu doğrulayan bir komut her çalıştırıldığında nginx gereksiz yere yeniden başlar. İkincisi, playbook çıktısı her çalıştırmada değişiklik olmuş gibi görünür ve gerçek değişiklikleri takip etmek zorlaşır.

changed_when bu sorunu çözer. Görevin ne zaman changed sayılacağını Ansible'a açıkça söylemiş olursunuz:

- name: nginx konfigürasyonunu doğrula
  ansible.builtin.command: nginx -t
  register: nginx_test
  changed_when: false

changed_when: false bu görevin hiçbir zaman changed dönmeyeceğini belirtir. Yalnızca doğrulama amacıyla çalışan görevlerde standart kullanımdır.

Komutun çıktısına göre koşullu da kullanılabilir. Puppet agent çalıştırmayı düşünelim: Puppet'ın çıkış kodu 0 "değişiklik yok", 2 "değişiklik yapıldı", diğerleri "hata" anlamına gelir. Bu sözleşmeyi Ansible'a şöyle aktarabilirsiniz:

- name: Puppet agent'ı çalıştır
  ansible.builtin.command: puppet agent --test
  register: puppet_result
  changed_when: puppet_result.rc == 2
  failed_when: puppet_result.rc not in [0, 2]

failed_when

failed_when bir görevin ne zaman başarısız sayılacağını tanımlar. Bazı komutlar sıfır dışında bir çıkış kodu döndürse de bu durum her zaman gerçek bir hata değildir. Ya da tam tersi: komut başarıyla tamamlanmış gibi görünse de çıktıda belirli bir ifade varsa bunu hata olarak ele almak gerekebilir.

- name: Servis durumunu kontrol et
  ansible.builtin.command: systemctl is-active nginx
  register: nginx_status
  failed_when: false

failed_when: false bu görevi hiçbir zaman başarısız saymaz; komut hangi çıkış kodunu döndürürse döndürsün Ansible devam eder. ignore_errors: true ile benzer etki yaratır ancak semantik olarak daha nettir.

Çıktıya göre başarısızlık tanımlamak da mümkündür:

- name: Uygulama sağlık kontrolü
  ansible.builtin.uri:
    url: http://localhost:8080/health
    return_content: true
  register: health_response
  failed_when: >
    health_response.status != 200 or
    'healthy' not in health_response.content

Birden fazla koşulla liste formatı kullanıldığında her satır and operatörüyle birleştirilmiş sayılır; yani tüm koşulların aynı anda doğru olması halinde görev başarısız sayılır:

- name: Uygulama çıktısını kontrol et
  ansible.builtin.command: /opt/myapp/healthcheck.sh
  register: health_result
  failed_when:
    - health_result.rc != 0
    - "'DEGRADED' not in health_result.stdout"

Bu görev yalnızca hem çıkış kodu sıfır dışındaysa hem de çıktıda DEGRADED ifadesi yoksa başarısız sayılır. İkisinden yalnızca biri doğruysa Ansible başarısız saymaz. or mantığı için Jinja2 ifade formatı kullanılır:

  failed_when: >
    health_result.rc != 0 or
    'ERROR' in health_result.stderr

changed_when ve failed_when birbirinden bağımsızdır ve aynı görevde birlikte kullanılabilir:

- name: Uygulama durumunu kontrol et
  ansible.builtin.command: /opt/app/status.sh
  register: app_status
  changed_when: false
  failed_when: app_status.rc not in [0, 1]

Bu görev hiçbir zaman changed dönmez ve yalnızca çıkış kodu 0 veya 1 dışında bir değer olduğunda başarısız sayılır.

$ quiz

failed_when liste formatında birden fazla koşul tanımlanırsa bu koşullar nasıl değerlendirilir?


Hata Yönetimi: block, rescue ve always

Ansible'da bir görev başarısız olduğunda playbook varsayılan olarak durur ve sonraki görevler çalışmaz. Çoğu durumda bu istenen davranıştır. Ancak bazı senaryolarda hatayı yakalamak, alternatif adımlar atmak veya başarı/başarısızlıktan bağımsız olarak temizlik işlemi yapmak gerekir. block/rescue/always yapısı bu ihtiyaç için tasarlanmıştır.

Yapının Mantığı

tasks:
  - block:
      # Ana görevler buraya yazılır
    rescue:
      # block içinde hata oluştuğunda çalışır
    always:
      # Başarı veya başarısızlıktan bağımsız olarak her zaman çalışır

block içindeki herhangi bir görev başarısız olduğunda Ansible o görevde durur ve rescue bloğuna geçer. block başarıyla tamamlanırsa rescue hiç çalışmaz. always ise her iki durumda da çalışır; block başarılı olsa da olmasa da, rescue devreye girse de girmese de.

Bu yapı Python'daki try/except/finally ile birebir aynı mantığa sahiptir. Python biliyor ve bu analojiye aşinaysanız, block try'a, rescue except'e, always finally'ye karşılık gelir.

Gerçek Bir Senaryo: Uygulama Versiyonu Güncelleme

Aşağıdaki örnek, üretim ortamında bir uygulama versiyonu güncellenirken hata oluşması durumunda otomatik olarak önceki versiyona dönen bir yapıyı gösteriyor. Bu tür deployment senaryoları block/rescue/always'in en çok değer kattığı yerlerdir.

- name: Uygulama versiyonu güncelleme
  hosts: appservers
  become: true
  vars:
    app_name: myapp
    new_version: "2.4.1"
    deploy_path: /opt/myapp
    backup_path: /opt/myapp_backup

  tasks:
    - name: Mevcut versiyonu yedekle ve yeni versiyonu deploy et
      block:
        - name: Mevcut kurulumu yedekle
          ansible.builtin.copy:
            src: "{{ deploy_path }}/"
            dest: "{{ backup_path }}/"
            remote_src: true
            mode: preserve

        - name: Servisi durdur
          ansible.builtin.service:
            name: "{{ app_name }}"
            state: stopped

        - name: Yeni versiyonu deploy et
          ansible.builtin.unarchive:
            src: "https://releases.myapp.io/{{ new_version }}/myapp.tar.gz"
            dest: "{{ deploy_path }}"
            remote_src: true

        - name: Veritabanı migration'larını çalıştır
          ansible.builtin.command:
            cmd: "{{ deploy_path }}/bin/migrate"
          register: migration_result
          changed_when: "'Applied' in migration_result.stdout"

        - name: Servisi başlat
          ansible.builtin.service:
            name: "{{ app_name }}"
            state: started

        - name: Uygulamanın sağlıklı başladığını doğrula
          ansible.builtin.uri:
            url: "http://localhost:8080/health"
            status_code: 200
          retries: 5
          delay: 10

      rescue:
        - name: Hata oluştu; önceki versiyona dönülüyor
          ansible.builtin.debug:
            msg: >
              {{ new_version }} versiyonuna geçiş başarısız oldu.
              Önceki versiyon geri yükleniyor.

        - name: Servisi durdur
          ansible.builtin.service:
            name: "{{ app_name }}"
            state: stopped
          ignore_errors: true

        - name: Yedeği geri yükle
          ansible.builtin.copy:
            src: "{{ backup_path }}/"
            dest: "{{ deploy_path }}/"
            remote_src: true
            mode: preserve

        - name: Servisi yedekten başlat
          ansible.builtin.service:
            name: "{{ app_name }}"
            state: started

      always:
        - name: Deployment sonucunu logla
          ansible.builtin.lineinfile:
            path: /var/log/deployments.log
            line: >
              {{ ansible_date_time.iso8601 }} |
              {{ inventory_hostname }} |
              {{ app_name }} |
              {{ new_version }} |
              {{ 'SUCCESS' if ansible_failed_task is not defined else 'FAILED' }}
            create: true
            mode: "0644"

Bu yapıda block içindeki herhangi bir adım başarısız olursa (yeni versiyon indirilemezse, migration başarısız olursa, sağlık kontrolü geçilemezse) Ansible rescue bloğuna geçer, servisi durdurur ve yedeği geri yükler. always bloğu ise her iki durumda da çalışır ve deployment sonucunu loglar.

Dikkat Edilmesi Gereken Noktalar

block içinde ignore_errors: true kullanmak rescue bloğunu devre dışı bırakır. Hata bastırıldığı için Ansible görevi başarılı saymış olur ve rescue hiç tetiklenmez. Bu iki yapıyı birlikte kullanmaktan kaçınılmalıdır.

rescue bloğunun kendisi de başarısız olabilir. rescue içinde bir görev hata verirse Ansible playbook'u durdurur; iç içe block/rescue yapısıyla bu durum da ele alınabilir.

always bloğu handler'lardan önce çalışır. Eğer always içindeki bir görevden sonra handler'ların tetiklenmesi gerekiyorsa bu sıraya dikkat etmek gerekir.

rescue bloğu içinde ignore_errors: true serbestçe kullanılabilir. Yukarıdaki örnekte rollback sırasında servisi durdurma adımına ignore_errors: true eklenmişti; çünkü servis zaten durmuş olabilir ve bu adımın başarısız olması rollback'i engellememelidir.

Ne Zaman Kullanılmalı?

Kritik servis kurulumları ve çok adımlı deployment'lar block/rescue/always için en uygun senaryolardır. Özellikle üretim ortamında bir şeyin yanlış gitmesi durumunda sistemin tutarlı bir durumda kalması gereken her yerde bu yapıyı kullanmak yerinde olur. Deployment log'u, bildirim veya temizlik gibi mutlaka çalışması gereken adımlar için always idealdir.

$ quiz

block içindeki bir görevde ignore_errors: true kullanılırsa ne olur?


Rol Doğrulama: ansible-lint

Rolleri yazdıktan sonra doğrulama adımı çoğunlukla atlanır. Ancak ansible-lint, rolünüzün Ansible en iyi pratiklerine uygunluğunu kontrol eden ve birçok hatayı çalıştırmadan önce yakalayan bir araçtır.

pip install ansible-lint --break-system-packages
ansible-lint roles/apache/

Rol yapısına özgü sık karşılaşılan uyarılar şunlardır:

fqcn[action-core]: Use FQCN for builtin actions (ansible.builtin.apt)
name[missing]: All tasks should be named
yaml[truthy]: Use "true" or "false" instead of "yes" or "no"
risky-file-permissions: File permissions unset or incorrect

fqcn[action-core] uyarısı özellikle dikkat edilmesi gereken noktadır. apt yazmak yerine ansible.builtin.apt yazmak, hangi collection'dan geldiğini açıkça belirtir ve olası isim çakışmalarını önler. Bu kural roller için de geçerlidir ve ansible-lint bunu otomatik olarak kontrol eder.

risky-file-permissions uyarısı ise ansible.builtin.copy veya ansible.builtin.template görevlerinde mode: belirtilmediğinde tetiklenir. Dosya izinlerini açıkça belirtmemek, farklı sistemlerde farklı varsayılan değerler nedeniyle beklenmedik güvenlik sorunlarına yol açabilir.

Projeye özel kurallar .ansible-lint dosyasıyla tanımlanabilir:

# .ansible-lint
skip_list:
  - yaml[line-length]
warn_list:
  - experimental

ansible-lint ile yamllint'i birlikte kullanmak en sağlıklı yaklaşımdır: yamllint genel YAML sözdizimini, ansible-lint Ansible'a özgü en iyi pratikleri denetler. Önceki yazılarda yamllint kullanımını ayrıntılı ele aldık.


Sık Sorulan Sorular

Rolleri kullanmak zorunda mıyım?

Hayır. Küçük ve tek amaçlı playbook'lar için roller zorunlu değildir. Ancak aynı yapılandırmayı birden fazla projede kullanmaya başladığınızda veya ekip büyüdükçe roller kaçınılmaz hale gelir. "Çalışıyor" ile "sürdürülebilir" arasındaki fark genellikle rollerin varlığıyla belirlenir.

Vault şifremi unutursam ne olur?

Vault parolası kaybolursa şifreli dosyaların içeriğine erişmek mümkün değildir. AES256 nedeniyle brute-force saldırısı pratik değildir. Bu yüzden Vault parolalarını güvenli bir parola yöneticisinde saklamak ve CI/CD ortamlarında HashiCorp Vault veya AWS Secrets Manager gibi araçlarla entegre etmek uzun vadede doğru yaklaşımdır.

block içindeki her göreve ayrı ayrı hata yönetimi eklemek yerine neden block kullanmalıyım?

Her göreve ignore_errors veya failed_when eklemek dağınık ve hataya açık bir yapı yaratır. block/rescue/always hata yönetim mantığını tek bir yerde toplar, okunabilirliği artırır ve rollback gibi çok adımlı tepkileri temiz bir şekilde ifade etmenizi sağlar.

Rol bağımlılıkları sonsuz döngüye girebilir mi?

Teknik olarak mümkündür. A rolü B'ye, B rolü A'ya bağımlıysa döngüsel bağımlılık oluşur. Ansible bunu tespit eder ve hata verir. meta/main.yml içindeki bağımlılıkları dikkatli tanımlamak yeterlidir.

group_vars, host_vars, vars, defaults... Hangisini ne zaman kullanmalıyım?

Yapı Kullanım Amacı Öncelik
defaults/ Override edilebilir başlangıç değerleri En düşük
group_vars/ Sunucu grubuna özgü değerler Orta
host_vars/ Tek bir sunucuya özgü değerler Orta-yüksek
vars/ Değişmemesi gereken sabit değerler Yüksek
extra-vars (-e) CLI üzerinden geçici override En yüksek

import_tasks ve include_tasks arasındaki fark nedir?

Tıpkı import_role ve include_role ayrımında olduğu gibi: import_tasks parse zamanında yüklenir ve statik bir yapıdır; include_tasks çalışma zamanında yüklenir ve when ile koşullu kullanılabilir. Rollerin tasks/main.yml dosyasından alt dosyaları çağırırken genellikle import_tasks tercih edilir çünkü tag'lerle daha iyi çalışır.


Bu yazıda Ansible rollerinin yapısını ve çalışma prensibini, görevleri dosyalara bölmeyi, Vault ile hassas veri yönetimini, changed_when ve failed_when direktiflerini ve block/rescue/always ile hata yönetimini ele aldık. Bu kavramlar birlikte kullanıldığında üretim ortamına uygun, sürdürülebilir ve güvenli bir Ansible yapısının temelini oluşturur. Bir sonraki yazıda Ansible'ı CI/CD pipeline'larına entegre etmeyi ve AWX üzerinde iş akışlarını yönetmeyi inceleyeceğiz.