Ansible ile Çoklu Sunucu Nginx Yapılandırması

On sunucu, yirmi sunucu, belki yüz sunucu… Her birine tek tek SSH bağlanıp Nginx kurmak, yapılandırma dosyasını düzenlemek, servisi yeniden başlatmak. Bu iş bir kez yapılabilir, belki iki kez, ama üçüncüde insan “daha iyi bir yol olmalı” diye düşünmeye başlıyor. İşte tam o noktada Ansible devreye giriyor.

Bu yazıda gerçek dünya senaryoları üzerinden Ansible ile çoklu sunucu Nginx yapılandırmasını ele alacağız. Basit kurulumdan başlayıp SSL termination, upstream tanımları, virtual host yönetimi ve ortama göre değişen konfigürasyonlara kadar kapsamlı bir yapı kuracağız.

Neden Ansible ile Nginx?

Manuel sunucu yönetiminin en büyük problemi tutarsızlık. Birinci sunucuya worker_processes auto; yazdın, ikincisine worker_processes 4; yazdın. Üçüncüyü kurarken acele ettin ve client_max_body_size ayarını unuttun. Üç ay sonra prodüksiyon ortamında garip davranışlar başladığında neyin nerede farklı olduğunu bulmak için saatlerce harcıyorsun.

Ansible bu sorunu idempotent yapısıyla çözüyor. Aynı playbook’u kaç kez çalıştırırsan çalıştır, sonuç her zaman aynı. Bir değişiklik yapman gerektiğinde tek bir dosyayı düzenleyip playbook’u çalıştırıyorsun, tüm sunucular güncelleniyor.

Proje Yapısını Kurmak

İyi bir Ansible projesi için doğru dizin yapısı kritik. Ansible’ın önerdiği roles yapısını kullanacağız.

mkdir -p ansible-nginx/{inventories/{production,staging},roles/nginx/{tasks,handlers,templates,vars,defaults,files},group_vars}
cd ansible-nginx
tree .

Beklenen yapı şöyle görünmeli:

ansible-nginx/
├── inventories/
│   ├── production/
│   │   ├── hosts.ini
│   │   └── group_vars/
│   └── staging/
│       ├── hosts.ini
│       └── group_vars/
├── roles/
│   └── nginx/
│       ├── tasks/
│       ├── handlers/
│       ├── templates/
│       ├── vars/
│       ├── defaults/
│       └── files/
├── group_vars/
│   └── all.yml
└── site.yml

Bu yapı küçük projelerde gereksiz görünebilir ama on sunucu olduğunda, farklı ortamlar eklendiğinde neden böyle bir yapıya ihtiyaç duyduğunu anlıyorsun.

Inventory Dosyasını Hazırlamak

Gerçek bir senaryo düşünelim: İki web sunucusu, bir load balancer ve bir staging ortamı.

# inventories/production/hosts.ini
[webservers]
web01 ansible_host=10.0.1.10 ansible_user=ubuntu
web02 ansible_host=10.0.1.11 ansible_user=ubuntu
web03 ansible_host=10.0.1.12 ansible_user=ubuntu

[loadbalancers]
lb01 ansible_host=10.0.1.5 ansible_user=ubuntu

[nginx:children]
webservers
loadbalancers

[nginx:vars]
ansible_ssh_private_key_file=~/.ssh/prod_key
ansible_python_interpreter=/usr/bin/python3

Staging için ayrı bir hosts dosyası:

# inventories/staging/hosts.ini
[webservers]
staging-web01 ansible_host=192.168.1.10 ansible_user=vagrant

[loadbalancers]
staging-lb01 ansible_host=192.168.1.5 ansible_user=vagrant

[nginx:children]
webservers
loadbalancers

nginx:children direktifi ile hem webservers hem de loadbalancers grubunu nginx grubu altında topluyoruz. Bu sayede tüm sunuculara ortak görevler uygulanırken gruba özel görevler de ayrı tutulabiliyor.

Default Değişkenler ve Group Variables

Değişkenleri iki katmanda tutmak iyi bir pratik. defaults/main.yml içindeki değerler override edilebilir, vars/main.yml içindekiler daha sabit tutulur.

# roles/nginx/defaults/main.yml
nginx_worker_processes: "auto"
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: "10m"
nginx_server_tokens: "off"
nginx_gzip_enabled: true
nginx_gzip_types:
  - text/plain
  - text/css
  - application/json
  - application/javascript
  - text/xml
  - application/xml

nginx_sites: []
nginx_upstreams: []

nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"

Ortama özel değerler için group_vars kullanıyoruz:

# inventories/production/group_vars/webservers.yml
nginx_worker_processes: "4"
nginx_worker_connections: 2048
nginx_client_max_body_size: "50m"

nginx_sites:
  - name: myapp
    server_name: "app.example.com"
    listen: 80
    root: /var/www/myapp
    index: "index.html index.htm"
    ssl_enabled: false
    locations:
      - path: "/"
        proxy_pass: "http://app_backend"
        proxy_set_header_host: true

Ana Task Dosyasını Yazmak

Şimdi işin özüne geliyoruz. tasks/main.yml dosyası tüm işlemleri organize ediyor:

# roles/nginx/tasks/main.yml
---
- name: Nginx paketini kur
  ansible.builtin.package:
    name: nginx
    state: present
  notify: nginx restart

- name: Nginx ana konfigürasyon dosyasını oluştur
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: '/usr/sbin/nginx -t -c %s'
  notify: nginx reload

- name: Mevcut default site konfigürasyonunu kaldır
  ansible.builtin.file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: nginx reload

- name: Sites-available ve sites-enabled dizinlerini oluştur
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: root
    group: root
    mode: '0755'
  loop:
    - /etc/nginx/sites-available
    - /etc/nginx/sites-enabled

- name: Virtual host konfigürasyonlarını oluştur
  ansible.builtin.template:
    src: vhost.conf.j2
    dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
    owner: root
    group: root
    mode: '0644'
  loop: "{{ nginx_sites }}"
  notify: nginx reload

- name: Virtual host'ları etkinleştir
  ansible.builtin.file:
    src: "/etc/nginx/sites-available/{{ item.name }}.conf"
    dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
    state: link
  loop: "{{ nginx_sites }}"
  notify: nginx reload

- name: Nginx servisini başlat ve enable et
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

validate parametresine dikkat edin. Konfigürasyon dosyasını uygulamadan önce nginx -t ile test ediyor. Hatalı bir konfig deploy etmenin önüne geçen basit ama hayat kurtarıcı bir özellik.

Handler Tanımları

Handlers, sadece bir değişiklik yapıldığında çalışan özel task’lar. Nginx için reload ve restart arasındaki farkı doğru kullanmak önemli:

# roles/nginx/handlers/main.yml
---
- name: nginx reload
  ansible.builtin.service:
    name: nginx
    state: reloaded

- name: nginx restart
  ansible.builtin.service:
    name: nginx
    state: restarted

- name: nginx test
  ansible.builtin.command:
    cmd: nginx -t
  changed_when: false

Reload, mevcut bağlantıları kesmeden konfigürasyonu yeniliyor. Restart ise servisi tamamen yeniden başlatıyor. Konfigürasyon değişikliklerinde her zaman reload kullanın, servis çökmüş veya ilk kurulum yapılıyorsa restart kullanın.

Jinja2 Template Dosyaları

Ansible’ın gerçek gücü template’larda kendini gösteriyor. Ana Nginx konfigürasyon template’i:

# roles/nginx/templates/nginx.conf.j2
user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections }};
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout {{ nginx_keepalive_timeout }};
    types_hash_max_size 2048;
    server_tokens {{ nginx_server_tokens }};
    client_max_body_size {{ nginx_client_max_body_size }};

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Loglama
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # Gzip
    {% if nginx_gzip_enabled %}
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types {{ nginx_gzip_types | join(' ') }};
    {% endif %}

    # SSL ayarları
    ssl_protocols {{ nginx_ssl_protocols }};
    ssl_ciphers {{ nginx_ssl_ciphers }};
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Upstream tanımları
    {% for upstream in nginx_upstreams %}
    upstream {{ upstream.name }} {
        {% if upstream.method is defined %}
        {{ upstream.method }};
        {% endif %}
        {% for server in upstream.servers %}
        server {{ server }};
        {% endfor %}
    }
    {% endfor %}

    include /etc/nginx/sites-enabled/*.conf;
}

Virtual host template’i biraz daha karmaşık ama okunabilirliği korumak için bloklara ayırıyoruz:

# roles/nginx/templates/vhost.conf.j2
{% if item.ssl_enabled is defined and item.ssl_enabled %}
server {
    listen 80;
    server_name {{ item.server_name }};
    return 301 https://$server_name$request_uri;
}
{% endif %}

server {
    {% if item.ssl_enabled is defined and item.ssl_enabled %}
    listen 443 ssl http2;
    ssl_certificate {{ item.ssl_certificate }};
    ssl_certificate_key {{ item.ssl_certificate_key }};
    {% else %}
    listen {{ item.listen | default(80) }};
    {% endif %}

    server_name {{ item.server_name }};

    {% if item.root is defined %}
    root {{ item.root }};
    index {{ item.index | default('index.html') }};
    {% endif %}

    {% for location in item.locations | default([]) %}
    location {{ location.path }} {
        {% if location.proxy_pass is defined %}
        proxy_pass {{ location.proxy_pass }};
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        {% if location.proxy_set_header_host | default(false) %}
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        {% endif %}
        {% elif location.return is defined %}
        return {{ location.return }};
        {% else %}
        try_files $uri $uri/ =404;
        {% endif %}
    }
    {% endfor %}

    access_log /var/log/nginx/{{ item.name }}_access.log main;
    error_log /var/log/nginx/{{ item.name }}_error.log warn;
}

Load Balancer Özel Görevleri

Load balancer sunucuları için ayrı bir task dosyası oluşturuyoruz:

# roles/nginx/tasks/loadbalancer.yml
---
- name: Upstream konfigürasyonunu oluştur
  ansible.builtin.template:
    src: upstream.conf.j2
    dest: /etc/nginx/conf.d/upstream.conf
    owner: root
    group: root
    mode: '0644'
    validate: '/usr/sbin/nginx -t -c /etc/nginx/nginx.conf'
  notify: nginx reload

- name: Health check endpoint'ini ekle
  ansible.builtin.template:
    src: health_check.conf.j2
    dest: /etc/nginx/sites-available/health_check.conf
    owner: root
    group: root
    mode: '0644'
  notify: nginx reload

- name: Health check site'ı etkinleştir
  ansible.builtin.file:
    src: /etc/nginx/sites-available/health_check.conf
    dest: /etc/nginx/sites-enabled/health_check.conf
    state: link
  notify: nginx reload

Bu task’ı tasks/main.yml içinde koşullu olarak çağırıyoruz:

# tasks/main.yml içine ekle
- name: Load balancer özel görevlerini çalıştır
  ansible.builtin.include_tasks: loadbalancer.yml
  when: inventory_hostname in groups['loadbalancers']

Ana Playbook ve Çalıştırma

Artık her şeyi bir araya getiren ana playbook’u yazabiliriz:

# site.yml
---
- name: Tüm nginx sunucularını yapılandır
  hosts: nginx
  become: true
  gather_facts: true

  pre_tasks:
    - name: Sistem paketlerini güncelle
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

  roles:
    - nginx

  post_tasks:
    - name: Nginx servis durumunu kontrol et
      ansible.builtin.service_facts:

    - name: Nginx çalışıyor mu doğrula
      ansible.builtin.assert:
        that:
          - "'nginx' in services"
          - "services['nginx'].state == 'running'"
        fail_msg: "Nginx servisi çalışmıyor! Kontrol gerekli."
        success_msg: "Nginx başarıyla çalışıyor."

Playbook’u çalıştırmak için:

# Önce dry-run ile test et
ansible-playbook -i inventories/production/hosts.ini site.yml --check --diff

# Sadece belirli bir grup için çalıştır
ansible-playbook -i inventories/production/hosts.ini site.yml --limit webservers

# Belirli bir tag ile çalıştır
ansible-playbook -i inventories/production/hosts.ini site.yml --tags nginx_config

# Verbose modda çalıştır
ansible-playbook -i inventories/production/hosts.ini site.yml -v

--check --diff kombinasyonu prodüksiyon ortamında çok değerli. Gerçekten neyin değişeceğini görmeden önce simüle ediyor. Diff çıktısı sayesinde konfigürasyon dosyasında tam olarak hangi satırların değişeceğini görüyorsun.

Gerçek Dünya: Zero-Downtime Deployment

Prodüksiyon ortamında yaygın bir senaryo: Nginx konfigürasyonu güncellenmesi gerekiyor ama hiç downtime olmamalı. Bunun için serial direktifini kullanıyoruz:

# zero_downtime_update.yml
---
- name: Zero-downtime Nginx güncelleme
  hosts: webservers
  become: true
  serial: 1  # Sunucuları tek tek güncelle
  max_fail_percentage: 0  # Herhangi bir hata olursa dur

  pre_tasks:
    - name: Bu sunucuyu load balancer'dan çıkar
      ansible.builtin.uri:
        url: "http://{{ hostvars['lb01']['ansible_host'] }}/admin/disable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
      ignore_errors: true

    - name: Aktif bağlantıların bitmesi için bekle
      ansible.builtin.wait_for:
        timeout: 30

  roles:
    - nginx

  post_tasks:
    - name: Nginx'in düzgün çalıştığını doğrula
      ansible.builtin.uri:
        url: "http://{{ ansible_host }}/health"
        status_code: 200
      retries: 5
      delay: 10

    - name: Sunucuyu load balancer'a geri ekle
      ansible.builtin.uri:
        url: "http://{{ hostvars['lb01']['ansible_host'] }}/admin/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost

serial: 1 parametresi sunucuları tek tek günceller. serial: 2 ile ikişer ikişer, serial: "30%" ile yüzde otuzunu aynı anda güncelleyebilirsin. Büyük cluster’larda serial: "25%" yaygın bir tercih.

SSL Sertifika Yönetimi

Let’s Encrypt ile SSL sertifika kurulumunu da otomatize edebiliriz:

# roles/nginx/tasks/ssl.yml
---
- name: Certbot'u kur
  ansible.builtin.package:
    name:
      - certbot
      - python3-certbot-nginx
    state: present

- name: SSL sertifikasını al
  ansible.builtin.command:
    cmd: >
      certbot certonly --nginx
      --non-interactive
      --agree-tos
      --email {{ ssl_admin_email }}
      --domains {{ item.server_name }}
    creates: "/etc/letsencrypt/live/{{ item.server_name }}/fullchain.pem"
  loop: "{{ nginx_sites | selectattr('ssl_enabled', 'defined') | selectattr('ssl_enabled') | list }}"
  notify: nginx reload

- name: Certbot renewal cron job'ını ayarla
  ansible.builtin.cron:
    name: "certbot renewal"
    minute: "0"
    hour: "3"
    day: "*/7"
    job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"

Konfigürasyon Testi ve Monitoring Entegrasyonu

Deploy sonrası otomatik test yapmak iyi bir pratik:

# test_nginx.yml
---
- name: Nginx konfigürasyonunu test et
  hosts: nginx
  become: true
  gather_facts: false

  tasks:
    - name: Nginx syntax kontrolü
      ansible.builtin.command:
        cmd: nginx -t
      register: nginx_test
      changed_when: false
      failed_when: nginx_test.rc != 0

    - name: HTTP response kontrolü
      ansible.builtin.uri:
        url: "http://{{ ansible_host }}"
        status_code:
          - 200
          - 301
          - 302
      delegate_to: localhost
      register: http_check

    - name: Test sonuçlarını raporla
      ansible.builtin.debug:
        msg: >
          Sunucu: {{ inventory_hostname }}
          HTTP Status: {{ http_check.status }}
          Nginx Durumu: OK

Sık Yapılan Hatalar ve Çözümleri

Hata 1: Template değişkeni tanımsız

Bir site konfigürasyonunda ssl_certificate tanımlı değilken SSL template’ini çalıştırmaya çalışıyorsun. Jinja2 default filtresini kullan:

ssl_certificate {{ item.ssl_certificate | default('/etc/ssl/certs/ssl-cert-snakeoil.pem') }};

Hata 2: Handler sıralaması

Birden fazla task aynı handler’ı tetikliyorsa, handler sadece bir kez çalışır ama bu her zaman istediğin sırada olmayabilir. flush_handlers kullanarak handler’ları belirli bir noktada zorla çalıştırabilirsin:

- name: Handler'ları şimdi çalıştır
  ansible.builtin.meta: flush_handlers

Hata 3: Dosya izinleri

Nginx log dizininin yazma izinleri yanlışsa servis başlamaz. Bunu önceden garantilemek için:

- name: Log dizini izinlerini ayarla
  ansible.builtin.file:
    path: /var/log/nginx
    state: directory
    owner: www-data
    group: adm
    mode: '0755'

Ansible Vault ile Hassas Veri Yönetimi

SSL private key path’leri, API anahtarları gibi hassas verileri düz metin olarak tutmak güvenlik açığı. Vault kullan:

# Vault dosyası oluştur
ansible-vault create inventories/production/group_vars/all/vault.yml

# Vault dosyasına ekle
vault_ssl_admin_email: "[email protected]"
vault_db_password: "supersecret123"

# Playbook'ta kullan
ssl_admin_email: "{{ vault_ssl_admin_email }}"

# Vault şifresiyle çalıştır
ansible-playbook -i inventories/production/hosts.ini site.yml --ask-vault-pass

# Ya da şifre dosyasıyla
ansible-playbook -i inventories/production/hosts.ini site.yml --vault-password-file ~/.vault_pass

Performans İpuçları

Çok sayıda sunucuyla çalışırken Ansible’ın performansını artırmak için ansible.cfg dosyasını düzenle:

# ansible.cfg
[defaults]
inventory = inventories/production/hosts.ini
roles_path = roles
forks = 20
pipelining = True
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

forks = 20: Aynı anda 20 sunucuya paralel bağlanır. Varsayılan 5’tir. pipelining = True: SSH üzerinden birden fazla Python modülünü tek bağlantıda çalıştırır, ciddi hız artışı sağlar. fact_caching: Gather facts sonuçlarını cache’ler. Aynı playbook birkaç kez çalıştırıldığında her defasında facts toplamaz.

Sonuç

Ansible ile Nginx otomasyonu başta karmaşık görünebilir, ama bir kez oturduğunda sysadmin hayatını kökten değiştiriyor. Artık 50 sunucuya yeni bir location bloğu eklemek, bir template dosyasını düzenleyip ansible-playbook çalıştırmaktan ibaret oluyor.

Bu yazıda kurduğumuz yapının en güçlü yanları şunlar:

  • Tekrar üretilebilirlik: Aynı playbook staging ve prodüksiyon için çalışıyor, sadece değişkenler farklı
  • Güvenlik: Validate parametresi hatalı konfigürasyonun deploy edilmesini engelliyor
  • Zero-downtime: Serial deployment ile kesintisiz güncelleme mümkün
  • Denetlenebilirlik: Tüm değişiklikler Git’te, kim ne zaman ne değiştirdi görünür

Bir sonraki adım olarak bu yapıyı CI/CD pipeline’ına entegre etmeyi düşün. GitLab CI veya GitHub Actions ile her merge request sonrası staging’e otomatik deploy, prodüksiyon için manuel onay akışı kurabilirsin. Kod review + Ansible + CI/CD üçlüsü bir araya geldiğinde altyapı yönetimi gerçekten profesyonel bir boyut kazanıyor.

Bir yanıt yazın

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