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
serialparametresiyle gruplama kontrolü tamamen sendemax_fail_percentageile 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_percentageile deployment durdurmak yeterli değil. Duran yerde ne yapacağını da bilmen lazım.
- Vault şifrelerini log’a yazdırmak:
no_log: yesdirektifini 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.
