Ansible CLI'dan Playbook'a: Temel Kavramlar ve Kullanımları

★ featured

Ansible'ı öğrenmeye başlayanların büyük çoğunluğu doğrudan playbook yazmaya atlar. Mantıklı da; sonuçta Ansible'ın asıl gücü otomasyon senaryoları üretmekte. Ancak CLI'ı ve ad-hoc komutları atlamak, evin temelini dökmeden duvar örmeye çalışmaktan farksızdır. Bu yazıda Ansible'ın komut satırı arayüzünden başlayarak playbook yazımının temel kavramlarına kadar olan her şeyi ele alıyoruz.


Ansible CLI

Ansible'ın CLI ekosistemi dört ana komut üzerine kuruludur. Her birinin belirli bir amaca hizmet ettiğini bilmek gereksiz karmaşıklıktan kaçınmayı sağlar.

Komut Ne İşe Yarar?
ansible Tek seferlik, hızlı müdahaleler için ad-hoc komutlar
ansible-playbook YAML tabanlı playbook dosyalarını çalıştırır
ansible-doc Modül belgelerini ve parametrelerini gösterir
ansible-inventory Inventory içeriğini listeler veya doğrular

Ad-hoc Komutlar

Ad-hoc komutlar, playbook yazmaya gerek kalmadan anlık müdahale için kullanılır. Bir servis başlatmak, dosya kopyalamak, hızlıca sistem bilgisi toplamak gibi işlemler için playbook yazmak genelde gerekmez.

Sık Kullanılan Parametreler

Parametre Kısaltma Ne İşe Yarar?
--inventory -i Inventory dosyasının yolu
--module-name -m Kullanılacak modül adı
--args -a Modüle geçilecek parametreler
--user -u Hedef sunucuya bağlanacak kullanıcı
--become -b Yetki yükselterek çalıştırma (sudo/root)
--check - Dry-run: ne değişeceğini gösterir, gerçek değişiklik yapmaz
--diff - Dosya değişikliklerini diff formatında gösterir
--limit - Komutun çalışacağı host/grubu sınırlar
--private-key - Özel SSH anahtar dosyasını belirtir
--ask-pass -k SSH parolası sorması için
--extra-vars -e CLI üzerinden değişken vermek için
--syntax-check - Sözdizimi hatalarını kontrol eder

Gerçek Bir Örnek

ansible webservers -i ./hosts \
  -u svcansible \
  -b \
  --private-key ~/.ssh/id_rsa \
  -m shell \
  -a "apt update && apt upgrade -y" \
  --check --diff --limit web1

Bu komut webservers grubundaki web1 sunucusunda apt update && apt upgrade -y komutunu dry-run modunda çalıştırır. --diff parametresi özellikle copy, template, lineinfile gibi dosya değiştiren modüllerle birlikte kullanıldığında eski ve yeni içerik arasındaki farkı diff formatında gösterir:

TASK [Update /etc/motd file]
--- before: /etc/motd
+++ after: /etc/motd
@@
-Old message
+Welcome to production servers!

--check ve --diff birlikte kullanılabilir. Böylece ne değişiklik yapılacağı hem dry-run hem de fark formatında görülür. --limit ise wildcard destekler: --limit web* gibi.


Faydalı CLI Tüyoları

Uzun soluklu Ansible kullanımında işe yarayan bazı kısa komutlar:

# Inventory'deki tüm host'ları görmek
ansible-inventory --list -i ./hosts

# Sadece özel yapılandırılmış (varsayılan dışında) ayarları görmek
ansible-config dump --only-changed

# Daha fazla debug çıktısı
ansible-playbook site.yml -vvv

# Belirli bir görevden başlamak
ansible-playbook deploy.yml --start-at-task="Install packages"

# Her görev öncesinde onay alarak ilerlemek
ansible-playbook update.yml --step

--step özellikle karmaşık playbook'larda veya production ortamında ilk kez çalıştırılan senaryolarda oldukça kullanışlıdır.


ansible-doc: Modül Kılavuzu

Yüzlerce modülü tabii ki ezberlemek gerekmez. ansible-doc komutunu kullanarak modül dokümantasyonuna saniyeler içinde ulaşılabilir:

# Bir modül hakkında genel bilgi
ansible-doc copy

# Kısa sözdizimini görmek
ansible-doc -s user

# Tüm modülleri listelemek
ansible-doc -l

Modül dokümantasyonu ile ilgili detaylı bilgilere aşağıdaki linkcard'dan ulaşabilirsiniz.


State-aware Yapı ve Idempotency

Ansible'ı shell scriptlerden ayıran en kritik özellik budur. Ansible bir görevi çalıştırmadan önce sistemin mevcut durumunu kontrol eder. Hedef durum zaten sağlanmışsa hiçbir şey yapmaz. Buna idempotency denir.

- name: Ensure nginx is running
  service:
    name: nginx
    state: started

Bu görev kaç kez çalıştırılırsa çalıştırılsın sonuç değişmez. Nginx çalışıyorsa ok döner, çalışmıyorsa başlatır ve changed döner. Bunu ad-hoc karşılığıyla karşılaştıralım:

ansible webservers -m shell -a "systemctl start nginx"

Bu komut nginx zaten çalışıyor olsa bile her seferinde start işlemini dener; idempotent değildir. Bu nedenle mümkün olduğunca amaca özel modüller (service, package, file, copy vb.) tercih edilmeli, shell ve command yalnızca başka seçenek kalmadığında kullanılmalıdır.

$ quiz

Ad-hoc komutlar neden idempotent değildir?


Playbook Anatomisi

Playbook, Ansible'ın temel otomasyon birimidir. Tek bir YAML dosyasında hangi sunuculara hangi görevlerin, hangi koşullarla uygulanacağı tanımlanır. Bir playbook birden fazla play içerebilir; her play, belirli bir host grubunu hedef alan bağımsız bir bloktur.

Aşağıda bir playbook'un temel iskeletini inceliyoruz:

---
- name: Web sunucusunu yapılandır       # Play'in adı
  hosts: webservers                     # Hedef host grubu
  become: true                          # sudo yetkisiyle çalıştır
  gather_facts: true                    # Sistem bilgilerini topla
  vars:                                 # Bu play'e özel değişkenler
    http_port: 80
    app_user: webadmin

  tasks:                                # Sırayla çalışacak görevler
    - name: nginx'i kur
      ansible.builtin.package:
        name: nginx
        state: present

    - name: nginx servisini başlat
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Her alanın ne işe yaradığına bakalım:

  • name play veya task için okunabilir bir etiket belirler. Zorunlu değildir ancak çıktıyı takip etmeyi kolaylaştırdığı için her zaman yazılması önerilir.
  • hosts hangi sunucularda çalışacağını belirler. all tüm inventory'yi, webservers gibi bir değer belirli bir grubu, web1,web2 gibi bir değer ise belirli host'ları hedefler.
  • become: true görevlerin sudo yetkisiyle çalışmasını sağlar. Paket kurulumu, servis yönetimi gibi root erişimi gerektiren işlemler için zorunludur. become_user ile hangi kullanıcıya geçileceği de belirtilebilir, varsayılan root'tur.
  • gather_facts playbook başında hedef sistemden bilgi toplayıp toplamayacağını belirler. Varsayılanı true'dur. Bu bilgilere ihtiyaç yoksa false yaparak playbook'un daha hızlı başlaması sağlanabilir.
  • vars bu play kapsamındaki değişkenleri tanımlar. {{ http_port }} şeklinde tasks içinde kullanılabilirler.
  • tasks çalışacak görevlerin listesidir. Her görev bir modül çağrısıdır ve yukarıdan aşağıya sırayla çalışır.

Birden Fazla Play

Tek bir playbook dosyasında birden fazla play tanımlanabilir. Her play farklı bir host grubunu hedefleyebilir:

---
- name: Web sunucularını yapılandır
  hosts: webservers
  become: true
  tasks:
    - name: nginx'i kur
      ansible.builtin.package:
        name: nginx
        state: present

- name: Veritabanı sunucularını yapılandır
  hosts: dbservers
  become: true
  tasks:
    - name: postgresql'i kur
      ansible.builtin.package:
        name: postgresql
        state: present
$ quiz

Bir playbook'ta become: true ne işe yarar?


{{ }} Sözdizimi ve Jinja2

Ansible'daki {{ }} ifadesi, değişkenlerin ve hesaplamaların runtime'da işlenmesini sağlar. Sistem bilgileri (facts), önceki görev çıktıları (register), elle tanımlanan değişkenler (vars, set_fact) ve döngüdeki item gibi özel değişkenler bu yapı sayesinde görevler içinde kullanılabilir hale gelir.

Neden "item" değil "{{ item }}" yazıyoruz? Çünkü "item" düz metin olarak kalır, Ansible bunu değişken olarak değil kelime olarak işler. {{ item }} yazıldığında ise Ansible bunun bir değişken olduğunu anlayarak değerini alır.

Somut bir örnek üzerinden gösterelim:

vars:
  app_port: 8080

tasks:
  - name: Portu yazdır
    debug:
      msg: "Uygulama portu: {{ app_port }}"

Çıktı şu şekilde olur:

ok: [web1] => {
    "msg": "Uygulama portu: 8080"
}

Bu yapı aslında Jinja2 adlı şablon motoruna dayanır. Yani kısacası {{ }} içine yazılan her şey Ansible tarafından değerlendirilecek bir ifadedir.


Facts ve gather_facts

Ansible Facts Nedir?

Ansible, playbook çalıştırıldığında hedef sistemlerden otomatik olarak bilgi toplar: işletim sistemi türü, hostname, IP adresleri, CPU sayısı, disk alanı... Bunların tümü ansible_facts sözlüğü altında tutulur ve varsayılan olarak her playbook başında setup modülü aracılığıyla çalışır.

Toplanan tüm facts değerlerini görmek için şu ad-hoc komut kullanılabilir:

ansible web1 -m setup

Bu komut oldukça uzun bir çıktı verir. Belirli bir değere ulaşmak için filtre kullanılabilir:

ansible web1 -m setup -a "filter=ansible_distribution*"

gather_facts: false Ne Zaman Kullanılır?

Çok sayıda host bulunan ortamlarda facts toplama süreci, toplam çalışma süresini ciddi şekilde uzatabilir. Bu verilere ihtiyaç yoksa devre dışı bırakmak mantıklıdır:

- name: Run something minimal
  hosts: all
  gather_facts: false
  tasks:
    - name: Run a simple command
      command: uptime

gather_facts: false yapıldığında ansible_facts sözlüğü mevcut olmaz. ansible_hostname, ansible_distribution gibi değişkenlere erişmek gerekiyorsa ya gather_facts: true bırakılmalı ya da setup modülü ilgili task'tan önce manuel olarak çağrılmalıdır:

- name: Setup'ı manuel çalıştır
  ansible.builtin.setup:

Değişken Tanımlama: vars

Playbook'larda tekrar kullanılacak değerler vars anahtarı altında tanımlanır. Bu sayede değerler tek yerden yönetilebilir ve okunabilirlik artar.

Temel Kullanım

- name: Kullanıcıları oluştur
  hosts: all
  vars:
    user_list:
      - alice
      - bob
  tasks:
    - name: Kullanıcıları ekle
      user:
        name: "{{ item }}"
        state: present
      loop: "{{ user_list }}"

Diğer Değişken Tanımlama Yöntemleri

vars_files

vars: bloğu tek bir play'e ait küçük değişkenler için pratiktir. Ancak aynı değişkenleri birden fazla playbook'ta kullanmak gerektiğinde ya da değişken sayısı arttıkça playbook dosyası şişmeye başlar. vars_files: bu durumda devreye girer; değişkenleri ayrı YAML dosyalarına taşıyarak hem tekrar kullanımını hem de yönetimi kolaylaştırır.

- name: Deploy
  hosts: all
  vars_files:
    - vars/common.yml
    - vars/production.yml

vars/common.yml tüm ortamlarda geçerli olan değişkenleri, vars/production.yml ise production'a özgü değerleri içerebilir. Aynı değişken birden fazla dosyada tanımlanmışsa listede sonra gelen dosya kazanır; yani production.yml, common.yml'deki bir değeri ezebilir. Bu özellik environment'a göre değişken katmanlama yaparken oldukça kullanışlıdır.

Örnek bir vars/common.yml:

app_name: myapp
app_port: 8080
log_dir: /var/log/myapp

Örnek bir vars/production.yml:

app_port: 443        # common.yml'deki 8080'i ezer
db_host: prod-db.internal
db_name: myapp_prod

Bu değişkenler yüklendikten sonra playbook içinde normal vars: değişkenlerinden hiçbir farkı olmadan kullanılır:

tasks:
  - name: Konfigürasyonu oluştur
    ansible.builtin.template:
      src: templates/app.conf.j2
      dest: /etc/myapp/app.conf
    # template içinde {{ app_name }}, {{ app_port }}, {{ db_host }} kullanılabilir

extra-vars (-e)

-e ile CLI üzerinden değişken geçilir. Playbook'u değiştirmeden dışarıdan parametre vermek gerektiğinde kullanılır. CI/CD pipeline'larında özellikle yaygındır; aynı playbook farklı version veya env değerleriyle çalıştırılır.

# Tek değişken
ansible-playbook deploy.yml -e "app_version=2.1.0"

# Birden fazla değişken
ansible-playbook deploy.yml -e "app_version=2.1.0 env=staging"

# YAML dosyasından okuma
ansible-playbook deploy.yml -e "@vars/override.yml"

-e ile geçilen değişkenler en yüksek önceliğe sahiptir; vars:, vars_files: veya inventory'den gelen aynı isimli değişkenlerin tamamını ezer. Bu hem gücü hem de dikkat gerektiren yanıdır.

Öncelik Sırası

Ansible'da değişkenler birden fazla yerden tanımlanabildiği için hangisinin kazanacağını bilmek önemlidir. Tam liste oldukça uzundur, ancak pratikte en sık karşılaşılan sıralama şöyledir (yukarıdan aşağıya öncelik artar):

Öncelik Kaynak
En düşük vars_files:
vars: (play seviyesi)
set_fact
En yüksek extra-vars (-e)

vars_files: ile vars: aynı öncelik seviyesindedir; play içinde vars_files: önce yüklendiği için vars: bloğu onu ezebilir. Bir değişkenin beklediğiniz değeri taşımadığı durumlarda öncelik sırası ilk bakılacak yerdir.


set_fact ve register

Bu iki yapı birbirine karıştırılabiliyor. Aralarındaki temel fark şu: register bir görevin çıktısını yakalar, set_fact ise playbook içinde istediğimiz zaman kendi tanımladığımız bir değişken oluşturur.

register

register, bir görevden dönen çıktıyı yakalar ve bir değişkende saklar. Daha sonra bu değişkenin içeriğine göre karar verilebilir ya da çıktı başka bir görevde kullanılabilir:

- name: Hostname'i al
  command: hostname
  register: hostname_out

- name: Hostname'i göster
  debug:
    msg: "Hostname: {{ hostname_out.stdout }}"

register ile kaydedilen değişkenler genellikle şu alanları içerir:

Alan İçeriği
stdout Komutun standart çıktısı
stderr Hata çıktısı
rc Return code (0 başarılı, 0 dışı hata)
changed Değişiklik yapılıp yapılmadığı

set_fact

set_fact modülü runtime'da dinamik olarak yeni bir değişken tanımlar. Playbook'un kalan kısmında kullanılabilir. Genellikle facts'ten veya register çıktısından türetilmiş değerleri daha okunabilir bir isimle saklamak için kullanılır:

- name: IP adresini değişkene ata
  set_fact:
    my_ip: "{{ ansible_facts['default_ipv4']['address'] }}"

- name: IP adresini göster
  debug:
    msg: "Sunucu IP: {{ my_ip }}"

Koşullu Çalıştırma: when

Gerçek ortamlarda envanterdeki tüm sunucular aynı işletim sistemini çalıştırmayabilir. Bazı görevlerin yalnızca RHEL sistemlerde, bazılarının yalnızca Debian sistemlerde çalışması gerekebilir. Ya da bir önceki görev başarısız olduğunda sonraki adımı atlamak isteyebilirsiniz. when ifadesi tam olarak bu ihtiyaç için var; bir task'ın yalnızca belirli bir koşul sağlandığında çalışmasını sağlar.

Facts ile Kullanım

- name: RHEL sistemlerde httpd kur
  ansible.builtin.package:
    name: httpd
    state: present
  when: ansible_facts['os_family'] == "RedHat"

- name: Debian sistemlerde apache2 kur
  ansible.builtin.package:
    name: apache2
    state: present
  when: ansible_facts['os_family'] == "Debian"

Bu sayede hem RHEL hem Debian makineleri aynı inventory'de tutulabilir, Ansible hangi task'ın nerede çalışacağına kendisi karar verir.

register Çıktısıyla Kullanım

- name: Servis durumunu kontrol et
  command: systemctl is-active nginx
  register: nginx_status
  ignore_errors: true

- name: Nginx çalışmıyorsa başlat
  service:
    name: nginx
    state: started
  when: nginx_status.rc != 0

ignore_errors: true olmadan ilk task başarısız olduğunda playbook durur. Burada amacımız durumu kontrol etmek olduğundan hatayı görmezden gelip sonucu when ile değerlendiriyoruz.

Birden Fazla Koşul

and ve or operatörleri ya da liste formatı kullanılabilir. Liste formatında her satır and ile birleştirilmiş sayılır:

# and operatörü
when: ansible_facts['distribution'] == "Ubuntu" and ansible_facts['distribution_major_version'] == "22"

# Liste formatı (and ile aynı anlama gelir, daha okunabilir)
when:
  - ansible_facts['distribution'] == "Ubuntu"
  - ansible_facts['distribution_major_version'] == "22"

# or operatörü
when: ansible_facts['distribution'] == "Ubuntu" or ansible_facts['distribution'] == "Debian"
$ quiz

when ifadesinde liste formatı kullanıldığında koşullar nasıl değerlendirilir?


Loop Kullanımı

Aynı görevi birden fazla öğe için tekrar tekrar yazmak yerine loop kullanılır. Onlarca paketi tek tek kurmak ya da onlarca kullanıcıyı ayrı ayrı tanımlamak yerine listeyi loop'a verip tek bir task yazmak yeterlidir.

Liste ile Loop

- name: Paketleri kur
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - git
    - curl
    - htop

Sözlük ile Loop

Birden fazla parametreyi birlikte değiştirmek gerektiğinde sözlük yapısı kullanılır:

- name: Kullanıcıları yönet
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: "{{ item.state }}"
    shell: "{{ item.shell }}"
  loop:
    - { name: "alice", state: "present", shell: "/bin/bash" }
    - { name: "bob",   state: "present", shell: "/bin/zsh"  }
    - { name: "carol", state: "absent",  shell: "/bin/bash" }

loop_control ile Çıktıyı Okunabilir Hale Getirme

Varsayılan olarak Ansible loop çıktısında item değişkeninin tamamını basar. Sözlük kullandığınızda bu çıktı uzun ve okuması zor bir JSON bloğuna dönüşür. loop_control ile hangi alanın gösterileceği belirtilebilir:

- name: Kullanıcıları yönet
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: "{{ item.state }}"
  loop:
    - { name: "alice", state: "present" }
    - { name: "bob",   state: "absent"  }
  loop_control:
    label: "{{ item.name }}"

loop_control olmadan çıktı şöyle görünür:

TASK [Kullanıcıları yönet] *****
ok: [web1] => (item={'name': 'alice', 'state': 'present'})
ok: [web1] => (item={'name': 'bob', 'state': 'absent'})

loop_control ile:

TASK [Kullanıcıları yönet] *****
ok: [web1] => (item=alice)
ok: [web1] => (item=bob)

Birkaç öğede fark küçük görünse de onlarca satırdan oluşan sözlüklerde çıktıyı takip etmek çok kolaylaşır.

with_items mi, loop mu?

Eski Ansible dokümantasyonlarında ve internetteki örneklerin büyük bölümünde loop yerine with_items kullanıldığını göreceksiniz. with_items işlevsel olarak loop ile aynıdır ancak Ansible 2.5'ten itibaren loop tercih edilen yöntem haline gelmiştir. with_items hâlâ çalışıyor olsa da yeni yazılan playbook'larda loop kullanılması önerilir.

Loop ile register Kullanımı

Loop içinde register kullanıldığında her iterasyonun sonucu ayrı ayrı biriktirilir ve tek bir değişken altında .results listesi olarak saklanır. Normal bir register kullanımında son task'ın çıktısı değişkenin üzerine yazılır; loop ile kullanıldığında ise Ansible otomatik olarak biriktirme moduna geçer. Her öğenin sonucuna .results üzerinden ulaşılır:

- name: Servisleri kontrol et
  command: systemctl is-active "{{ item }}"
  register: service_status
  loop:
    - nginx
    - postgresql
  ignore_errors: true   # Çalışmayan servis rc != 0 döneceğinden playbook'un durmaması için gerekli

- name: Sonuçları göster
  debug:
    msg: "{{ item.item }}: {{ item.stdout }}"
  loop: "{{ service_status.results }}"
$ quiz

loop_control ile label tanımlamanın amacı nedir?


Handlers

Konfigürasyon değişikliklerinden sonra servislerin yeniden başlatılması çok yaygın bir ihtiyaçtır. Bunu normal bir task olarak yazsaydık servis her playbook çalıştırıldığında gereksiz yere restart olurdu. Handler'lar tam olarak bu sorunu çözmek için var; yalnızca bir değişiklik gerçekten gerçekleştiğinde, yani ilgili task changed döndüğünde tetiklenirler.

---
- name: nginx yapılandırması
  hosts: webservers
  become: true

  tasks:
    - name: nginx konfigürasyonunu kopyala
      ansible.builtin.copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: nginx'i yeniden başlat

    - name: SSL sertifikasını kopyala
      ansible.builtin.copy:
        src: files/cert.pem
        dest: /etc/nginx/cert.pem
      notify: nginx'i yeniden başlat

  handlers:
    - name: nginx'i yeniden başlat
      ansible.builtin.service:
        name: nginx
        state: restarted

Handler'ların birkaç önemli özelliği vardır. Aynı handler birden fazla task tarafından tetiklense bile play sonunda yalnızca bir kez çalışır; yukarıdaki örnekte hem konfigürasyon hem sertifika değişse de handler sadece bir defa çalışır. Handler'lar tüm tasks tamamlandıktan sonra devreye girer, task sırasında değil. Herhangi bir task failed olursa handler varsayılan olarak çalışmaz; --force-handlers parametresiyle bu davranış değiştirilebilir.

Büyük/küçük harf ve boşluk dahil olmak üzere notify değeri ile handlers bölümündeki name değeri birebir aynı olmalıdır. Bu uyumsuzluk handler'ların sessizce çalışmamasının en yaygın sebebidir.

$ quiz

Bir handler aynı play içinde 3 farklı task tarafından notify edilirse kaç kez çalışır?


Tags

Fazla task içeren bir playbook'u her değişiklikte baştan sona çalıştırmak hem zaman kaybettirir hem de gereksiz değişiklik riski yaratır. Tags, playbook'un yalnızca belirli bölümlerini çalıştırmayı sağlar. Örneğin sadece konfigürasyon dosyalarını güncellemek istiyorsunuz; paket kurulum adımlarını ve servis yönetimini atlamak için --tags config ile çalıştırmak yeterlidir.

tasks:
  - name: nginx'i kur
    ansible.builtin.package:
      name: nginx
      state: present
    tags:
      - install
      - nginx

  - name: nginx konfigürasyonunu güncelle
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    tags:
      - config
      - nginx

  - name: nginx'i başlat
    ansible.builtin.service:
      name: nginx
      state: started
    tags:
      - service
      - nginx

Kullanım örnekleri:

# Sadece nginx tag'li tüm görevleri çalıştır
ansible-playbook site.yml --tags nginx

# Sadece kurulum görevlerini çalıştır
ansible-playbook site.yml --tags install

# Config görevlerini atla, geri kalanını çalıştır
ansible-playbook site.yml --skip-tags config

always ve never

Ansible'ın iki özel tag'i vardır. always tag'i taşıyan bir task, --tags ile başka bir şey belirtilse bile her zaman çalışır. never tag'i taşıyan bir task ise tam tersine, açıkça --tags never ile çağrılmadıkça hiçbir zaman çalışmaz. never genellikle debug amaçlı ya da tehlikeli olduğu için yanlışlıkla tetiklenmesini istemediğiniz görevler için kullanılır.

- name: Değişken değerlerini göster
  ansible.builtin.debug:
    msg: "app_name={{ app_name }}, nginx_port={{ nginx_port }}"
  tags:
    - never
    - debug

- name: Fact'leri topla
  ansible.builtin.setup:
  tags:
    - always

when, loop ve register: Birlikte Kullanım

Bu üç yapının ayrı ayrı nasıl çalıştığını gördük. Gerçek playbook'larda ise çoğunlukla bir arada kullanılırlar. Küçük bir örnek üzerinden gösterelim. Birden fazla servisin durumunu kontrol edip yalnızca çalışmayanları yeniden başlatalım.

- name: Kritik servisleri kontrol et
  ansible.builtin.command:
    cmd: systemctl is-active "{{ item }}"
  register: service_checks
  loop:
    - nginx
    - postgresql
    - redis
  ignore_errors: true   # Çalışmayan servis rc != 0 döneceğinden playbook'un durmaması için gerekli
  changed_when: false

- name: Çalışmayan servisleri başlat
  ansible.builtin.service:
    name: "{{ item.item }}"
    state: started
  loop: "{{ service_checks.results }}"
  when: item.rc != 0
  loop_control:
    label: "{{ item.item }}"

İlk task tüm servislerin durumunu kontrol eder, sonuçları service_checks değişkenine kaydeder ve changed_when: false ile bu adımın hiçbir zaman changed dönmemesini sağlar. İkinci task ise bu sonuçlar üzerinde dönerek yalnızca rc != 0 dönen, yani çalışmayan servisleri başlatır. loop_control ile de çıktıda servis adından fazlası görünmez.


Tam Senaryo: nginx Kurulumu ve Yapılandırması

Şimdiye kadar anlattığımız tüm kavramları bir arada kullanan, gerçek senaryolarda çalışan bir playbook örneği görelim. Bu playbook web sunucularına nginx kurar, bir konfigürasyon dosyası kopyalar ve servisi ayağa kaldırır.

---
- name: Web sunucusunu hazırla
  hosts: webservers
  become: true
  gather_facts: true
  vars:
    nginx_port: 80
    nginx_user: www-data
    app_name: myapp

  tasks:
    - name: Sadece Debian tabanlı sistemlerde devam et
      ansible.builtin.fail:
        msg: "Bu playbook yalnızca Debian/Ubuntu sistemleri destekler."
      when: ansible_facts['os_family'] != "Debian"

    - name: Paket listesini güncelle
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      tags: install

    - name: nginx'i kur
      ansible.builtin.apt:
        name: nginx
        state: present
      tags: install

    - name: nginx konfigürasyonunu kopyala
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_name }}
        owner: root
        group: root
        mode: "0644"
      notify: nginx'i yeniden yükle
      tags: config

    - name: Siteyi etkinleştir
      ansible.builtin.file:
        src: /etc/nginx/sites-available/{{ app_name }}
        dest: /etc/nginx/sites-enabled/{{ app_name }}
        state: link
      notify: nginx'i yeniden yükle
      tags: config

    - name: nginx servisini başlat ve boot'ta otomatik başlamasını sağla
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true
      tags: service

    - name: nginx'in çalıştığını doğrula
      ansible.builtin.command:
        cmd: systemctl is-active nginx
      register: nginx_check
      changed_when: false

    - name: Sonucu göster
      ansible.builtin.debug:
        msg: "nginx durumu: {{ nginx_check.stdout }}"

  handlers:
    - name: nginx'i yeniden yükle
      ansible.builtin.service:
        name: nginx
        state: reloaded

Bu playbook'ta dikkat edilmesi gereken birkaç nokta var. cache_valid_time: 3600 parametresi paket listesinin son 1 saat içinde güncellenmiş olması durumunda tekrar güncelleme yapılmasını engeller, böylece her çalıştırmada gereksiz ağ trafiği oluşmaz. changed_when: false ise command modülünün doğrulama amacıyla kullanıldığını ve bir değişiklik yapmadığını Ansible'a belirtir; aksi halde her çalıştırmada changed dönerdi. state: link ile sembolik link oluşturulması, Debian/Ubuntu'nun nginx site etkinleştirme mekanizmasının idempotent bir şekilde kullanılmasını sağlar.


YAML Format ve Doğrulama

Ansible playbook'ları YAML formatında yazılır. YAML'ın sözdizimi basit görünse de küçük hatalar saatlerce süren debug süreçlerine yol açabilir.

Temel Kurallar

Kural Açıklama
Girinti 2 space kullanın, TAB kesinlikle kullanmayın
Liste - ile başlar, altındaki satırlarla hizalanır
Anahtar key: value formatındadır
Boolean true/false küçük harfle yazılır
String Özel karakter içeriyorsa tırnak içine alınmalı
Boş satır Anlam taşımaz, okunabilirlik için kullanılabilir

Sık Yapılan Hatalar

Yanlış girinti

En yaygın hatadır. YAML'da girinti anlam taşır; bir key'in hangi bloğa ait olduğu tamamen girintiye göre belirlenir.

# YANLIŞ
tasks:
- name: nginx kur        # tasks altında olmalı, hizalama yanlış
  package:
    name: nginx
    state: present

# DOĞRU
tasks:
  - name: nginx kur
    package:
      name: nginx
      state: present

TAB karakteri kullanımı

YAML, TAB karakterini desteklemez. Editörünüzün TAB tuşunu space'e çevirdiğinden emin olun.

Tırnaksız özel karakterler

:, {, }, [, ], # gibi karakterler değer içinde kullanılıyorsa tırnak zorunludur.

# YANLIŞ
msg: Sunucu: {{ hostname }}

# DOĞRU
msg: "Sunucu: {{ hostname }}"

Boolean karmaşası

Yes, No, TRUE, FALSE, on, off gibi değerler YAML'da boolean olarak yorumlanır. String olarak kullanılması gerekiyorsa tırnak içine alınmalıdır.

# "yes" string değil boolean true olarak yorumlanır
state: yes       # Dikkatli olun

# String olarak kullanmak için
state: "yes"

yamllint ile Doğrulama

yamllint, YAML dosyalarını çalıştırmadan önce sözdizimi ve stil hatalarını tespit eden bir araçtır. Ansible'ın kendi syntax kontrolünden daha kapsamlıdır; girinti tutarsızlıkları, gereksiz boşluklar ve satır uzunluğu gibi stil sorunlarını da raporlar.

pip install yamllint
yamllint myplaybook.yml

Örnek çıktı:

myplaybook.yml
  3:1   warning  missing document start "---"  (document-start)
  8:81  error    line too long (85 > 80 characters)  (line-length)
 12:5   error    wrong indentation: expected 4 but found 2  (indentation)

Projeye özel kurallar .yamllint adlı bir konfigürasyon dosyasıyla tanımlanabilir:

# .yamllint
extends: default
rules:
  line-length:
    max: 120          # Satır uzunluğu limitini artır
  truthy:
    allowed-values:   # Yalnızca bu boolean değerlerine izin ver
      - "true"
      - "false"
  comments:
    min-spaces-from-content: 1

ansible-lint ile Daha Derin Analiz

yamllint sözdizimini kontrol ederken ansible-lint Ansible'a özgü en iyi pratikleri denetler. Deprecated modül kullanımı, eksik name alanları, idempotency sorunları gibi konularda uyarı verir.

pip install ansible-lint
ansible-lint myplaybook.yml

Örnek çıktı:

WARNING  Listing 2 violation(s) that are fatal
yaml[truthy]: Use "true" or "false" instead of "yes" or "no"
fqcn[action-core]: Use FQCN for builtin actions (ansible.builtin.package)

İkisi birbirini tamamlar: yamllint önce çalıştırılır, ardından ansible-lint ile Ansible'a özgü kontroller yapılır.

$ quiz

yamllint ve ansible-lint arasındaki temel fark nedir?


Sık Sorulan Sorular

set_fact ile register arasındaki fark nedir?

register bir görevin çıktısını olduğu gibi yakalar. set_fact ise istediğimiz zaman, istediğimiz değerle yeni bir değişken tanımlamamızı sağlar. Genellikle register çıktısından ihtiyaç duyulan kısım set_fact ile daha okunabilir bir isme atanır.

set_fact ile tanımlanan değişkenler ne kadar süre geçerlidir?

Playbook boyunca geçerlidir. Başka bir playbook çağrıldığında geçerliliğini yitirir.

--diff tüm modüllerde işe yarar mı?

Hayır. Sadece state-aware modüllerde ve dosya üzerinde değişiklik yapan modüllerde (copy, template, lineinfile) etkilidir.

Handler çalışmadı, neden olabilir?

En yaygın sebep notify değeriyle handlers bölümündeki name değerinin birebir eşleşmemesidir. Büyük/küçük harf ve boşluk dahil tamamen aynı olmalıdır. İkinci sebep ise ilgili task'ın changed değil ok dönmesidir; handler yalnızca changed durumunda tetiklenir.

Ansible Windows sistemleri yönetebilir mi?

Evet, ancak yapılandırma gerektirir. Linux/macOS için SSH kullanılırken Windows için WinRM protokolü üzerinden iletişim sağlanır.

ansible-playbook çalıştırıldığında her zaman gather_facts yapılır mı?

Varsayılan olarak evet. gather_facts: false ile devre dışı bırakılabilir.


Bu yazıda Ansible'ın komut satırı arayüzünden başlayarak playbook yazımının temel kavramlarına kadar olan her şeyi ele aldık. Playbook anatomisi, facts yönetimi, koşullu çalıştırma, handler'lar ve tags gibi konular, ilerleyen yazılarda ele alacağımız rol mimarisi ve Vault ile secret yönetimi için sağlam bir temel oluşturuyor.