Ansible ile Deployment Otomasyonu: Temel Yapı ve Başlangıç Rehberi
Deployment yaparken her seferinde aynı komutları çalıştırmaktan, sunucudan sunucuya atlayıp elle konfigürasyon yapmaktan bıktıysanız, Ansible tam da aradığınız şey. Birkaç YAML dosyası ve tek bir komutla onlarca sunucuya uygulama dağıtabilmek, konfigürasyonları tutarlı tutmak ve insan hatasını minimuma indirmek mümkün. Bu yazıda Ansible’ın deployment otomasyonunda nasıl kullanılacağını, temel yapısını ve gerçek dünya senaryolarıyla nasıl hayata geçirileceğini ele alacağız.
Ansible Neden Bu Kadar Popüler?
Piyasada Puppet, Chef, SaltStack gibi alternatifler varken Ansible’ın bu kadar yaygınlaşmasının birkaç pratik nedeni var. Her şeyden önce agentless çalışıyor. Yönetmek istediğiniz sunuculara herhangi bir ajan yazılımı kurmanıza gerek yok, sadece SSH erişimi yeterli. Bu, özellikle yüzlerce sunucu yönetirken ciddi bir avantaj sağlıyor.
İkincisi, YAML tabanlı yapısı sayesinde öğrenme eğrisi oldukça düz. Daha önce hiç Ansible yazmamış biri bile bir playbook’u okuyup ne yaptığını anlayabiliyor. Üçüncüsü ise idempotency prensibi. Aynı playbook’u kaç kez çalıştırırsanız çalıştırın, sonuç her zaman aynı oluyor. Sunucu zaten istenen durumda ise Ansible bir değişiklik yapmıyor.
Temel Kavramlar ve Yapı
Ansible’ı anlamak için birkaç temel kavramı kafaya oturtmak gerekiyor.
Inventory: Yönetmek istediğiniz sunucuların listesi. Statik bir dosya olabileceği gibi dinamik kaynaklardan da üretilebilir.
Playbook: Hangi sunucularda hangi işlemlerin yapılacağını tanımlayan YAML dosyası. Deployment otomasyonunun kalbi burada atıyor.
Task: Playbook içindeki tek bir işlem birimi. Bir paketi kur, bir servisi başlat, bir dosyayı kopyala gibi.
Module: Ansible’ın görevleri yerine getirmek için kullandığı hazır araçlar. apt, yum, copy, template, service gibi yüzlerce hazır modül var.
Role: Belirli bir işlevi yerine getiren, yeniden kullanılabilir Ansible bileşeni. Bir web sunucusu rolü, bir veritabanı rolü gibi.
Handler: Sadece bir değişiklik gerçekleştiğinde tetiklenen özel görevler. Örneğin nginx konfigürasyonu değiştiğinde servisi yeniden başlatmak için kullanılır.
Kurulum ve İlk Hazırlık
Ansible’ı kurmak son derece basit. Control node olarak kullandığınız makinede Python kurulu olması yeterli.
# Ubuntu/Debian üzerinde kurulum
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible
# CentOS/RHEL üzerinde kurulum
sudo dnf install -y epel-release
sudo dnf install -y ansible
# pip ile kurulum (tüm dağıtımlar için)
pip3 install ansible
# Kurulum doğrulama
ansible --version
Kurulumdan sonra yapmanız gereken ilk şey inventory dosyasını oluşturmak. Varsayılan inventory dosyası /etc/ansible/hosts konumunda yer alır ama proje bazlı çalışmak için kendi dizininizde tutmanızı öneririm.
# Proje dizinini oluşturun
mkdir -p ~/ansible-project
cd ~/ansible-project
# Inventory dosyasını oluşturun
cat > inventory.ini << 'EOF'
[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
[dbservers]
db01 ansible_host=192.168.1.20 ansible_user=ubuntu
db02 ansible_host=192.168.1.21 ansible_user=ubuntu
[loadbalancers]
lb01 ansible_host=192.168.1.5 ansible_user=ubuntu
[production:children]
webservers
dbservers
loadbalancers
[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python3
EOF
Bağlantıyı test etmek için ping modülünü kullanın:
# Tüm sunucuları test et
ansible all -i inventory.ini -m ping
# Sadece webservers grubunu test et
ansible webservers -i inventory.ini -m ping
# Ad-hoc komut çalıştır
ansible webservers -i inventory.ini -m command -a "uptime"
# Disk kullanımını kontrol et
ansible all -i inventory.ini -m shell -a "df -h | grep '/'"
İlk Playbook: Basit Bir Web Sunucusu Kurulumu
Teoriden pratiğe geçelim. Nginx kurulumu, konfigürasyonu ve servis yönetimini kapsayan basit ama gerçekçi bir playbook yazalım.
# webserver-setup.yml
---
- name: Web Sunucusu Kurulum ve Konfigürasyon
hosts: webservers
become: yes
vars:
nginx_port: 80
app_user: www-data
app_dir: /var/www/myapp
tasks:
- name: Sistem paketlerini güncelle
apt:
update_cache: yes
cache_valid_time: 3600
- name: Gerekli paketleri kur
apt:
name:
- nginx
- python3
- python3-pip
- git
- curl
state: present
- name: Uygulama dizinini oluştur
file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Nginx konfigürasyonunu kopyala
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp
owner: root
group: root
mode: '0644'
notify: Nginx'i yeniden başlat
- name: Site konfigürasyonunu etkinleştir
file:
src: /etc/nginx/sites-available/myapp
dest: /etc/nginx/sites-enabled/myapp
state: link
- name: Default site konfigürasyonunu kaldır
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Nginx'i yeniden başlat
- name: Nginx servisini başlat ve otomatik başlatmayı etkinleştir
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Nginx'i yeniden başlat
service:
name: nginx
state: reloaded
Şimdi bu playbook’ta kullanılan Nginx konfigürasyon şablonunu oluşturalım. Jinja2 template motoru sayesinde değişkenler ve koşullar kullanabiliyoruz.
# Template dizinini oluşturun
mkdir -p templates
# Nginx template dosyasını oluşturun
cat > templates/nginx.conf.j2 << 'EOF'
server {
listen {{ nginx_port }};
server_name {{ ansible_hostname }};
root {{ app_dir }};
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
location /api {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
access_log /var/log/nginx/{{ inventory_hostname }}_access.log;
error_log /var/log/nginx/{{ inventory_hostname }}_error.log;
}
EOF
Playbook’u çalıştırmak için:
# Önce dry-run yapın, ne değişecek görmek için
ansible-playbook -i inventory.ini webserver-setup.yml --check
# Gerçek çalıştırma
ansible-playbook -i inventory.ini webserver-setup.yml
# Verbose mod ile detaylı çıktı
ansible-playbook -i inventory.ini webserver-setup.yml -v
# Sadece belirli tag'e sahip görevleri çalıştır
ansible-playbook -i inventory.ini webserver-setup.yml --tags "nginx"
# Belirli bir sunucuya limit et
ansible-playbook -i inventory.ini webserver-setup.yml --limit web01
Role Yapısı ile Daha Organize Deployment
Gerçek dünya projelerinde her şeyi tek bir playbook’ta tutmak kaotik hale geliyor. Role’ler bu sorunu çözüyor. Standart bir role dizin yapısı şöyle görünür:
# Role iskeletini oluşturun
ansible-galaxy init roles/webapp
# Oluşan yapı:
# roles/webapp/
# ├── tasks/
# │ └── main.yml
# ├── handlers/
# │ └── main.yml
# ├── templates/
# ├── files/
# ├── vars/
# │ └── main.yml
# ├── defaults/
# │ └── main.yml
# └── meta/
# └── main.yml
Gerçek bir deployment senaryosu için webapp role’ünü dolduralım. Diyelim ki bir Python Flask uygulaması deploy ediyorsunuz:
# roles/webapp/tasks/main.yml
---
- name: Uygulama bağımlılıklarını kur
apt:
name:
- python3-venv
- python3-pip
state: present
- name: Uygulama kullanıcısı oluştur
user:
name: "{{ app_user }}"
system: yes
shell: /bin/bash
home: "{{ app_home }}"
create_home: yes
- name: Git reposunu çek
git:
repo: "{{ app_repo }}"
dest: "{{ app_dir }}"
version: "{{ app_version | default('main') }}"
force: yes
become_user: "{{ app_user }}"
notify: Uygulamayı yeniden başlat
- name: Virtual environment oluştur
pip:
requirements: "{{ app_dir }}/requirements.txt"
virtualenv: "{{ app_dir }}/venv"
virtualenv_command: python3 -m venv
become_user: "{{ app_user }}"
- name: Systemd servis dosyasını oluştur
template:
src: webapp.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: '0644'
notify:
- Systemd daemon'ını yeniden yükle
- Uygulamayı yeniden başlat
- name: Uygulama .env dosyasını oluştur
template:
src: env.j2
dest: "{{ app_dir }}/.env"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0600'
notify: Uygulamayı yeniden başlat
- name: Uygulamayı başlat ve otomatik başlatmayı etkinleştir
systemd:
name: "{{ app_name }}"
state: started
enabled: yes
daemon_reload: yes
Değişkenler ve Şifreli Veriler: Ansible Vault
Production ortamında veritabanı şifrelerini, API anahtarlarını ve benzeri hassas verileri düz metin olarak tutamazsınız. Ansible Vault bu iş için var.
# Şifreli değişken dosyası oluştur
ansible-vault create group_vars/production/vault.yml
# Editör açılacak, içine şunları yazın:
# vault_db_password: "super_secret_password_123"
# vault_api_key: "abc123def456ghi789"
# vault_jwt_secret: "my_jwt_secret_key"
# Mevcut dosyayı şifrele
ansible-vault encrypt group_vars/production/vars.yml
# Şifreli dosyayı düzenle
ansible-vault edit group_vars/production/vault.yml
# Şifreli dosyayı görüntüle
ansible-vault view group_vars/production/vault.yml
# Vault şifresiyle playbook çalıştır
ansible-playbook -i inventory.ini deploy.yml --ask-vault-pass
# Vault şifresini dosyadan oku (CI/CD için ideal)
ansible-playbook -i inventory.ini deploy.yml --vault-password-file ~/.vault_pass
Vault kullanırken değişken dosyalarını şöyle organize etmek iyi bir pratik:
# Dizin yapısı
mkdir -p group_vars/{all,webservers,production}
# Açık değişkenler
cat > group_vars/webservers/vars.yml << 'EOF'
app_name: mywebapp
app_user: webapp
app_home: /home/webapp
app_dir: /opt/mywebapp
app_repo: https://github.com/myorg/mywebapp.git
nginx_port: 80
gunicorn_workers: 4
gunicorn_port: 8000
EOF
# Vault referansları - düz değişken dosyasında vault değişkenlerini referans alın
cat > group_vars/webservers/secrets.yml << 'EOF'
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
EOF
Gerçek Dünya Senaryosu: Zero-Downtime Deployment
Üretim ortamında en kritik konu, deploy sırasında servis kesintisi yaşamamak. Rolling update stratejisiyle bunu nasıl başaracağımıza bakalım.
# deploy-zero-downtime.yml
---
- name: Zero-Downtime Deployment
hosts: webservers
serial: 1 # Sunucuları birer birer güncelle
max_fail_percentage: 0 # Herhangi bir başarısızlıkta dur
pre_tasks:
- name: Sunucuyu load balancer'dan çıkar
uri:
url: "http://{{ lb_host }}/api/servers/{{ inventory_hostname }}/disable"
method: POST
status_code: 200
delegate_to: lb01
- name: Aktif bağlantıların bitmesini bekle
wait_for:
timeout: 30
roles:
- webapp
post_tasks:
- name: Uygulama sağlık kontrolü
uri:
url: "http://{{ inventory_hostname }}:{{ gunicorn_port }}/health"
method: GET
status_code: 200
retries: 5
delay: 10
register: health_check
- name: Sunucuyu load balancer'a geri ekle
uri:
url: "http://{{ lb_host }}/api/servers/{{ inventory_hostname }}/enable"
method: POST
status_code: 200
delegate_to: lb01
when: health_check.status == 200
- name: Deployment başarısız oldu, rollback yap
include_tasks: tasks/rollback.yml
when: health_check.status != 200
Bu senaryo gerçekten işe yarıyor çünkü serial: 1 parametresi sayesinde Ansible sunucuları tek tek güncelliyor. Birinde sorun çıkarsa diğerlerine geçmiyor.
Ansible Konfigürasyon Dosyası: ansible.cfg
Her projede tekrar tekrar aynı parametreleri komut satırında yazmaktan kurtulmak için proje dizinine bir ansible.cfg dosyası ekleyin:
cat > ansible.cfg << 'EOF'
[defaults]
inventory = inventory.ini
remote_user = ubuntu
private_key_file = ~/.ssh/id_rsa
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callback_whitelist = profile_tasks, timer
interpreter_python = auto_silent
forks = 10
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
EOF
host_key_checking = False: İlk bağlantıda SSH fingerprint onayını atlar, CI/CD ortamlarında kullanışlı.
pipelining = True: SSH bağlantılarını optimize eder, playbook çalışma süresini ciddi ölçüde kısaltır.
forks = 10: Eş zamanlı bağlantı sayısı. Sunucu sayınıza göre ayarlayın.
stdout_callback = yaml: Çıktıyı daha okunabilir formatta gösterir.
CI/CD Pipeline’a Entegrasyon
Ansible’ı GitLab CI veya GitHub Actions ile birleştirdiğinizde gerçek bir deployment otomasyonu kurmuş oluyorsunuz. İşte bir GitLab CI örneği:
# .gitlab-ci.yml
stages:
- test
- build
- deploy-staging
- deploy-production
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_FORCE_COLOR: "True"
.ansible_base:
image: registry.gitlab.com/myorg/ansible-runner:latest
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d 'r' | ssh-add -
- echo "$VAULT_PASSWORD" > ~/.vault_pass
- chmod 600 ~/.vault_pass
deploy-staging:
extends: .ansible_base
stage: deploy-staging
script:
- ansible-playbook -i inventory/staging.ini deploy.yml
--vault-password-file ~/.vault_pass
--extra-vars "app_version=$CI_COMMIT_SHA"
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
deploy-production:
extends: .ansible_base
stage: deploy-production
script:
- ansible-playbook -i inventory/production.ini deploy.yml
--vault-password-file ~/.vault_pass
--extra-vars "app_version=$CI_COMMIT_TAG"
environment:
name: production
url: https://myapp.com
only:
- tags
when: manual
Bu pipeline ile develop branch’e her push yapıldığında staging’e otomatik deploy edilirken, production deploy’u manuel onay gerektiriyor. CI_COMMIT_SHA ve CI_COMMIT_TAG değişkenleri sayesinde hangi commit’in hangi ortamda çalıştığını tam olarak takip edebiliyorsunuz.
Hata Ayıklama ve En İyi Pratikler
Ansible ile çalışırken karşılaşacağınız en yaygın sorunlar ve çözümleri:
SSH bağlantı sorunları için:
# Bağlantıyı verbose modda test edin
ansible webservers -i inventory.ini -m ping -vvv
# SSH konfigürasyonunu manuel test edin
ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no [email protected]
Playbook debug için:
# Belirli bir task'tan itibaren çalıştır
ansible-playbook deploy.yml --start-at-task="Uygulamayı yeniden başlat"
# Step by step onay alarak çalıştır
ansible-playbook deploy.yml --step
# Değişken değerlerini kontrol et
ansible webservers -i inventory.ini -m debug -a "var=hostvars[inventory_hostname]"
İyi pratikler listesi:
- Her role için README.md yazın, hangi değişkenlerin zorunlu olduğunu belgeleyin
- Değişken isimlendirmede role prefix kullanın:
nginx_portyerine kendi role’ünüzdemyapp_nginx_portgibi - Production’a dokunmadan önce mutlaka
--checkile dry-run yapın - Playbook’larınızı idempotent yazın, ikinci çalıştırmada “changed” sayısı sıfır olmalı
ansible-lintkullanarak playbook’larınızı kod kalitesi açısından kontrol edin- Hassas çıktıları loglara yazmamak için
no_log: truekullanın
# ansible-lint kurulumu ve kullanımı
pip3 install ansible-lint
ansible-lint webserver-setup.yml
# Çıktısı log dosyasına yazılmasın istenen task
- name: Veritabanı şifresini ayarla
command: "mysql -u root -e "SET PASSWORD = '{{ db_password }}';""
no_log: true
Sonuç
Ansible ile deployment otomasyonu kurmanın en büyük faydası, zamanla biriken operasyonel bilgiyi kod olarak ifade edebilmek. Bir sunucuyu nasıl hazırlayacağınızı, uygulamayı nasıl deploy edeceğinizi bir playbook’ta yazdığınızda, o bilgi artık sadece sizin kafanızda değil, versiyon kontrol sisteminde yaşıyor.
Başlangıç için küçük tutun: Önce manuel yaptığınız tek bir işlemi Ansible ile otomatize edin. Belki bir konfigürasyon dosyası güncellemesi ya da basit bir paket kurulumu. Deneyim kazandıkça role’ler yazın, vault kullanmaya başlayın ve CI/CD pipeline’ınıza entegre edin.
Zero-downtime deployment, sağlık kontrolleri ve otomatik rollback mekanizmaları production ortamında gerçekten hayat kurtarıyor. Bu yazıda ele aldığımız temel yapıyı özümsedikten sonra Ansible’ın dinamik inventory, custom module yazımı ve AWX/Tower gibi ileri konularına geçmek çok daha kolay olacak.
Herhangi bir sorunla karşılaşırsanız önce --check ve -vvv paranızı koruyacak iki flagdir. Ansible topluluğu da oldukça aktif, resmi dokümantasyon ve Galaxy üzerindeki hazır role’ler işinizi hızlandıracak.
