kertenkerem_
· DevOps · 5 dk okuma

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:

  1. Dependency: Gerekli harici roller indirilir.
  2. Lint: Ansible-lint ile kod standartları taranır.
  3. Destroy/Create: Eski test container’ları silinir ve yenileri ayağa kaldırılır.
  4. Converge: Playbook’unuz ilk kez çalıştırılır (kurulumlar yapılır).
  5. 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.
  6. 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!