Ansible ile Idempotent Playbook Yazmanın İncelikleri
Sektörde beş yılı deviren her devops mühendisinin ortak kabuslarından biri, “benden önce yazılmış” ve her çalıştığında farklı bir sürpriz sunan Ansible playbook’larıdır. Hepimiz o yollardan geçtik: Bir config dosyasındaki tek parametreyi değiştirmek için çalıştırdığınız playbook sunucudaki üç servisi yeniden başlatır, SSL sertifikalarını sıfırlar ve deployment pipeline’ını kilitler.
Modern IaC (Infrastructure as Code) felsefesinin kalbinde idempotent (eşgüçlü) kavramı yatar. Gerçek bir altyapı otomasyonu, aynı playbook’u ister 1 ister 1000 kez çalıştırın, hedef sistemi her zaman tam olarak hedeflediğiniz kararlı durumda (desired state) bırakmalıdır. Bu yazıda işin “YAML yazmaktan” çıkıp gerçek bir yazılım mühendisliği disiplinine dönüştüğü o ince çizgiyi inceleyeceğiz.
1. “Her şey yolunda” illüzyonu: changed_when ve failed_when
Ansible modüllerinin büyük çoğunluğu idempotent çalışacak şekilde tasarlanmıştır. Örneğin ansible.builtin.apt veya ansible.builtin.template modülleri, hedef sistemin durumunu kontrol eder ve bir değişiklik gerekmiyorsa yeşil yanarak yoluna devam eder. Ancak iş shell veya command modüllerine geldiğinde Ansible körleşir. Bu modüller her çalıştıklarında sisteme bir etki ettiklerini varsayar ve her zaman sarı (changed: true) döner.
Bu yalnızca göz zevkimizi bozmaz, Ansible handler’larının (örneğin servis restart işlemleri) gereksiz yere tetiklenmesine yol açarak production ortamında kesintilere sebep olur. Bu kontrolsüzlüğü dizginlemek için elimizdeki en güçlü silahlar changed_when ve failed_when.
İyi, kötü ve çirkin: shell modülünü terbiye etmek
Bir uygulamanın CLI aracıyla konfigürasyon yapacağınızı düşünelim. Konfigürasyon zaten yapılmışsa komutu tekrar çalıştırmamalı, çalıştırsak bile Ansible’a “hey, burada yeni bir şey yapmadın, sakin ol” demeliyiz.
Aşağıdaki kötü pratiğe bir göz atalım:
# KÖTÜ PRATİK (Her çalışmada "changed" döner, handler'ları tetikler)
- name: Enable custom application plugin
ansible.builtin.shell: "myapp-cli plugin enable prometheus"
register: plugin_output
notify: Restart MyApp
Şimdi bunu düzeltelim. Önce eklentinin durumunu sorgulayalım, eklenti aktif değilse işlem yapalım ve durumu Ansible’a doğru bildirelim:
# İYİ PRATİK (Idempotent ve güvenli)
- name: Check if prometheus plugin is already enabled
ansible.builtin.shell: "myapp-cli plugin list | grep -E '^prometheus.*enabled'"
register: plugin_status
failed_when: false
changed_when: false
- name: Enable custom application plugin if not enabled
ansible.builtin.shell: "myapp-cli plugin enable prometheus"
when: plugin_status.rc != 0
register: enable_result
changed_when: "'plugin successfully enabled' in enable_result.stdout"
notify: Restart MyApp
İlk task’ta failed_when: false ve changed_when: false diyerek yalnızca bir durum sorguladığımızı ve bu sorgunun sistemi değiştirmediğini belirttik. Ayrıca grep başarısız olsa bile (yani plugin kurulu değilse) pipeline’ın kırılmaması gerektiğini söyledik. İkinci task yalnızca plugin aktif değilse çalışıyor ve çıktıdaki spesifik bir loga bakarak değişikliğin gerçekten olup olmadığını teyit ediyor.
2. Sırları sızdırmadan yönetmek: Ansible Vault ve best practice’ler
IaC kodlarınızı Git reposunda tutuyorsanız (ki tutmalısınız), API key’ler, veritabanı şifreleri veya SSL private key’ler gibi hassas verileri (secrets) asla düz metin (plain text) olarak commit etmemelisiniz. Ansible bu problemi çözmek için dahili bir şifreleme mekanizması olan Ansible Vault’u sunar.
Projelerde sık yapılan hata, tüm group_vars/all.yml dosyasını toptan şifrelemektir. Bu, kod incelemelerinde (Code Review) hangi değişkenlerin değiştiğini görmeyi imkansız hale getirir. Bunun yerine yalnızca hassas değerleri şifreleyip referans vermek çok daha temizdir.
Tek satır şifreleme (vault_encrypted) pratiği
Tüm dosyayı şifrelemek yerine yalnızca gizli tutmak istediğiniz değeri terminalde şifreleyin:
ansible-vault encrypt_string 'super_secret_db_password' --name 'vault_db_password'
Bu komut YAML formatında şifrelenmiş bir blok verir. Bloğu değişken dosyanıza güvenle yapıştırabilirsiniz:
# group_vars/production.yml
db_username: "app_user" # Düz metin olarak kalmasında sakınca yok
db_password: "{{ vault_db_password }}" # Şifrelenmiş değere referans
# Şifrelenmiş blok:
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
3565393065646134373463326139613661646231363539303362333633643730306265353866
62376663363236373961633535366430333766396662366264370a3736363339386333343632
3431663030313534663831633633636636316262333066366432366164343164376435383561
32303533663731306132373335623233390a3666363435303964313032393335396161613936
653063616664653838323630
Playbook’u çalıştırırken Vault şifresini güvenli biçimde okutmak için --vault-password-file parametresini ya da çevresel değişkenleri (environment variables) kullanabilirsiniz. CI/CD pipeline’larında bu şifreyi runner’a bir secret olarak tanımlamak en güvenli yoldur.
3. Altyapıyı test etmek: Molecule ile TDD yaklaşımı
“Yazdığım playbook production’da çalışır mı?” sorusunun cevabı hiçbir zaman “deneyip görelim” olmamalıdır. Yazılımdaki unit/integration test kavramının IaC’deki karşılığı Molecule’dür. Molecule, Ansible rollerinizi izole ortamlarda (genellikle Docker veya Podman üzerinde, bazen de Vagrant/AWS’te) otomatik olarak ayağa kaldırır, playbook’unuzu çalıştırır (idempotency testi dahil) ve ardından ortamı temizler.
Molecule kurulumu ve örnek senaryo
Molecule’ü projenize eklemek için Docker driver’ı ile birlikte yükleyin:
pip install molecule molecule-plugins[docker] ansible-lint
Bir Ansible rolü içinde Molecule senaryosu başlatmak için:
molecule init scenario --driver-name docker
Bu komut rolünüzün altında molecule/default adında bir klasör oluşturur. Buradaki en önemli dosya, testlerin nasıl koşulacağını belirleyen molecule.yml:
# molecule/default/molecule.yml
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: test-ubuntu-target
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
provisioner:
name: ansible
playbooks:
converge: ${MOLECULE_PLAYBOOK:-converge.yml}
verifier:
name: ansible
molecule test komutunu çalıştırdığınızda şu adımlar sırayla işletilir:
- Dependency: Gerekli harici roller indirilir.
- Lint: Ansible-lint ile kod standartları taranır.
- Destroy/Create: Eski test container’ları silinir ve yenileri ayağa kaldırılır.
- Converge: Playbook’unuz ilk kez çalıştırılır (kurulumlar yapılır).
- Idempotence: Playbook ikinci kez çalıştırılır. Herhangi bir task “changed” dönerse test başarısız sayılır. Gerçek idempotent testi budur.
- Verify: Belirlediğiniz test script’leri (örneğin servis gerçekten çalışıyor mu kontrolü) koşturulur.
4. Performans optimizasyonu: Dakikaları saniyelere indirmek
Ansible, mimarisi gereği her bir task için hedef makineye SSH bağlantısı açar, modülü transfer eder, çalıştırır ve sonucu geri alır. Yüzlerce sunucudan oluşan bir envanterde bu ciddi bir performans darboğazı (bottleneck) yaratır. Neyse ki birkaç ince ayarla bu süreyi belirgin biçimde azaltabiliriz.
SSH pipelining’i aktifleştirmek
Ansible’ın varsayılan davranışında, modül dosyaları önce hedef makineye kopyalanır ve ardından SSH üzerinden çalıştırılır. SSH Pipelining aktifleştirildiğinde ise kopyalama adımı atlanır ve işlemler doğrudan SSH oturumu üzerinden (piped) yapılır.
# ansible.cfg
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Not: Pipelining kullanabilmek için hedef sunuculardaki /etc/sudoers dosyasında requiretty seçeneğinin aktif olmaması gerekir (modern dağıtımlarda varsayılan olarak pasiftir).
Fact gathering mekanizmasını optimize etmek
Ansible her playbook başlangıcında hedef sisteme dair tüm bilgileri (IP, CPU, RAM, disk durumları vb.) toplar (Gathering Facts). Playbook’unuzda bu değişkenleri (örn: ansible_distribution) kullanmıyorsanız bu adımı tamamen kapatın:
- hosts: webservers
gather_facts: false
tasks:
# ...
Bazı task’lar için bu bilgilere ihtiyacınız varsa akıllı cache (fact caching) mekanizmasını devreye sokarak her çalıştırmada bu süreyi tekrar ödemekten kurtulabilirsiniz:
# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 86400 # 24 saat cache'le
Özet ve kapanış
Ansible yazması kolay, yönetmesi zor bir araçtır. Onu sıradan bir shell script tetikleyicisinden profesyonel bir IaC aracına dönüştüren şey, yazdığınız playbook’lardaki detay seviyesidir. changed_when ile kontrolü elinizde tutmak, secrets yönetiminde titiz davranmak, Molecule ile test güvencesi sağlamak ve performans parametrelerini optimize etmek sizi ekipten bir adım öne çıkaracaktır. Altyapınız hep tahmin edilebilir, kararlı ve hızlı kalsın!