Nginx Kurulumu: Ansible Playbook ile Otomasyon

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 ping ile bağlantıyı test et, SSH key’lerin doğru ayarlandığından emin ol.
  • “sudo: a password is required” hatası: ansible.cfg dosyasında become_ask_pass = True ekle veya passwordless sudo ayarla.
  • “nginx -t” başarısız oluyor: Template’deki Jinja2 ifadelerinde hata var. --check --diff ile tam olarak neyin render edildiğini gör.
  • Handler çalışmıyor: Task’ın changed durumuna geçmesi gerekiyor. Eğer idempotent çalışma nedeniyle değişiklik yoksa handler tetiklenmiyor. meta: flush_handlers ile handler’ları zorla çalıştırabilirsin.
  • “Permission denied” on template: become: true ayarı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.

Yorum yapın