Ansible ile Rolling Update: Sıfır Kesintili Güncelleme Nasıl Yapılır?

Prodüksiyon ortamında bir güncelleme yapacaksın, servisler ayakta kalacak ve kullanıcılar hiçbir şey fark etmeyecek. Kulağa harika geliyor, değil mi? Ama çoğu ekip hâlâ gece yarısı “maintenance window” açıp kullanıcılara “site şu an bakımda” mesajı gösteriyor. Bunun sebebi teknik imkânsızlık değil, doğru araçları ve stratejileri kullanmıyor olmak. Ansible’ın rolling update mekanizması ile bunu geçmişte bırakabilirsin.

Rolling Update Nedir ve Neden Önemli?

Rolling update, bir servisi güncellerken tüm sunucuları aynı anda kapatmak yerine sırayla, küçük gruplar hâlinde güncelleyen bir stratejidir. Diyelim ki 10 tane web sunucun var. Hepsini aynı anda güncellersen, güncelleme sırasında servis tamamen durur. Rolling update ile önce 2 sunucuyu günceller, onlar ayağa kalkar, sonra diğer 2’ye geçersin. Kullanıcı trafiği her zaman sağlıklı sunuculara yönlendirildiğinden kesinti yaşanmaz.

Ansible bu işi neden iyi yapar?

  • Agentless çalışır, sunuculara ekstra yazılım kurman gerekmez
  • serial parametresiyle gruplama kontrolü tamamen sende
  • max_fail_percentage ile hata toleransını tanımlarsın
  • Load balancer entegrasyonu için modüller hazır gelir
  • Delegate ve pre/post task özellikleriyle orkestrasyon kolaylaşır

Temel Senaryo: 6 Nginx Sunucusunu Sıfır Kesintili Güncelleme

Önce basit bir senaryoyla başlayalım. 6 Nginx sunucusu var, önünde bir HAProxy load balancer. Amacımız Nginx’i güncellerken hiçbir kullanıcının “connection refused” almaması.

Inventory Dosyası

# inventory/production.ini
[webservers]
web01 ansible_host=192.168.1.10
web02 ansible_host=192.168.1.11
web03 ansible_host=192.168.1.12
web04 ansible_host=192.168.1.13
web05 ansible_host=192.168.1.14
web06 ansible.host=192.168.1.15

[loadbalancer]
haproxy01 ansible_host=192.168.1.5

[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key

Temel Rolling Update Playbook’u

# rolling_update.yml
---
- name: Rolling Update - Nginx Web Sunucuları
  hosts: webservers
  serial: 2                    # Her seferinde 2 sunucu güncelle
  max_fail_percentage: 30      # %30'dan fazla hata olursa dur

  pre_tasks:
    - name: Sunucuyu load balancer'dan çıkar
      community.general.haproxy:
        state: disabled
        host: "{{ inventory_hostname }}"
        backend: web_backend
        socket: /var/run/haproxy/admin.sock
        wait: yes
        wait_interval: 5
      delegate_to: haproxy01

    - name: Mevcut bağlantıların bitmesini bekle
      wait_for:
        timeout: 30
      delegate_to: "{{ inventory_hostname }}"

  tasks:
    - name: Nginx paketini güncelle
      apt:
        name: nginx
        state: latest
        update_cache: yes
      become: yes

    - name: Nginx konfigürasyonunu kopyala
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        backup: yes
      become: yes
      notify: nginx restart

    - name: Konfigürasyonu test et
      command: nginx -t
      become: yes
      register: nginx_test
      failed_when: nginx_test.rc != 0

  handlers:
    - name: nginx restart
      service:
        name: nginx
        state: restarted
      become: yes

  post_tasks:
    - name: Nginx'in ayakta olduğunu doğrula
      uri:
        url: "http://{{ ansible_host }}/health"
        status_code: 200
        timeout: 10
      retries: 5
      delay: 3

    - name: Sunucuyu load balancer'a geri ekle
      community.general.haproxy:
        state: enabled
        host: "{{ inventory_hostname }}"
        backend: web_backend
        socket: /var/run/haproxy/admin.sock
        wait: yes
        wait_interval: 5
      delegate_to: haproxy01

Bu playbook’ta dikkat edilmesi gereken kritik nokta: pre_tasks ve post_tasks blokları. Güncelleme öncesi sunucu load balancer’dan çıkarılıyor, güncelleme sonrası health check geçtikten sonra geri ekleniyor.

serial Parametresini Doğru Kullanmak

serial parametresi Ansible’ın rolling update motorunun kalbidir. Üç farklı kullanım şekli var:

Sabit sayı: Her seferinde kaç sunucu güncellensin

Yüzde değeri: Toplam sunucu sayısına göre oran

Liste: Kademeli artış için sıralı değerler

# serial ile farklı kullanım örnekleri

# Seçenek 1: Sabit sayı - her seferinde 1 sunucu
- hosts: webservers
  serial: 1

# Seçenek 2: Yüzde - her seferinde %20'si
- hosts: webservers
  serial: "20%"

# Seçenek 3: Kademeli artış - önce 1, sonra 3, sonra %50
- hosts: webservers
  serial:
    - 1
    - 3
    - "50%"

Kademeli artış neden kullanılır? Prodüksiyona ilk çıkarken 1 sunucuyu güncelle, bir süre izle, sorun yoksa daha büyük gruplarla devam et. Bu yaklaşım özellikle kritik güncellemelerde hayat kurtarır. Canary deployment mantığı bu.

Gerçek Dünya: E-Ticaret Platformu Güncelleme Senaryosu

Daha karmaşık bir senaryoya geçelim. Diyelim ki bir e-ticaret platformun var. Uygulama sunucuları, Redis cache, ve önünde bir Nginx reverse proxy. Bu üçünü koordineli güncellemen gerekiyor.

# ecommerce_deploy.yml
---
# Adım 1: Önce 1 canary sunucu
- name: Canary Deployment - İlk Test Sunucusu
  hosts: app_servers
  serial: 1
  gather_facts: yes

  vars:
    app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
    deploy_dir: /opt/ecommerce
    health_check_url: "http://{{ ansible_host }}:8080/api/health"

  pre_tasks:
    - name: Deployment başlangıç zamanını kaydet
      set_fact:
        deploy_start_time: "{{ ansible_date_time.iso8601 }}"

    - name: Sunucuyu maintenance moduna al
      uri:
        url: "http://lb01:9000/api/servers/{{ inventory_hostname }}/disable"
        method: POST
        headers:
          Authorization: "Bearer {{ vault_lb_token }}"
        body_format: json
      delegate_to: localhost

  tasks:
    - name: Uygulama dizinini oluştur
      file:
        path: "{{ deploy_dir }}/releases/{{ app_version }}"
        state: directory
        owner: appuser
        group: appgroup
        mode: '0755'
      become: yes

    - name: Artifact'i indir
      get_url:
        url: "https://artifacts.company.com/ecommerce/{{ app_version }}.tar.gz"
        dest: "/tmp/ecommerce_{{ app_version }}.tar.gz"
        checksum: "sha256:{{ app_checksum }}"

    - name: Artifact'i çıkart
      unarchive:
        src: "/tmp/ecommerce_{{ app_version }}.tar.gz"
        dest: "{{ deploy_dir }}/releases/{{ app_version }}"
        remote_src: yes

    - name: Konfigürasyon dosyasını yaz
      template:
        src: templates/app_config.yml.j2
        dest: "{{ deploy_dir }}/releases/{{ app_version }}/config/app.yml"
        owner: appuser
        group: appgroup
        mode: '0640'
      become: yes

    - name: Current symlink'i güncelle
      file:
        src: "{{ deploy_dir }}/releases/{{ app_version }}"
        dest: "{{ deploy_dir }}/current"
        state: link
      become: yes

    - name: Uygulamayı yeniden başlat
      systemd:
        name: ecommerce-app
        state: restarted
        daemon_reload: yes
      become: yes

    - name: Uygulamanın başlamasını bekle
      wait_for:
        port: 8080
        host: "{{ ansible_host }}"
        timeout: 60
        state: started

  post_tasks:
    - name: Health check yap
      uri:
        url: "{{ health_check_url }}"
        status_code: 200
        timeout: 15
      register: health_result
      retries: 10
      delay: 5
      until: health_result.status == 200

    - name: Smoke test - ana sayfa kontrolü
      uri:
        url: "http://{{ ansible_host }}:8080/"
        status_code: 200
        return_content: yes
      register: smoke_result
      failed_when: "'ecommerce' not in smoke_result.content"

    - name: Sunucuyu load balancer'a geri ekle
      uri:
        url: "http://lb01:9000/api/servers/{{ inventory_hostname }}/enable"
        method: POST
        headers:
          Authorization: "Bearer {{ vault_lb_token }}"
        body_format: json
      delegate_to: localhost

    - name: Canary monitoring - 60 saniye izle
      pause:
        seconds: 60
        prompt: "Canary sunucu izleniyor. Metrikler normal görünüyorsa Enter'a bas, sorun varsa Ctrl+C"

# Adım 2: Geri kalan sunucular kademeli
- name: Production Rollout - Kalan Sunucular
  hosts: app_servers[1:]
  serial:
    - 2
    - "50%"
  max_fail_percentage: 20

  # ... aynı tasks bloğu

Hata Durumunda Rollback Mekanizması

Güncelleme sırasında bir şeyler ters giderse ne olacak? Rollback stratejisi olmadan rolling update yarım kalır. Symlink tabanlı deployment bu problemi çözer.

# rollback.yml
---
- name: Hızlı Rollback
  hosts: app_servers
  serial: "100%"   # Rollback'te hepsini aynı anda yapalım, zaten sorun var

  vars:
    deploy_dir: /opt/ecommerce

  tasks:
    - name: Mevcut ve önceki release'leri listele
      find:
        paths: "{{ deploy_dir }}/releases"
        file_type: directory
      register: releases

    - name: Releases'ı tarihe göre sırala
      set_fact:
        sorted_releases: "{{ releases.files | sort(attribute='mtime', reverse=True) | map(attribute='path') | list }}"

    - name: Önceki versiyonu belirle
      set_fact:
        previous_version: "{{ sorted_releases[1] }}"
      when: sorted_releases | length > 1

    - name: Rollback işlemini gerçekleştir
      file:
        src: "{{ previous_version }}"
        dest: "{{ deploy_dir }}/current"
        state: link
      become: yes
      when: previous_version is defined

    - name: Servisi yeniden başlat
      systemd:
        name: ecommerce-app
        state: restarted
      become: yes

    - name: Rollback sonrası health check
      uri:
        url: "http://{{ ansible_host }}:8080/api/health"
        status_code: 200
      retries: 5
      delay: 5

    - name: Rollback sonucunu bildir
      debug:
        msg: "Rollback tamamlandı. Aktif versiyon: {{ previous_version | basename }}"

max_fail_percentage ile Akıllı Hata Yönetimi

max_fail_percentage parametresini doğru ayarlamak kritik. Çok düşük ayarlarsan tek bir sunucu hatası tüm deployment’ı durdurur. Çok yüksek ayarlarsan fark etmeden yarısını bozarsın.

# Farklı senaryolar için max_fail_percentage önerileri

# Kritik finans uygulaması - sıfır tolerans
- hosts: finance_servers
  serial: 1
  max_fail_percentage: 0

# Standart web uygulaması
- hosts: webservers
  serial: "25%"
  max_fail_percentage: 25

# Internal tool - biraz daha toleranslı
- hosts: internal_tools
  serial: "50%"
  max_fail_percentage: 50

Pratik bir örnek verelim: 10 sunucun var, serial: 2 ve max_fail_percentage: 30 ayarladın. İlk batch’te 2 sunucu güncelleniyor, 1 tanesi başarısız oldu. Yüzde 50 hata var ama toplam 10 üzerinden yüzde 10. Ansible devam eder. Eğer 3 sunucu başarısız olsaydı (yüzde 30), durur.

Delegate_to ile Orkestrasyon

Rolling update’in güçlü yanlarından biri delegate_to. Bir task’ı başka bir host’ta çalıştırabilirsin.

# delegate_to kullanım örnekleri
---
- name: Gelişmiş Orkestrasyon
  hosts: webservers
  serial: 2

  tasks:
    - name: Monitoring sisteminden alertleri devre dışı bırak
      uri:
        url: "https://grafana.internal/api/alerts/silence"
        method: POST
        body:
          comment: "Deployment - {{ inventory_hostname }}"
          duration: "30m"
        body_format: json
        headers:
          Authorization: "Bearer {{ vault_grafana_token }}"
      delegate_to: localhost
      run_once: false

    - name: DNS'ten sunucuyu çıkar
      nsupdate:
        server: dns01.internal
        zone: internal.company.com
        record: "{{ inventory_hostname }}"
        type: A
        state: absent
      delegate_to: dns01.internal

    - name: Asıl güncellemeyi yap
      apt:
        name: myapp
        state: latest
      become: yes

    - name: Sunucuyu DNS'e geri ekle
      nsupdate:
        server: dns01.internal
        zone: internal.company.com
        record: "{{ inventory_hostname }}"
        type: A
        value: "{{ ansible_host }}"
        state: present
      delegate_to: dns01.internal

    - name: Monitoring alertlerini yeniden aktif et
      uri:
        url: "https://grafana.internal/api/alerts/silence/{{ silence_id }}/expire"
        method: DELETE
        headers:
          Authorization: "Bearer {{ vault_grafana_token }}"
      delegate_to: localhost

Kubernetes ile Entegrasyon

Bazı ortamlarda VM’ler ve Kubernetes bir arada çalışır. Ansible bunu da yönetebilir.

# k8s_rolling_update.yml
---
- name: Kubernetes Deployment Rolling Update
  hosts: localhost
  gather_facts: no

  vars:
    namespace: production
    deployment_name: ecommerce-api
    new_image: "registry.company.com/ecommerce:{{ app_version }}"
    rollout_timeout: 300

  tasks:
    - name: Mevcut deployment bilgilerini al
      kubernetes.core.k8s_info:
        api_version: apps/v1
        kind: Deployment
        name: "{{ deployment_name }}"
        namespace: "{{ namespace }}"
      register: current_deployment

    - name: Image'ı güncelle
      kubernetes.core.k8s:
        api_version: apps/v1
        kind: Deployment
        name: "{{ deployment_name }}"
        namespace: "{{ namespace }}"
        definition:
          spec:
            template:
              spec:
                containers:
                  - name: api
                    image: "{{ new_image }}"
        strategic_merge_patch: yes

    - name: Rollout'un tamamlanmasını bekle
      command: >
        kubectl rollout status deployment/{{ deployment_name }}
        -n {{ namespace }}
        --timeout={{ rollout_timeout }}s
      register: rollout_status
      failed_when: rollout_status.rc != 0

    - name: Yeni pod'ların sağlıklı olduğunu doğrula
      kubernetes.core.k8s_info:
        api_version: v1
        kind: Pod
        namespace: "{{ namespace }}"
        label_selectors:
          - "app={{ deployment_name }}"
      register: pods
      until: >
        pods.resources | selectattr('status.phase', 'equalto', 'Running') | list | length ==
        current_deployment.resources[0].spec.replicas
      retries: 30
      delay: 10

CI/CD Pipeline ile Entegrasyon

Rolling update’i CI/CD pipeline’ına entegre etmek için GitLab CI örneği:

# .gitlab-ci.yml içinde Ansible çağrısı
deploy_production:
  stage: deploy
  image: cytopia/ansible:latest
  environment:
    name: production
  when: manual
  only:
    - main
  script:
    - echo "$ANSIBLE_VAULT_PASS" > /tmp/vault_pass
    - ansible-playbook
        -i inventory/production.ini
        rolling_update.yml
        --vault-password-file /tmp/vault_pass
        --extra-vars "app_version=$CI_COMMIT_TAG"
        --extra-vars "deploy_user=$CI_COMMIT_AUTHOR"
        -v
    - rm -f /tmp/vault_pass
  after_script:
    - rm -f /tmp/vault_pass
  artifacts:
    when: always
    paths:
      - ansible.log
    expire_in: 1 week

İzleme ve Loglama

Rolling update sırasında ne olduğunu takip etmek için callback plugin kullanımı:

# ansible.cfg
[defaults]
log_path = /var/log/ansible/rolling_update.log
callback_whitelist = profile_tasks, timer
stdout_callback = yaml

[callback_profile_tasks]
task_output_limit = 20
sort_order = descending

Deployment sırasında Slack bildirimi için:

# notification_tasks.yml - include edilebilir task dosyası
---
- name: Slack bildirimi gönder
  community.general.slack:
    token: "{{ vault_slack_token }}"
    channel: "#deployments"
    color: "{{ 'good' if deploy_status == 'success' else 'danger' }}"
    msg: |
      *Deployment Durumu:* {{ deploy_status | upper }}
      *Ortam:* Production
      *Versiyon:* {{ app_version }}
      *Host:* {{ inventory_hostname }}
      *Başlangıç:* {{ deploy_start_time }}
      *Süre:* {{ ansible_date_time.iso8601 }}
  delegate_to: localhost
  when: slack_notifications | default(true)

Sık Yapılan Hatalar

Yıllarca Ansible ile rolling update yapan biri olarak en çok gördüğüm hataları şöyle sıralayabilirim:

  • Health check yapmadan load balancer’a ekleme: Sunucu ayağa kalktı diye servis sağlıklıdır diyemezsin. Her zaman uygulama seviyesinde health check yap.
  • serial değerini çok büyük tutmak: “Daha hızlı olsun” diye %80 ayarlarsanız, sorun çıktığında zaten çoğunu güncellemiş olursunuz.
  • Rollback planı olmadan deploy etmek: max_fail_percentage ile deployment durdurmak yeterli değil. Duran yerde ne yapacağını da bilmen lazım.
  • Vault şifrelerini log’a yazdırmak: no_log: yes direktifini hassas task’larda kullanmayı unutma.
  • Connection timeout’larını ayarlamamak: Özellikle büyük artifact indirmelerinde default timeout’lar yetersiz kalır.
# Timeout ayarları için ansible.cfg
[defaults]
timeout = 30
command_timeout = 120

[persistent_connection]
connect_timeout = 30
command_timeout = 120

Sonuç

Rolling update ile sıfır kesintili güncelleme artık sadece büyük şirketlerin lüksü değil. Ansible’ın serial, max_fail_percentage, pre_tasks, post_tasks ve delegate_to kombinasyonu ile küçük ekipler bile prodüksiyona güvenle deploy edebilir.

Önemli olan doğru sırayı takip etmek: önce load balancer’dan çıkar, güncelle, sağlığını doğrula, geri ekle. Bu döngüyü otomatize ettiğinde gece yarısı maintenance window’ları tarihe karışır. Canary deployment ile de riski minimize edersin; önce tek sunucuya çıkar, metrikler normal görünüyorsa devam et.

Bir sonraki adım olarak Ansible AWX veya Red Hat Automation Platform ile bu playbook’ları bir arayüz üzerinden yönetmeni ve approval workflow eklemeni tavsiye ederim. Prod’a atma işi bir ticket onayına bağlandığında herkes çok daha rahat uyur.

Bir yanıt yazın

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