Ansible ile Otomatik Güvenlik Güncellemeleri

Güvenlik güncellemelerini manuel olarak yapmak, özellikle onlarca veya yüzlerce sunucu yönettiğinizde gerçek bir kabus haline gelebilir. Bir sunucuyu güncelledin, öbürünü unuttun, birinde bağımlılık sorunu çıktı… Bu kaotik sürecin tam ortasında bir güvenlik açığı haberi geliyor ve panikle ne yapacağını şaşırıyorsun. Ansible tam da bu noktada devreye giriyor. Playbook’lar sayesinde güvenlik güncellemelerini standartlaştırabilir, otomatize edebilir ve tüm altyapını tutarlı bir şekilde güncel tutabilirsin.

Neden Ansible ile Güvenlik Güncellemeleri?

Geleneksel yaklaşımda her sunucuya SSH ile bağlanıp apt upgrade veya yum update komutu çalıştırıyordun. Bu yöntemin en büyük problemi tekrarlanabilirlik ve izlenebilirlik eksikliği. Kim ne zaman hangi sunucuyu güncelledi? Hangi paketler güncellendi? Güncelleme sonrası servis yeniden başlatıldı mı?

Ansible bu sorulara net yanıtlar vermeni sağlıyor:

  • Idempotency: Aynı playbook’u kaç kez çalıştırırsan çalıştır, sonuç aynı olur
  • Merkezi yönetim: Tüm altyapını tek bir yerden yönetirsin
  • Versiyon kontrolü: Playbook’ların Git’te duruyor, kim ne değiştirdi görünür
  • Rollback imkânı: Bir şeyler ters giderse önceki duruma dönmek kolaylaşır
  • Raporlama: Her çalışmanın detaylı logu var

Ortam Hazırlığı ve Inventory Yapılandırması

Önce sağlıklı bir inventory yapısı kurman lazım. Üretim, staging ve geliştirme ortamlarını birbirinden ayırmak, güvenlik güncellemelerini önce staging’de test etmeni sağlar.

# /etc/ansible/inventory/hosts dosyası
[production_web]
web01.ornek.com
web02.ornek.com
web03.ornek.com

[production_db]
db01.ornek.com ansible_user=dbadmin

[staging]
staging01.ornek.com
staging02.ornek.com

[all:vars]
ansible_user=ansible
ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python3

Grup değişkenlerini ayrı dosyalarda tutmak daha temiz bir yapı sağlar:

# group_vars/production_web.yml
ansible_become: true
ansible_become_method: sudo
reboot_required: true
update_cache_timeout: 3600

Temel Güvenlik Güncelleme Playbook’u

İlk playbook’umuz basit ama etkili. Sadece güvenlik yamalarını uyguluyor, gereksiz paketlere dokunmuyor.

# security-updates.yml
---
- name: Otomatik Güvenlik Güncellemeleri
  hosts: "{{ target_hosts | default('all') }}"
  become: true
  serial: "{{ batch_size | default('30%') }}"
  
  vars:
    log_dir: /var/log/ansible-updates
    
  pre_tasks:
    - name: Log dizinini oluştur
      file:
        path: "{{ log_dir }}"
        state: directory
        mode: '0755'
        
    - name: Güncelleme başlangıç zamanını kaydet
      set_fact:
        update_start_time: "{{ ansible_date_time.iso8601 }}"
        
  tasks:
    - name: APT cache güncelle (Debian/Ubuntu)
      apt:
        update_cache: yes
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"
      
    - name: Güvenlik güncellemelerini uygula (Debian/Ubuntu)
      apt:
        upgrade: dist
        autoremove: yes
        autoclean: yes
      register: apt_update_result
      when: ansible_os_family == "Debian"
      
    - name: Güvenlik güncellemelerini uygula (RHEL/CentOS)
      yum:
        name: '*'
        security: yes
        state: latest
        update_cache: yes
      register: yum_update_result
      when: ansible_os_family == "RedHat"
      
    - name: Yeniden başlatma gerekiyor mu kontrol et
      stat:
        path: /var/run/reboot-required
      register: reboot_required_file
      when: ansible_os_family == "Debian"
      
  post_tasks:
    - name: Güncelleme sonucunu logla
      copy:
        content: |
          Başlangıç: {{ update_start_time }}
          Bitiş: {{ ansible_date_time.iso8601 }}
          Sunucu: {{ inventory_hostname }}
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
        dest: "{{ log_dir }}/update-{{ ansible_date_time.date }}.log"

Burada serial: "30%" parametresine dikkat et. Bu parametre sunucuların yüzde otuzunu aynı anda günceller. Tüm sunucuları aynı anda güncellemek yerine kademeli ilerliyoruz, böylece bir sorun çıkarsa tüm altyapı etkilenmiyor.

Reboot Yönetimi

Kernel güncellemeleri sonrası sunucuların yeniden başlatılması gerekiyor. Ama bunu kontrollü yapmak şart. Üretim saatlerinde toplu reboot yapamazsın.

# reboot-handler.yml
---
- name: Kontrollü Reboot Yönetimi
  hosts: "{{ target_hosts }}"
  become: true
  
  vars:
    reboot_timeout: 300
    maintenance_window: "02:00-04:00"
    
  tasks:
    - name: Reboot gerekli mi kontrol et (Debian)
      stat:
        path: /var/run/reboot-required
      register: debian_reboot_check
      when: ansible_os_family == "Debian"
      
    - name: Reboot gerekli mi kontrol et (RHEL)
      command: needs-restarting -r
      register: rhel_reboot_check
      changed_when: false
      failed_when: false
      when: ansible_os_family == "RedHat"
      
    - name: Yük dengeleyiciden çıkar (HAProxy)
      haproxy:
        state: disabled
        host: "{{ inventory_hostname }}"
        socket: /var/run/haproxy/admin.sock
        wait: yes
      delegate_to: "{{ haproxy_server }}"
      when: 
        - haproxy_server is defined
        - (debian_reboot_check.stat.exists | default(false)) or
          (rhel_reboot_check.rc | default(1) == 1)
          
    - name: Sunucuyu yeniden başlat
      reboot:
        msg: "Ansible güvenlik güncellemesi sonrası reboot"
        connect_timeout: 5
        reboot_timeout: "{{ reboot_timeout }}"
        pre_reboot_delay: 10
        post_reboot_delay: 30
        test_command: whoami
      when:
        - (debian_reboot_check.stat.exists | default(false)) or
          (rhel_reboot_check.rc | default(1) == 1)
          
    - name: Yük dengeleyiciye geri ekle
      haproxy:
        state: enabled
        host: "{{ inventory_hostname }}"
        socket: /var/run/haproxy/admin.sock
        wait: yes
        wait_interval: 5
      delegate_to: "{{ haproxy_server }}"
      when: haproxy_server is defined

Gerçek Dünya Senaryosu: Kritik CVE Müdahalesi

2021’deki Log4Shell açığını hatırlıyorsundur. Binlerce sistem etkilendi ve sistem yöneticileri günlerce uyumadan yamalar uyguladı. Ansible’ın olduğu bir ortamda bu süreç çok daha yönetilebilir hale gelir.

Diyelim ki kritik bir CVE duyurusu geldi ve sadece belirli bir paketi acil güncellemeniz gerekiyor:

# critical-cve-patch.yml
---
- name: Kritik CVE Yaması - Log4j Örneği
  hosts: production_web:production_app
  become: true
  serial: 1
  max_fail_percentage: 0
  
  vars:
    cve_id: "CVE-2021-44228"
    affected_package: "log4j2"
    notification_email: "[email protected]"
    
  pre_tasks:
    - name: Acil bildirim gönder
      mail:
        to: "{{ notification_email }}"
        subject: "{{ cve_id }} yaması başlıyor - {{ inventory_hostname }}"
        body: "{{ inventory_hostname }} sunucusunda {{ cve_id }} yaması uygulanmaya başlandı."
      delegate_to: localhost
      ignore_errors: yes
      
    - name: Mevcut paket versiyonunu kaydet
      command: "dpkg -l | grep -i log4j"
      register: pre_patch_version
      changed_when: false
      failed_when: false
      
  tasks:
    - name: Paket listesini güncelle
      apt:
        update_cache: yes
      when: ansible_os_family == "Debian"
      
    - name: Etkilenen paketi güncelle
      apt:
        name: "{{ affected_package }}"
        state: latest
      register: patch_result
      when: ansible_os_family == "Debian"
      
    - name: Java uygulamalarını yeniden başlat
      systemd:
        name: "{{ item }}"
        state: restarted
      loop:
        - tomcat9
        - elasticsearch
        - logstash
      ignore_errors: yes
      when: patch_result.changed
      
    - name: Patch sonrası versiyon kontrolü
      command: "dpkg -l | grep -i log4j"
      register: post_patch_version
      changed_when: false
      failed_when: false
      
  post_tasks:
    - name: Tamamlanma bildirimi
      mail:
        to: "{{ notification_email }}"
        subject: "{{ cve_id }} yaması tamamlandı - {{ inventory_hostname }}"
        body: |
          Önceki versiyon: {{ pre_patch_version.stdout }}
          Yeni versiyon: {{ post_patch_version.stdout }}
          Durum: Başarılı
      delegate_to: localhost
      ignore_errors: yes

Role Tabanlı Yapı

Playbook’larını büyüdükçe bir role yapısına taşıman gerekiyor. security-updates adında bir role oluşturalım:

# Role dizin yapısı
ansible-galaxy init security-updates

# Oluşan yapı:
# security-updates/
# ├── tasks/
# │   ├── main.yml
# │   ├── debian.yml
# │   ├── redhat.yml
# │   └── post_update.yml
# ├── handlers/
# │   └── main.yml
# ├── vars/
# │   └── main.yml
# ├── defaults/
# │   └── main.yml
# └── templates/
#     └── update_report.j2
# security-updates/tasks/main.yml
---
- name: OS ailesine göre güncelleme yap
  include_tasks: "{{ ansible_os_family | lower }}.yml"
  
- name: Post-update kontrolleri
  include_tasks: post_update.yml

# security-updates/tasks/debian.yml
---
- name: APT güvenlik güncellemeleri
  block:
    - name: Cache güncelle
      apt:
        update_cache: yes
        cache_valid_time: "{{ cache_valid_time }}"
        
    - name: Sadece güvenlik güncellemelerini filtrele
      shell: |
        apt-get --just-print upgrade 2>&1 | 
        grep -i "security" | 
        awk '{print $2}' | 
        sort -u
      register: security_packages
      changed_when: false
      
    - name: Güvenlik paketlerini güncelle
      apt:
        name: "{{ security_packages.stdout_lines }}"
        state: latest
      when: security_packages.stdout_lines | length > 0
      register: debian_update_result
      notify: restart_services
      
  rescue:
    - name: Hata bildir
      debug:
        msg: "Güncelleme sırasında hata: {{ ansible_failed_result }}"

Handler’ları da düzgün tanımlamak önemli:

# security-updates/handlers/main.yml
---
- name: restart_services
  systemd:
    name: "{{ item }}"
    state: restarted
  loop: "{{ services_to_restart | default([]) }}"
  when: services_to_restart is defined

- name: reload_nginx
  systemd:
    name: nginx
    state: reloaded
    
- name: reload_apache
  systemd:
    name: apache2
    state: reloaded

CI/CD Pipeline Entegrasyonu

GitLab CI veya Jenkins ile Ansible playbook’larını otomatik tetikleyebilirsin. Şöyle bir senaryo düşün: Her Salı gecesi saat 02:00’de otomatik güvenlik güncellemesi çalışsın, log dosyaları toplanıp güvenlik ekibine rapor gönderilsin.

# .gitlab-ci.yml
stages:
  - validate
  - staging-update
  - production-update
  - report

variables:
  ANSIBLE_FORCE_COLOR: "1"
  ANSIBLE_HOST_KEY_CHECKING: "False"

validate-playbook:
  stage: validate
  script:
    - ansible-playbook --syntax-check security-updates.yml
    - ansible-lint security-updates.yml
  only:
    - merge_requests
    - main

staging-security-update:
  stage: staging-update
  script:
    - ansible-playbook -i inventory/staging security-updates.yml
      --extra-vars "target_hosts=staging"
      --extra-vars "batch_size=50%"
  environment:
    name: staging
  only:
    - schedules
    - main

production-security-update:
  stage: production-update
  script:
    - ansible-playbook -i inventory/production security-updates.yml
      --extra-vars "target_hosts=all"
      --extra-vars "batch_size=20%"
  environment:
    name: production
  when: manual
  only:
    - main
    
generate-report:
  stage: report
  script:
    - ansible-playbook -i inventory/production collect-update-report.yml
  artifacts:
    paths:
      - reports/
    expire_in: 30 days
  only:
    - schedules

Güncelleme Öncesi ve Sonrası Doğrulama

Güncellemeden sonra servislerin hala çalışıp çalışmadığını kontrol etmek kritik. Özellikle web sunucuları için:

# verification-tasks.yml
---
- name: Güncelleme Sonrası Doğrulama
  hosts: production_web
  become: true
  
  vars:
    health_check_urls:
      - "http://localhost:80/health"
      - "http://localhost:8080/actuator/health"
    expected_services:
      - nginx
      - php8.1-fpm
      - mysql
      
  tasks:
    - name: Kritik servislerin durumunu kontrol et
      systemd:
        name: "{{ item }}"
      register: service_status
      loop: "{{ expected_services }}"
      
    - name: Çalışmayan servisleri raporla
      debug:
        msg: "UYARI: {{ item.item }} servisi çalışmıyor!"
      loop: "{{ service_status.results }}"
      when: item.status.ActiveState != "active"
      
    - name: HTTP health check
      uri:
        url: "{{ item }}"
        method: GET
        status_code: 200
        timeout: 10
      loop: "{{ health_check_urls }}"
      register: health_check_result
      ignore_errors: yes
      retries: 3
      delay: 10
      
    - name: Health check başarısız olan URL'leri raporla
      debug:
        msg: "KRITIK: {{ item.item }} yanıt vermiyor!"
      loop: "{{ health_check_result.results }}"
      when: item.failed | default(false)
      
    - name: Disk kullanımını kontrol et
      shell: df -h / | awk 'NR==2 {print $5}' | tr -d '%'
      register: disk_usage
      changed_when: false
      
    - name: Disk doluluk uyarısı
      fail:
        msg: "UYARI: Disk doluluk oranı %{{ disk_usage.stdout }} - güncelleme sonrası temizlik gerekebilir!"
      when: disk_usage.stdout | int > 85

Ansible Vault ile Hassas Bilgilerin Korunması

Güvenlik güncellemeleri için kullanılan playbook’larda veritabanı şifreleri, API anahtarları gibi hassas bilgiler olabilir. Bunları asla plain text olarak tutma:

# Vault şifreli değişken dosyası oluştur
ansible-vault create group_vars/production/vault.yml

# İçeriği şifrele
ansible-vault encrypt_string 'super-gizli-sifre' --name 'db_password'

# Playbook'u vault şifresiyle çalıştır
ansible-playbook security-updates.yml --vault-password-file ~/.vault_pass

# Vault şifresini environment variable'dan al
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault_pass
ansible-playbook security-updates.yml
# group_vars/production/vault.yml (şifreli dosya içeriği örneği)
vault_notification_api_key: "sk-prod-xxxxxxxxxxxx"
vault_slack_webhook: "https://hooks.slack.com/services/xxx/yyy/zzz"
vault_db_password: "gizli-sifre-buraya"

Loglama ve Raporlama Altyapısı

Güvenlik güncellemelerinin izlenebilir olması şart. Bir Jinja2 template ile güzel bir HTML raporu oluşturabilirsin:

# templates/update_report.j2
<!DOCTYPE html>
<html>
<head><title>Güvenlik Güncelleme Raporu - {{ ansible_date_time.date }}</title></head>
<body>
<h1>Güvenlik Güncelleme Raporu</h1>
<p>Tarih: {{ ansible_date_time.iso8601 }}</p>
<h2>Güncellenen Sunucular</h2>
{% for host in ansible_play_hosts %}
<div class="host">
  <h3>{{ host }}</h3>
  <p>OS: {{ hostvars[host]['ansible_distribution'] }}</p>
  <p>Durum: {% if hostvars[host]['update_result'].changed %}Güncellendi{% else %}Güncel{% endif %}</p>
</div>
{% endfor %}
</body>
</html>

Slack bildirimleri de ekleyebilirsin:

# Slack bildirimi görevi
- name: Slack bildirimi gönder
  uri:
    url: "{{ vault_slack_webhook }}"
    method: POST
    body_format: json
    body:
      text: ":shield: *Güvenlik Güncellemesi Tamamlandı*"
      attachments:
        - color: "good"
          fields:
            - title: "Güncellenen Sunucu Sayısı"
              value: "{{ ansible_play_hosts | length }}"
              short: true
            - title: "Tarih"
              value: "{{ ansible_date_time.iso8601 }}"
              short: true
  delegate_to: localhost
  run_once: true

Yaygın Hatalar ve Çözümleri

Ansible ile güvenlik güncellemesi yaparken sık karşılaşılan sorunlar:

  • SSH bağlantı zaman aşımı: ansible.cfg dosyasında timeout = 30 ve ssh_args = -o ConnectTimeout=30 -o ServerAliveInterval=60 ekle
  • Sudo şifre problemi: ansible_become_password değişkenini vault’ta sakla, playbook’da become: true kullan
  • Paket bağımlılık çakışmaları: apt-get -f install ile önce bağımlılıkları düzelt, bunu bir pre_task olarak ekle
  • Reboot sonrası bağlantı sorunu: reboot modülünün post_reboot_delay parametresini artır, 30-60 saniye yeterli
  • Kısmi güncelleme sorunu: max_fail_percentage: 20 ile belirli bir hata eşiğinde playbook’u durdur

Düzenli Çalışma İçin Cron Entegrasyonu

GitLab CI olmayan ortamlar için doğrudan cron ile de çalıştırabilirsin:

# /etc/cron.d/ansible-security-updates
# Her Salı ve Cuma saat 02:30'da çalış
30 2 * * 2,5 ansible /usr/bin/ansible-playbook 
  -i /etc/ansible/inventory/hosts 
  /opt/ansible/playbooks/security-updates.yml 
  --extra-vars "target_hosts=all" 
  --vault-password-file /root/.vault_pass 
  >> /var/log/ansible-security-updates.log 2>&1

Sonuç

Ansible ile güvenlik güncellemelerini otomatize etmek, başlangıçta biraz setup gerektiriyor ama bu yatırım kısa sürede kendini geri ödüyor. Onlarca sunucuyu tek tek güncelleme derdi ortadan kalkıyor, insan hatası minimuma iniyor ve her güncellemenin izi Git’te ve log dosyalarında kalıyor.

En kritik nokta şu: Playbook’larını önce staging ortamında test et, sonra üretim ortamına geçir. serial parametresiyle kademeli güncelleme yap, max_fail_percentage ile hata eşiği belirle. Reboot gerektiren güncellemeleri bakım pencerelerinde planla.

Bu yazıda anlattıklarımı uyguladıktan sonra güvenlik güncellemelerinin artık seni korkutmadığını göreceksin. Bir CVE duyurusu geldiğinde “Nasıl yapacağım?” yerine “Playbook’u çalıştır, bitsin gitsin” diyebileceksin. Ve bu duygu gerçekten paha biçilmez.

Sorularını ve senaryolarını yorumlara yazabilirsin, elimden geldiğince yanıtlamaya çalışırım.

Bir yanıt yazın

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