Ansible ile WordPress Otomatik Kurulum

Üretim ortamında onlarca WordPress sitesi kurmak zorunda kaldıysanız, her seferinde aynı adımları tekrarlamanın ne kadar sinir bozucu olduğunu biliyorsunuzdur. Apache veya Nginx kur, MySQL ayarla, PHP yükle, WordPress indir, wp-config.php dosyasını elle düzenle… Ve bir hata yaptın mı baştan başla. İşte tam bu noktada Ansible devreye giriyor ve tüm bu süreci tek bir komutla otomatikleştiriyor.

Bu yazıda, sıfırdan başlayarak tamamen işlevsel bir WordPress kurulumunu Ansible ile nasıl otomatik hale getireceğimizi inceleyeceğiz. Gerçek bir prodüksiyon senaryosunu baz alacağız: birden fazla sunucuya, farklı domain isimleriyle, güvenli MySQL kullanıcıları ve düzgün Nginx konfigürasyonu ile WordPress dağıtımı.

Ortam ve Gereksinimler

Başlamadan önce neye ihtiyacımız olduğunu netleştirelim. Ansible control node olarak Ubuntu 22.04 kullanacağız ve hedef sunucularımız da Ubuntu 22.04 tabanlı. Gerçek hayatta bu sunucular bir cloud provider üzerinde olabilir, bare metal olabilir ya da şirket içi VM’ler olabilir. Fark etmez, Ansible hepsinde aynı şekilde çalışır.

Gereksinimler:

  • Ansible 2.10 veya üzeri yüklü bir control node
  • Hedef sunuculara SSH erişimi (key-based authentication şart)
  • Hedef sunucularda sudo yetkisi olan kullanıcı
  • Python 3 yüklü hedef sunucular (Ubuntu’da genellikle zaten mevcuttur)

Control node üzerinde Ansible’ı yüklemek için:

sudo apt update
sudo apt install ansible -y
ansible --version

MySQL modülü için pymysql kütüphanesini de kurmamız gerekiyor. Bu kütüphane Ansible’ın MySQL modülleriyle iletişim kurmasını sağlıyor:

pip3 install pymysql

Proje Yapısı

İyi organize edilmiş bir Ansible projesi, ilerleyen dönemlerde sizi çok kurtarır. Özellikle takım ortamında çalışıyorsanız, dosya yapısının mantıklı ve tahmin edilebilir olması kritik önem taşır.

Projemizin dizin yapısı şu şekilde olacak:

wordpress-ansible/
├── inventory/
│   ├── hosts.ini
│   └── group_vars/
│       └── webservers.yml
├── roles/
│   ├── common/
│   │   └── tasks/
│   │       └── main.yml
│   ├── nginx/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── templates/
│   │       └── wordpress.conf.j2
│   ├── mysql/
│   │   └── tasks/
│   │       └── main.yml
│   ├── php/
│   │   └── tasks/
│   │       └── main.yml
│   └── wordpress/
│       ├── tasks/
│       │   └── main.yml
│       └── templates/
│           └── wp-config.php.j2
├── playbooks/
│   └── wordpress.yml
└── ansible.cfg

Bu yapıyı oluşturmak için:

mkdir -p wordpress-ansible/{inventory/group_vars,playbooks}
mkdir -p wordpress-ansible/roles/{common,nginx,mysql,php,wordpress}/tasks
mkdir -p wordpress-ansible/roles/{nginx,wordpress}/templates
cd wordpress-ansible

Inventory ve Değişken Tanımlamaları

inventory/hosts.ini dosyamızı oluşturalım. Burada gerçek sunucu IP adreslerinizi veya DNS isimlerini kullanacaksınız:

[webservers]
web01 ansible_host=192.168.1.101 ansible_user=ubuntu domain_name=site1.example.com
web02 ansible_host=192.168.1.102 ansible_user=ubuntu domain_name=site2.example.com

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

Şimdi inventory/group_vars/webservers.yml dosyasında ortak değişkenlerimizi tanımlayalım. Şifreler gibi hassas verileri gerçek hayatta Ansible Vault ile şifrelemenizi kesinlikle tavsiye ederim:

# Genel ayarlar
wordpress_version: "6.4.2"
wordpress_path: "/var/www"
wordpress_owner: "www-data"
wordpress_group: "www-data"

# MySQL ayarları
mysql_root_password: "SuperSecureRootPass123!"
mysql_wordpress_db: "wordpress_db"
mysql_wordpress_user: "wp_user"
mysql_wordpress_password: "WPUserSecurePass456!"

# PHP ayarları
php_version: "8.1"

# Nginx ayarları
nginx_worker_processes: "auto"
nginx_client_max_body_size: "64m"

Ansible Konfigürasyonu

ansible.cfg dosyası projenizin kök dizininde olmalı:

[defaults]
inventory = inventory/hosts.ini
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
gathering = smart
fact_caching = memory

[privilege_escalation]
become = True
become_method = sudo
become_user = root

host_key_checking = False ayarı prodüksiyonda dikkatli kullanılmalıdır. Yeni sunucular eklenirken işinize yarar ama köklü bir prodüksiyon ortamında known_hosts yönetimini düzgün yapmanız daha güvenlidir.

Role Tanımlamaları

Common Role

roles/common/tasks/main.yml dosyasında tüm sunucularda olmasını istediğimiz temel paketleri ve sistem ayarlarını yapılandırıyoruz:

---
- name: Sistem paketlerini guncelle
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Temel paketleri yukle
  apt:
    name:
      - curl
      - wget
      - unzip
      - git
      - software-properties-common
      - apt-transport-https
      - ca-certificates
      - python3-pip
      - python3-pymysql
    state: present

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

- name: UFW guvenlik duvari aktif et
  ufw:
    state: enabled
    policy: deny

- name: SSH portuna izin ver
  ufw:
    rule: allow
    port: "22"
    proto: tcp

- name: HTTP portuna izin ver
  ufw:
    rule: allow
    port: "80"
    proto: tcp

- name: HTTPS portuna izin ver
  ufw:
    rule: allow
    port: "443"
    proto: tcp

- name: Swap dosyasi olustur (2GB)
  command: fallocate -l 2G /swapfile
  args:
    creates: /swapfile

- name: Swap dosyasi izinlerini ayarla
  file:
    path: /swapfile
    mode: "0600"

- name: Swap formatla
  command: mkswap /swapfile
  when: ansible_swaptotal_mb < 1

- name: Swap aktif et
  command: swapon /swapfile
  when: ansible_swaptotal_mb < 1

- name: fstab'a swap ekle
  lineinfile:
    path: /etc/fstab
    line: "/swapfile swap swap defaults 0 0"
    state: present

PHP Role

WordPress için PHP 8.1 ve gerekli tüm eklentileri yüklüyoruz:

---
- name: PHP PPA deposunu ekle
  apt_repository:
    repo: "ppa:ondrej/php"
    state: present
    update_cache: yes

- name: PHP ve gerekli eklentileri yukle
  apt:
    name:
      - "php{{ php_version }}-fpm"
      - "php{{ php_version }}-mysql"
      - "php{{ php_version }}-curl"
      - "php{{ php_version }}-gd"
      - "php{{ php_version }}-mbstring"
      - "php{{ php_version }}-xml"
      - "php{{ php_version }}-xmlrpc"
      - "php{{ php_version }}-soap"
      - "php{{ php_version }}-intl"
      - "php{{ php_version }}-zip"
      - "php{{ php_version }}-bcmath"
      - "php{{ php_version }}-imagick"
      - "php{{ php_version }}-opcache"
    state: present

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

- name: PHP memory limitini artir
  lineinfile:
    path: "/etc/php/{{ php_version }}/fpm/php.ini"
    regexp: "^memory_limit"
    line: "memory_limit = 256M"
    state: present
  notify: restart php-fpm

- name: Upload max filesize ayarla
  lineinfile:
    path: "/etc/php/{{ php_version }}/fpm/php.ini"
    regexp: "^upload_max_filesize"
    line: "upload_max_filesize = 64M"
    state: present
  notify: restart php-fpm

- name: Post max size ayarla
  lineinfile:
    path: "/etc/php/{{ php_version }}/fpm/php.ini"
    regexp: "^post_max_size"
    line: "post_max_size = 64M"
    state: present
  notify: restart php-fpm

MySQL Role

roles/mysql/tasks/main.yml dosyasında MySQL kurulumu ve güvenlik konfigürasyonunu yapıyoruz. Dikkat edin, mysql_secure_installation komutunu Ansible modülleriyle simüle ediyoruz:

---
- name: MySQL server yukle
  apt:
    name:
      - mysql-server
      - mysql-client
      - python3-pymysql
    state: present

- name: MySQL servisini baslat
  service:
    name: mysql
    state: started
    enabled: yes

- name: MySQL root sifresini ayarla
  mysql_user:
    name: root
    password: "{{ mysql_root_password }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock
    host: localhost
    state: present

- name: MySQL credentials dosyasi olustur
  template:
    src: my.cnf.j2
    dest: /root/.my.cnf
    mode: "0600"

- name: Anonim MySQL kullanicilari sil
  mysql_user:
    name: ""
    host_all: yes
    state: absent
    login_user: root
    login_password: "{{ mysql_root_password }}"

- name: Remote root girisi engelle
  mysql_user:
    name: root
    host: "{{ item }}"
    state: absent
    login_user: root
    login_password: "{{ mysql_root_password }}"
  loop:
    - "{{ ansible_hostname }}"
    - "127.0.0.1"
    - "::1"
    - "%"
  ignore_errors: yes

- name: Test veritabanini sil
  mysql_db:
    name: test
    state: absent
    login_user: root
    login_password: "{{ mysql_root_password }}"

- name: WordPress veritabani olustur
  mysql_db:
    name: "{{ mysql_wordpress_db }}"
    encoding: utf8mb4
    collation: utf8mb4_unicode_ci
    state: present
    login_user: root
    login_password: "{{ mysql_root_password }}"

- name: WordPress MySQL kullanicisi olustur
  mysql_user:
    name: "{{ mysql_wordpress_user }}"
    password: "{{ mysql_wordpress_password }}"
    priv: "{{ mysql_wordpress_db }}.*:ALL"
    host: localhost
    state: present
    login_user: root
    login_password: "{{ mysql_root_password }}"

Nginx Role

roles/nginx/templates/wordpress.conf.j2 şablonunu oluşturalım:

server {
    listen 80;
    server_name {{ domain_name }} www.{{ domain_name }};
    root {{ wordpress_path }}/{{ domain_name }};

    index index.php index.html index.htm;

    client_max_body_size {{ nginx_client_max_body_size }};

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

    location ~ .php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php{{ php_version }}-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location ~ /.ht {
        deny all;
    }

    location /wp-content/uploads/ {
        location ~ .php$ {
            deny all;
        }
    }
}

WordPress Role

En kritik kısma geldik. roles/wordpress/tasks/main.yml dosyası:

---
- name: WordPress dizini olustur
  file:
    path: "{{ wordpress_path }}/{{ domain_name }}"
    state: directory
    owner: "{{ wordpress_owner }}"
    group: "{{ wordpress_group }}"
    mode: "0755"

- name: WordPress indir
  get_url:
    url: "https://wordpress.org/wordpress-{{ wordpress_version }}.tar.gz"
    dest: /tmp/wordpress.tar.gz
    mode: "0644"

- name: WordPress arsivini ac
  unarchive:
    src: /tmp/wordpress.tar.gz
    dest: /tmp/
    remote_src: yes

- name: WordPress dosyalarini hedefe kopyala
  copy:
    src: /tmp/wordpress/
    dest: "{{ wordpress_path }}/{{ domain_name }}/"
    owner: "{{ wordpress_owner }}"
    group: "{{ wordpress_group }}"
    remote_src: yes

- name: WordPress yapilandirma dosyasi olustur
  template:
    src: wp-config.php.j2
    dest: "{{ wordpress_path }}/{{ domain_name }}/wp-config.php"
    owner: "{{ wordpress_owner }}"
    group: "{{ wordpress_group }}"
    mode: "0640"

- name: WordPress uploads dizini olustur
  file:
    path: "{{ wordpress_path }}/{{ domain_name }}/wp-content/uploads"
    state: directory
    owner: "{{ wordpress_owner }}"
    group: "{{ wordpress_group }}"
    mode: "0755"

- name: WordPress gecici dosyalari temizle
  file:
    path: "{{ item }}"
    state: absent
  loop:
    - /tmp/wordpress.tar.gz
    - /tmp/wordpress

- name: Dizin izinlerini duzelt
  shell: |
    find {{ wordpress_path }}/{{ domain_name }} -type d -exec chmod 755 {} ;
    find {{ wordpress_path }}/{{ domain_name }} -type f -exec chmod 644 {} ;
    chmod 640 {{ wordpress_path }}/{{ domain_name }}/wp-config.php

roles/wordpress/templates/wp-config.php.j2 şablonu da WordPress’in veritabanı bağlantı bilgilerini ve güvenlik anahtarlarını otomatik oluşturacak:

<?php
define( 'DB_NAME', '{{ mysql_wordpress_db }}' );
define( 'DB_USER', '{{ mysql_wordpress_user }}' );
define( 'DB_PASSWORD', '{{ mysql_wordpress_password }}' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

define( 'AUTH_KEY',         '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'SECURE_AUTH_KEY',  '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'LOGGED_IN_KEY',    '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'NONCE_KEY',        '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'AUTH_SALT',        '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'SECURE_AUTH_SALT', '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'LOGGED_IN_SALT',   '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );
define( 'NONCE_SALT',       '{{ lookup("password", "/dev/null chars=ascii_letters,digits length=64") }}' );

$table_prefix = 'wp_';

define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );

define( 'DISALLOW_FILE_EDIT', true );
define( 'FORCE_SSL_ADMIN', false );

define( 'WP_MEMORY_LIMIT', '256M' );

if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', __DIR__ . '/' );
}

require_once ABSPATH . 'wp-settings.php';

Ana Playbook

playbooks/wordpress.yml dosyasında tüm rolleri bir araya getiriyoruz:

---
- name: WordPress Otomatik Kurulum
  hosts: webservers
  become: yes

  pre_tasks:
    - name: Kurulum oncesi sistem bilgilerini goster
      debug:
        msg: |
          Sunucu: {{ inventory_hostname }}
          IP: {{ ansible_host }}
          Domain: {{ domain_name }}
          OS: {{ ansible_distribution }} {{ ansible_distribution_version }}

  roles:
    - role: common
      tags: [common, base]
    - role: php
      tags: [php]
    - role: mysql
      tags: [mysql, database]
    - role: nginx
      tags: [nginx, webserver]
    - role: wordpress
      tags: [wordpress]

  post_tasks:
    - name: Tum servislerin calisiyor oldugunu dogrula
      service:
        name: "{{ item }}"
        state: started
      loop:
        - nginx
        - mysql
        - "php{{ php_version }}-fpm"

    - name: Kurulum tamamlandi mesaji
      debug:
        msg: |
          WordPress basariyla kuruldu!
          Site URL: http://{{ domain_name }}
          Kurulum URL: http://{{ domain_name }}/wp-admin/install.php

Playbook’u Calistirmak

Her şey hazır olduğuna göre önce syntax kontrolü yapıp ardından test modunda çalıştıralım:

# Syntax kontrolu
ansible-playbook playbooks/wordpress.yml --syntax-check

# Test modu (dry run) - hicbir degisiklik yapmaz
ansible-playbook playbooks/wordpress.yml --check

# Sadece belirli tagler ile calistirma
ansible-playbook playbooks/wordpress.yml --tags "mysql,wordpress"

# Sadece belirli bir host icin
ansible-playbook playbooks/wordpress.yml --limit web01

# Verbose modda calistirma (hata ayiklama icin)
ansible-playbook playbooks/wordpress.yml -vvv

# Tam kurulum
ansible-playbook playbooks/wordpress.yml

Ansible Vault ile Gizli Bilgileri Korumak

Gerçek bir prodüksiyon ortamında şifreleri açık metin olarak tutmak ciddi bir güvenlik açığıdır. Ansible Vault bu sorunu çözüyor:

# Sifrelenmis degisken dosyasi olustur
ansible-vault create inventory/group_vars/vault.yml

# Icerigini duzenle
ansible-vault edit inventory/group_vars/vault.yml

# Mevcut dosyayi sifrele
ansible-vault encrypt inventory/group_vars/webservers.yml

# Playbook'u vault sifresini kullanarak calistir
ansible-playbook playbooks/wordpress.yml --ask-vault-pass

# Ya da sifre dosyasindan oku
ansible-playbook playbooks/wordpress.yml --vault-password-file ~/.vault_pass

inventory/group_vars/vault.yml dosyasında şifreli değişkenler şu formatta tutulur:

vault_mysql_root_password: "SuperSecureRootPass123!"
vault_mysql_wordpress_password: "WPUserSecurePass456!"

Ardından webservers.yml dosyasında bu değişkenlere referans verirsiniz:

mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_wordpress_password: "{{ vault_mysql_wordpress_password }}"

Gercek Dunya Senaryolari ve Ipuclari

Birden fazla site yönetimi: Inventory dosyasına host değişkenleri ekleyerek farklı domainler, farklı PHP sürümleri veya farklı veritabanı isimleriyle aynı anda 10-20 WordPress sitesi kurabilirsiniz. Ben bir müşteri projesinde bu yöntemi kullanarak 15 farklı müşteri sitesini tek bir playbook çalıştırmasıyla kurmuştum. Elle yapıldığında 2-3 günlük iş, Ansible ile 25 dakikaya indi.

Idempotency önemi: Ansible’ın en büyük gücü idempotent olması, yani aynı playbook’u birden fazla kez çalıştırsanız bile sistemi değiştirmiyor, zaten istenilen durumda olduğunu anlıyor. Bir adım başarısız olursa playbook’u düzeltip tekrar çalıştırabilirsiniz, daha önce başarıyla tamamlanan adımlar tekrar uygulanmaz.

Tag kullanımı: Sadece WordPress güncellemesi yapmanız gerekiyorsa --tags wordpress ile sadece ilgili rolü çalıştırırsınız. MySQL veya Nginx’e dokunmaz.

Hata yönetimi: ignore_errors: yes seçeneğini dikkatli kullanın. Ben genellikle sadece idem potent olmayan komutlarda, örneğin zaten sileceğimiz geçici bir şeyin olmadığı durumlarda kullanıyorum.

Kurulum Sonrasi Dogrulama

Kurulum tamamlandıktan sonra her şeyin yolunda olduğunu doğrulamak için kısa bir verification playbook yazabilirsiniz:

# Servislerin durumunu kontrol et
ansible webservers -m service_facts | grep -E "(nginx|mysql|php)"

# WordPress dosyasinin varligi
ansible webservers -m stat -a "path=/var/www/site1.example.com/wp-config.php"

# MySQL baglantisini test et
ansible webservers -m mysql_db -a "name=wordpress_db state=present login_user=wp_user login_password=WPUserSecurePass456!"

Sonuc

Bu playbook yapısı, tek bir WordPress kurulumundan onlarca sitenin yönetimine kadar ölçeklenebilir. Önemli olan şu: bir kez doğru kurulduktan sonra bu kodu versiyon kontrolüne (Git) koyun, değişiklikleri takip edin ve ekibinizle paylaşın. Artık “ben nasıl kurdum ki bunu?” diye düşünmek zorunda kalmayacaksınız.

Pratik faydaları özetlemek gerekirse, sunucu başına kurulum süresi 45-60 dakikadan 8-10 dakikaya iniyor. İnsan kaynaklı hatalar neredeyse sıfıra düşüyor. Yeni bir ekip üyesi, nasıl kurulum yapıldığını öğrenmek için sadece playbook dosyalarını okumak zorunda. Ve en önemlisi, tüm sunucularınız birbiriyle tutarlı, her biri aynı konfigürasyona sahip.

Bir sonraki adım olarak bu playbook’a Let’s Encrypt SSL entegrasyonu, WP-CLI ile otomatik WordPress konfigürasyonu ve Certbot desteği ekleyebilirsiniz. Ayrıca bu yapıyı bir CI/CD pipeline’ına entegre ederek GitHub Actions veya Jenkins üzerinden tetiklenebilir hale getirebilirsiniz. Ama o konu, başka bir yazının konusu.

Bir yanıt yazın

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