Ansible Playbook ile Uygulama Kurulumu ve Yapılandırma

Ansible ile otomasyon yolculuğuna başlayanların çoğu, ilk adımda birkaç komut çalıştıran basit playbook’lar yazar. Ama işin gerçeği şu: Ansible’ın asıl gücü, karmaşık uygulama kurulumlarını ve yapılandırmalarını tekrarlanabilir, güvenilir bir şekilde yönetebilmesinde yatıyor. Bu yazıda sıfırdan başlayarak gerçek dünyada kullanabileceğiniz playbook’lar yazacağız, hataları ele alacağız ve production ortamına uygun yapılandırma yönetimini konuşacağız.

Playbook Anatomisi: Temel Yapıyı Anlamak

Bir playbook yazmadan önce yapıyı kafanızda oturtmanız gerekiyor. Playbook, bir veya daha fazla play içeren YAML dosyasıdır. Her play, belirli bir host grubuna belirli görevler (task) atar.

# Basit bir playbook yapısı
---
- name: Web sunucusu kurulumu
  hosts: webservers
  become: yes
  vars:
    http_port: 80
    app_user: webadmin

  tasks:
    - name: Nginx kur
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Nginx servisini başlat
      service:
        name: nginx
        state: started
        enabled: yes

Burada dikkat etmeniz gereken birkaç nokta var. become: yes satırı, görevlerin sudo ile çalışacağını söylüyor. state: present ise paketin kurulu olmasını garanti ediyor, yoksa kuruyor, varsa dokunmuyor. Bu idempotency kavramının ta kendisi: aynı playbook’u on kez çalıştırsanız da sonuç aynı.

Envanter Yönetimi ve Değişken Hiyerarşisi

Playbook’ların etkin çalışması için iyi organize edilmiş bir envanter şart. Statik envanter dosyası basit ortamlar için yeterli olsa da production’da dinamik envanter kullanmak çok daha mantıklı.

# /etc/ansible/hosts veya proje klasörünüzdeki inventory/hosts dosyası
[webservers]
web01.ornekfirma.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/prod_key
web02.ornekfirma.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/prod_key

[dbservers]
db01.ornekfirma.com ansible_user=ubuntu

[webservers:vars]
nginx_worker_processes=4
nginx_worker_connections=1024

[dbservers:vars]
postgresql_version=15
max_connections=200

Değişken hiyerarşisi Ansible’da çok önemli. En düşük öncelikten en yükseğe doğru sıralama şöyle:

  • role defaults: En kolay ezilen değişkenler
  • inventory vars: Envanterde tanımlanan değişkenler
  • playbook vars: Playbook içinde tanımlanan vars
  • host_vars: Host bazlı değişken dosyaları
  • extra vars (-e): Komut satırından geçilen değişkenler, her şeyi ezer
# host_vars/web01.ornekfirma.com.yml
---
server_role: primary
ssl_enabled: true
app_version: "2.4.1"

# group_vars/webservers.yml
---
nginx_port: 80
app_dir: /var/www/html
log_level: warn

Gerçek Dünya Senaryosu: LAMP Stack Kurulumu

Artık teoriden pratiğe geçelim. Bir müşteri için klasik LAMP stack kuracağız: Ubuntu 22.04 üzerinde Apache, MySQL ve PHP. Bu senaryo benim en sık karşılaştığım kurulum tiplerinden biri.

# lamp_setup.yml
---
- name: LAMP Stack Kurulumu
  hosts: webservers
  become: yes
  vars_files:
    - vars/main.yml
    - vars/secrets.yml  # ansible-vault ile şifrelenmiş

  pre_tasks:
    - name: Sistem güncellemesi
      apt:
        update_cache: yes
        cache_valid_time: 3600

  tasks:
    - name: Gerekli paketleri kur
      apt:
        name:
          - apache2
          - mysql-server
          - php8.1
          - php8.1-mysql
          - php8.1-curl
          - php8.1-gd
          - php8.1-mbstring
          - php8.1-xml
          - libapache2-mod-php8.1
          - python3-pymysql
        state: present

    - name: Apache modüllerini etkinleştir
      apache2_module:
        name: "{{ item }}"
        state: present
      loop:
        - rewrite
        - headers
        - ssl
      notify: Apache yeniden başlat

    - name: Apache yapılandırma dosyasını kopyala
      template:
        src: templates/apache_vhost.conf.j2
        dest: /etc/apache2/sites-available/{{ app_name }}.conf
        owner: root
        group: root
        mode: '0644'
      notify: Apache yeniden başlat

    - name: Virtual host'u etkinleştir
      command: a2ensite {{ app_name }}.conf
      notify: Apache yeniden başlat

    - name: MySQL root şifresini ayarla
      mysql_user:
        name: root
        password: "{{ mysql_root_password }}"
        login_unix_socket: /var/run/mysqld/mysqld.sock
        host_all: yes

    - name: Uygulama veritabanını oluştur
      mysql_db:
        name: "{{ db_name }}"
        state: present
        login_user: root
        login_password: "{{ mysql_root_password }}"

    - name: Uygulama DB kullanıcısını oluştur
      mysql_user:
        name: "{{ db_user }}"
        password: "{{ db_password }}"
        priv: "{{ db_name }}.*:ALL"
        state: present
        login_user: root
        login_password: "{{ mysql_root_password }}"

  handlers:
    - name: Apache yeniden başlat
      service:
        name: apache2
        state: restarted

Handlers konusuna biraz daha değinelim. Birden fazla task aynı handler’ı tetiklese bile handler yalnızca bir kez çalışır ve play’in sonunda. Bu sayede üç ayrı config değişikliği yapıldığında Apache üç kez yeniden başlamaz, sadece bir kez başlar.

Jinja2 Template’leri ile Dinamik Yapılandırma

Sabit config dosyaları kopyalamak yerine Jinja2 template’leri kullanmak, playbook’larınızı gerçekten güçlü kılan şeydir. Farklı ortamlar için farklı değerler, koşullu bloklar ve döngüler kullanabilirsiniz.

# templates/apache_vhost.conf.j2
<VirtualHost *:{{ nginx_port | default(80) }}>
    ServerName {{ server_name }}
    ServerAlias www.{{ server_name }}
    DocumentRoot {{ app_dir }}

    <Directory {{ app_dir }}>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/{{ app_name }}_error.log
    CustomLog ${APACHE_LOG_DIR}/{{ app_name }}_access.log combined

    {% if ssl_enabled | default(false) %}
    # SSL yapılandırması
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/{{ app_name }}.crt
    SSLCertificateKeyFile /etc/ssl/private/{{ app_name }}.key
    {% endif %}

    {% for header in security_headers | default([]) %}
    Header always set {{ header.name }} "{{ header.value }}"
    {% endfor %}
</VirtualHost>

Template’deki | default() filtresi, değişken tanımlanmamışsa fallback değeri kullanır. Bu, playbook’ları farklı ortamlarda çalıştırırken hayat kurtarır.

Roller: Playbook’ları Modüler Hale Getirme

Gerçek production ortamlarında her şeyi tek bir playbook dosyasına yazmak büyük bir kaos yaratır. Ansible rolleri, yapılandırmaları mantıksal birimler halinde organize etmenizi sağlar.

# Rol yapısı oluşturma
ansible-galaxy init roles/nodejs_app

# Oluşan yapı:
# roles/nodejs_app/
# ├── defaults/
# │   └── main.yml
# ├── files/
# ├── handlers/
# │   └── main.yml
# ├── tasks/
# │   └── main.yml
# ├── templates/
# ├── tests/
# └── vars/
#     └── main.yml
# roles/nodejs_app/tasks/main.yml
---
- name: Node.js repository ekle
  shell: |
    curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
  args:
    creates: /etc/apt/sources.list.d/nodesource.list

- name: Node.js ve npm kur
  apt:
    name:
      - nodejs
      - build-essential
    state: present

- name: PM2 process manager kur
  npm:
    name: pm2
    global: yes
    state: present

- name: Uygulama dizinini oluştur
  file:
    path: "{{ app_dir }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: '0755'

- name: Uygulama kodunu kopyala
  synchronize:
    src: "{{ local_app_path }}/"
    dest: "{{ app_dir }}/"
    delete: yes
    rsync_opts:
      - "--exclude=node_modules"
      - "--exclude=.git"

- name: npm bağımlılıklarını kur
  npm:
    path: "{{ app_dir }}"
    state: present
  become_user: "{{ app_user }}"

- name: PM2 ecosystem dosyasını oluştur
  template:
    src: ecosystem.config.js.j2
    dest: "{{ app_dir }}/ecosystem.config.js"
    owner: "{{ app_user }}"
    mode: '0644'
  notify: PM2 yeniden başlat

- name: PM2 startup script ayarla
  command: pm2 startup systemd -u {{ app_user }} --hp /home/{{ app_user }}
  become: yes

Rolleri kullanmak da son derece basit:

# site.yml - Ana playbook
---
- name: Web sunucularını yapılandır
  hosts: webservers
  become: yes
  roles:
    - common
    - nginx
    - nodejs_app
    - monitoring

- name: Veritabanı sunucularını yapılandır
  hosts: dbservers
  become: yes
  roles:
    - common
    - postgresql
    - backup_agent

Koşullar, Döngüler ve Hata Yönetimi

Gerçek ortamlarda her zaman “eğer şu koşul varsa şunu yap” mantığına ihtiyaç duyarsınız. Ansible’ın when, loop ve block yapıları bu iş için biçilmiş kaftan.

# Koşullar ve hata yönetimi örneği
---
- name: Uygulama deployment ve sağlık kontrolü
  hosts: appservers
  become: yes

  tasks:
    - name: İşletim sistemi kontrol et
      debug:
        msg: "Bu sunucu {{ ansible_distribution }} {{ ansible_distribution_version }} çalıştırıyor"

    - name: Ubuntu 20.04 veya üzeri için özel paket kur
      apt:
        name: somepackage
        state: present
      when:
        - ansible_distribution == "Ubuntu"
        - ansible_distribution_version is version('20.04', '>=')

    - name: Mevcut servis durumunu kontrol et
      command: systemctl is-active myapp
      register: service_status
      failed_when: false
      changed_when: false

    - name: Eski versiyonu yedekle
      copy:
        src: "{{ app_dir }}/app.jar"
        dest: "{{ app_dir }}/app.jar.backup"
        remote_src: yes
      when: service_status.stdout == "active"

    - name: Kritik işlemler bloğu
      block:
        - name: Yeni versiyonu indir
          get_url:
            url: "https://releases.ornekfirma.com/app-{{ app_version }}.jar"
            dest: "{{ app_dir }}/app.jar"
            checksum: "sha256:{{ app_checksum }}"

        - name: Servisi yeniden başlat
          service:
            name: myapp
            state: restarted

        - name: Sağlık kontrolü
          uri:
            url: "http://localhost:{{ app_port }}/health"
            status_code: 200
            timeout: 30
          retries: 5
          delay: 10
          register: health_check
          until: health_check.status == 200

      rescue:
        - name: Hata durumunda eski versiyona geri dön
          copy:
            src: "{{ app_dir }}/app.jar.backup"
            dest: "{{ app_dir }}/app.jar"
            remote_src: yes

        - name: Servisi eski versiyonla başlat
          service:
            name: myapp
            state: restarted

        - name: Ekibe hata bildirimi gönder
          mail:
            to: "[email protected]"
            subject: "KRITIK: {{ inventory_hostname }} deployment başarısız"
            body: "Deployment başarısız oldu, eski versiyona geri dönüldü."

      always:
        - name: Deployment logunu kaydet
          lineinfile:
            path: /var/log/deployments.log
            line: "{{ ansible_date_time.iso8601 }} - {{ inventory_hostname }} - v{{ app_version }} - {{ 'BASARILI' if health_check.status is defined and health_check.status == 200 else 'BASARISIZ' }}"
            create: yes

Bu yapıdaki block/rescue/always kombinasyonu, try/catch/finally mantığının Ansible karşılığı. Production deployment’larında bu pattern’i mutlaka kullanın.

Ansible Vault ile Gizli Bilgi Yönetimi

Şifreler, API anahtarları ve sertifikaları playbook’larınıza düz metin olarak yazmak büyük bir güvenlik açığı. Ansible Vault bu sorunu çözüyor.

# Vault ile şifreli dosya oluşturma
ansible-vault create vars/secrets.yml

# Mevcut dosyayı şifreleme
ansible-vault encrypt vars/secrets.yml

# Şifreli dosyayı düzenleme
ansible-vault edit vars/secrets.yml

# Şifreyi görüntüleme (dikkatli kullanın)
ansible-vault view vars/secrets.yml

# Şifreyi değiştirme
ansible-vault rekey vars/secrets.yml

# Playbook'u vault şifresiyle çalıştırma
ansible-playbook site.yml --ask-vault-pass

# CI/CD ortamında şifre dosyasıyla çalıştırma
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# Sadece belirli değerleri şifreleme (inline vault)
ansible-vault encrypt_string 'super_gizli_sifre123' --name 'db_password'

Vault şifreli bir secrets.yml dosyası şu şekilde görünür:

# vars/secrets.yml - şifreli hali (vault ile encrypt edilmiş)
$ANSIBLE_VAULT;1.1;AES256
66386134653765386232383236353337623062303435363466306636323564393866613330363739
3839363939316136653833633834313061623937363439350a386665623865396464353564633161
...

# Şifre çözülmüş içeriği:
# ---
# db_password: "SifremCokGuclu123!"
# api_key: "sk-prod-xxxxxxxxxxxxxxxxxxx"
# ssl_private_key: |
#   -----BEGIN PRIVATE KEY-----
#   ...

CI/CD Pipeline ile Ansible Entegrasyonu

Ansible’ı sadece manuel olarak çalıştırmak, otomasyon yolculuğunun ilk adımı. Gerçek otomasyon, GitLab CI veya Jenkins gibi araçlarla entegrasyon yapıldığında başlıyor.

# .gitlab-ci.yml - GitLab CI ile Ansible deployment
stages:
  - test
  - staging_deploy
  - production_deploy

variables:
  ANSIBLE_FORCE_COLOR: "true"
  ANSIBLE_HOST_KEY_CHECKING: "False"

.ansible_base:
  image: cytopia/ansible:latest
  before_script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | tr -d 'r' > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - echo "$VAULT_PASSWORD" > ~/.vault_pass
    - chmod 600 ~/.vault_pass

ansible_lint:
  stage: test
  extends: .ansible_base
  script:
    - ansible-lint playbooks/site.yml
    - ansible-playbook playbooks/site.yml --syntax-check

deploy_staging:
  stage: staging_deploy
  extends: .ansible_base
  script:
    - ansible-playbook
        -i inventory/staging
        --vault-password-file ~/.vault_pass
        -e "app_version=${CI_COMMIT_TAG:-latest}"
        playbooks/site.yml
  environment:
    name: staging
  only:
    - develop

deploy_production:
  stage: production_deploy
  extends: .ansible_base
  script:
    - ansible-playbook
        -i inventory/production
        --vault-password-file ~/.vault_pass
        -e "app_version=${CI_COMMIT_TAG}"
        playbooks/site.yml
  environment:
    name: production
  when: manual
  only:
    - tags

Bu pipeline’da production deployment’ı when: manual ile korunuyor, yani birisi GitLab arayüzünden onaylama butonu tıklamadan production’a hiçbir şey gitmiyor. Bu, gece 2’de yanlışlıkla production’a deployment yapılmasını önleyen basit ama etkili bir güvenlik önlemi.

Playbook Performansı ve Best Practice’ler

Yüzlerce sunucuyu yönetmeye başladığınızda playbook performansı kritik hale geliyor. Birkaç önemli optimizasyon:

  • gather_facts: no: Sunucular hakkında bilgi toplamak zaman alır. Gereksizse kapatın
  • serial: Deployment’ı batch’ler halinde yapın, hepsini aynı anda güncellemeyin
  • async ve poll: Uzun süren görevleri asenkron çalıştırın
  • delegate_to: Bir görevi farklı bir host üzerinde çalıştırın
  • run_once: Cluster’daki sadece bir node’da çalıştırılması gereken görevler için
# Performans odaklı rolling deployment
---
- name: Rolling deployment - %25 batch
  hosts: webservers
  become: yes
  serial: "25%"    # Sunucuların %25'ini aynı anda güncelle
  max_fail_percentage: 10  # %10'dan fazla başarısız olursa dur
  gather_facts: yes

  pre_tasks:
    - name: Load balancer'dan çıkar
      uri:
        url: "http://lb.ornekfirma.com/api/remove/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost

  tasks:
    - name: Uzun süren kurulumu asenkron başlat
      apt:
        name: some-large-package
        state: present
      async: 600    # Maksimum 600 saniye bekle
      poll: 0       # Sonucu bekleme, devam et
      register: install_job

    - name: Kurulum tamamlandı mı kontrol et
      async_status:
        jid: "{{ install_job.ansible_job_id }}"
      register: install_result
      until: install_result.finished
      retries: 30
      delay: 20

  post_tasks:
    - name: Sağlık kontrolü sonrası load balancer'a geri ekle
      uri:
        url: "http://lb.ornekfirma.com/api/add/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      when: install_result.finished

Sonuç

Ansible playbook’ları ile uygulama kurulumu ve yapılandırması, başta karmaşık görünse de doğru yapı kurulduğunda inanılmaz bir özgürlük sağlıyor. Artık “hangi sunucuya ne kurdum” diye kayıt tutmak yerine, tüm altyapı durumunuz Git reponuzda, okunabilir YAML dosyaları olarak yaşıyor.

Şu ana kadar ele aldığımız konuları özetleyecek olursak: İyi organize edilmiş envanter ve değişken hiyerarşisi temeli oluşturuyor. Jinja2 template’leri yapılandırmalarınızı dinamik ve esnek kılıyor. Roller, büyük projelerde tekrar kullanılabilirlik ve düzen sağlıyor. Block/rescue yapısı, deployment hatalarında otomatik rollback imkanı veriyor. Vault, gizli bilgileri güvende tutarken CI/CD entegrasyonu tüm süreci otomatize ediyor.

Benim tavsiyem: Her şeyi bir anda mükemmel yapmaya çalışmayın. Bugün tek bir uygulamanın kurulumunu otomatize eden basit bir playbook yazın. Yarın bunu bir role dönüştürün. Öbür gün CI/CD’ye bağlayın. Ansible öğrenme eğrisi görece düşük ama derinliği çok, her gün yeni bir şey keşfedeceksiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir