Nginx ve PHP-FPM Dağıtımı: Ansible Playbook ile Otomatik Kurulum

Bir production sunucusuna Nginx ve PHP-FPM kurmak, yapılandırmak ve servisleri ayağa kaldırmak elle yapıldığında saatler alabilir. Üstelik her seferinde aynı adımları tekrarlamak hem zaman kaybı hem de insan hatasına açık bir süreç. İşte tam bu noktada Ansible devreye giriyor. Bu yazıda gerçek dünyada kullanılabilir bir Ansible playbook ile Nginx ve PHP-FPM deployment sürecini baştan sona ele alacağız.

Neden Ansible ile Otomatik Deployment?

Manuel kurulum süreçlerinde en büyük sorun tekrarlanabilirlik. Bir sunucuya kurulum yaparken yaptığın bir konfigürasyon değişikliğini diğer sunucuya uygulamayı unutabilirsin. Ya da üç ay sonra aynı kurulumu tekrar yapman gerektiğinde hangi adımları attığını hatırlamayabilirsin. Ansible bu sorunları ortadan kaldırır çünkü altyapını kod olarak tanımlarsın ve bu kodu istediğin zaman istediğin kadar sunucuda çalıştırabilirsin.

Ansible’ın bu iş için özellikle güzel yanı agent gerektirmemesi. SSH yeterli. Hedef sunucunda Python yüklü olması yeterli, başka bir şey kurmana gerek yok. Büyük bir PHP uygulaması çalıştıran 10 sunucun varsa hepsine aynı anda aynı konfigürasyonu uygulayabilirsin.

Proje Yapısı

İyi organize edilmiş bir Ansible projesi için şu dizin yapısını kullanacağız:

nginx-php-fpm-deployment/
├── inventory/
│   ├── production
│   └── staging
├── group_vars/
│   ├── all.yml
│   └── webservers.yml
├── roles/
│   ├── common/
│   │   └── tasks/
│   │       └── main.yml
│   ├── nginx/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   ├── templates/
│   │   │   ├── nginx.conf.j2
│   │   │   └── vhost.conf.j2
│   │   └── handlers/
│   │       └── main.yml
│   └── php-fpm/
│       ├── tasks/
│       │   └── main.yml
│       ├── templates/
│       │   ├── php-fpm.conf.j2
│       │   └── www.conf.j2
│       └── handlers/
│           └── main.yml
├── site.yml
└── requirements.yml

Bu yapı roles kullanıyor çünkü roller sayesinde kodu modüler tutabilir ve farklı projelerde yeniden kullanabilirsin.

Inventory Dosyası

Önce hangi sunuculara deploy edeceğimizi tanımlayalım:

# inventory/production
[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

[webservers:vars]
ansible_ssh_private_key_file=~/.ssh/production_key
ansible_python_interpreter=/usr/bin/python3

Staging ortamı için ayrı bir inventory tutmak her zaman iyi bir pratik. Production’a geçmeden önce staging’de test edebilirsin.

Değişkenler

Konfigürasyon değerlerini değişkenlere almak playbook’u çok daha esnek yapar. group_vars/webservers.yml dosyasında sunucu grubuna özel değişkenleri tanımlıyoruz:

# group_vars/webservers.yml
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 64m

php_version: "8.2"
php_fpm_pool_name: www
php_fpm_user: www-data
php_fpm_group: www-data
php_fpm_pm: dynamic
php_fpm_max_children: 50
php_fpm_start_servers: 5
php_fpm_min_spare_servers: 5
php_fpm_max_spare_servers: 35
php_fpm_max_requests: 500

app_domain: "myapp.example.com"
app_root: "/var/www/myapp/public"
app_user: "deploy"

php_extensions:
  - php8.2-fpm
  - php8.2-cli
  - php8.2-mysql
  - php8.2-curl
  - php8.2-mbstring
  - php8.2-xml
  - php8.2-zip
  - php8.2-redis
  - php8.2-opcache

Common Role: Temel Sistem Hazırlığı

Her deployment öncesinde sistem güncel olmalı ve bazı temel paketler kurulu olmalı:

# roles/common/tasks/main.yml
---
- name: Sistem paketlerini guncelle
  apt:
    update_cache: yes
    upgrade: safe
    cache_valid_time: 3600
  become: yes

- name: Gerekli temel paketleri kur
  apt:
    name:
      - curl
      - wget
      - git
      - unzip
      - software-properties-common
      - apt-transport-https
      - ca-certificates
      - gnupg
      - lsb-release
    state: present
  become: yes

- name: Timezone ayarla
  timezone:
    name: Europe/Istanbul
  become: yes

- name: Deploy kullanicisini olustur
  user:
    name: "{{ app_user }}"
    system: no
    shell: /bin/bash
    groups: www-data
    append: yes
    create_home: yes
  become: yes

- name: Uygulama dizinini olustur
  file:
    path: "{{ app_root | dirname }}"
    state: directory
    owner: "{{ app_user }}"
    group: www-data
    mode: "0755"
  become: yes

Nginx Role

Nginx kurulumu ve konfigürasyonu için tasks dosyasını hazırlayalım:

# roles/nginx/tasks/main.yml
---
- name: Nginx resmi repo anahtarini ekle
  apt_key:
    url: https://nginx.org/keys/nginx_signing.key
    state: present
  become: yes

- name: Nginx stable repo ekle
  apt_repository:
    repo: "deb http://nginx.org/packages/ubuntu {{ ansible_distribution_release }} nginx"
    state: present
    filename: nginx
  become: yes

- name: Nginx kur
  apt:
    name: nginx
    state: present
    update_cache: yes
  become: yes
  notify: nginx reload

- name: Nginx ana konfigurasyonu yerlestir
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    backup: yes
  become: yes
  notify: nginx reload

- name: Varsayilan site konfigurasyonunu kaldir
  file:
    path: /etc/nginx/conf.d/default.conf
    state: absent
  become: yes
  notify: nginx reload

- name: Vhost konfigurasyonunu yerlestir
  template:
    src: vhost.conf.j2
    dest: "/etc/nginx/conf.d/{{ app_domain }}.conf"
    owner: root
    group: root
    mode: "0644"
  become: yes
  notify: nginx reload

- name: Nginx konfigurasyonunu test et
  command: nginx -t
  become: yes
  changed_when: false

- name: Nginx servisini baslat ve enable et
  service:
    name: nginx
    state: started
    enabled: yes
  become: yes

Nginx ana 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 }};
    use epoll;
    multi_accept on;
}

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

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

    # Logging
    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
    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript;

    include /etc/nginx/conf.d/*.conf;
}

Virtual host template’i, PHP-FPM ile çalışacak şekilde:

# roles/nginx/templates/vhost.conf.j2
server {
    listen 80;
    server_name {{ app_domain }} www.{{ app_domain }};
    root {{ app_root }};
    index index.php index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ .php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php{{ php_version }}-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_index index.php;
        fastcgi_read_timeout 300;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

    location ~ /.(?!well-known).* {
        deny all;
    }

    location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires max;
        log_not_found off;
        access_log off;
    }

    access_log /var/log/nginx/{{ app_domain }}_access.log main;
    error_log /var/log/nginx/{{ app_domain }}_error.log;
}

Nginx handler’ları:

# roles/nginx/handlers/main.yml
---
- name: nginx reload
  service:
    name: nginx
    state: reloaded
  become: yes

- name: nginx restart
  service:
    name: nginx
    state: restarted
  become: yes

PHP-FPM Role

PHP-FPM kurulum ve konfigürasyonu için:

# roles/php-fpm/tasks/main.yml
---
- name: PHP PPA ekle
  apt_repository:
    repo: "ppa:ondrej/php"
    state: present
    update_cache: yes
  become: yes

- name: PHP-FPM ve uzantilari kur
  apt:
    name: "{{ php_extensions }}"
    state: present
  become: yes
  notify: php-fpm restart

- name: PHP-FPM ana konfigurasyonu yerlestir
  template:
    src: php-fpm.conf.j2
    dest: "/etc/php/{{ php_version }}/fpm/php-fpm.conf"
    owner: root
    group: root
    mode: "0644"
    backup: yes
  become: yes
  notify: php-fpm restart

- name: PHP-FPM pool konfigurasyonu yerlestir
  template:
    src: www.conf.j2
    dest: "/etc/php/{{ php_version }}/fpm/pool.d/www.conf"
    owner: root
    group: root
    mode: "0644"
    backup: yes
  become: yes
  notify: php-fpm restart

- name: OPcache konfigurasyonunu ayarla
  lineinfile:
    path: "/etc/php/{{ php_version }}/fpm/php.ini"
    regexp: "^;?{{ item.key }}"
    line: "{{ item.key }} = {{ item.value }}"
    backup: yes
  become: yes
  loop:
    - { key: "opcache.enable", value: "1" }
    - { key: "opcache.memory_consumption", value: "256" }
    - { key: "opcache.interned_strings_buffer", value: "16" }
    - { key: "opcache.max_accelerated_files", value: "10000" }
    - { key: "opcache.validate_timestamps", value: "0" }
    - { key: "opcache.save_comments", value: "1" }
  notify: php-fpm restart

- name: PHP-FPM servisini baslat ve enable et
  service:
    name: "php{{ php_version }}-fpm"
    state: started
    enabled: yes
  become: yes

PHP-FPM pool konfigürasyon template’i, üretim ortamına uygun process manager ayarlarıyla:

# roles/php-fpm/templates/www.conf.j2
[{{ php_fpm_pool_name }}]
user = {{ php_fpm_user }}
group = {{ php_fpm_group }}

listen = /run/php/php{{ php_version }}-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

; Process manager
pm = {{ php_fpm_pm }}
pm.max_children = {{ php_fpm_max_children }}
pm.start_servers = {{ php_fpm_start_servers }}
pm.min_spare_servers = {{ php_fpm_min_spare_servers }}
pm.max_spare_servers = {{ php_fpm_max_spare_servers }}
pm.max_requests = {{ php_fpm_max_requests }}

; Logging
slowlog = /var/log/php{{ php_version }}-fpm-slow.log
request_slowlog_timeout = 10s

; Environment variables
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

; PHP admin values
php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f [email protected]
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php{{ php_version }}-fpm-www-error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M

PHP-FPM handler’ları:

# roles/php-fpm/handlers/main.yml
---
- name: php-fpm restart
  service:
    name: "php{{ php_version }}-fpm"
    state: restarted
  become: yes

- name: php-fpm reload
  service:
    name: "php{{ php_version }}-fpm"
    state: reloaded
  become: yes

Ana Playbook

Tüm rolleri bir araya getiren site.yml:

# site.yml
---
- name: Nginx ve PHP-FPM Deployment
  hosts: webservers
  become: yes
  gather_facts: yes

  pre_tasks:
    - name: Ansible versiyonunu kontrol et
      assert:
        that:
          - ansible_version.full is version('2.12', '>=')
        fail_msg: "Ansible 2.12 veya uzeri gerekli"

    - name: OS destegi kontrol et
      assert:
        that:
          - ansible_distribution == "Ubuntu"
          - ansible_distribution_major_version | int >= 20
        fail_msg: "Bu playbook sadece Ubuntu 20.04+ destekliyor"

  roles:
    - role: common
      tags: ['common', 'base']
    - role: nginx
      tags: ['nginx', 'web']
    - role: php-fpm
      tags: ['php', 'fpm']

  post_tasks:
    - name: Nginx servis durumunu kontrol et
      service_facts:

    - name: Nginx calisiyor mu?
      assert:
        that:
          - ansible_facts.services['nginx.service'].state == 'running'
        fail_msg: "Nginx servisi calismıyor!"

    - name: PHP-FPM calisiyor mu?
      assert:
        that:
          - ansible_facts.services['php{{ php_version }}-fpm.service'].state == 'running'
        fail_msg: "PHP-FPM servisi calismıyor!"

    - name: HTTP durumunu kontrol et
      uri:
        url: "http://{{ ansible_host }}"
        return_content: no
        status_code: [200, 301, 302]
      delegate_to: localhost
      ignore_errors: yes
      register: http_check

    - name: HTTP kontrol sonucunu goster
      debug:
        msg: "HTTP kontrol sonucu: {{ http_check.status | default('HATA') }}"

Playbook’u Calistirmak

Syntax kontrolü yapmak için:

# Syntax kontrolu
ansible-playbook -i inventory/staging site.yml --syntax-check

# Dry-run (check mode) - degisiklikleri uygulamadan kontrol et
ansible-playbook -i inventory/staging site.yml --check --diff

# Staging ortamina deploy
ansible-playbook -i inventory/staging site.yml

# Sadece nginx tagine sahip tasklari calistir
ansible-playbook -i inventory/production site.yml --tags "nginx"

# Belirli bir sunucuya deploy
ansible-playbook -i inventory/production site.yml --limit web01

# Verbose mod ile calistir
ansible-playbook -i inventory/production site.yml -v

# Production deploy - onay iste
ansible-playbook -i inventory/production site.yml --ask-become-pass

Gercek Dunya Senaryosu: Laravel Uygulamasi

Ekipler genellikle bu altyapıyı Laravel veya benzeri bir PHP frameworkü ile kullanır. Bu durumda birkaç ek görev gerekiyor. Uygulama deployment için ayrı bir role ekleyebilirsin:

# Uygulama deployment gorevleri - app role icin ornek
---
- name: Composer yukle
  get_url:
    url: https://getcomposer.org/installer
    dest: /tmp/composer-installer.php
    mode: "0644"

- name: Composer kur
  command: php /tmp/composer-installer.php --install-dir=/usr/local/bin --filename=composer
  args:
    creates: /usr/local/bin/composer
  become: yes

- name: Uygulamayi git ile cek
  git:
    repo: "https://github.com/yourorg/yourapp.git"
    dest: "{{ app_root | dirname }}"
    version: "{{ app_version | default('main') }}"
    force: yes
  become: yes
  become_user: "{{ app_user }}"

- name: Composer bagimlilikları yukle
  composer:
    command: install
    working_dir: "{{ app_root | dirname }}"
    no_dev: yes
    optimize_autoloader: yes
  become: yes
  become_user: "{{ app_user }}"
  environment:
    COMPOSER_ALLOW_SUPERUSER: "1"

- name: .env dosyasini olustur
  template:
    src: env.j2
    dest: "{{ app_root | dirname }}/.env"
    owner: "{{ app_user }}"
    group: www-data
    mode: "0640"
  become: yes

- name: Storage ve bootstrap dizin izinlerini ayarla
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ app_user }}"
    group: www-data
    mode: "0775"
    recurse: yes
  become: yes
  loop:
    - "{{ app_root | dirname }}/storage"
    - "{{ app_root | dirname }}/bootstrap/cache"

- name: Artisan cache temizle
  command: php artisan config:cache
  args:
    chdir: "{{ app_root | dirname }}"
  become: yes
  become_user: "{{ app_user }}"

- name: Artisan route cache
  command: php artisan route:cache
  args:
    chdir: "{{ app_root | dirname }}"
  become: yes
  become_user: "{{ app_user }}"

Idempotency ve Guvenligi Saglamak

Ansible’ın en güçlü özelliklerinden biri idempotency yani aynı playbook’u birden fazla çalıştırsan sonuç her zaman aynı olmalı. Bunu sağlamak için dikkat etmen gereken birkaç nokta var.

command ve shell modüllerini dikkatli kullan: Bu modüller her çalıştırmada değişiklik olduğunu rapor eder. creates veya removes parametreleri ya da when koşulları ekleyerek bunu kontrol altına al.

Konfigürasyon dosyalarını her zaman template veya copy ile yönet: Direkt dosya düzenleme yerine Ansible’ın modüllerini kullan. Böylece dosya içeriği her zaman template’le eşleşir.

Handler’ları servis yeniden başlatmak için kullan: Handler’lar sadece değişiklik olduğunda tetiklenir. Her playbook çalışmasında servisleri gereksiz yere yeniden başlatmaz.

Vault ile hassas verileri koru: Veritabanı şifreleri, API anahtarları gibi değerleri asla düz metin olarak saklamazsın:

# Vault ile sifre olustur
ansible-vault create group_vars/production/vault.yml

# Vault ile calistir
ansible-playbook -i inventory/production site.yml --ask-vault-pass

# Ya da vault sifresini dosyadan oku
ansible-playbook -i inventory/production site.yml --vault-password-file ~/.vault_pass

Troubleshooting Ipuclari

Deployment sırasında karşılaşabileceğin yaygın sorunlar ve çözümleri:

Nginx konfigürasyon hataları: nginx -t komutunu her konfigürasyon değişikliğinden önce çalıştıran bir task eklemek hayat kurtarır. Template içinde sözdizimi hatası varsa servis reload edilmeden önce tespit edilir.

PHP-FPM socket izin sorunları: Nginx ve PHP-FPM aynı kullanıcı veya grupta değilse socket üzerinden iletişim kurulamaz. listen.owner ve listen.group değerlerinin nginx kullanıcısıyla eşleştiğinden emin ol.

Paket versiyon çakışmaları: Farklı sunucularda farklı OS sürümleri varsa paket isimleri değişebilir. ansible_distribution_release değişkenini kullanarak dinamik paket isimleri oluşturabilirsin.

SSH bağlantı sorunları: Playbook başlamadan önce bağlantıyı test et:

# Tum sunuculara ping at
ansible -i inventory/production webservers -m ping

# Sunucu bilgilerini topla
ansible -i inventory/production webservers -m gather_facts --tree /tmp/facts

# Ad-hoc komut calistir
ansible -i inventory/production webservers -m shell -a "systemctl status nginx"

Sonuc

Bu playbook ile birkaç dakika içinde sıfırdan tam çalışan bir Nginx ve PHP-FPM ortamı kurabilirsin. Daha da önemlisi bu kurulumu 1 sunucuya da 100 sunucuya da aynı sürede uygulayabilirsin. Konfigürasyon değişikliklerini versiyon kontrolünde tutabilir, ekip arkadaşlarınla paylaşabilir ve staging’de test ettikten sonra production’a güvenle uygulayabilirsin.

Bir sonraki adım olarak bu playbook’a Let’s Encrypt SSL entegrasyonu, fail2ban güvenlik konfigürasyonu veya uygulama bazlı monitoring ayarları ekleyebilirsin. Ansible’ın modüler yapısı sayesinde yeni bir role eklemek son derece kolay. Altyapını kod olarak yönetmeye başladıktan sonra manuel kurulum yapmak istemeyeceğini garantileyebilirim.

Bir yanıt yazın

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