Her MySQL kurulumu elle yapılmak zorunda değil. Bir gün 10 sunucuya MySQL kurmam gerektiğinde, her birine tek tek bağlanıp aynı komutları çalıştırmak yerine Ansible playbook’u çalıştırıp kahvemi içmeye gittim. İşte bu yazıda tam olarak bunu nasıl yapacağınızı anlatacağım.
Neden Ansible ile MySQL?
Manuel MySQL kurulumunda bildiğiniz şeyler var: paketi kur, servisi başlat, root şifresini ayarla, gereksiz kullanıcıları sil, test veritabanını kaldır, uygulama için kullanıcı oluştur… Ve bunu her seferinde tekrar et. Production, staging, development ortamları için ayrı ayrı. Bir adımı atlarsanız güvenlik açığı, konfigürasyon tutarsızlığı, sonra saatler süren debug.
Ansible bu döngüyü kırıyor. Playbook’unuzu bir kez yazıyorsunuz, idempotent yapısı sayesinde kaç kez çalıştırırsanız çalıştırın sonuç aynı oluyor. Hem versiyon kontrol edebiliyorsunuz hem de başka ekip üyeleri tam olarak ne yapıldığını görebiliyor.
Proje Yapısı
Önce düzgün bir dizin yapısı kuralım. Büyük projelerde her şeyi tek playbook’a doldurmak sonradan başınızı ağrıtır.
mysql-ansible/
├── inventory/
│ ├── production
│ └── staging
├── group_vars/
│ ├── all.yml
│ └── mysql_servers.yml
├── roles/
│ └── mysql/
│ ├── tasks/
│ │ ├── main.yml
│ │ ├── install.yml
│ │ ├── configure.yml
│ │ └── secure.yml
│ ├── templates/
│ │ └── my.cnf.j2
│ ├── handlers/
│ │ └── main.yml
│ └── defaults/
│ └── main.yml
└── site.yml
Bu yapıyı oluşturmak için:
mkdir -p mysql-ansible/{inventory,group_vars}
mkdir -p mysql-ansible/roles/mysql/{tasks,templates,handlers,defaults}
cd mysql-ansible
Inventory Dosyası
Hangi sunuculara kuracağımızı tanımlayalım.
# inventory/production
[mysql_servers]
db01.example.com ansible_user=ubuntu ansible_become=true
db02.example.com ansible_user=ubuntu ansible_become=true
[mysql_servers:vars]
ansible_python_interpreter=/usr/bin/python3
Staging için ayrı bir inventory tutmak, production’ı yanlışlıkla etkileme riskini ortadan kaldırır. Bunu küçük bir detay gibi görmeyin, production’da yanlış bir playbook çalıştırmanın acısını yaşadıktan sonra bu alışkanlık vazgeçilmez oluyor.
Değişkenler ve Defaults
Rol değişkenlerini defaults dosyasında tanımlamak, playbook’u yeniden kullanılabilir yapıyor.
# roles/mysql/defaults/main.yml
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_port: 3306
mysql_bind_address: "127.0.0.1"
mysql_datadir: /var/lib/mysql
mysql_max_connections: 150
mysql_innodb_buffer_pool_size: "256M"
mysql_innodb_log_file_size: "64M"
mysql_query_cache_size: "32M"
mysql_slow_query_log: true
mysql_slow_query_log_time: 2
mysql_databases:
- name: appdb
encoding: utf8mb4
collation: utf8mb4_unicode_ci
mysql_users:
- name: appuser
password: "{{ vault_appuser_password }}"
priv: "appdb.*:ALL"
host: "localhost"
vault_mysql_root_password ve vault_appuser_password değişkenlerini Ansible Vault ile şifreleyeceğiz. Şifrelerinizi asla plaintext olarak kaydetmeyin, bu bir tavsiye değil, kural.
Group vars dosyasına ortama özel değerleri koyabilirsiniz:
# group_vars/mysql_servers.yml
mysql_innodb_buffer_pool_size: "512M"
mysql_max_connections: 200
Ansible Vault ile Şifre Yönetimi
Önce vault dosyası oluşturalım:
ansible-vault create group_vars/vault.yml
Editör açılır, buraya şifrelerinizi yazın:
vault_mysql_root_password: "SuperGizliSifre123!"
vault_appuser_password: "AppKullanicisiSifre456!"
Kaydedin ve çıkın. Artık bu dosya şifreli. Git’e commit etseniz bile içerik okunamaz. Tabii ki vault şifrenizi de güvenli bir yerde saklayın, bunu unutmak ciddi sorunlara yol açar.
Kurulum Görevi
Şimdi asıl işi yapan task dosyalarını yazalım.
# roles/mysql/tasks/install.yml
---
- name: MySQL gerekli paketleri kur (Debian/Ubuntu)
apt:
name:
- mysql-server
- mysql-client
- python3-mysqldb
- python3-pymysql
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: MySQL gerekli paketleri kur (RedHat/CentOS)
yum:
name:
- mysql-server
- mysql
- python3-PyMySQL
state: present
enablerepo: mysql80-community
when: ansible_os_family == "RedHat"
- name: MySQL servisini başlat ve otomatik başlatmayı etkinleştir
service:
name: mysql
state: started
enabled: true
- name: MySQL versiyon bilgisini kontrol et
command: mysql --version
register: mysql_version
changed_when: false
- name: MySQL versiyonunu göster
debug:
msg: "Kurulan MySQL versiyonu: {{ mysql_version.stdout }}"
python3-pymysql paketine dikkat edin. Ansible’ın MySQL modülleri Python tarafında bu kütüphaneye ihtiyaç duyuyor. Bunu unuttuğunuzda “unable to find the socket file” gibi yanıltıcı hata mesajları alırsınız, saatlerce uğraşırsınız.
MySQL Yapılandırması
Jinja2 template ile my.cnf dosyasını oluşturalım:
# roles/mysql/templates/my.cnf.j2
[mysqld]
# Temel Ayarlar
port = {{ mysql_port }}
bind-address = {{ mysql_bind_address }}
datadir = {{ mysql_datadir }}
# InnoDB Ayarları
innodb_buffer_pool_size = {{ mysql_innodb_buffer_pool_size }}
innodb_log_file_size = {{ mysql_innodb_log_file_size }}
innodb_file_per_table = ON
innodb_flush_log_at_trx_commit = 1
# Bağlantı Ayarları
max_connections = {{ mysql_max_connections }}
max_connect_errors = 10
connect_timeout = 10
wait_timeout = 28800
interactive_timeout = 28800
# Query Cache (MySQL 5.7 ve altı için)
{% if mysql_query_cache_size is defined %}
query_cache_size = {{ mysql_query_cache_size }}
query_cache_type = 1
{% endif %}
# Slow Query Log
{% if mysql_slow_query_log %}
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = {{ mysql_slow_query_log_time }}
{% endif %}
# Karakter Seti
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
Bu template’i uygulayan task:
# roles/mysql/tasks/configure.yml
---
- name: MySQL konfigürasyon dosyasını oluştur
template:
src: my.cnf.j2
dest: /etc/mysql/mysql.conf.d/mysqld.cnf
owner: root
group: root
mode: "0644"
backup: true
notify: restart mysql
- name: Slow query log dizinini oluştur
file:
path: /var/log/mysql
state: directory
owner: mysql
group: mysql
mode: "0755"
when: mysql_slow_query_log
- name: InnoDB için gereken dizin izinlerini ayarla
file:
path: "{{ mysql_datadir }}"
state: directory
owner: mysql
group: mysql
mode: "0750"
Güvenlik Yapılandırması
mysql_secure_installation komutunun yaptığı şeyleri Ansible ile yapacağız. Bu adım çoğu rehberde geçiştirilir ama production ortamda hayati önem taşıyor.
# roles/mysql/tasks/secure.yml
---
- name: Root şifresini ayarla
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
state: present
no_log: true
- name: .my.cnf dosyasını oluştur (root için kolaylık)
template:
src: my.cnf.credentials.j2
dest: /root/.my.cnf
owner: root
group: root
mode: "0600"
no_log: true
- name: Anonim kullanıcıları kaldır
mysql_user:
name: ""
host_all: true
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Root'un uzaktan bağlanmasını 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: true
- name: Test veritabanını kaldır
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Uygulama veritabanlarını oluştur
mysql_db:
name: "{{ item.name }}"
encoding: "{{ item.encoding | default('utf8mb4') }}"
collation: "{{ item.collation | default('utf8mb4_unicode_ci') }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
loop: "{{ mysql_databases }}"
no_log: true
- name: Uygulama kullanıcılarını oluştur
mysql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
priv: "{{ item.priv }}"
host: "{{ item.host | default('localhost') }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
loop: "{{ mysql_users }}"
no_log: true
no_log: true direktifine dikkat edin. Şifre içeren task’larda bunu kullanmazsanız, Ansible loglarına şifreler düz metin olarak yazılır. Log dosyalarını izleyen biri veya CI/CD sisteminin output’unu gören herkes şifrelerinize ulaşabilir.
Handler Tanımları
Konfigürasyon değiştiğinde servisi yeniden başlatmak için handler kullanıyoruz:
# roles/mysql/handlers/main.yml
---
- name: restart mysql
service:
name: mysql
state: restarted
- name: reload mysql
service:
name: mysql
state: reloaded
Handler’lar sadece “notify” edildiğinde ve play sonunda bir kez çalışır. Bu özellik sayesinde 5 farklı konfigürasyon değişikliği yapılsa bile servis sadece bir kez yeniden başlatılır.
Main Tasks Dosyası
Tüm task dosyalarını bir araya getiren ana dosya:
# roles/mysql/tasks/main.yml
---
- name: MySQL kurulum görevlerini dahil et
include_tasks: install.yml
tags:
- install
- mysql
- name: MySQL yapılandırma görevlerini dahil et
include_tasks: configure.yml
tags:
- configure
- mysql
- name: MySQL güvenlik görevlerini dahil et
include_tasks: secure.yml
tags:
- secure
- mysql
Ana Playbook
# site.yml
---
- name: MySQL Sunucuları Yapılandır
hosts: mysql_servers
become: true
vars_files:
- group_vars/vault.yml
pre_tasks:
- name: Sistem paketlerini güncelle
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Gerekli sistem araçlarını kur
package:
name:
- curl
- wget
- net-tools
state: present
roles:
- role: mysql
post_tasks:
- name: MySQL servis durumunu kontrol et
service_facts:
- name: MySQL'in çalıştığını doğrula
assert:
that:
- "'mysql.service' in ansible_facts.services"
- "ansible_facts.services['mysql.service']['state'] == 'running'"
success_msg: "MySQL başarıyla çalışıyor"
fail_msg: "MySQL servisi beklendiği gibi çalışmıyor"
Playbook’u Çalıştırmak
Önce dry-run ile ne yapılacağını görelim:
ansible-playbook -i inventory/staging site.yml --check --diff --ask-vault-pass
Her şey yolundaysa gerçekten çalıştıralım:
ansible-playbook -i inventory/staging site.yml --ask-vault-pass
Production için:
ansible-playbook -i inventory/production site.yml --ask-vault-pass
Sadece belirli tag’lere sahip task’ları çalıştırmak için:
# Sadece güvenlik ayarlarını uygula
ansible-playbook -i inventory/production site.yml --tags secure --ask-vault-pass
# Sadece konfigürasyonu güncelle
ansible-playbook -i inventory/production site.yml --tags configure --ask-vault-pass
Gerçek Dünya Senaryosu: Master-Replica Kurulumu
Production ortamlarında genellikle tek MySQL sunucusu yetmez. Basit bir master-replica senaryosu için inventory’yi genişletelim:
# inventory/production
[mysql_master]
db-master.example.com ansible_user=ubuntu ansible_become=true
[mysql_replica]
db-replica01.example.com ansible_user=ubuntu ansible_become=true
db-replica02.example.com ansible_user=ubuntu ansible_become=true
[mysql_servers:children]
mysql_master
mysql_replica
Master için ek konfigürasyon:
# group_vars/mysql_master.yml
mysql_server_id: 1
mysql_log_bin: true
mysql_binlog_format: "ROW"
mysql_expire_logs_days: 7
mysql_replication_user:
name: replicator
password: "{{ vault_replication_password }}"
Replica task’ı:
# Replica konfigürasyonu için ek task
- name: Replica'yı master'a bağla
mysql_replication:
mode: changeprimary
primary_host: "{{ mysql_master_host }}"
primary_user: replicator
primary_password: "{{ vault_replication_password }}"
primary_auto_position: true
login_user: root
login_password: "{{ mysql_root_password }}"
when: "'mysql_replica' in group_names"
no_log: true
Monitoring ve Healthcheck
Kurulum sonrası izleme için temel bir healthcheck task’ı ekleyelim:
# roles/mysql/tasks/healthcheck.yml
---
- name: MySQL bağlantısını test et
command: >
mysql -u root -p{{ mysql_root_password }}
-e "SELECT 1 as health_check;"
register: mysql_health
changed_when: false
no_log: true
- name: Veritabanlarının varlığını doğrula
mysql_db:
name: "{{ item.name }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
loop: "{{ mysql_databases }}"
check_mode: true
no_log: true
- name: MySQL process listesini göster
command: mysqladmin -u root -p{{ mysql_root_password }} processlist
register: mysql_processes
changed_when: false
no_log: true
- name: MySQL istatistiklerini göster
debug:
msg: "MySQL bağlantı bilgisi: {{ mysql_health.stdout_lines }}"
Idempotency Testi
Ansible’ın en büyük avantajlarından biri idempotency. Playbook’u ikinci kez çalıştırdığınızda “changed” olan task sayısı sıfır olmalı. Bunu test etmek için:
# İlk çalıştırma
ansible-playbook -i inventory/staging site.yml --ask-vault-pass
# İkinci çalıştırma - changed: 0 olmalı
ansible-playbook -i inventory/staging site.yml --ask-vault-pass
Eğer ikinci çalıştırmada hala “changed” çıkan task’lar varsa, o task’larınızı gözden geçirmeniz gerekiyor. Genellikle command/shell modülü kullanan ve changed_when tanımlanmamış task’larda bu sorun oluşur.
Sık Karşılaşılan Sorunlar
“Access denied for user ‘root’@’localhost'” hatası: Bu çoğunlukla root şifresinin zaten ayarlandığı ama playbook’un bunu bilmediği durumlarda olur. login_unix_socket parametresini kullanmak çözüm olabilir, özellikle ilk kurulumda.
“pymysql not found” hatası: python3-pymysql paketini kurmayı unutmuşsunuzdur. Bu paket olmadan Ansible’ın mysql_* modülleri çalışmaz.
Template değişikliği sonrası servis başlatılamıyor: my.cnf’te syntax hatası yapılmış olabilir. Önce mysqld --validate-config ile test edin. Ansible task’ınıza validate parametresi ekleyebilirsiniz.
Handler çalışmıyor: Task’ınız “changed” statüsünde olmadığında handler tetiklenmez. Konfigürasyon zaten doğruysa bu beklenen davranıştır.
CI/CD Entegrasyonu
GitLab CI için basit bir pipeline:
# .gitlab-ci.yml
stages:
- lint
- test
- deploy
ansible-lint:
stage: lint
image: pipelinecomponents/ansible-lint
script:
- ansible-lint site.yml
deploy-staging:
stage: test
script:
- echo "$ANSIBLE_VAULT_PASSWORD" > vault_pass.txt
- ansible-playbook -i inventory/staging site.yml
--vault-password-file vault_pass.txt
only:
- develop
deploy-production:
stage: deploy
script:
- echo "$ANSIBLE_VAULT_PASSWORD" > vault_pass.txt
- ansible-playbook -i inventory/production site.yml
--vault-password-file vault_pass.txt
only:
- main
when: manual
Production deploy’unu when: manual yapmanızı öneririm. Otomatik production deploy kurgusunun ne zaman patladığını gören biri olarak bunu söylüyorum.
Sonuç
Ansible ile MySQL kurulumu ve yapılandırması, elle yapılan işlemlerin büyük bölümünü ortadan kaldırıyor. Anlattığımız yapıyla şunları elde ettiniz: tekrar edilebilir ve tutarlı kurulumlar, versiyon kontrolü altında konfigürasyon, Vault ile güvenli şifre yönetimi, hem Debian hem RedHat ailesini destekleyen esnek rol yapısı ve tag’ler sayesinde kısmi güncellemeler.
Bu playbook’u kendi ortamınıza uyarlarken dikkat etmeniz gereken noktalar şunlar: buffer pool size sunucunuzun RAM’inin yüzde yetmiş ila seksenini geçmesin, max_connections değerini gereğinden yüksek tutmak bellek sorunlarına yol açar ve slow_query_log’u mutlaka açık bırakın, performans sorunlarını tespit etmenin en pratik yolu bu.
Kodu GitHub’da bir repoya koyun, ekip arkadaşlarınızın erişimine açın ve her değişikliği pull request üzerinden geçirin. Böylece hem “kim ne değiştirdi” sorusunun cevabı her zaman elinizde olur hem de peer review ile hata yapma olasılığını azaltırsınız. Bunu birkaç ay uygulayan herkes geri dönüp neden daha önce yapmadım diye düşünüyor.