Idempotency Nedir ve Neden Önemlidir?

Otomasyon dünyasına adım attığınızda, er ya da geç karşınıza çıkan bir kavram var: idempotency. Türkçeye “kuvvetini yitirmeme” ya da “etkisizlik” olarak çevirebiliriz ama sysadmin dünyasında çok daha somut bir anlamı var. Kısaca şöyle açıklayabiliriz: Bir işlemi bir kez çalıştırmakla yüz kez çalıştırmak aynı sonucu veriyorsa, o işlem idempotent’tir. Kulağa basit geliyor, değil mi? Ama bu prensip, özellikle Ansible ile çalışırken altyapınızın ne kadar güvenilir ve öngörülebilir olacağını doğrudan belirliyor.

Idempotency’yi Gerçek Hayattan Bir Örnekle Anlamak

Düşünün ki sabah uyandınız ve ışığı açtınız. Işık zaten açıksa tekrar açmaya çalıştığınızda ne olur? Hiçbir şey. Işık açık kalır. Ama aynı düğmeye iki kez bastığınızda ışık önce kapanır, sonra açılır. Yani ikinci davranış idempotent değildir.

Bunu bir script örneğiyle somutlaştıralım. Klasik bir Bash scripti yazıyorsunuz:

#!/bin/bash
# Idempotent OLMAYAN bir örnek
useradd deploy_user
echo "deploy_user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

Bu scripti ikinci kez çalıştırdığınızda ne olur? useradd komutu hata verir çünkü kullanıcı zaten var. /etc/sudoers dosyasına ise aynı satır tekrar eklenir. Artık dosyanızda mükerrer girişler var ve bu potansiyel bir güvenlik sorununa ya da beklenmedik davranışa yol açabilir.

Şimdi aynı işlemi idempotent hale getirelim:

#!/bin/bash
# Idempotent bir Bash örneği
if ! id "deploy_user" &>/dev/null; then
    useradd deploy_user
    echo "Kullanıcı oluşturuldu."
else
    echo "Kullanıcı zaten mevcut, atlanıyor."
fi

SUDOERS_LINE="deploy_user ALL=(ALL) NOPASSWD:ALL"
if ! grep -qF "$SUDOERS_LINE" /etc/sudoers; then
    echo "$SUDOERS_LINE" >> /etc/sudoers
    echo "Sudoers satırı eklendi."
else
    echo "Sudoers satırı zaten mevcut, atlanıyor."
fi

Bu versiyon istediğiniz kadar çalıştırabilirsiniz. Sonuç her zaman aynı olacak. İşte Ansible’ın temel felsefesi tam olarak bu.

Ansible’ın Idempotency ile İlişkisi

Ansible modülleri tasarım gereği idempotent olmak üzere yazılmıştır. Bir playbook çalıştırdığınızda Ansible önce mevcut durumu kontrol eder, ardından hedef duruma ulaşmak için gereken minimum değişikliği yapar. Zaten istenen durumda olan bir şeyi tekrar değiştirmez.

Bu, Ansible’ın çıktısında gördüğünüz ok, changed ve failed durumlarıyla doğrudan ilişkilidir:

  • ok: Görev zaten istenen durumda, hiçbir şey yapılmadı
  • changed: Bir değişiklik yapıldı, sistem istenen duruma getirildi
  • failed: Bir hata oluştu

Şimdi birkaç Ansible örneğine bakalım ve idempotency’nin nasıl çalıştığını görelim.

Paket Yönetimi

---
- name: Web sunucusu kurulumu
  hosts: webservers
  become: yes
  tasks:
    - name: Nginx paketini kur
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Nginx servisini başlat ve aktif et
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

Bu playbook’u ilk çalıştırdığınızda nginx kurulur ve başlatılır. İkinci, üçüncü, onuncu çalıştırmada Ansible “nginx zaten kurulu ve çalışıyor” der ve ok döndürür. Hiçbir gereksiz işlem yapılmaz.

Dosya ve Dizin Yönetimi

- name: Uygulama dizinlerini oluştur
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: www-data
    group: www-data
    mode: '0755'
  loop:
    - /var/www/myapp
    - /var/www/myapp/logs
    - /var/www/myapp/uploads
    - /var/log/myapp

file modülü dizinin var olup olmadığını kontrol eder. Varsa ve izinler doğruysa dokunmaz. Yoksa oluşturur. İzinler yanlışsa sadece izinleri düzeltir, dizini silip yeniden oluşturmaz. Bu davranış production ortamında çok kritik, çünkü /var/www/myapp/uploads dizininde kullanıcıların yüklediği dosyalar olabilir.

Idempotency’nin Kırıldığı Noktalar

İşte burada dikkatli olmak gerekiyor. Ansible modüllerinin büyük çoğunluğu idempotent olsa da bazı durumlar bu prensibi bozabilir.

Shell ve Command Modülleri

# Bu IDEMPOTENT DEĞİL!
- name: Veritabanını başlat
  ansible.builtin.shell: |
    cd /opt/myapp
    python manage.py migrate
    python manage.py collectstatic --noinput

shell ve command modülleri her çalıştırıldığında komutu execute eder ve her seferinde changed döndürür. Bunu idempotent hale getirmenin birkaç yolu var:

# creates parametresiyle idempotent hale getirme
- name: Veritabanını ilk kez başlat
  ansible.builtin.command:
    cmd: python manage.py migrate
    chdir: /opt/myapp
    creates: /opt/myapp/.db_initialized

- name: Migrate tamamlandı işareti oluştur
  ansible.builtin.file:
    path: /opt/myapp/.db_initialized
    state: touch
    modification_time: preserve
    access_time: preserve

Ya da changed_when ve failed_when direktiflerini kullanabilirsiniz:

- name: Django migration durumunu kontrol et
  ansible.builtin.command:
    cmd: python manage.py showmigrations --plan
    chdir: /opt/myapp
  register: migration_status
  changed_when: false  # Bu görev hiçbir zaman changed sayılmasın

- name: Bekleyen migration varsa uygula
  ansible.builtin.command:
    cmd: python manage.py migrate
    chdir: /opt/myapp
  when: "' [ ]' in migration_status.stdout"
  changed_when: migration_status.stdout != ''

Lineinfile ve Blockinfile Modülleri

Bu modüller idempotent çalışır ama yanlış kullanıldığında beklenmedik sonuçlar doğurabilir:

# DOĞRU kullanım - regexp ile idempotent
- name: SSH port ayarını güncelle
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?Ports+'
    line: 'Port 2222'
    state: present
    backup: yes

# YANLIŞ kullanım - regexp olmadan her çalıştırmada aynı satırı ekler
# (satır zaten varsa eklemez, ama eski satır da kalır)
- name: SSH port ayarı - YANLIŞ
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    line: 'Port 2222'
    state: present

Gerçek Dünya Senaryosu: Production Veritabanı Sunucusu Konfigürasyonu

Şimdi gerçekten karmaşık bir senaryo üzerinden gidelim. Bir PostgreSQL sunucusu kurup yapılandıracaksınız ve bu playbook’un production’da güvenle defalarca çalışabilmesi gerekiyor.

---
- name: PostgreSQL Sunucu Konfigürasyonu
  hosts: db_servers
  become: yes
  vars:
    pg_version: 15
    pg_data_dir: /var/lib/postgresql/15/main
    pg_max_connections: 200
    pg_shared_buffers: "4GB"
    app_db_name: "production_db"
    app_db_user: "appuser"

  tasks:
    - name: PostgreSQL repository ekle
      ansible.builtin.apt:
        deb: "https://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-15/postgresql-15_15.5-1.pgdg22.04+1_amd64.deb"
      when: ansible_os_family == "Debian"

    - name: PostgreSQL kur
      ansible.builtin.package:
        name: "postgresql-{{ pg_version }}"
        state: present

    - name: PostgreSQL servisini başlat
      ansible.builtin.service:
        name: postgresql
        state: started
        enabled: yes

    - name: PostgreSQL konfigürasyonunu güncelle
      ansible.builtin.lineinfile:
        path: "{{ pg_data_dir }}/postgresql.conf"
        regexp: "^#?{{ item.key }}\s*="
        line: "{{ item.key }} = {{ item.value }}"
        backup: yes
      loop:
        - { key: "max_connections", value: "{{ pg_max_connections }}" }
        - { key: "shared_buffers", value: "'{{ pg_shared_buffers }}'" }
        - { key: "log_statement", value: "'all'" }
        - { key: "log_min_duration_statement", value: "1000" }
      notify: Restart PostgreSQL

    - name: Uygulama veritabanını oluştur
      community.postgresql.postgresql_db:
        name: "{{ app_db_name }}"
        encoding: UTF-8
        lc_collate: tr_TR.UTF-8
        lc_ctype: tr_TR.UTF-8
        state: present
      become_user: postgres

    - name: Uygulama kullanıcısını oluştur
      community.postgresql.postgresql_user:
        name: "{{ app_db_user }}"
        password: "{{ vault_db_password }}"
        priv: "{{ app_db_name }}:ALL"
        state: present
      become_user: postgres

  handlers:
    - name: Restart PostgreSQL
      ansible.builtin.service:
        name: postgresql
        state: restarted

Bu playbook’u ikinci kez çalıştırdığınızda ne olur? PostgreSQL zaten kuruluysa ok. Veritabanı zaten varsa ok. Kullanıcı zaten varsa ok. Konfigürasyon değerleri zaten doğruysa ok. Sadece gerçekten bir şey değiştiğinde changed görürsünüz.

Idempotency Test Etme

Bir playbook yazdıktan sonra idempotent olup olmadığını test etmenin en iyi yolu şu:

# İlk çalıştırma
ansible-playbook -i inventory/production site.yml

# Hemen ardından tekrar çalıştır
ansible-playbook -i inventory/production site.yml

# İkinci çalıştırmada tüm görevler "ok" olmalı
# "changed" görüyorsanız idempotency sorunu var demektir

Bunu CI/CD pipeline’ınıza entegre edebilirsiniz:

#!/bin/bash
set -euo pipefail

echo "=== İlk playbook çalıştırması ==="
ansible-playbook -i inventory/staging site.yml

echo "=== Idempotency testi: İkinci çalıştırma ==="
RESULT=$(ansible-playbook -i inventory/staging site.yml 2>&1)

# Changed satırlarını say
CHANGED_COUNT=$(echo "$RESULT" | grep -oP 'changed=K[0-9]+' | head -1)

if [ "$CHANGED_COUNT" -gt 0 ]; then
    echo "HATA: Playbook idempotent değil! $CHANGED_COUNT değişiklik algılandı."
    echo "$RESULT"
    exit 1
else
    echo "BAŞARILI: Playbook idempotent."
fi

Ansible Check Mode ve Diff Mode

Ansible’ın --check ve --diff modları, idempotency anlayışıyla çok iyi bir uyum içinde çalışır:

# Dry-run: Hiçbir şeyi değiştirmeden ne yapacağını göster
ansible-playbook -i inventory/production site.yml --check

# Diff: Dosyalarda yapılacak değişiklikleri göster
ansible-playbook -i inventory/production site.yml --diff

# İkisini birlikte kullan
ansible-playbook -i inventory/production site.yml --check --diff

Check mode özellikle production değişikliklerinden önce “ne değişecek?” sorusunu yanıtlamak için mükemmel. Ama dikkat edin: bazı görevler check mode’da doğru çalışmaz, özellikle bir önceki görevin çıktısına bağımlı olanlar.

Idempotency ile İlgili Yaygın Hatalar

Yıllar içinde karşılaştığım en yaygın idempotency sorunlarını listeleyelim:

  • Rastgele değer kullanan görevler: Parola üretimi, UUID oluşturma gibi işlemler her çalıştırmada farklı değer üretir. Bunları bir kez oluşturup bir yere kaydedin, sonraki çalıştırmalarda o kaydedilen değeri kullanın.
  • Zaman damgası bağımlı görevler: touch /tmp/lockfile gibi komutlar her çalıştırmada dosyanın modification time’ını değiştirir. modification_time: preserve kullanın.
  • Sıralama bağımlı konfigürasyonlar: Bazı konfigürasyon dosyaları sırayı önemser. blockinfile kullanırken marker değerlerinin benzersiz olduğundan emin olun.
  • Template değişkenleri: Ansible template modülü, şablon içeriği değişmemişse dosyaya dokunmaz. Ama şablon değişkenlerini dinamik hesaplıyorsanız (örneğin tarih/saat), her çalıştırmada changed görürsünüz.
  • Package state: state: latest kullanmak idempotency’yi bozabilir çünkü yeni sürüm geldiğinde her çalıştırmada changed döner. Versiyonu sabitlemek ya da state: present kullanmak genellikle daha iyidir.

Özel Modüller ve Idempotency

Kendi Ansible modülünüzü yazıyorsanız, idempotency’yi modülün içine kodlamanız gerekir. Modülünüz şu yapıyı takip etmelidir:

  • Mevcut durumu kontrol et
  • İstenen durumla karşılaştır
  • Fark varsa değişikliği yap ve changed=True döndür
  • Fark yoksa hiçbir şey yapma ve changed=False döndür

Bu prensibi check_mode ile de uyumlu hale getirmeniz gerekir: check mode aktifken değişiklik yapma, sadece yapılacak değişikliği raporla.

Neden Bu Kadar Önemli?

Idempotency’nin neden bu denli kritik olduğunu birkaç gerçek dünya senaryosuyla açıklayalım:

Scenario 1 – Gece yarısı production olayı: Bir servis çöktü, elle müdahale ettiniz, sorunu çözdünüz. Sabah kalkıp Ansible playbook’unu çalıştırdığınızda ne olur? Eğer playbook idempotent değilse, gece manuel yaptığınız değişiklikler üzerine bir de playbook değişiklikleri eklenebilir. Idempotent bir playbook ise zaten doğru durumda olan şeylere dokunmaz, sadece eksik olanları tamamlar.

Scenario 2 – Yeni sunucu ekleme: 50 sunucunuzun konfigürasyonunu yöneten bir playbook var. 5 yeni sunucu eklediniz ve playbook’u çalıştırdınız. İdempotent bir playbook, eski 50 sunucuda hiçbir şey değiştirmez, sadece 5 yeni sunucuyu konfigüre eder.

Scenario 3 – Konfigürasyon kayması: Birisi production sunucusunda manuel bir değişiklik yaptı. Playbook’u çalıştırdığınızda bu sapma düzeltilir ve sunucu istenen duruma geri döner. Bu, “configuration drift” probleminin çözümüdür.

Scenario 4 – CI/CD entegrasyonu: Her merge request’te Ansible playbook’unuzu otomatik çalıştırıyorsunuz. Idempotent bir playbook, production’da zaten doğru olan konfigürasyonlara dokunmadan sadece gerekli değişiklikleri uygular.

AWX/Ansible Tower ile Scheduled Run’lar

Ansible Tower ya da AWX kullanıyorsanız, playbook’larınızı düzenli aralıklarla çalıştırabilirsiniz. Örneğin her gece saat 02:00’de tüm altyapı playbook’larınızı çalıştırın. Bu sayede:

  • Gün içinde yapılan manuel değişiklikler gece otomatik düzeltilir
  • Konfigürasyon sapması minimum düzeyde kalır
  • Altyapınız sürekli “desired state”de kalır

Bu pattern’in çalışabilmesi için playbook’larınızın idempotent olması şart. Aksi takdirde her gece gereksiz servis yeniden başlatmaları, dosya değişiklikleri ve potansiyel kesintilerle karşılaşırsınız.

# Tower/AWX'te schedule oluşturmak için Ansible Tower CLI örneği
awx schedules create 
    --name "Nightly Compliance Run" 
    --job_template 42 
    --rrule "DTSTART:20240101T020000Z RRULE:FREQ=DAILY;INTERVAL=1" 
    --enabled true

Sonuç

Idempotency, Ansible ile otomasyon yaparken sadece “güzel bir özellik” değil, temel bir gerekliliktir. Playbook’larınızın idempotent olması demek; güvenle tekrar çalıştırabilmek, production’da beklenmedik sürprizlerle karşılaşmamak ve altyapınızı kod olarak gerçek anlamda yönetebilmek demektir.

Özetlemek gerekirse:

  • Ansible modüllerini kullanmayı tercih edin, shell ve command modüllerini son çare olarak görün
  • shell ve command kullanmak zorundaysanız creates, removes, changed_when ve failed_when direktiflerini kullanın
  • Playbook’larınızı her değişiklikten sonra iki kez çalıştırın ve ikinci çalıştırmada changed görmemeyi hedefleyin
  • CI/CD pipeline’ınıza idempotency testi ekleyin, bu küçük adım uzun vadede çok büyük sorunları önler
  • Check mode’u sık kullanın, production değişikliklerinden önce alışkanlık haline getirin

Ansible ile çalışmaya başladığınızda idempotency bazen kısıtlayıcı gelir, bazı şeyleri “zorla” yapmak istersiniz. Ama bu anlayışı içselleştirdikten sonra fark edersiniz ki; öngörülebilir, güvenilir ve sürdürülebilir bir altyapı yönetiminin temeli tam olarak burada yatıyor.

Yorum yapın