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.
