Elle her sunucuya SSH atıp Nginx kurmak, konfigürasyon dosyalarını düzenlemek, servisi başlatmak… Bunları bir kez yapmak belki kabul edilebilir ama 10, 20, 50 sunucu için aynı işlemi tekrar etmek gerçek bir işkenceye dönüşüyor. İşte tam bu noktada Ansible devreye giriyor ve hayatını kurtarıyor.
Bu yazıda sıfırdan başlayarak production kalitesinde bir Nginx Ansible Playbook’u yazacağız. Sadece kurulum değil, sanal host konfigürasyonu, SSL sertifikası yönetimi, güvenlik hardening ve servis yönetimini de kapsayan gerçek dünya senaryolarını ele alacağız.
Ansible Neden Bu İş İçin Biçilmiş Kaftan?
Ansible’ın en büyük avantajı agentless yapısı. Hedef sunuculara herhangi bir ajan yazılımı kurmana gerek yok, sadece SSH erişimi yeterli. Üstüne bir de idempotent olması geliyor, yani aynı playbook’u 10 kez çalıştırsan da sonuç değişmiyor, sistem her zaman istediğin duruma geliyor.
Nginx kurulumu için Ansible kullanmanın somut faydaları şunlar:
- Tekrarlanabilirlik: Development, staging, production ortamlarının birebir aynı konfigürasyona sahip olması
- Dokümantasyon: Playbook’un kendisi aynı zamanda bir konfigürasyon dokümanı
- Hız: 50 sunucuya paralel kurulum dakikalar içinde tamamlanıyor
- Hata azaltma: İnsan hatasının minimuma inmesi
- Versiyon kontrolü: Git ile tüm konfigürasyon geçmişini takip etme imkanı
Proje Yapısını Oluşturma
İyi bir Ansible projesi için doğru klasör yapısı kritik önem taşıyor. Ansible Galaxy standartlarına uygun bir role yapısı kullanacağız.
mkdir -p nginx-ansible/{roles/nginx/{tasks,handlers,templates,files,vars,defaults,meta},inventory,group_vars}
cd nginx-ansible
tree
Projemizin nihai yapısı şöyle görünecek:
nginx-ansible/
├── ansible.cfg
├── inventory/
│ ├── production
│ └── staging
├── group_vars/
│ ├── all.yml
│ └── webservers.yml
├── roles/
│ └── nginx/
│ ├── defaults/
│ │ └── main.yml
│ ├── tasks/
│ │ ├── main.yml
│ │ ├── install.yml
│ │ ├── configure.yml
│ │ └── ssl.yml
│ ├── handlers/
│ │ └── main.yml
│ ├── templates/
│ │ ├── nginx.conf.j2
│ │ └── vhost.conf.j2
│ └── vars/
│ └── main.yml
└── site.yml
Ansible Konfigürasyonu
Önce ansible.cfg dosyasını oluşturalım. Bu dosya projeye özel Ansible ayarlarını barındırıyor.
cat > ansible.cfg << 'EOF'
[defaults]
inventory = ./inventory/production
roles_path = ./roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
gathering = smart
fact_caching = memory
forks = 10
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_user = root
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
EOF
pipelining = True ayarı özellikle dikkat çekici, bu sayede SSH bağlantı sayısı azalıyor ve playbook çalışma süresi önemli ölçüde düşüyor.
Inventory Dosyası
cat > inventory/production << 'EOF'
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=ubuntu
web02 ansible_host=192.168.1.11 ansible_user=ubuntu
web03 ansible_host=192.168.1.12 ansible_user=ubuntu
[loadbalancers]
lb01 ansible_host=192.168.1.5 ansible_user=ubuntu
[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
[staging]
staging-web01 ansible_host=10.0.0.10 ansible_user=ubuntu
EOF
Group Variables ile Merkezi Konfigürasyon
group_vars/all.yml dosyası tüm sunuculara uygulanacak genel değişkenleri barındırıyor:
# group_vars/all.yml
---
timezone: "Europe/Istanbul"
ntp_servers:
- "0.tr.pool.ntp.org"
- "1.tr.pool.ntp.org"
# Genel paket yönetimi
package_update_cache: true
package_cache_valid_time: 3600
Web sunucularına özel değişkenler için group_vars/webservers.yml:
# group_vars/webservers.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: "10m"
nginx_server_tokens: "off"
# Virtual host konfigürasyonları
nginx_vhosts:
- server_name: "example.com www.example.com"
document_root: "/var/www/example.com"
listen_port: 80
ssl_enabled: false
access_log: "/var/log/nginx/example.com_access.log"
error_log: "/var/log/nginx/example.com_error.log"
- server_name: "api.example.com"
document_root: "/var/www/api"
listen_port: 443
ssl_enabled: true
ssl_cert: "/etc/nginx/ssl/api.example.com.crt"
ssl_key: "/etc/nginx/ssl/api.example.com.key"
access_log: "/var/log/nginx/api_access.log"
error_log: "/var/log/nginx/api_error.log"
# Güvenlik ayarları
nginx_security_headers: true
nginx_rate_limiting: true
nginx_rate_limit_zone: "10m"
nginx_rate_limit_rate: "10r/s"
Role Defaults
roles/nginx/defaults/main.yml dosyası override edilebilir varsayılan değerleri içeriyor:
# roles/nginx/defaults/main.yml
---
nginx_package_name: nginx
nginx_service_name: nginx
nginx_user: www-data
nginx_group: www-data
nginx_conf_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_pid_file: /run/nginx.pid
# Performans defaults
nginx_worker_processes: auto
nginx_worker_connections: 768
nginx_multi_accept: "on"
nginx_use_epoll: true
# Güvenlik defaults
nginx_server_tokens: "off"
nginx_security_headers: false
# SSL defaults
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384"
nginx_ssl_prefer_server_ciphers: "off"
nginx_ssl_session_cache: "shared:SSL:10m"
nginx_ssl_session_timeout: "1d"
Tasks Dosyaları
Ana task dosyası diğer task dosyalarını include ediyor:
# roles/nginx/tasks/main.yml
---
- name: Nginx kurulum taskları dahil et
ansible.builtin.include_tasks: install.yml
tags: [nginx, install]
- name: Nginx konfigürasyon taskları dahil et
ansible.builtin.include_tasks: configure.yml
tags: [nginx, configure]
- name: SSL konfigürasyon taskları dahil et
ansible.builtin.include_tasks: ssl.yml
when: nginx_vhosts | selectattr('ssl_enabled', 'equalto', true) | list | length > 0
tags: [nginx, ssl]
Kurulum taskları:
# roles/nginx/tasks/install.yml
---
- name: Apt cache güncelle
ansible.builtin.apt:
update_cache: true
cache_valid_time: "{{ package_cache_valid_time }}"
when: ansible_os_family == "Debian"
- name: Nginx'i kur
ansible.builtin.package:
name: "{{ nginx_package_name }}"
state: present
notify: Nginx'i yeniden başlat
- name: Gerekli dizinleri oluştur
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0755"
loop:
- "{{ nginx_conf_dir }}/sites-available"
- "{{ nginx_conf_dir }}/sites-enabled"
- "{{ nginx_conf_dir }}/conf.d"
- "{{ nginx_conf_dir }}/ssl"
- "{{ nginx_log_dir }}"
- name: Document root dizinlerini oluştur
ansible.builtin.file:
path: "{{ item.document_root }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_group }}"
mode: "0755"
loop: "{{ nginx_vhosts }}"
when: nginx_vhosts is defined
- name: Nginx servisini etkinleştir
ansible.builtin.systemd:
name: "{{ nginx_service_name }}"
enabled: true
daemon_reload: true
Konfigürasyon taskları:
# roles/nginx/tasks/configure.yml
---
- name: Ana nginx.conf dosyasını yerleştir
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ nginx_conf_dir }}/nginx.conf"
owner: root
group: root
mode: "0644"
validate: nginx -t -c %s
notify: Nginx'i yeniden yükle
- name: Virtual host konfigürasyonlarını oluştur
ansible.builtin.template:
src: vhost.conf.j2
dest: "{{ nginx_conf_dir }}/sites-available/{{ item.server_name.split()[0] }}.conf"
owner: root
group: root
mode: "0644"
loop: "{{ nginx_vhosts }}"
when: nginx_vhosts is defined
notify: Nginx'i yeniden yükle
- name: Virtual hostları etkinleştir
ansible.builtin.file:
src: "{{ nginx_conf_dir }}/sites-available/{{ item.server_name.split()[0] }}.conf"
dest: "{{ nginx_conf_dir }}/sites-enabled/{{ item.server_name.split()[0] }}.conf"
state: link
loop: "{{ nginx_vhosts }}"
when: nginx_vhosts is defined
notify: Nginx'i yeniden yükle
- name: Default nginx sayfasını devre dışı bırak
ansible.builtin.file:
path: "{{ nginx_conf_dir }}/sites-enabled/default"
state: absent
notify: Nginx'i yeniden yükle
- name: Nginx konfigürasyonunu test et
ansible.builtin.command: nginx -t
register: nginx_test_result
changed_when: false
failed_when: nginx_test_result.rc != 0
Handlers
Handlers yalnızca bir değişiklik olduğunda tetikleniyor, bu da gereksiz servis yeniden başlatmalarının önüne geçiyor:
# roles/nginx/handlers/main.yml
---
- name: Nginx'i yeniden başlat
ansible.builtin.systemd:
name: "{{ nginx_service_name }}"
state: restarted
- name: Nginx'i yeniden yükle
ansible.builtin.systemd:
name: "{{ nginx_service_name }}"
state: reloaded
- name: Nginx'i başlat
ansible.builtin.systemd:
name: "{{ nginx_service_name }}"
state: started
Jinja2 Template’leri
Ana nginx.conf template’i:
# roles/nginx/templates/nginx.conf.j2
# Bu dosya Ansible tarafından yönetilmektedir. Manuel değişiklik yapmayın!
# Ansible role: nginx | Sunucu: {{ inventory_hostname }} | Tarih: {{ ansible_date_time.date }}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid {{ nginx_pid_file }};
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections {{ nginx_worker_connections }};
multi_accept {{ nginx_multi_accept }};
{% if nginx_use_epoll %}
use epoll;
{% endif %}
}
http {
# Temel ayarlar
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{ nginx_keepalive_timeout | default(65) }};
types_hash_max_size 2048;
server_tokens {{ nginx_server_tokens }};
client_max_body_size {{ nginx_client_max_body_size | default('1m') }};
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Loglama ayarları
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 {{ nginx_log_dir }}/access.log main;
error_log {{ nginx_log_dir }}/error.log;
# Gzip sıkıştırma
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
{% if nginx_security_headers %}
# Güvenlik başlıkları
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
{% endif %}
{% if nginx_rate_limiting %}
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:{{ nginx_rate_limit_zone }} rate={{ nginx_rate_limit_rate }};
{% endif %}
# SSL global ayarları
ssl_protocols {{ nginx_ssl_protocols }};
ssl_ciphers {{ nginx_ssl_ciphers }};
ssl_prefer_server_ciphers {{ nginx_ssl_prefer_server_ciphers }};
ssl_session_cache {{ nginx_ssl_session_cache }};
ssl_session_timeout {{ nginx_ssl_session_timeout }};
ssl_session_tickets off;
# Virtual host konfigürasyonları
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Virtual host template’i:
# roles/nginx/templates/vhost.conf.j2
# {{ item.server_name }} - Ansible tarafından yönetilmektedir
{% if item.ssl_enabled %}
# HTTP'den HTTPS'e yönlendirme
server {
listen 80;
server_name {{ item.server_name }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ item.server_name }};
ssl_certificate {{ item.ssl_cert }};
ssl_certificate_key {{ item.ssl_key }};
{% else %}
server {
listen {{ item.listen_port | default(80) }};
server_name {{ item.server_name }};
{% endif %}
root {{ item.document_root }};
index index.html index.htm index.php;
access_log {{ item.access_log }};
error_log {{ item.error_log }};
{% if nginx_rate_limiting %}
limit_req zone=general burst=20 nodelay;
{% endif %}
location / {
try_files $uri $uri/ =404;
}
location ~* .(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~ /. {
deny all;
}
}
Ana Playbook
# site.yml
---
- name: Web sunucularına Nginx kur ve yapılandır
hosts: webservers
gather_facts: true
become: true
pre_tasks:
- name: Sistem bilgilerini göster
ansible.builtin.debug:
msg: "{{ inventory_hostname }} sunucusuna Nginx kuruluyor - OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"
- name: Python3 kurulu mu kontrol et
ansible.builtin.raw: which python3
register: python3_check
changed_when: false
failed_when: false
roles:
- role: nginx
tags: nginx
post_tasks:
- name: Nginx servis durumunu kontrol et
ansible.builtin.systemd:
name: nginx
register: nginx_service_status
- name: Nginx durumunu raporla
ansible.builtin.debug:
msg: "Nginx durumu: {{ nginx_service_status.status.ActiveState }}"
- name: HTTP erişimini test et
ansible.builtin.uri:
url: "http://{{ ansible_default_ipv4.address }}"
status_code: [200, 301, 302]
timeout: 10
register: http_test
failed_when: false
- name: HTTP test sonucunu raporla
ansible.builtin.debug:
msg: "HTTP test sonucu: {{ http_test.status | default('ULASILAMADI') }}"
Playbook’u Çalıştırma
Playbook’u çalıştırmadan önce syntax kontrolü yapmalısın:
# Syntax kontrolü
ansible-playbook site.yml --syntax-check
# Dry-run (neyin değişeceğini görmek için)
ansible-playbook site.yml --check --diff
# Sadece belirli sunuculara uygulamak için
ansible-playbook site.yml --limit web01
# Sadece belirli tag'lere sahip taskları çalıştır
ansible-playbook site.yml --tags "configure"
# Belirli tag'leri atla
ansible-playbook site.yml --skip-tags "ssl"
# Verbose mod ile çalıştır
ansible-playbook site.yml -v
# Asıl çalıştırma komutu
ansible-playbook site.yml
Gerçek Dünya: Sıfır Kesinti ile Konfigürasyon Güncelleme
Üretim ortamında konfigürasyon güncellemesi yaparken nginx’i reload etmek, restart‘a göre çok daha güvenli. Zaten handler’ımız bunu yapıyor ama bunu özellikle belirtmek gerekiyor. Nginx reload işlemi mevcut bağlantıları kesmeden yeni worker process başlatıyor ve eski worker’lar mevcut istekleri tamamlayınca kapanıyor.
Load balancer arkasında birden fazla Nginx sunucun varsa rolling update stratejisi uygulayabilirsin:
# rolling-update.yml
---
- name: Rolling update ile Nginx güncelle
hosts: webservers
serial: 1
max_fail_percentage: 0
become: true
pre_tasks:
- name: Sunucuyu load balancer'dan çıkar
ansible.builtin.uri:
url: "http://lb01/api/disable/{{ inventory_hostname }}"
method: POST
delegate_to: lb01
when: groups['loadbalancers'] is defined
- name: Aktif bağlantıların bitmesini bekle
ansible.builtin.pause:
seconds: 10
roles:
- role: nginx
tags: nginx
post_tasks:
- name: Nginx sağlık kontrolü
ansible.builtin.uri:
url: "http://{{ ansible_default_ipv4.address }}/health"
status_code: 200
retries: 3
delay: 5
- name: Sunucuyu load balancer'a geri ekle
ansible.builtin.uri:
url: "http://lb01/api/enable/{{ inventory_hostname }}"
method: POST
delegate_to: lb01
when: groups['loadbalancers'] is defined
Molecule ile Role Testi
Playbook’u production’a almadan önce test etmek için Molecule kullanabilirsin:
# Molecule kurulumu
pip install molecule molecule-docker
# Nginx role için test ortamı oluştur
cd roles/nginx
molecule init scenario --driver-name docker
# Testleri çalıştır
molecule test
# Sadece lint kontrolü
molecule lint
# Test ortamını ayağa kaldır ve içine gir
molecule converge
molecule login
Sık Karşılaşılan Sorunlar ve Çözümleri
Playbook çalıştırırken karşılaşabileceğin tipik sorunlar ve çözümleri:
- “UNREACHABLE” hatası: SSH bağlantısı kurulamıyor.
ansible all -m pingile bağlantıyı test et, SSH key’lerin doğru ayarlandığından emin ol.
- “sudo: a password is required” hatası:
ansible.cfgdosyasındabecome_ask_pass = Trueekle veya passwordless sudo ayarla.
- “nginx -t” başarısız oluyor: Template’deki Jinja2 ifadelerinde hata var.
--check --diffile tam olarak neyin render edildiğini gör.
- Handler çalışmıyor: Task’ın
changeddurumuna geçmesi gerekiyor. Eğer idempotent çalışma nedeniyle değişiklik yoksa handler tetiklenmiyor.meta: flush_handlersile handler’ları zorla çalıştırabilirsin.
- “Permission denied” on template:
become: trueayarını kontrol et, hedef dizin izinlerini gözden geçir.
Vault ile Hassas Verileri Koruma
SSL private key’ler ve şifreler gibi hassas verileri Ansible Vault ile şifrelemelisin:
# Vault şifresi ile şifreli değişken dosyası oluştur
ansible-vault create group_vars/all/vault.yml
# Mevcut dosyayı şifrele
ansible-vault encrypt roles/nginx/vars/secrets.yml
# Vault şifresi ile playbook çalıştır
ansible-playbook site.yml --ask-vault-pass
# Vault şifresini dosyadan oku (CI/CD için)
ansible-playbook site.yml --vault-password-file ~/.vault_pass
group_vars/all/vault.yml içeriği örneği:
# ansible-vault ile şifrelenmiş
vault_ssl_cert_content: |
-----BEGIN CERTIFICATE-----
... sertifika içeriği ...
-----END CERTIFICATE-----
vault_ssl_key_content: |
-----BEGIN PRIVATE KEY-----
... private key içeriği ...
-----END PRIVATE KEY-----
Sonuç
Bu playbook ile artık tek bir komutla onlarca sunucuya tutarlı Nginx kurulumu yapabiliyorsun. Daha da önemlisi, konfigürasyon değişikliklerini güvenli ve tekrarlanabilir şekilde dağıtabiliyorsun.
Burada anlattığımız yapının en güçlü yönü modülerliği. Role yapısı sayesinde bu Nginx role’ünü başka projelerde de kullanabilir, defaults/main.yml üzerinden kolayca özelleştirebilirsin. group_vars ile farklı ortamlara farklı konfigürasyonlar uygulayabiliyorsun.
Bir sonraki adım olarak bu playbook’u CI/CD pipeline’ına entegre etmeyi düşünebilirsin. GitLab CI veya GitHub Actions üzerinde --check modunda otomatik syntax ve konfigürasyon doğrulaması, ardından staging ortamına otomatik deploy, onay sonrası production’a rolling update. Bu noktaya geldiğinde gerçek anlamda bir Infrastructure as Code pratiği uygulamış oluyorsun.
Aklında bulundurmak isteyeceğin en önemli prensip şu: playbook’un her zaman çalışabilir ve idempotent olmalı. Sabah 3’te bir sorun çıktığında ve saatlerdir uyanık olduğun için kafan çalışmadığında, güvenilir bir playbook’a sahip olmak paha biçilemez bir değere sahip.