Ansible ile Servis Sağlık Kontrolü ve Otomatik Rollback
Prodüksiyon ortamında bir deployment yaptınız, her şey güzel gözüküyor, ama birkaç dakika sonra monitoring sistemleriniz alarm vermeye başlıyor. Servis ayakta ama düzgün yanıt vermiyor, veritabanı bağlantıları zaman aşımına uğruyor ya da uygulama hata kodu döndürüyor. İşte tam bu noktada “deployment öncesine nasıl dönerim?” sorusu hayati önem kazanıyor. Ansible ile sağlam bir sağlık kontrolü ve rollback mekanizması kurarsanız, bu tür krizleri dakikalar içinde çözebilirsiniz.
Neden Sağlık Kontrolü ve Rollback Şart?
Deployment süreçlerinde en büyük yanılgılardan biri şu: Servis ayağa kalktıysa her şey yolunda demektir. Oysa bir servisin process olarak çalışması ile gerçekten sağlıklı çalışması arasında dağlar kadar fark var. Uygulama başlamış olabilir ama HTTP endpoint’leri 500 döndürüyor olabilir, cache bağlantısı kopuk olabilir ya da konfigürasyon yanlış yüklenmiş olabilir.
Ansible’ın gücü tam da burada devreye giriyor. Tek bir playbook içinde hem deployment’ı yapabilir, hem sağlık kontrolü uygulayabilir, hem de bir şeyler ters giderse önceki kararlı sürüme otomatik olarak dönebilirsiniz. Bu yaklaşım size şunları kazandırır:
- Güven: Her deployment sonrası sisteminizin gerçekten çalıştığını doğrularsınız
- Hız: Manuel müdahale beklemeden otomatik rollback ile downtime’ı minimize edersiniz
- Tekrarlanabilirlik: Aynı prosedürü her ortamda birebir uygularsınız
- Audit trail: Ansible logları sayesinde ne zaman ne yapıldığını takip edebilirsiniz
Temel Kavramlar ve Yaklaşım
Sağlıklı bir deployment pipeline’ı için şu adımları takip edeceğiz:
- Mevcut sürümü yedekle (backup)
- Yeni sürümü dağıt
- Sağlık kontrollerini çalıştır
- Kontroller başarısız olursa önceki sürüme dön
- Rollback sonrası tekrar doğrula
Bu akışı Ansible’da uygulamak için block, rescue ve always yapısını kullanacağız. Bu yapı temelde bir try-catch-finally mekanizması gibi çalışır.
Proje Yapısı
Önce dizin yapımızı oluşturalım:
deployment/
├── inventory/
│ ├── production
│ └── staging
├── roles/
│ └── webapp/
│ ├── tasks/
│ │ ├── main.yml
│ │ ├── deploy.yml
│ │ ├── health_check.yml
│ │ └── rollback.yml
│ ├── vars/
│ │ └── main.yml
│ └── handlers/
│ └── main.yml
├── group_vars/
│ └── all.yml
└── deploy.yml
Değişkenlerin Tanımlanması
İlk adım olarak deployment sürecinde kullanacağımız değişkenleri tanımlıyoruz:
# group_vars/all.yml
---
app_name: "mywebapp"
app_user: "deploy"
app_base_dir: "/opt/apps"
app_current_dir: "{{ app_base_dir }}/{{ app_name }}/current"
app_releases_dir: "{{ app_base_dir }}/{{ app_name }}/releases"
app_backup_dir: "{{ app_base_dir }}/{{ app_name }}/backups"
# Kaç adet eski release tutulacak
keep_releases: 5
# Sağlık kontrolü ayarları
health_check_url: "http://localhost:8080/health"
health_check_retries: 5
health_check_delay: 10
health_check_timeout: 30
# Port ve servis bilgileri
app_port: 8080
app_service_name: "{{ app_name }}"
# Rollback etkinleştirme
enable_rollback: true
Ana Deploy Playbook’u
Ana playbook’umuz block/rescue/always yapısını kullanarak deployment ve rollback mantığını yönetiyor:
# deploy.yml
---
- name: "{{ app_name }} Deployment"
hosts: webservers
become: yes
serial: "{{ deploy_serial | default('30%') }}"
vars:
release_timestamp: "{{ ansible_date_time.epoch }}"
release_dir: "{{ app_releases_dir }}/{{ release_timestamp }}"
pre_tasks:
- name: Deployment bilgilerini göster
debug:
msg: |
Uygulama: {{ app_name }}
Versiyon: {{ app_version | default('latest') }}
Hedef: {{ inventory_hostname }}
Release Dir: {{ release_dir }}
tasks:
- name: Deployment bloğu
block:
- name: Mevcut sürümü yedekle
include_tasks: roles/webapp/tasks/backup.yml
- name: Yeni sürümü dağıt
include_tasks: roles/webapp/tasks/deploy.yml
- name: Servisi yeniden başlat
systemd:
name: "{{ app_service_name }}"
state: restarted
daemon_reload: yes
- name: Servisin ayağa kalkmasını bekle
wait_for:
port: "{{ app_port }}"
host: "127.0.0.1"
delay: 5
timeout: 60
state: started
- name: Sağlık kontrollerini çalıştır
include_tasks: roles/webapp/tasks/health_check.yml
- name: Başarılı deployment kaydı
set_fact:
deployment_status: "success"
rescue:
- name: HATA - Deployment başarısız, rollback başlatılıyor
debug:
msg: "Deployment başarısız oldu! Rollback işlemi başlatılıyor..."
- name: Rollback işlemini gerçekleştir
include_tasks: roles/webapp/tasks/rollback.yml
when: enable_rollback | bool
- name: Deployment başarısız olarak işaretle
set_fact:
deployment_status: "failed"
- name: Kritik hata fırlat
fail:
msg: "Deployment başarısız oldu ve rollback tamamlandı. Manuel kontrol gerekiyor!"
always:
- name: Deployment durumunu logla
local_action:
module: lineinfile
path: "/var/log/ansible/deployments.log"
line: "{{ ansible_date_time.iso8601 }} | {{ inventory_hostname }} | {{ app_name }} | {{ app_version | default('latest') }} | {{ deployment_status | default('unknown') }}"
create: yes
Yedekleme Görevi
Rollback’in çalışabilmesi için mevcut sürümü güvenli bir şekilde yedeklememiz gerekiyor:
# roles/webapp/tasks/backup.yml
---
- name: Backup dizinini oluştur
file:
path: "{{ app_backup_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Mevcut sürümün varlığını kontrol et
stat:
path: "{{ app_current_dir }}"
register: current_release_stat
- name: Mevcut sürüm dizinini belirle
command: "readlink -f {{ app_current_dir }}"
register: current_release_path
when: current_release_stat.stat.exists
changed_when: false
- name: Önceki release bilgisini kaydet
set_fact:
previous_release: "{{ current_release_path.stdout }}"
when:
- current_release_stat.stat.exists
- current_release_path.stdout != ""
- name: Önceki release bilgisini dosyaya yaz
copy:
content: "{{ previous_release | default('') }}"
dest: "{{ app_backup_dir }}/previous_release"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0644'
when: previous_release is defined
- name: Eski release dizinlerini temizle
shell: |
cd {{ app_releases_dir }} &&
ls -t | tail -n +{{ keep_releases + 1 }} | xargs -r rm -rf
args:
warn: false
changed_when: false
Sağlık Kontrolü Görevleri
Bu kısım pipeline’ımızın kalbi. Sadece port açık mı diye bakmıyoruz, gerçek uygulama sağlığını test ediyoruz:
# roles/webapp/tasks/health_check.yml
---
- name: HTTP sağlık endpoint kontrolü
uri:
url: "{{ health_check_url }}"
method: GET
status_code: 200
timeout: "{{ health_check_timeout }}"
headers:
Accept: "application/json"
register: health_response
retries: "{{ health_check_retries }}"
delay: "{{ health_check_delay }}"
until: health_response.status == 200
failed_when: health_response.status != 200
- name: Sağlık yanıtını doğrula
assert:
that:
- health_response.json.status is defined
- health_response.json.status == "healthy"
fail_msg: "Uygulama sağlık durumu 'healthy' değil: {{ health_response.json }}"
success_msg: "Uygulama sağlıklı çalışıyor"
- name: Veritabanı bağlantısını kontrol et
uri:
url: "http://localhost:{{ app_port }}/health/db"
method: GET
status_code: 200
timeout: 15
register: db_health
retries: 3
delay: 5
until: db_health.status == 200
failed_when: db_health.status != 200
- name: Cache bağlantısını kontrol et
uri:
url: "http://localhost:{{ app_port }}/health/cache"
method: GET
status_code: 200
timeout: 10
register: cache_health
ignore_errors: yes
- name: Cache uyarısını logla
debug:
msg: "UYARI: Cache sağlık kontrolü başarısız - {{ cache_health.msg | default('bilinmeyen hata') }}"
when: cache_health is failed
- name: Servis loglarında kritik hata kontrolü
shell: |
journalctl -u {{ app_service_name }} --since "2 minutes ago"
| grep -iE "FATAL|CRITICAL|OutOfMemory" | wc -l
register: error_count
changed_when: false
- name: Kritik hata sayısını değerlendir
fail:
msg: "Servis loglarında {{ error_count.stdout }} kritik hata bulundu!"
when: error_count.stdout | int > 0
- name: Sağlık kontrolü başarılı
debug:
msg: "Tüm sağlık kontrolleri başarıyla tamamlandı. Response time: {{ health_response.elapsed.total_seconds() }}s"
Rollback Görevi
Sağlık kontrolü başarısız olduğunda devreye girecek rollback mekanizması:
# roles/webapp/tasks/rollback.yml
---
- name: Önceki release bilgisini oku
slurp:
src: "{{ app_backup_dir }}/previous_release"
register: previous_release_file
ignore_errors: yes
- name: Geri dönülecek release'i belirle
set_fact:
rollback_target: "{{ previous_release_file.content | b64decode | trim }}"
when:
- previous_release_file is succeeded
- previous_release_file.content | b64decode | trim != ""
- name: Rollback hedefinin varlığını kontrol et
stat:
path: "{{ rollback_target }}"
register: rollback_target_stat
when: rollback_target is defined
- name: Rollback hedefi bulunamadı - kritik hata
fail:
msg: |
Rollback hedefi bulunamadı!
Hedef: {{ rollback_target | default('tanımlanmamış') }}
Manuel müdahale gerekiyor!
when: >
rollback_target is not defined or
(rollback_target_stat is defined and not rollback_target_stat.stat.exists)
- name: Yeni sürümü durdur
systemd:
name: "{{ app_service_name }}"
state: stopped
ignore_errors: yes
- name: Symlink'i önceki sürüme döndür
file:
src: "{{ rollback_target }}"
dest: "{{ app_current_dir }}"
state: link
force: yes
owner: "{{ app_user }}"
group: "{{ app_user }}"
- name: Önceki sürümü başlat
systemd:
name: "{{ app_service_name }}"
state: started
- name: Rollback sonrası servisin ayağa kalkmasını bekle
wait_for:
port: "{{ app_port }}"
host: "127.0.0.1"
delay: 5
timeout: 60
state: started
- name: Rollback sağlık doğrulaması
uri:
url: "{{ health_check_url }}"
method: GET
status_code: 200
timeout: 30
register: rollback_health
retries: 3
delay: 10
until: rollback_health.status == 200
- name: Rollback başarı mesajı
debug:
msg: |
ROLLBACK BAŞARILI
Geri dönülen versiyon: {{ rollback_target }}
Sağlık durumu: {{ rollback_health.json.status | default('OK') }}
- name: Rollback olayını bildir
local_action:
module: uri
url: "{{ slack_webhook_url | default('') }}"
method: POST
body_format: json
body:
text: ":rotating_light: *ROLLBACK* - {{ inventory_hostname }} üzerinde {{ app_name }} önceki sürüme döndürüldü!"
when: slack_webhook_url is defined and slack_webhook_url != ""
ignore_errors: yes
Gerçek Dünya Senaryosu: Blue-Green Deployment ile Sağlık Kontrolü
Gerçek prodüksiyon ortamlarında blue-green deployment kullanmak rollback sürecini çok daha temiz hale getirir. Aşağıdaki örnek Nginx upstream’ini kullanarak bunu gösteriyor:
# roles/webapp/tasks/blue_green_switch.yml
---
- name: Aktif ortamı belirle
shell: "cat {{ app_base_dir }}/active_env || echo 'blue'"
register: active_env_result
changed_when: false
- name: Aktif ve pasif ortamları ayarla
set_fact:
active_env: "{{ active_env_result.stdout.strip() }}"
passive_env: "{{ 'green' if active_env_result.stdout.strip() == 'blue' else 'blue' }}"
- name: Pasif ortama deploy et
include_tasks: deploy_to_env.yml
vars:
target_env: "{{ passive_env }}"
target_port: "{{ 8081 if passive_env == 'blue' else 8082 }}"
- name: Pasif ortam sağlık kontrolü
uri:
url: "http://localhost:{{ 8081 if passive_env == 'blue' else 8082 }}/health"
method: GET
status_code: 200
timeout: 30
register: passive_health
retries: 5
delay: 10
until: passive_health.status == 200
- name: Nginx'i yeni ortama yönlendir
template:
src: nginx_upstream.conf.j2
dest: /etc/nginx/conf.d/upstream.conf
vars:
upstream_port: "{{ 8081 if passive_env == 'blue' else 8082 }}"
notify: reload nginx
- name: Nginx yeniden yüklenmesini bekle
wait_for:
timeout: 5
- name: Nginx üzerinden son sağlık kontrolü
uri:
url: "https://{{ inventory_hostname }}/health"
method: GET
status_code: 200
timeout: 30
validate_certs: no
register: final_health
retries: 3
delay: 5
until: final_health.status == 200
- name: Aktif ortamı güncelle
copy:
content: "{{ passive_env }}"
dest: "{{ app_base_dir }}/active_env"
when: final_health.status == 200
Monitoring ile Entegrasyon
Deployment sonrası Prometheus veya benzeri monitoring araçlarıyla entegrasyon kurmak, sağlık durumunu daha kapsamlı değerlendirmenizi sağlar:
# roles/webapp/tasks/monitoring_check.yml
---
- name: Prometheus metrikleri kontrol et
uri:
url: "http://localhost:{{ app_port }}/metrics"
method: GET
status_code: 200
timeout: 10
return_content: yes
register: prometheus_metrics
retries: 3
delay: 5
until: prometheus_metrics.status == 200
- name: Hata oranını hesapla
shell: |
echo "{{ prometheus_metrics.content }}" |
grep 'http_requests_total{.*status="5' |
awk '{sum += $2} END {print sum+0}'
register: error_rate
changed_when: false
- name: Hata oranı eşiğini kontrol et
fail:
msg: "Hata oranı çok yüksek: {{ error_rate.stdout }} istek hata aldı!"
when: error_rate.stdout | int > 10
- name: Response time kontrolü
shell: |
for i in $(seq 1 5); do
curl -o /dev/null -s -w "%{time_total}n"
"http://localhost:{{ app_port }}/health"
done | awk '{sum+=$1; count++} END {printf "%.3fn", sum/count}'
register: avg_response_time
changed_when: false
- name: Response time değerlendirme
fail:
msg: "Ortalama response time çok yüksek: {{ avg_response_time.stdout }}s (limit: 2s)"
when: avg_response_time.stdout | float > 2.0
- name: Performans raporu
debug:
msg: |
Ortalama Response Time: {{ avg_response_time.stdout }}s
5xx Hata Sayısı: {{ error_rate.stdout }}
Durum: SAĞLIKLI
Inventory ve Kademeli Deployment
Büyük ortamlarda tüm sunucuları aynı anda güncellemek yerine kademeli deployment yapmak riskleri minimize eder:
# inventory/production
[webservers]
web01.example.com
web02.example.com
web03.example.com
web04.example.com
[webservers:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
Ana playbook’ta serial parametresini kullanarak kademeli dağıtımı kontrol edebilirsiniz. Önce tek bir sunucuya deploy edilir, sağlık kontrolü geçerse diğer sunuculara devam edilir:
# Kullanım örnekleri
# Tek sunucu ile başla, sonra tümüne geç
serial:
- 1
- "50%"
- "100%"
Hata Ayıklama ve Test
Deployment playbook’larını prodüksiyona almadan önce test etmek kritik önem taşıyor. Check mode ve verbose çıktı bunun için oldukça faydalı:
# Dry-run ile test et
ansible-playbook -i inventory/staging deploy.yml --check --diff
# Belirli bir tag ile çalıştır
ansible-playbook -i inventory/staging deploy.yml --tags "health_check"
# Sadece rollback'i test et
ansible-playbook -i inventory/staging deploy.yml --tags "rollback"
-e "enable_rollback=true"
# Verbose mod ile detaylı çıktı al
ansible-playbook -i inventory/production deploy.yml -vvv
2>&1 | tee /var/log/ansible/deploy_$(date +%Y%m%d_%H%M%S).log
# Belirli bir sunucuya limit et
ansible-playbook -i inventory/production deploy.yml
--limit "web01.example.com"
-e "app_version=2.1.5"
Yaygın Sorunlar ve Çözümleri
Gerçek ortamlarda karşılaşılan tipik problemler ve bunlara yaklaşım şekilleri:
- Sağlık kontrolü zaman aşımı:
health_check_retriesvehealth_check_delaydeğerlerini uygulamanızın başlangıç süresine göre ayarlayın. Java uygulamaları genellikle 30-60 saniye ısınma süresi ister.
- Rollback hedefi bulunamıyor: İlk deployment’ta henüz backup olmadığı için
when: previous_release is definedkontrolü eklemeyi unutmayın.
- Sembolik link sorunları:
readlink -fyerinerealpathkullanmak bazı sistemlerde daha güvenilir sonuç verir. Her iki komutun da kurulu olduğundan emin olun.
- Systemd daemon-reload eksikliği: Servis dosyasını değiştirdiyseniz restart öncesi mutlaka
daemon_reload: yeskullanın, aksi takdirde eski konfigürasyon yüklenebilir.
- Race condition: Birden fazla sunucuya paralel deployment yaparken
serialparametresini dikkatli kullanın. Load balancer sağlığını da kontrol etmeyi unutmayın.
Sonuç
Ansible ile kurduğunuz sağlık kontrolü ve rollback mekanizması, deployment süreçlerinizi gerçek anlamda güvenli hale getirir. Burada anlattığım yaklaşımın özü şu: Deployment işlemini bir bütün olarak değerlendirin, sadece “servis ayakta mı?” sorusunu değil, “servis gerçekten sağlıklı mı?” sorusunu sorun.
block/rescue/always yapısı bu senaryolar için Ansible’ın sunduğu en güçlü araç. Backup alma, deployment, çok katmanlı sağlık kontrolü ve otomatik rollback adımlarını tek bir tutarlı playbook içinde yönetebiliyorsunuz.
Pratikte şunu öneririm: Önce staging ortamında bu yapıyı kurun, kasıtlı olarak başarısız sağlık kontrolleri oluşturun ve rollback’in düzgün çalıştığını doğrulayın. Sisteminize güveniniz tam olduğunda prodüksiyona alın. Çünkü bir rollback mekanizmasının en kötü test zamanı, gerçek bir kriz anıdır.
