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.
