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: Sadece bir config dosyasındaki 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. İşte bu noktada, modern iac (Infrastructure as Code) felsefesinin kalbi olan idempotent (eşgüçlü) kavramı devreye giriyor. Gerçek bir altyapı otomasyon süreci, 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 makalede, 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” İllü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 doğası gereği her çalıştığında sisteme bir etki ettiklerini varsayarlar ve her zaman sarı (changed: true) dönerler.
Bu durum sadece göz zevkimizi bozmakla kalmaz; Ansible handler’larının (örneğin servis restart işlemleri) gereksiz yere tetiklenmesine yol açarak production ortamında kesintilere sebep olur. İşte 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
Farz edelim ki bir uygulamanın CLI aracıyla bir konfigürasyon yapacaksınız. Eğer bu konfigürasyon zaten yapılmışsa, komutu tekrar çalıştırmamalı veya ç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 profesyonelce revize edelim. Önce eklentinin durumunu sorgulayalım, ardından sadece eklenti aktif değilse işlem yapalım ve durumu Ansible’a doğru şekilde 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
Burada ne yaptık? İlk task’ta failed_when: false ve changed_when: false diyerek, sadece bir durum sorguladığımızı ve bu sorgunun sistemi değiştirmediğini, ayrıca grep başarısız olsa bile (yani plugin kurulu değilse) pipeline’ın kırılmaması gerektiğini belirttik. İkinci task’ta ise sadece plugin aktif değilse çalıştık ve çıktıdaki spesifik bir loga bakarak değişikliğin gerçekten gerçekleşip gerçekleşmediğini teyit ettik.
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.
Ancak projelerde sıklıkla 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, sadece hassas değerleri şifreleyip referans vermek çok daha temiz bir yaklaşımdır.
Tek Satır Şifreleme (vault_encrypted) Pratiği
Tüm dosyayı şifrelemek yerine, sadece gizli tutmak istediğiniz değeri terminalde şifreleyin:
ansible-vault encrypt_string 'super_secret_db_password' --name 'vault_db_password'
Bu komut size YAML formatında şifrelenmiş bir blok verecektir. Bu 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 bir şekilde okutmak için --vault-password-file parametresini veya ç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ım dünyasındaki Unit/Integration test kavramının IaC dünyasındaki karşılığı Molecule‘dur. 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 dahil etmek 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 kritik dosya testlerin nasıl koşulacağını belirleyen molecule.yml dosyasıdır:
# 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ırasıyla 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. Eğer herhangi bir task “changed” dönerse test başarısız sayılır! İşte 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 İndirmek
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 durum ciddi bir performans darboğazı (bottleneck) yaratır. Neyse ki birkaç ince ayarla bu süreyi dramatik şekilde azaltabiliriz.
SSH Pipelining Aktifleştirme
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 bu işlemler kopyalama adımı atlanarak doğrudan SSH oturumu üzerinden (piped) gerçekleştirilir.
# 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 sistem bilgilerini (IP, CPU, RAM, disk durumları vb.) toplar (Gathering Facts). Eğer playbook’unuzda bu değişkenleri (örn: ansible_distribution) kullanmıyorsanız, bu adımı tamamen kapatın:
- hosts: webservers
gather_facts: false
tasks:
# ...
Eğer 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ış
Yazması kolay, yönetmesi zor bir araçtır Ansible. Onu sıradan bir shell script tetikleyicisinden profesyonel bir iac aracına dönüştüren şey, sizin yazdığınız playbook’lardaki detay seviyesidir. changed_when ile kontrolü elinizde tutmak, secrets yönetiminde hassas davranmak, Molecule ile test güvencesi sağlamak ve performans parametrelerini optimize etmek sizi ekipten bir adım öne çıkaracaktır. Altyapınızın her zaman tahmin edilebilir, kararlı ve hızlı kalması dileğiyle!