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.
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:
defaults/— Varsayılan değişkenler yüklenir. En düşük önceliğe sahiptirler.vars/— Sabit değişkenler yüklenir. Daha yüksek önceliğe sahiptirler.meta/— Bağımlılıklar kontrol edilir. Tanımlı bağımlı roller önce çalıştırılır.tasks/main.yml— Görevler sırayla çalıştırılmaya başlar.handlers/—notifyçağrıları sonucunda tetiklenmeye hazır hale gelir. Tüm görevler tamamlandıktan sonra çalışır.templates/vefiles/—templateveyacopygibi 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:
- Playbook'un bulunduğu dizindeki
roles/klasörü ansible.cfgiçinde tanımlıroles_path~/.ansible/roles//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.
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.
no_log: true neden kullanılır?
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.
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.
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.
