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.
