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_retries ve health_check_delay değ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 defined kontrolü eklemeyi unutmayın.
  • Sembolik link sorunları: readlink -f yerine realpath kullanmak 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: yes kullanın, aksi takdirde eski konfigürasyon yüklenebilir.
  • Race condition: Birden fazla sunucuya paralel deployment yaparken serial parametresini 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.

Bir yanıt yazın

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