Ansible’da Döngüler: loop ve with_items Kullanımı

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 item değ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, loop ile dict2items filtresi 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, loop ile range() kullanabilirsin
  • with_nested: İç içe döngüler için, loop ile product filtresi 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.

Yorum yapın