MySQL Kurulumu ve Yapılandırması: Ansible ile Tam Rehber

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.

Yorum yapın