Çoklu Ortam Deployment: Ansible ile Staging ve Production Ayrımı
Bir production ortamına yanlış konfigürasyonla deployment yapmak, gecenin üçünde telefon almak demek. Bunu bir kez yaşayan her sysadmin, bir daha yaşamamak için staging ortamı kurar. Ama staging ve production arasındaki ayrımı doğru yönetmek, sadece “iki farklı sunucu var” demekten çok daha fazlasını gerektiriyor. Ansible ile bu ayrımı sistematik, tekrarlanabilir ve güvenli bir şekilde nasıl yapacağınıza bakalım.
Neden Staging ve Production Ayrımı Bu Kadar Önemli?
Gerçek bir senaryo düşünelim: Bir e-ticaret şirketinde çalışıyorsunuz. Geliştirici ekip yeni bir ödeme modülü yazdı, doğrudan production’a deploy edildi ve veritabanı migration’ı beklediğiniz gibi çalışmadı. Sonuç: 2 saatlik downtime, kızgın müşteriler ve patronun sinir bozucu soruları. Staging ortamı olsaydı bu sorun oraya çarpar, production hiç etkilenmezdi.
Staging ortamının amacı sadece “test etmek” değil. Şunları kapsar:
- Konfigürasyon doğrulama: Production’daki gerçek değerlere yakın ama izole bir ortamda test
- Deployment sürecini prova etme: Ansible playbook’larınızın gerçekten çalışıp çalışmadığını görmek
- Rollback senaryolarını test etme: Bir şeyler ters giderse nasıl geri döneceğinizi bilmek
- Performans testleri: Yükü simüle etmek ve bottleneck’leri bulmak
Ansible Inventory Yapısını Doğru Kurmak
Ansible’da ortam ayrımının temeli inventory dosyalarıdır. Yanlış yapılandırılmış inventory, yanlış sunucuya deployment yapmanıza neden olur. Bu yüzden yapıyı baştan doğru kurmak kritik.
Önerilen dizin yapısı şu şekilde olmalı:
project/
├── inventories/
│ ├── staging/
│ │ ├── hosts
│ │ └── group_vars/
│ │ ├── all.yml
│ │ ├── webservers.yml
│ │ └── databases.yml
│ └── production/
│ ├── hosts
│ └── group_vars/
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
├── roles/
│ ├── webapp/
│ ├── nginx/
│ └── database/
├── playbooks/
│ ├── deploy.yml
│ ├── rollback.yml
│ └── maintenance.yml
└── ansible.cfg
Staging inventory dosyası şöyle görünür:
# inventories/staging/hosts
[webservers]
staging-web-01 ansible_host=10.0.1.10
staging-web-02 ansible_host=10.0.1.11
[databases]
staging-db-01 ansible_host=10.0.1.20
[loadbalancers]
staging-lb-01 ansible_host=10.0.1.5
[staging:children]
webservers
databases
loadbalancers
[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
environment_name=staging
Production inventory ise şöyle:
# inventories/production/hosts
[webservers]
prod-web-01 ansible_host=192.168.10.10
prod-web-02 ansible_host=192.168.10.11
prod-web-03 ansible_host=192.168.10.12
[databases]
prod-db-master ansible_host=192.168.10.20
prod-db-slave ansible_host=192.168.10.21
[loadbalancers]
prod-lb-01 ansible_host=192.168.10.5
[production:children]
webservers
databases
loadbalancers
[all:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
environment_name=production
Group Variables ile Ortam Bazlı Konfigürasyon
Her ortamın farklı değerlere ihtiyacı var. Veritabanı bağlantı stringleri, API anahtarları, cache süreleri bunların hepsi ortama göre değişir. Group variables burada devreye giriyor.
# inventories/staging/group_vars/all.yml
app_version: "latest"
app_env: "staging"
# Veritabani ayarlari
db_host: "staging-db-01"
db_port: 5432
db_name: "myapp_staging"
db_pool_size: 5
# Cache ayarlari
redis_host: "staging-redis-01"
redis_port: 6379
cache_ttl: 300
# Uygulama ayarlari
app_workers: 2
app_debug: true
app_log_level: "debug"
max_upload_size: "10M"
# Bildirim ayarlari
slack_webhook_url: "{{ vault_staging_slack_webhook }}"
notify_on_deploy: true
# inventories/production/group_vars/all.yml
app_version: "{{ lookup('env', 'APP_VERSION') | default('1.0.0') }}"
app_env: "production"
# Veritabani ayarlari
db_host: "prod-db-master"
db_port: 5432
db_name: "myapp_production"
db_pool_size: 20
# Cache ayarlari
redis_host: "prod-redis-cluster"
redis_port: 6379
cache_ttl: 3600
# Uygulama ayarlari
app_workers: 8
app_debug: false
app_log_level: "error"
max_upload_size: "50M"
# Bildirim ayarlari
slack_webhook_url: "{{ vault_production_slack_webhook }}"
notify_on_deploy: true
Hassas bilgiler için Ansible Vault kullanmak zorundasınız. Production şifrelerini düz metin olarak asla tutmayın:
# Vault ile sifreleme
ansible-vault create inventories/production/group_vars/vault.yml
# Icerik:
# vault_db_password: "super_secret_prod_password"
# vault_production_slack_webhook: "https://hooks.slack.com/..."
# vault_api_key: "prod_api_key_here"
# Staging icin ayri vault
ansible-vault create inventories/staging/group_vars/vault.yml
Ana Deployment Playbook’u
Şimdi her iki ortamda da çalışacak ama ortama göre farklı davranacak bir playbook yazalım:
# playbooks/deploy.yml
---
- name: "{{ environment_name | upper }} Ortamina Deployment"
hosts: webservers
serial: "{{ deployment_batch_size | default('50%') }}"
max_fail_percentage: 20
vars:
deployment_timestamp: "{{ ansible_date_time.epoch }}"
backup_dir: "/opt/backups/{{ app_name }}/{{ deployment_timestamp }}"
pre_tasks:
- name: Deployment oncesi kontroller
block:
- name: Disk alanini kontrol et
shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
register: disk_usage
changed_when: false
- name: Disk kullanimi yuksekse dur
fail:
msg: "Disk kullanimi %{{ disk_usage.stdout }} - deployment iptal edildi!"
when: disk_usage.stdout | int > 85
- name: Uygulama dizinini yedekle
archive:
path: "/var/www/{{ app_name }}"
dest: "{{ backup_dir }}/app_backup.tar.gz"
format: gz
when: environment_name == "production"
- name: Loadbalancer'dan sunucuyu cikar (sadece production)
uri:
url: "http://{{ hostvars[groups['loadbalancers'][0]]['ansible_host'] }}/api/disable/{{ inventory_hostname }}"
method: POST
when: environment_name == "production"
delegate_to: localhost
roles:
- role: webapp
- role: nginx
post_tasks:
- name: Uygulama saglik kontrolu
uri:
url: "http://{{ ansible_host }}:{{ app_port }}/health"
status_code: 200
timeout: 30
retries: 5
delay: 10
register: health_check
until: health_check.status == 200
- name: Loadbalancer'a sunucuyu geri ekle (sadece production)
uri:
url: "http://{{ hostvars[groups['loadbalancers'][0]]['ansible_host'] }}/api/enable/{{ inventory_hostname }}"
method: POST
when:
- environment_name == "production"
- health_check.status == 200
delegate_to: localhost
- name: Deployment bildirimi gonder
slack:
token: "{{ slack_webhook_url }}"
msg: "{{ environment_name | upper }}: {{ app_name }} v{{ app_version }} basariyla deploy edildi!"
color: "good"
delegate_to: localhost
when: notify_on_deploy | bool
Rolling Deployment ve Zero Downtime
Production’da zero downtime deployment için rolling update stratejisi şart. Tüm sunucuları aynı anda güncellemek production için intihar. serial keyword’ü ile bunu yönetebilirsiniz:
# playbooks/rolling_deploy.yml
---
- name: Rolling Deployment
hosts: webservers
serial:
- 1 # Once tek sunucu
- "25%" # Sonra %25'i
- "50%" # Geri kalani
max_fail_percentage: 0 # Herhangi bir hata olursa dur
tasks:
- name: Nginx'i maintenance moduna al
template:
src: maintenance.html.j2
dest: /var/www/html/maintenance.html
notify: reload nginx
- name: Aktif baglantilarin bitmesini bekle
wait_for:
host: "{{ ansible_host }}"
port: "{{ app_port }}"
state: drained
timeout: 60
delegate_to: localhost
- name: Uygulamayi guncelle
include_role:
name: webapp
- name: Servis kontrolu yap
systemd:
name: "{{ app_service_name }}"
state: restarted
enabled: yes
- name: Saglik kontrolu
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 10
delay: 5
register: result
until: result.status == 200
- name: Maintenance sayfasini kaldir
file:
path: /var/www/html/maintenance.html
state: absent
notify: reload nginx
handlers:
- name: reload nginx
systemd:
name: nginx
state: reloaded
Ortama Özel Rollback Mekanizması
Her deployment’ın bir rollback planı olmalı. Staging’de rollback test edip production’da uygulamak isteyebilirsiniz:
# playbooks/rollback.yml
---
- name: Rollback Islemi
hosts: webservers
vars_prompt:
- name: confirm_rollback
prompt: "{{ environment_name | upper }} ortaminda rollback yapilacak. Onayliyor musunuz? (yes/no)"
private: no
tasks:
- name: Rollback onayini kontrol et
fail:
msg: "Rollback iptal edildi."
when: confirm_rollback != "yes"
- name: Production rollback icin ekstra onay
pause:
prompt: "UYARI: PRODUCTION rollback yapiyorsunuz! 10 saniye icinde Ctrl+C ile iptal edebilirsiniz."
seconds: 10
when: environment_name == "production"
- name: Mevcut versiyonu kaydet
shell: cat /opt/current_version
register: current_version
changed_when: false
- name: Son basarili versiyonu bul
shell: ls -t /opt/backups/{{ app_name }}/ | head -2 | tail -1
register: rollback_version
changed_when: false
- name: Yedekten geri yukle
unarchive:
src: "/opt/backups/{{ app_name }}/{{ rollback_version.stdout }}/app_backup.tar.gz"
dest: "/var/www/{{ app_name }}"
remote_src: yes
- name: Servisi yeniden baslat
systemd:
name: "{{ app_service_name }}"
state: restarted
- name: Rollback saglik kontrolu
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 5
delay: 10
- name: Rollback bildir
slack:
token: "{{ slack_webhook_url }}"
msg: "ROLLBACK: {{ environment_name | upper }} ortaminda {{ app_name }} rollback yapildi! Versiyon: {{ rollback_version.stdout }}"
color: "warning"
delegate_to: localhost
CI/CD Pipeline ile Entegrasyon
GitLab CI veya GitHub Actions ile Ansible’ı entegre etmek, staging ve production ayrımını otomatikleştirir. Şöyle bir GitLab CI yapısı kullanabilirsiniz:
# .gitlab-ci.yml
stages:
- validate
- deploy-staging
- test-staging
- deploy-production
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_FORCE_COLOR: "true"
ansible-lint:
stage: validate
image: cytopia/ansible-lint
script:
- ansible-lint playbooks/deploy.yml
only:
- merge_requests
- main
deploy-to-staging:
stage: deploy-staging
image: willhallonline/ansible:latest
script:
- echo "$STAGING_VAULT_PASSWORD" > .vault_pass
- ansible-playbook
-i inventories/staging/
--vault-password-file .vault_pass
-e "app_version=${CI_COMMIT_SHORT_SHA}"
playbooks/deploy.yml
after_script:
- rm -f .vault_pass
environment:
name: staging
url: https://staging.myapp.com
only:
- main
integration-tests:
stage: test-staging
script:
- curl -f https://staging.myapp.com/health
- python tests/integration/run_tests.py --env staging
only:
- main
deploy-to-production:
stage: deploy-production
image: willhallonline/ansible:latest
script:
- echo "$PROD_VAULT_PASSWORD" > .vault_pass
- ansible-playbook
-i inventories/production/
--vault-password-file .vault_pass
-e "app_version=${CI_COMMIT_SHORT_SHA}"
playbooks/deploy.yml
after_script:
- rm -f .vault_pass
environment:
name: production
url: https://myapp.com
when: manual
only:
- main
when: manual satırı kritik. Production’a deployment elle onay gerektiriyor. Staging otomatik çalışırken production’da birinin “git” demesi gerekiyor.
Yaygın Hatalar ve Bunlardan Kaçınma Yolları
Tek inventory dosyası kullanmak: Bazıları [staging] ve [production] gruplarını tek bir hosts dosyasına koyar. Bu tehlikeli. Yanlış -l flag’i ile production’a gitmeyi kolaylaştırır.
Ortam değişkenlerini hardcode etmek: Vault kullanmadan şifreleri group_vars’a yazmak, git geçmişinde kalır.
Staging’i production ile aynı kapasitede tutmamak: Staging’in tek sunucuyla production’ın 10 sunuculu yapısını simüle etmeye çalışmak, ölçekleme sorunlarını yakalamanızı engeller.
Health check yapmadan rollback yapmak: Rollback sonrası da uygulama çalışmayabilir. Her zaman sonrasında doğrulama yapın.
serial değerini 100% bırakmak: Tüm sunucuları aynı anda güncellemek, bozuk deployment’ta tüm production’ı aynı anda çökertir.
Bir güvenlik katmanı daha eklemek için production playbook’larının başına şu kontrolü koyabilirsiniz:
# roles/webapp/tasks/safety_check.yml
---
- name: Production deployment guvenlik kontrolleri
block:
- name: Is saatlerinde mi?
fail:
msg: "Production deployment'lari sadece mesai saatlerinde yapilabilir (09:00-18:00)!"
when:
- environment_name == "production"
- ansible_date_time.hour | int < 9 or ansible_date_time.hour | int > 18
- name: Git branch kontrolu
local_action:
module: shell
cmd: git branch --show-current
register: current_branch
changed_when: false
- name: Production'a sadece main branch deploy edilebilir
fail:
msg: "Production'a sadece 'main' branch'ten deploy yapilabilir! Mevcut branch: {{ current_branch.stdout }}"
when:
- environment_name == "production"
- current_branch.stdout != "main"
Ansible Tags ile Seçici Deployment
Büyük playbook’larda sadece belirli bileşenleri güncellemek gerekebilir. Örneğin sadece nginx konfigürasyonunu değiştirmek istiyorsunuz ama uygulama koduna dokunmak istemiyorsunuz:
# tags kullanimi
# Sadece nginx konfigurasyonunu guncelle
ansible-playbook -i inventories/staging/ playbooks/deploy.yml --tags "nginx,config"
# Sadece veritabani migration'i calistir
ansible-playbook -i inventories/staging/ playbooks/deploy.yml --tags "db-migration"
# Nginx'i atlayarak deploy et
ansible-playbook -i inventories/production/ playbooks/deploy.yml --skip-tags "nginx"
# Dry-run ile once ne yapacagini gor
ansible-playbook -i inventories/production/ playbooks/deploy.yml --check --diff
--check --diff kombinasyonu özellikle production öncesi çok değerli. Neyin değişeceğini gerçekten uygulamadan görürsünüz.
Sonuç
Staging ve production ayrımı bir lüks değil, operasyonel olgunluğun göstergesi. Ansible ile bu ayrımı doğru kurduğunuzda şunları elde edersiniz: gece yarısı çağrıları azalır, deployment güveni artar, yeni ekip üyeleri bile kendi başına staging’e deploy yapabilir hale gelir.
Özet olarak kritik noktalara bakacak olursak:
- Ayrı inventory dizinleri kullanın, tek dosyaya sığdırmaya çalışmayın
- Hassas veriler için Ansible Vault zorunlu, vault şifrelerini CI/CD secret olarak saklayın
- Production deployment’larını her zaman manuel onay gerektirin
- Rolling deployment ile zero downtime hedefleyin
- Her deployment’ın geri alınabilir bir rollback planı olsun
- Health check olmadan deployment tamamlanmış sayılmaz
Bu yapıyı kurduktan sonra en büyük kazanım şu oluyor: Bir şeyler ters gittiğinde panik değil, prosedür devreye giriyor. Ve bu fark, uzun vadede hem sinir sisteminizi hem de şirketin SLA’larını koruyor.
