Zero Downtime Deployment: Ansible ile Kesintisiz Dağıtım Stratejileri

Production ortamında bir deployment yapmanın en stresli anı, o “şimdi mi kesilecek?” sorusunun cevabını beklediğin birkaç saniyedir. Kullanıcılar bağlıyken servisi kesmek artık kabul edilemez bir durum. Zero downtime deployment, yani sıfır kesinti ile dağıtım, modern sysadmin’lerin temel becerilerinden biri haline geldi. Ansible bu konuda oldukça güçlü araçlar sunuyor ama doğru stratejiyi seçmek ve doğru yapılandırmak bambaşka bir iş. Bu yazıda gerçek dünya senaryolarıyla Ansible üzerinde zero downtime deployment stratejilerini derinlemesine inceleyeceğiz.

Neden Zero Downtime Deployment?

Klasik deployment senaryosunu düşün: servisi durdur, yeni sürümü yükle, servisi başlat. Bu yaklaşım 2005’te belki mantıklıydı ama bugün SLA anlaşmaları, müşteri beklentileri ve rekabet baskısı altında bu lüksü karşılayamazsın.

E-ticaret sitesinde bir deployment sırasında 3 dakika servis kesintisi yaşandığında ne olur? Binlerce TL sepet kaybı, müşteri şikayetleri ve itibar hasarı. Ya da bir fintech uygulamasında mesai saatleri içinde deployment? Kullanıcılar işlem ortasında koptu mu, bu destek talebi olarak masana gelir.

Zero downtime deployment’ın temel hedefleri şunlardır:

  • Kullanıcı deneyimi sürekliliği: Aktif session’lar kesilmemeli
  • İş sürekliliği: Gelir kaybının önüne geçmek
  • Risk azaltma: Hızlı rollback imkanı
  • Güven: Ekip deployment’tan korkmaktan çıkar

Ansible’ın Temel Mekanizmaları

Stratejilere geçmeden önce Ansible’ın bu konuda bize sağladığı temel araçları anlamak gerekiyor.

Serial Execution

Ansible’ın en temel zero downtime aracı serial parametresidir. Normalde Ansible tüm host’lara paralel task çalıştırır. serial ile bunu kontrol altına alırsın.

---
- name: Web sunucu deployment
  hosts: web_servers
  serial: 1
  tasks:
    - name: Uygulamayı guncelle
      apt:
        name: myapp
        state: latest

Bu yapıda Ansible web sunucularına sırayla gider, birini bitirmeden diğerine geçmez. Ama bu çok yavaş olabilir. Daha akıllıca bir yaklaşım yüzde bazlı serial kullanmak:

---
- name: Web sunucu deployment
  hosts: web_servers
  serial:
    - 1        # Önce 1 sunucu (canary)
    - 25%      # Sonra %25
    - 50%      # Sonra %50
    - 100%     # Kalan hepsi
  max_fail_percentage: 20
  tasks:
    - name: Load balancer'dan cikar
      shell: lb_manage.sh remove {{ inventory_hostname }}
      delegate_to: loadbalancer

    - name: Aktif baglantilarin bitmesini bekle
      wait_for:
        port: 8080
        state: drained
        timeout: 30

    - name: Uygulamayi guncelle
      apt:
        name: myapp
        state: latest
        update_cache: yes

    - name: Servisi baslat
      systemd:
        name: myapp
        state: restarted
        enabled: yes

    - name: Health check
      uri:
        url: "http://{{ inventory_hostname }}:8080/health"
        status_code: 200
      retries: 5
      delay: 10

    - name: Load balancer'a geri ekle
      shell: lb_manage.sh add {{ inventory_hostname }}
      delegate_to: loadbalancer

Bu yaklaşımda max_fail_percentage: 20 parametresi kritik. Eğer sunucuların yüzde 20’sinden fazlası başarısız olursa Ansible tüm playbook’u durdurur. Bu erken uyarı mekanizmanı oluşturur.

Rolling Update Stratejisi

Rolling update, zero downtime deployment’ın en yaygın ve güvenilir yöntemidir. Load balancer arkasındaki sunuculara sırayla güncelleme yaparsın.

Gerçek dünya senaryosu: 10 web sunucunlu bir uygulama. Nginx load balancer arkasında çalışıyorlar. HAProxy da olabilir, bu mantık değişmiyor.

---
- name: Rolling Update - Web Tier
  hosts: web_servers
  serial: 2
  max_fail_percentage: 0
  
  pre_tasks:
    - name: Sunucunun aktif baglanti sayisini kontrol et
      shell: ss -tn state established '( dport = :8080 )' | wc -l
      register: active_connections
      
    - name: Cok fazla aktif baglanti varsa bekle
      wait_for:
        timeout: 60
      when: active_connections.stdout | int > 100

  tasks:
    - name: Upstream'den cikar (HAProxy)
      shell: >
        echo "disable server myapp/{{ inventory_hostname }}" | 
        socat stdio /var/run/haproxy/admin.sock
      delegate_to: "{{ groups['load_balancers'][0] }}"
      
    - name: Drain suresi
      wait_for:
        timeout: 15
        
    - name: Mevcut versiyonu yedekle
      copy:
        src: /opt/myapp/current
        dest: /opt/myapp/previous
        remote_src: yes
        
    - name: Yeni paketi yukle
      unarchive:
        src: "{{ artifact_url }}"
        dest: /opt/myapp/releases/{{ version }}
        remote_src: yes
        
    - name: Symlink'i guncelle
      file:
        src: /opt/myapp/releases/{{ version }}
        dest: /opt/myapp/current
        state: link
        
    - name: Servisi yeniden baslat
      systemd:
        name: myapp
        state: restarted
        
    - name: Servis saglik kontrolu
      uri:
        url: "http://localhost:8080/health"
        status_code: 200
        return_content: yes
      register: health_check
      retries: 10
      delay: 5
      until: health_check.status == 200
      
    - name: Load balancer'a geri ekle
      shell: >
        echo "enable server myapp/{{ inventory_hostname }}" |
        socat stdio /var/run/haproxy/admin.sock
      delegate_to: "{{ groups['load_balancers'][0] }}"

  post_tasks:
    - name: Deployment sonrasi dogrulama
      uri:
        url: "http://{{ inventory_hostname }}:8080/version"
        return_content: yes
      register: version_check
      
    - name: Versiyon eslesme kontrolu
      assert:
        that:
          - version_check.json.version == expected_version
        fail_msg: "Versiyon eslesmiyor! Beklenen: {{ expected_version }}, Gelen: {{ version_check.json.version }}"

Blue-Green Deployment

Blue-green, iki özdeş ortamdan birini aktif tutarken diğerini güncelleme mantığına dayanır. Ansible ile bu stratejiyi uygulamak oldukça temiz.

---
- name: Blue-Green Deployment
  hosts: localhost
  gather_facts: no
  
  vars:
    blue_servers: "{{ groups['blue'] }}"
    green_servers: "{{ groups['green'] }}"
    
  tasks:
    - name: Aktif ortami tespit et
      shell: cat /etc/myapp/active_env
      register: current_env
      delegate_to: loadbalancer
      
    - name: Hedef ortami belirle
      set_fact:
        target_env: "{{ 'green' if current_env.stdout == 'blue' else 'blue' }}"
        source_env: "{{ current_env.stdout }}"
        
    - name: Bilgi ver
      debug:
        msg: "Aktif: {{ source_env }}, Hedef: {{ target_env }}"

- name: Hedef ortami guncelle
  hosts: "{{ target_env }}"
  
  tasks:
    - name: Yeni versiyon deploy et
      include_tasks: deploy_app.yml
      
    - name: Smoke test calistir
      uri:
        url: "http://{{ inventory_hostname }}:8080/health"
        status_code: 200
      retries: 5
      delay: 5
      
    - name: Entegrasyon testleri
      shell: /opt/tests/run_integration_tests.sh {{ inventory_hostname }}
      delegate_to: localhost
      register: test_result
      
    - name: Test sonucunu dogrula
      fail:
        msg: "Entegrasyon testleri basarisiz! Rollback gerekiyor."
      when: test_result.rc != 0

- name: Traffic'i cevir
  hosts: loadbalancer
  
  tasks:
    - name: Nginx upstream'i guncelle
      template:
        src: nginx_upstream.j2
        dest: /etc/nginx/conf.d/upstream.conf
      vars:
        active_servers: "{{ groups[target_env] }}"
        
    - name: Nginx config test
      command: nginx -t
      
    - name: Nginx graceful reload
      command: nginx -s reload
      
    - name: Aktif ortami kaydet
      copy:
        content: "{{ target_env }}"
        dest: /etc/myapp/active_env

Nginx upstream template’i de şöyle görünür:

# /etc/ansible/templates/nginx_upstream.j2
upstream myapp_backend {
    least_conn;
    {% for server in active_servers %}
    server {{ server }}:8080 weight=1 max_fails=3 fail_timeout=30s;
    {% endfor %}
    
    keepalive 32;
}

Canary Release

Canary deployment, yeni versiyonu önce küçük bir kullanıcı kitlesine açmak demek. Risk yönetimi açısından en sofistike yaklaşım.

---
- name: Canary Release
  hosts: web_servers
  gather_facts: yes
  
  vars:
    canary_percentage: 10
    canary_hosts: []
    
  tasks:
    - name: Canary host sayisini hesapla
      set_fact:
        canary_count: "{{ (groups['web_servers'] | length * canary_percentage / 100) | round | int | max(1) }}"
      run_once: true
      
    - name: Canary host listesi olustur
      set_fact:
        canary_hosts: "{{ groups['web_servers'][:canary_count | int] }}"
      run_once: true
      
    - name: Canary deploy
      include_tasks: deploy_single.yml
      when: inventory_hostname in canary_hosts
      
    - name: Canary metriklerini izle (5 dakika)
      pause:
        minutes: 5
        prompt: "Canary sunuculari izleniyor. Sorun varsa Ctrl+C ile iptal edin."
      run_once: true
      
    - name: Canary hata oranini kontrol et
      uri:
        url: "http://monitoring.internal/api/error_rate?host={{ item }}&minutes=5"
        return_content: yes
      register: error_metrics
      loop: "{{ canary_hosts }}"
      delegate_to: localhost
      run_once: true
      
    - name: Hata orani kontrolu
      fail:
        msg: "Canary hata orani yuksek! Deployment durduruluyor."
      when: >
        error_metrics.results | 
        map(attribute='json.error_rate') | 
        map('float') | 
        max > 0.05
      run_once: true
      
    - name: Kalan sunuculara deploy et
      include_tasks: deploy_single.yml
      when: inventory_hostname not in canary_hosts

Rollback Mekanizması

Her zero downtime stratejisinin en kritik parçası hızlı ve güvenilir rollback. Deployment başarısız olduğunda paniklememek için önceden hazırlanmış rollback playbook’una ihtiyacın var.

---
- name: Acil Rollback
  hosts: "{{ target_hosts | default('web_servers') }}"
  serial: 5
  
  tasks:
    - name: Onceki versiyonu kontrol et
      stat:
        path: /opt/myapp/previous
      register: previous_exists
      
    - name: Onceki versiyon yoksa hata ver
      fail:
        msg: "Rollback yapilamaz! Onceki versiyon bulunamadi."
      when: not previous_exists.stat.exists
      
    - name: Load balancer'dan cikar
      shell: >
        echo "disable server myapp/{{ inventory_hostname }}" |
        socat stdio /var/run/haproxy/admin.sock
      delegate_to: "{{ groups['load_balancers'][0] }}"
      ignore_errors: yes
      
    - name: Servisi durdur
      systemd:
        name: myapp
        state: stopped
        
    - name: Mevcut versiyonu kopyala (hata analizi icin)
      copy:
        src: /opt/myapp/current
        dest: "/opt/myapp/failed_{{ ansible_date_time.epoch }}"
        remote_src: yes
      ignore_errors: yes
        
    - name: Symlink'i onceki versiyona al
      file:
        src: /opt/myapp/previous
        dest: /opt/myapp/current
        state: link
        force: yes
        
    - name: Servisi baslat
      systemd:
        name: myapp
        state: started
        
    - name: Health check
      uri:
        url: "http://localhost:8080/health"
        status_code: 200
      retries: 5
      delay: 5
      
    - name: Load balancer'a geri ekle
      shell: >
        echo "enable server myapp/{{ inventory_hostname }}" |
        socat stdio /var/run/haproxy/admin.sock
      delegate_to: "{{ groups['load_balancers'][0] }}"
      
    - name: Rollback bildirimi gonder
      uri:
        url: "{{ slack_webhook }}"
        method: POST
        body_format: json
        body:
          text: "ROLLBACK tamamlandi! Sunucu: {{ inventory_hostname }}, Zaman: {{ ansible_date_time.iso8601 }}"
      delegate_to: localhost
      ignore_errors: yes

Database Migration ile Uyum

Zero downtime deployment’ın en zor kısmı database migration’larıdır. Uygulama değişirken şema da değişiyorsa ne yaparsın?

---
- name: Database Migration - Zero Downtime
  hosts: db_primary
  
  tasks:
    - name: Migration dosyasini kontrol et
      stat:
        path: "{{ migration_file }}"
      register: migration_exists
      
    - name: Migration'i backward compatible yap
      debug:
        msg: |
          KURAL: Zero downtime migration adimi:
          1. Yeni kolon/tablo EKLE (eski uygulama etkisiz)
          2. Yeni uygulama deploy et (her iki yapiya yazabilir)
          3. Veriyi migrate et
          4. Eski kolon/tabloyu kaldir (bir sonraki release)
          
    - name: Additive migration calistir
      shell: |
        psql -U {{ db_user }} -d {{ db_name }} -f {{ migration_file }}
      environment:
        PGPASSWORD: "{{ db_password }}"
      register: migration_result
      
    - name: Migration sonucunu dogrula
      assert:
        that:
          - migration_result.rc == 0
        fail_msg: "Migration basarisiz: {{ migration_result.stderr }}"
        
    - name: Migration log kaydi
      lineinfile:
        path: /var/log/myapp/migrations.log
        line: "{{ ansible_date_time.iso8601 }} - Migration basarili: {{ migration_file }}"
        create: yes

Monitoring Entegrasyonu

Deployment sırasında ve sonrasında monitoring sistemleriyle konuşmak, otomatik rollback kararı almak için kritik.

---
- name: Deployment ile Monitoring Entegrasyonu
  hosts: web_servers
  serial: 1
  
  tasks:
    - name: Datadog/Prometheus deployment eventi gonder
      uri:
        url: "https://api.datadoghq.com/api/v1/events"
        method: POST
        headers:
          DD-API-KEY: "{{ datadog_api_key }}"
        body_format: json
        body:
          title: "Deployment Basladi"
          text: "{{ inventory_hostname }} - Versiyon: {{ version }}"
          tags:
            - "env:production"
            - "service:myapp"
            - "version:{{ version }}"
      delegate_to: localhost
      ignore_errors: yes
      
    - name: Deployment islemi
      include_tasks: deploy_app.yml
      
    - name: Deployment sonrasi metrik kontrolu
      uri:
        url: "http://prometheus.internal/api/v1/query"
        body_format: form-urlencoded
        body:
          query: 'rate(http_errors_total{job="myapp",instance="{{ inventory_hostname }}"}[2m])'
      register: error_rate
      delegate_to: localhost
      retries: 3
      delay: 30
      
    - name: Hata orani degerlendirmesi
      fail:
        msg: "Deployment sonrasi hata orani yuksek: {{ error_rate.json.data.result[0].value[1] }}"
      when: >
        error_rate.json.data.result | length > 0 and
        error_rate.json.data.result[0].value[1] | float > 0.01

Inventory Yapısı ve Gruplama

Doğru inventory yapısı olmadan hiçbir strateji düzgün çalışmaz. Gerçek bir production ortamı için inventory yapısı:

# inventory/production/hosts.yml
all:
  children:
    load_balancers:
      hosts:
        lb-01.prod.example.com:
        lb-02.prod.example.com:
    
    web_servers:
      children:
        web_canary:
          hosts:
            web-01.prod.example.com:
        web_stable:
          hosts:
            web-02.prod.example.com:
            web-03.prod.example.com:
            web-04.prod.example.com:
      vars:
        app_port: 8080
        deploy_path: /opt/myapp
    
    blue:
      hosts:
        web-01.prod.example.com:
        web-02.prod.example.com:
    
    green:
      hosts:
        web-03.prod.example.com:
        web-04.prod.example.com:
    
    db_servers:
      children:
        db_primary:
          hosts:
            db-01.prod.example.com:
        db_replicas:
          hosts:
            db-02.prod.example.com:
            db-03.prod.example.com:

Sık Karşılaşılan Sorunlar

Pratikte zero downtime deployment yaparken birkaç tuzağa düşmek çok kolay.

Session stickiness sorunu: Load balancer oturum tutarlılığı olmadan kullanıcı farklı sunuculara yönlenirse session kaybedebilir. Çözüm ya sticky session ya da merkezi session store (Redis, Memcached).

Health check yanlış yapılandırma: Health endpoint gerçekten uygulamanın hazır olduğunu test etmeli. Sadece HTTP 200 dönmek yetmez, veritabanı bağlantısı, cache bağlantısı gibi kritik bağımlılıkları da kontrol etmeli.

Drain süresi yetersizliği: wait_for ile drain beklersen, uzun süren işlemler (dosya yükleme gibi) kesilebilir. Bağlantı sayısını izleyerek dinamik bekleme daha güvenli.

Rollback testi eksikliği: Rollback prosedürünü hiç test etmemek ve kriz anında çalışmadığını fark etmek en klasik hatalardan biri. Her sprint’te rollback testini mutlaka yapın.

Pratik Öneriler

Yıllar içinde öğrendiğim birkaç kritik nokta:

  • Her deployment öncesi backup al: Otomatik bile olsa bir checkpoint oluştur
  • Idempotency’e dikkat et: Ansible playbook’larını birden fazla kez çalıştırabilir olmak zorunda
  • Verbose loglama ekle: Neyin ne zaman olduğunu sonradan analiz edebilmek için
  • Timeout’ları gerçekçi belirle: Çok kısa timeout hatalı rollback tetikler, çok uzun timeout sorunu gizler
  • Deployment window tanımla: Kritik sistemler için düşük trafik saatlerini tercih et, sıfır downtime olsa bile risk azalır
  • Her adımı test et: Staging ortamında tam anlamıyla simüle et, production’da sürpriz istemezsin

Sonuç

Zero downtime deployment Ansible ile yapılabilir, hatta oldukça zarif bir şekilde yapılabilir. Ama tek bir “doğru” strateji yok. Rolling update basit ve güvenilir, blue-green daha hızlı geçiş sağlar, canary ise risk en aza iner.

Sisteminizin ihtiyaçlarına göre bu stratejileri karıştırabilirsiniz. Örneğin canary + rolling kombinasyonu çok yaygın. İlk yüzde 10’a canary olarak ver, sorun yoksa rolling ile devam et.

En önemli nokta şu: strateji ne olursa olsun, rollback mekanizması test edilmiş ve güvenilir olmalı. Deployment güvenle değil, hazırlıkla yapılır. Ansible bu hazırlığı koda dökmeni sağlar, gerisi senin disiplinine kalıyor.

Bir yanıt yazın

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