Ansible ile otomasyon yaparken en sık karşılaştığın durumlardan biri aynı görevi birden fazla öğe için tekrar etme ihtiyacıdır. Birkaç kullanıcı ekleyeceksin, bir sürü paket kuracaksın ya da onlarca dosyayı aynı izinlerle oluşturacaksın. İşte tam bu noktada Ansible’ın döngü mekanizmaları devreye giriyor. loop ve with_items direktifleri, playbook’larını tekrarsız, okunabilir ve bakımı kolay hale getirmenin temel araçları. Bu yazıda bu iki yapıyı derinlemesine inceleyeceğiz, aralarındaki farkları konuşacağız ve gerçek dünya senaryolarıyla pekiştireceğiz.
Döngülerin Temel Mantığı
Ansible’da bir task yazarken normalde tek bir öğe üzerinde işlem yaparsın. Örneğin tek bir paketi kurarsın, tek bir kullanıcı eklersin. Peki ya aynı işlemi 10 farklı paket için yapman gerekiyorsa? 10 ayrı task mı yazacaksın? Tabii ki hayır. Döngüler tam olarak bu sorunu çözmek için var.
Döngü mantığı şu şekilde çalışır: Ansible, listendeki her öğe için task’ı tekrar tekrar çalıştırır. Her iterasyonda ilgili öğeyi item adlı özel bir değişken üzerinden erişilebilir hale getirir. Sen de task içinde {{ item }} yazarak o anki öğeye ulaşırsın.
Basit bir örnekle başlayalım. with_items ile birkaç paket kuralım:
---
- name: Temel paketleri kur
hosts: webservers
become: yes
tasks:
- name: Gerekli paketleri yükle
apt:
name: "{{ item }}"
state: present
with_items:
- nginx
- curl
- git
- htop
- vim
Bu playbook çalıştığında Ansible, listedeki her paket için apt modülünü ayrı ayrı çağırır. Gördüğün gibi tek task ile 5 paketi kurabiliyorsun.
with_items: Eski Ama Güvenilir
with_items, Ansible’ın eski sürümlerinden beri var olan ve hala yaygın kullanılan bir direktif. Pek çok legacy playbook’ta göreceksin bunu. Temel sözdizimi oldukça basit:
---
- name: Kullanıcı hesapları oluştur
hosts: all
become: yes
tasks:
- name: Sistem kullanıcılarını ekle
user:
name: "{{ item.username }}"
uid: "{{ item.uid }}"
shell: "{{ item.shell }}"
state: present
with_items:
- { username: "ahmet", uid: 1001, shell: "/bin/bash" }
- { username: "mehmet", uid: 1002, shell: "/bin/bash" }
- { username: "ayse", uid: 1003, shell: "/bin/zsh" }
- { username: "deploy", uid: 2001, shell: "/bin/sh" }
Burada dikkat etmem gereken önemli bir nokta var: with_items ile sözlük (dictionary) yapısı kullanıyorsan, item.anahtar şeklinde erişirsin. Bu örnekte item.username, item.uid ve item.shell şeklinde erişiyoruz.
with_items‘ın önemli bir özelliği, eğer liste içinde liste geçersen onu düzleştirir (flatten). Yani iç içe listeler tek bir düz listeye dönüşür. Bu bazen işine yarar, bazen de beklenmedik sonuçlar doğurabilir. loop direktifi bunu yapmaz, bu yüzden davranışı daha öngörülebilirdir.
loop: Modern ve Tercih Edilen Yol
Ansible 2.5 ile birlikte loop direktifi tanıtıldı ve resmi olarak with_* yapılarının çoğunun yerini alması önerildi. loop daha tutarlı davranır ve daha fazla esneklik sunar.
Aynı paket kurma örneğini loop ile yazalım:
---
- name: Web sunucu kurulumu
hosts: webservers
become: yes
tasks:
- name: Nginx ve bağımlılıklarını kur
apt:
name: "{{ item }}"
state: latest
update_cache: yes
loop:
- nginx
- python3-pip
- certbot
- python3-certbot-nginx
notify: Nginx'i yeniden başlat
handlers:
- name: Nginx'i yeniden başlat
service:
name: nginx
state: restarted
loop ile yazım tarzı with_items‘a çok benziyor ama bazı önemli farklar var. loop her zaman tam bir listeyi bekler ve iç içe listeleri otomatik düzleştirmez. Bu daha güvenli bir davranış çünkü ne aldığını tam olarak bilirsin.
Gerçek Dünya Senaryosu 1: Dizin Yapısı Oluşturma
Bir uygulama deploy ediyorsun ve her uygulama için belirli dizin yapıları oluşturman gerekiyor. Manuel yapmak hata prone, scripting ile yapmak ise idempotent değil. İşte Ansible döngülerinin parladığı yer burası:
---
- name: Uygulama dizin yapısını hazırla
hosts: appservers
become: yes
vars:
app_base: /opt/myapp
app_user: appuser
directories:
- path: "{{ app_base }}"
mode: "0755"
- path: "{{ app_base }}/logs"
mode: "0750"
- path: "{{ app_base }}/config"
mode: "0700"
- path: "{{ app_base }}/tmp"
mode: "0770"
- path: "{{ app_base }}/releases"
mode: "0755"
- path: "/var/log/myapp"
mode: "0750"
tasks:
- name: Uygulama dizinlerini oluştur
file:
path: "{{ item.path }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "{{ item.mode }}"
loop: "{{ directories }}"
Bu örnekte döngü listesini vars bölümünde tanımladık. Bu yaklaşım playbook’u çok daha temiz ve okunabilir kılıyor. Dizin listesi büyüdükçe task kısmını değiştirmene gerek kalmıyor, sadece vars‘a ekliyorsun.
Döngülerde Koşullu İfadeler
Döngüleri when direktifi ile birleştirebilirsin. Her iterasyon için ayrı ayrı koşul değerlendirilir:
---
- name: İşletim sistemine göre paket yükle
hosts: all
become: yes
tasks:
- name: RedHat ailesi için paketler
yum:
name: "{{ item }}"
state: present
loop:
- httpd
- mod_ssl
- firewalld
when: ansible_os_family == "RedHat"
- name: Debian ailesi için paketler
apt:
name: "{{ item }}"
state: present
loop:
- apache2
- ufw
- libapache2-mod-security2
when: ansible_os_family == "Debian"
Bu pattern özellikle heterojen bir ortamda çalışıyorsan, yani hem Ubuntu hem CentOS hem de belki RHEL makinelerin varsa, çok işine yarar.
Gerçek Dünya Senaryosu 2: Güvenlik Duvarı Kuralları
Üretim ortamında firewall kurallarını yönetmek döngülerin en güzel kullanım alanlarından biri. İzin verilecek portları bir listede tutup tek task ile tümünü ekleyebilirsin:
---
- name: Firewall kurallarını yapılandır
hosts: webservers
become: yes
vars:
allowed_tcp_ports:
- { port: 22, comment: "SSH erişimi" }
- { port: 80, comment: "HTTP" }
- { port: 443, comment: "HTTPS" }
- { port: 8080, comment: "Uygulama portu" }
- { port: 9090, comment: "Prometheus metrics" }
tasks:
- name: UFW ile portları aç
ufw:
rule: allow
port: "{{ item.port }}"
proto: tcp
comment: "{{ item.comment }}"
loop: "{{ allowed_tcp_ports }}"
- name: UFW varsayılan politikayı ayarla ve etkinleştir
ufw:
state: enabled
policy: deny
direction: incoming
Bu yaklaşımın güzelliği şu: Yeni bir port açman gerektiğinde sadece allowed_tcp_ports listesine ekliyorsun. Playbook mantığına dokunmana gerek yok. Ayrıca bu listeyi group_vars veya host_vars içine taşırsam farklı sunucu grupları için farklı port listeleri tutabilirsin.
loop_control ile Döngüleri Özelleştirme
loop_control direktifi, döngünün nasıl davranacağını ve çıktıların nasıl görüneceğini kontrol etmeni sağlar. Bunu bilmeden Ansible döngülerini tam anlamıyla kullanmış sayılmazsın.
---
- name: Servis yönetimi
hosts: all
become: yes
tasks:
- name: Servisleri yönet
service:
name: "{{ item.name }}"
state: "{{ item.state }}"
enabled: "{{ item.enabled }}"
loop:
- { name: nginx, state: started, enabled: true }
- { name: mysql, state: started, enabled: true }
- { name: redis, state: started, enabled: true }
- { name: memcached, state: stopped, enabled: false }
loop_control:
label: "{{ item.name }}"
loop_control altındaki label parametresi, Ansible çıktısında her iterasyon için neyin gösterileceğini belirler. Bunu kullanmazsan tüm sözlük yapısı ekrana dökülür ve çıktı okunaksız bir hale gelir. label: "{{ item.name }}" diyerek sadece servis adının görünmesini sağlarsın.
loop_control‘ün diğer kullanışlı seçenekleri:
- index_var: Döngü indeksini bir değişkende tutar,
loop_control: index_var: idxşeklinde kullanırsın - loop_var: Varsayılan
itemdeğişkeni yerine farklı bir isim kullanmak istersen - pause: Her iterasyon arasında saniye cinsinden bekleme süresi
---
- name: Veritabanı yedeklerini al
hosts: dbservers
become: yes
tasks:
- name: Sırayla yedek al
shell: "mysqldump {{ item }} > /backup/{{ item }}_$(date +%Y%m%d).sql"
loop:
- production_db
- users_db
- analytics_db
- logs_db
loop_control:
pause: 10
label: "{{ item }} veritabanı yedekleniyor"
pause: 10 ile her veritabanı yedeklemesi arasında 10 saniye bekliyoruz. Bu, sunucu üzerindeki yükü dağıtmak için gerçekten işe yarayan bir teknik.
Döngülerde register Kullanımı
Döngü sonuçlarını kaydetmek ve sonraki task’larda kullanmak istiyorsan register direktifini döngülerle birlikte kullanabilirsin. Sonuç bir liste olarak döner:
---
- name: Servis durumlarını kontrol et
hosts: all
become: yes
tasks:
- name: Servislerin durumunu kontrol et
command: "systemctl is-active {{ item }}"
register: service_status
loop:
- nginx
- mysql
- redis
ignore_errors: yes
- name: Sorunlu servisleri raporla
debug:
msg: "{{ item.item }} servisi çalışmıyor! Durum: {{ item.stdout }}"
loop: "{{ service_status.results }}"
when: item.rc != 0
loop_control:
label: "{{ item.item }}"
Bu örnekte önce servislerin durumunu kontrol ediyoruz ve sonuçları service_status değişkenine kaydediyoruz. register bir döngüde kullanıldığında, service_status.results adında bir liste oluşur. Her liste elemanı bir iterasyonun sonucunu içerir. Sonra bu listeyi döngüye sokup sadece hatalı olanları raporluyoruz.
with_items vs loop: Hangisini Kullanmalısın?
Şimdi kritik soru: Yeni bir playbook yazarken hangisini seçmelisin?
Kısa cevap: loop kullan.
Uzun cevap: Eğer Ansible 2.5 veya üzerini kullanıyorsan (ki artık herkes 2.9+ kullanıyor), loop direktifini tercih et. Resmi Ansible dokümantasyonu da bunu öneriyor. with_items hala çalışıyor ve deprecated değil ama loop daha modern, daha tutarlı ve gelecekte daha iyi desteklenecek.
Ancak with_* ailesinden bazı direktiflerin yerini loop tam olarak almıyor. Örneğin:
- with_dict: Sözlükleri döngüye sokmak için,
loopiledict2itemsfiltresi kullanarak yapabilirsin - with_fileglob: Dosya örüntülerine göre döngü için hala yaygın kullanılıyor
- with_sequence: Sayı aralığı oluşturmak için,
loopilerange()kullanabilirsin - with_nested: İç içe döngüler için,
loopileproductfiltresi kullanabilirsin
Eski bir codebase’de with_items görürsen paniklemene gerek yok. Çalışır, güvenilirdir. Ama yeni yazıyorsan loop yolunu seç.
Gerçek Dünya Senaryosu 3: Nginx Sanal Sunucu Yapılandırması
Bir hosting ortamında birden fazla domain için Nginx virtual host yapılandırmaları oluşturuyorsun. Her domain için ayrı bir config dosyası ve log dizini gerekiyor:
---
- name: Nginx virtual host yapılandırmalarını oluştur
hosts: webservers
become: yes
vars:
virtual_hosts:
- domain: "example.com"
port: 80
root: "/var/www/example.com"
ssl: false
- domain: "shop.example.com"
port: 443
root: "/var/www/shop"
ssl: true
- domain: "api.example.com"
port: 443
root: "/var/www/api"
ssl: true
tasks:
- name: Web kök dizinlerini oluştur
file:
path: "{{ item.root }}"
state: directory
owner: www-data
group: www-data
mode: "0755"
loop: "{{ virtual_hosts }}"
loop_control:
label: "{{ item.domain }}"
- name: Log dizinlerini oluştur
file:
path: "/var/log/nginx/{{ item.domain }}"
state: directory
owner: www-data
group: adm
mode: "0750"
loop: "{{ virtual_hosts }}"
loop_control:
label: "{{ item.domain }} log dizini"
- name: Virtual host konfigürasyonlarını oluştur
template:
src: "templates/nginx_vhost.j2"
dest: "/etc/nginx/sites-available/{{ item.domain }}.conf"
owner: root
group: root
mode: "0644"
loop: "{{ virtual_hosts }}"
loop_control:
label: "{{ item.domain }} config"
notify: Nginx reload
- name: Virtual hostları etkinleştir
file:
src: "/etc/nginx/sites-available/{{ item.domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.domain }}.conf"
state: link
loop: "{{ virtual_hosts }}"
loop_control:
label: "{{ item.domain }} aktif ediliyor"
handlers:
- name: Nginx reload
service:
name: nginx
state: reloaded
Bu örnek production ortamında kullanabileceğin gerçek bir senaryo. Yeni bir domain eklemen gerektiğinde sadece virtual_hosts listesine yeni bir giriş ekliyorsun ve playbook’u tekrar çalıştırıyorsun. Mevcut konfigürasyonlara dokunulmaz çünkü Ansible idempotent çalışır.
dict2items ile Sözlük Döngüleri
with_dict yerine loop kullanmak istediğinde dict2items Jinja2 filtresi seni kurtarır:
---
- name: Ortam değişkenlerini yapılandır
hosts: appservers
become: yes
vars:
app_env_vars:
APP_ENV: production
APP_DEBUG: "false"
DB_HOST: "db.internal.example.com"
DB_PORT: "5432"
CACHE_TTL: "3600"
LOG_LEVEL: warning
tasks:
- name: Uygulama ortam değişkenlerini /etc/environment'a yaz
lineinfile:
path: /etc/environment
regexp: "^{{ item.key }}="
line: "{{ item.key }}={{ item.value }}"
create: yes
loop: "{{ app_env_vars | dict2items }}"
loop_control:
label: "{{ item.key }}"
dict2items filtresi sözlüğü key ve value alanlarına sahip bir listeye dönüştürür. Bu sayede item.key ile anahtara, item.value ile değere erişebilirsin.
Performans İpuçları
Döngülerle çalışırken performans konusunda da birkaç şeyi aklında tutman gerekiyor.
Birincisi, bazı modüller liste kabul eder. apt, yum, dnf gibi paket yöneticisi modülleri name parametresine liste geçirmeyi destekler. Bu durumda döngü yerine direkt liste vermek daha verimlidir çünkü tek bir işlem gerçekleşir:
# Döngü yerine bunu tercih et:
- name: Paketleri toplu kur
apt:
name:
- nginx
- curl
- git
- htop
state: present
update_cache: yes
Bu yaklaşım paket yöneticisini tek seferde çağırır, döngü ise her paket için ayrı ayrı çağırır. Özellikle apt-get update adımını düşünürsen, döngüyle yaparsan her paket için bu adım tekrarlanabilir ve ciddi zaman kaybına yol açar.
İkincisi, strategy: free kullanmayı düşün. Varsayılan olarak Ansible tüm hostlarda bir task’ı tamamlamadan bir sonrakine geçmez. strategy: free ile her host bağımsız ilerler. Büyük döngülerde bu ciddi hız kazandırır.
Yaygın Hatalar ve Çözümleri
Hata 1: item değişkeni iç içe döngülerde çakışması
İç içe döngülerde (nested loops) dışarıdaki ve içerideki item değişkeni çakışır. Bunu loop_var ile çözersin:
- name: Her sunucu için servis kontrolü
include_tasks: check_service.yml
loop: "{{ servers }}"
loop_control:
loop_var: current_server
İç task dosyasında item yerine current_server kullanırsın.
Hata 2: with_items’da None değeri
Liste değişkenin tanımlı olmadığı durumlarda with_items hata verir. default([]) filtresiyle bunu güvence altına alırsın:
- name: Güvenli döngü
apt:
name: "{{ item }}"
state: present
loop: "{{ extra_packages | default([]) }}"
Hata 3: Döngüde çok fazla task çalıştırma
Her iterasyonda remote bağlantı açılıp kapandığını unutma. Döngüde 100 öğen varsa ve her birinde command modülü çalıştırıyorsan, bu 100 ayrı SSH komutu demek. Bunu shell modülünde birleştirerek ya da liste destekleyen modüller kullanarak azaltabilirsin.
Sonuç
Ansible döngüleri, playbook’larını gerçek anlamda güçlü kılan yapı taşlarından biri. with_items eski ve yaygın, loop ise modern ve önerilen yol. İkisi de aynı temel işi yapıyor ama loop daha tutarlı davranışı ve loop_control gibi ek özellikleriyle öne çıkıyor.
Pratikte en çok kullanacağın senaryolar paket kurulumu, kullanıcı yönetimi, dosya ve dizin oluşturma, servis yönetimi ve konfigürasyon dosyası üretimi. Bu senaryoların hepsinde döngüler playbook’unun boyutunu küçültür, okunabilirliği artırır ve değişiklik yapmayı kolaylaştırır.
En önemli alıntı noktaları şunlar: loop_control ile çıktılarını temiz tut, label kullanmayı alışkanlık haline getir, liste destekleyen modüllerde döngü yerine direkt liste geç, ve döngü değişkenlerini vars ya da group_vars‘ta tanımlayarak task’ları sade bırak. Bu prensipleri benimsersen Ansible playbook’ların hem daha performanslı hem de çok daha bakımı kolay olacak. Gerisi deneyim meselesi.