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/lockfilegibi komutlar her çalıştırmada dosyanın modification time’ını değiştirir.modification_time: preservekullanın.
- Sıralama bağımlı konfigürasyonlar: Bazı konfigürasyon dosyaları sırayı önemser.
blockinfilekullanırkenmarkerdeğ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
changedgörürsünüz.
- Package state:
state: latestkullanmak idempotency’yi bozabilir çünkü yeni sürüm geldiğinde her çalıştırmadachangeddöner. Versiyonu sabitlemek ya dastate: presentkullanmak 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=Truedöndür - Fark yoksa hiçbir şey yapma ve
changed=Falsedö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,
shellvecommandmodüllerini son çare olarak görün shellvecommandkullanmak zorundaysanızcreates,removes,changed_whenvefailed_whendirektiflerini kullanın- Playbook’larınızı her değişiklikten sonra iki kez çalıştırın ve ikinci çalıştırmada
changedgö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.