Ansible Playbook ile Uygulama Kurulumu ve Yapılandırma
Ansible ile otomasyon yolculuğuna başlayanların çoğu, ilk adımda birkaç komut çalıştıran basit playbook’lar yazar. Ama işin gerçeği şu: Ansible’ın asıl gücü, karmaşık uygulama kurulumlarını ve yapılandırmalarını tekrarlanabilir, güvenilir bir şekilde yönetebilmesinde yatıyor. Bu yazıda sıfırdan başlayarak gerçek dünyada kullanabileceğiniz playbook’lar yazacağız, hataları ele alacağız ve production ortamına uygun yapılandırma yönetimini konuşacağız.
Playbook Anatomisi: Temel Yapıyı Anlamak
Bir playbook yazmadan önce yapıyı kafanızda oturtmanız gerekiyor. Playbook, bir veya daha fazla play içeren YAML dosyasıdır. Her play, belirli bir host grubuna belirli görevler (task) atar.
# Basit bir playbook yapısı
---
- name: Web sunucusu kurulumu
hosts: webservers
become: yes
vars:
http_port: 80
app_user: webadmin
tasks:
- name: Nginx kur
apt:
name: nginx
state: present
update_cache: yes
- name: Nginx servisini başlat
service:
name: nginx
state: started
enabled: yes
Burada dikkat etmeniz gereken birkaç nokta var. become: yes satırı, görevlerin sudo ile çalışacağını söylüyor. state: present ise paketin kurulu olmasını garanti ediyor, yoksa kuruyor, varsa dokunmuyor. Bu idempotency kavramının ta kendisi: aynı playbook’u on kez çalıştırsanız da sonuç aynı.
Envanter Yönetimi ve Değişken Hiyerarşisi
Playbook’ların etkin çalışması için iyi organize edilmiş bir envanter şart. Statik envanter dosyası basit ortamlar için yeterli olsa da production’da dinamik envanter kullanmak çok daha mantıklı.
# /etc/ansible/hosts veya proje klasörünüzdeki inventory/hosts dosyası
[webservers]
web01.ornekfirma.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/prod_key
web02.ornekfirma.com ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/prod_key
[dbservers]
db01.ornekfirma.com ansible_user=ubuntu
[webservers:vars]
nginx_worker_processes=4
nginx_worker_connections=1024
[dbservers:vars]
postgresql_version=15
max_connections=200
Değişken hiyerarşisi Ansible’da çok önemli. En düşük öncelikten en yükseğe doğru sıralama şöyle:
- role defaults: En kolay ezilen değişkenler
- inventory vars: Envanterde tanımlanan değişkenler
- playbook vars: Playbook içinde tanımlanan vars
- host_vars: Host bazlı değişken dosyaları
- extra vars (-e): Komut satırından geçilen değişkenler, her şeyi ezer
# host_vars/web01.ornekfirma.com.yml
---
server_role: primary
ssl_enabled: true
app_version: "2.4.1"
# group_vars/webservers.yml
---
nginx_port: 80
app_dir: /var/www/html
log_level: warn
Gerçek Dünya Senaryosu: LAMP Stack Kurulumu
Artık teoriden pratiğe geçelim. Bir müşteri için klasik LAMP stack kuracağız: Ubuntu 22.04 üzerinde Apache, MySQL ve PHP. Bu senaryo benim en sık karşılaştığım kurulum tiplerinden biri.
# lamp_setup.yml
---
- name: LAMP Stack Kurulumu
hosts: webservers
become: yes
vars_files:
- vars/main.yml
- vars/secrets.yml # ansible-vault ile şifrelenmiş
pre_tasks:
- name: Sistem güncellemesi
apt:
update_cache: yes
cache_valid_time: 3600
tasks:
- name: Gerekli paketleri kur
apt:
name:
- apache2
- mysql-server
- php8.1
- php8.1-mysql
- php8.1-curl
- php8.1-gd
- php8.1-mbstring
- php8.1-xml
- libapache2-mod-php8.1
- python3-pymysql
state: present
- name: Apache modüllerini etkinleştir
apache2_module:
name: "{{ item }}"
state: present
loop:
- rewrite
- headers
- ssl
notify: Apache yeniden başlat
- name: Apache yapılandırma dosyasını kopyala
template:
src: templates/apache_vhost.conf.j2
dest: /etc/apache2/sites-available/{{ app_name }}.conf
owner: root
group: root
mode: '0644'
notify: Apache yeniden başlat
- name: Virtual host'u etkinleştir
command: a2ensite {{ app_name }}.conf
notify: Apache yeniden başlat
- name: MySQL root şifresini ayarla
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
host_all: yes
- name: Uygulama veritabanını oluştur
mysql_db:
name: "{{ db_name }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Uygulama DB kullanıcısını oluştur
mysql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
priv: "{{ db_name }}.*:ALL"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
handlers:
- name: Apache yeniden başlat
service:
name: apache2
state: restarted
Handlers konusuna biraz daha değinelim. Birden fazla task aynı handler’ı tetiklese bile handler yalnızca bir kez çalışır ve play’in sonunda. Bu sayede üç ayrı config değişikliği yapıldığında Apache üç kez yeniden başlamaz, sadece bir kez başlar.
Jinja2 Template’leri ile Dinamik Yapılandırma
Sabit config dosyaları kopyalamak yerine Jinja2 template’leri kullanmak, playbook’larınızı gerçekten güçlü kılan şeydir. Farklı ortamlar için farklı değerler, koşullu bloklar ve döngüler kullanabilirsiniz.
# templates/apache_vhost.conf.j2
<VirtualHost *:{{ nginx_port | default(80) }}>
ServerName {{ server_name }}
ServerAlias www.{{ server_name }}
DocumentRoot {{ app_dir }}
<Directory {{ app_dir }}>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/{{ app_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/{{ app_name }}_access.log combined
{% if ssl_enabled | default(false) %}
# SSL yapılandırması
SSLEngine on
SSLCertificateFile /etc/ssl/certs/{{ app_name }}.crt
SSLCertificateKeyFile /etc/ssl/private/{{ app_name }}.key
{% endif %}
{% for header in security_headers | default([]) %}
Header always set {{ header.name }} "{{ header.value }}"
{% endfor %}
</VirtualHost>
Template’deki | default() filtresi, değişken tanımlanmamışsa fallback değeri kullanır. Bu, playbook’ları farklı ortamlarda çalıştırırken hayat kurtarır.
Roller: Playbook’ları Modüler Hale Getirme
Gerçek production ortamlarında her şeyi tek bir playbook dosyasına yazmak büyük bir kaos yaratır. Ansible rolleri, yapılandırmaları mantıksal birimler halinde organize etmenizi sağlar.
# Rol yapısı oluşturma
ansible-galaxy init roles/nodejs_app
# Oluşan yapı:
# roles/nodejs_app/
# ├── defaults/
# │ └── main.yml
# ├── files/
# ├── handlers/
# │ └── main.yml
# ├── tasks/
# │ └── main.yml
# ├── templates/
# ├── tests/
# └── vars/
# └── main.yml
# roles/nodejs_app/tasks/main.yml
---
- name: Node.js repository ekle
shell: |
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
args:
creates: /etc/apt/sources.list.d/nodesource.list
- name: Node.js ve npm kur
apt:
name:
- nodejs
- build-essential
state: present
- name: PM2 process manager kur
npm:
name: pm2
global: yes
state: present
- name: Uygulama dizinini oluştur
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Uygulama kodunu kopyala
synchronize:
src: "{{ local_app_path }}/"
dest: "{{ app_dir }}/"
delete: yes
rsync_opts:
- "--exclude=node_modules"
- "--exclude=.git"
- name: npm bağımlılıklarını kur
npm:
path: "{{ app_dir }}"
state: present
become_user: "{{ app_user }}"
- name: PM2 ecosystem dosyasını oluştur
template:
src: ecosystem.config.js.j2
dest: "{{ app_dir }}/ecosystem.config.js"
owner: "{{ app_user }}"
mode: '0644'
notify: PM2 yeniden başlat
- name: PM2 startup script ayarla
command: pm2 startup systemd -u {{ app_user }} --hp /home/{{ app_user }}
become: yes
Rolleri kullanmak da son derece basit:
# site.yml - Ana playbook
---
- name: Web sunucularını yapılandır
hosts: webservers
become: yes
roles:
- common
- nginx
- nodejs_app
- monitoring
- name: Veritabanı sunucularını yapılandır
hosts: dbservers
become: yes
roles:
- common
- postgresql
- backup_agent
Koşullar, Döngüler ve Hata Yönetimi
Gerçek ortamlarda her zaman “eğer şu koşul varsa şunu yap” mantığına ihtiyaç duyarsınız. Ansible’ın when, loop ve block yapıları bu iş için biçilmiş kaftan.
# Koşullar ve hata yönetimi örneği
---
- name: Uygulama deployment ve sağlık kontrolü
hosts: appservers
become: yes
tasks:
- name: İşletim sistemi kontrol et
debug:
msg: "Bu sunucu {{ ansible_distribution }} {{ ansible_distribution_version }} çalıştırıyor"
- name: Ubuntu 20.04 veya üzeri için özel paket kur
apt:
name: somepackage
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version is version('20.04', '>=')
- name: Mevcut servis durumunu kontrol et
command: systemctl is-active myapp
register: service_status
failed_when: false
changed_when: false
- name: Eski versiyonu yedekle
copy:
src: "{{ app_dir }}/app.jar"
dest: "{{ app_dir }}/app.jar.backup"
remote_src: yes
when: service_status.stdout == "active"
- name: Kritik işlemler bloğu
block:
- name: Yeni versiyonu indir
get_url:
url: "https://releases.ornekfirma.com/app-{{ app_version }}.jar"
dest: "{{ app_dir }}/app.jar"
checksum: "sha256:{{ app_checksum }}"
- name: Servisi yeniden başlat
service:
name: myapp
state: restarted
- name: Sağlık kontrolü
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
timeout: 30
retries: 5
delay: 10
register: health_check
until: health_check.status == 200
rescue:
- name: Hata durumunda eski versiyona geri dön
copy:
src: "{{ app_dir }}/app.jar.backup"
dest: "{{ app_dir }}/app.jar"
remote_src: yes
- name: Servisi eski versiyonla başlat
service:
name: myapp
state: restarted
- name: Ekibe hata bildirimi gönder
mail:
to: "[email protected]"
subject: "KRITIK: {{ inventory_hostname }} deployment başarısız"
body: "Deployment başarısız oldu, eski versiyona geri dönüldü."
always:
- name: Deployment logunu kaydet
lineinfile:
path: /var/log/deployments.log
line: "{{ ansible_date_time.iso8601 }} - {{ inventory_hostname }} - v{{ app_version }} - {{ 'BASARILI' if health_check.status is defined and health_check.status == 200 else 'BASARISIZ' }}"
create: yes
Bu yapıdaki block/rescue/always kombinasyonu, try/catch/finally mantığının Ansible karşılığı. Production deployment’larında bu pattern’i mutlaka kullanın.
Ansible Vault ile Gizli Bilgi Yönetimi
Şifreler, API anahtarları ve sertifikaları playbook’larınıza düz metin olarak yazmak büyük bir güvenlik açığı. Ansible Vault bu sorunu çözüyor.
# Vault ile şifreli dosya oluşturma
ansible-vault create vars/secrets.yml
# Mevcut dosyayı şifreleme
ansible-vault encrypt vars/secrets.yml
# Şifreli dosyayı düzenleme
ansible-vault edit vars/secrets.yml
# Şifreyi görüntüleme (dikkatli kullanın)
ansible-vault view vars/secrets.yml
# Şifreyi değiştirme
ansible-vault rekey vars/secrets.yml
# Playbook'u vault şifresiyle çalıştırma
ansible-playbook site.yml --ask-vault-pass
# CI/CD ortamında şifre dosyasıyla çalıştırma
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Sadece belirli değerleri şifreleme (inline vault)
ansible-vault encrypt_string 'super_gizli_sifre123' --name 'db_password'
Vault şifreli bir secrets.yml dosyası şu şekilde görünür:
# vars/secrets.yml - şifreli hali (vault ile encrypt edilmiş)
$ANSIBLE_VAULT;1.1;AES256
66386134653765386232383236353337623062303435363466306636323564393866613330363739
3839363939316136653833633834313061623937363439350a386665623865396464353564633161
...
# Şifre çözülmüş içeriği:
# ---
# db_password: "SifremCokGuclu123!"
# api_key: "sk-prod-xxxxxxxxxxxxxxxxxxx"
# ssl_private_key: |
# -----BEGIN PRIVATE KEY-----
# ...
CI/CD Pipeline ile Ansible Entegrasyonu
Ansible’ı sadece manuel olarak çalıştırmak, otomasyon yolculuğunun ilk adımı. Gerçek otomasyon, GitLab CI veya Jenkins gibi araçlarla entegrasyon yapıldığında başlıyor.
# .gitlab-ci.yml - GitLab CI ile Ansible deployment
stages:
- test
- staging_deploy
- production_deploy
variables:
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "False"
.ansible_base:
image: cytopia/ansible:latest
before_script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d 'r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- echo "$VAULT_PASSWORD" > ~/.vault_pass
- chmod 600 ~/.vault_pass
ansible_lint:
stage: test
extends: .ansible_base
script:
- ansible-lint playbooks/site.yml
- ansible-playbook playbooks/site.yml --syntax-check
deploy_staging:
stage: staging_deploy
extends: .ansible_base
script:
- ansible-playbook
-i inventory/staging
--vault-password-file ~/.vault_pass
-e "app_version=${CI_COMMIT_TAG:-latest}"
playbooks/site.yml
environment:
name: staging
only:
- develop
deploy_production:
stage: production_deploy
extends: .ansible_base
script:
- ansible-playbook
-i inventory/production
--vault-password-file ~/.vault_pass
-e "app_version=${CI_COMMIT_TAG}"
playbooks/site.yml
environment:
name: production
when: manual
only:
- tags
Bu pipeline’da production deployment’ı when: manual ile korunuyor, yani birisi GitLab arayüzünden onaylama butonu tıklamadan production’a hiçbir şey gitmiyor. Bu, gece 2’de yanlışlıkla production’a deployment yapılmasını önleyen basit ama etkili bir güvenlik önlemi.
Playbook Performansı ve Best Practice’ler
Yüzlerce sunucuyu yönetmeye başladığınızda playbook performansı kritik hale geliyor. Birkaç önemli optimizasyon:
- gather_facts: no: Sunucular hakkında bilgi toplamak zaman alır. Gereksizse kapatın
- serial: Deployment’ı batch’ler halinde yapın, hepsini aynı anda güncellemeyin
- async ve poll: Uzun süren görevleri asenkron çalıştırın
- delegate_to: Bir görevi farklı bir host üzerinde çalıştırın
- run_once: Cluster’daki sadece bir node’da çalıştırılması gereken görevler için
# Performans odaklı rolling deployment
---
- name: Rolling deployment - %25 batch
hosts: webservers
become: yes
serial: "25%" # Sunucuların %25'ini aynı anda güncelle
max_fail_percentage: 10 # %10'dan fazla başarısız olursa dur
gather_facts: yes
pre_tasks:
- name: Load balancer'dan çıkar
uri:
url: "http://lb.ornekfirma.com/api/remove/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
tasks:
- name: Uzun süren kurulumu asenkron başlat
apt:
name: some-large-package
state: present
async: 600 # Maksimum 600 saniye bekle
poll: 0 # Sonucu bekleme, devam et
register: install_job
- name: Kurulum tamamlandı mı kontrol et
async_status:
jid: "{{ install_job.ansible_job_id }}"
register: install_result
until: install_result.finished
retries: 30
delay: 20
post_tasks:
- name: Sağlık kontrolü sonrası load balancer'a geri ekle
uri:
url: "http://lb.ornekfirma.com/api/add/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
when: install_result.finished
Sonuç
Ansible playbook’ları ile uygulama kurulumu ve yapılandırması, başta karmaşık görünse de doğru yapı kurulduğunda inanılmaz bir özgürlük sağlıyor. Artık “hangi sunucuya ne kurdum” diye kayıt tutmak yerine, tüm altyapı durumunuz Git reponuzda, okunabilir YAML dosyaları olarak yaşıyor.
Şu ana kadar ele aldığımız konuları özetleyecek olursak: İyi organize edilmiş envanter ve değişken hiyerarşisi temeli oluşturuyor. Jinja2 template’leri yapılandırmalarınızı dinamik ve esnek kılıyor. Roller, büyük projelerde tekrar kullanılabilirlik ve düzen sağlıyor. Block/rescue yapısı, deployment hatalarında otomatik rollback imkanı veriyor. Vault, gizli bilgileri güvende tutarken CI/CD entegrasyonu tüm süreci otomatize ediyor.
Benim tavsiyem: Her şeyi bir anda mükemmel yapmaya çalışmayın. Bugün tek bir uygulamanın kurulumunu otomatize eden basit bir playbook yazın. Yarın bunu bir role dönüştürün. Öbür gün CI/CD’ye bağlayın. Ansible öğrenme eğrisi görece düşük ama derinliği çok, her gün yeni bir şey keşfedeceksiniz.
