Üretim ortamına yanlış bir playbook çalıştırmak, kariyerinizin en uzun gecesini yaşamanıza neden olabilir. “Staging’de test ettim, production’da da çalışır” mantığı teoride güzel görünse de, iki ortamın birbirinden net biçimde ayrılmadığı bir yapıda bu cümle çok tehlikeli. Ansible ile çoklu ortam yönetimi, sadece bir iyileştirme değil, operasyonel olgunluğun temel göstergelerinden biridir.
Bu yazıda gerçek dünya senaryolarına dayanan, production’ı korurken staging’i özgürce kullanabileceğiniz bir Ansible yapısını baştan sona kuruyoruz.
Neden Ortam Ayrımı Şart?
Çoğu ekip başlangıçta tek bir inventory dosyasıyla çalışır. Her şey küçükken bu yönetilebilir görünür. Ama ekip büyüdükçe, sunucu sayısı arttıkça ve değişiklik sıklığı yükseldikçe şu sorular kaçınılmaz hale gelir:
- Bu değişkeni staging için mi yoksa production için mi set ettim?
- Bu playbook hangi ortamda çalışacak?
- Staging’deki veritabanı şifresi production’dakiyle aynı mı?
Ansible’ın güçlü yanı, bu soruları mimariye gömerek ortadan kaldırabilmesidir. Doğru bir inventory yapısı, değişken hiyerarşisi ve vault kullanımı bir araya geldiğinde, yanlış ortamda bir şey çalıştırmak neredeyse imkansız hale gelir.
Proje Dizin Yapısı
İyi bir çoklu ortam yapısının temeli, dizin organizasyonudur. Ansible’ın resmi best practice dökümanında önerilen yapıyı biraz daha genişleterek başlayalım:
ansible-project/
├── inventories/
│ ├── staging/
│ │ ├── hosts.ini
│ │ ├── group_vars/
│ │ │ ├── all.yml
│ │ │ ├── webservers.yml
│ │ │ └── dbservers.yml
│ │ └── host_vars/
│ │ └── staging-web-01.yml
│ └── production/
│ ├── hosts.ini
│ ├── group_vars/
│ │ ├── all.yml
│ │ ├── webservers.yml
│ │ └── dbservers.yml
│ └── host_vars/
│ └── prod-web-01.yml
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/
│ ├── site.yml
│ ├── deploy.yml
│ └── maintenance.yml
├── ansible.cfg
└── Makefile
Bu yapının güzelliği şu: inventories/staging ve inventories/production birbirinden tamamen bağımsız. Aynı role, aynı playbook, farklı değişkenlerle farklı ortamlarda çalışıyor.
Inventory Dosyalarını Yapılandırmak
Staging Inventory
# inventories/staging/hosts.ini
[webservers]
staging-web-01 ansible_host=10.0.1.10
staging-web-02 ansible_host=10.0.1.11
[dbservers]
staging-db-01 ansible_host=10.0.1.20
[loadbalancers]
staging-lb-01 ansible_host=10.0.1.5
[staging:children]
webservers
dbservers
loadbalancers
[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
env=staging
Production Inventory
# inventories/production/hosts.ini
[webservers]
prod-web-01 ansible_host=10.1.1.10
prod-web-02 ansible_host=10.1.1.11
prod-web-03 ansible_host=10.1.1.12
[dbservers]
prod-db-primary ansible_host=10.1.1.20
prod-db-replica ansible_host=10.1.1.21
[loadbalancers]
prod-lb-01 ansible_host=10.1.1.5
prod-lb-02 ansible_host=10.1.1.6
[production:children]
webservers
dbservers
loadbalancers
[all:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
env=production
Dikkat edin: env değişkeni her iki inventory’de de tanımlı ama farklı değerlere sahip. Bu değişkeni playbook ve role içinde koruyucu mantık kurmak için kullanacağız.
Group Vars ile Ortam Bazlı Konfigürasyon
Group vars, aynı rolün farklı ortamlarda farklı davranmasını sağlamanın en temiz yoludur.
Staging All Vars
# inventories/staging/group_vars/all.yml
# Ortam kimliği
environment_name: staging
environment_color: yellow # Bazı ekipler banner rengi kullanır
# Uygulama ayarları
app_version: latest
app_debug: true
app_log_level: debug
# Veritabanı bağlantısı (vault ile şifrelenmiş değerler ayrı dosyada)
db_host: staging-db-01
db_port: 5432
db_name: myapp_staging
db_pool_size: 5
# Web sunucusu
nginx_worker_processes: 2
nginx_worker_connections: 512
# Monitoring
enable_monitoring: false
datadog_enabled: false
# Backup
backup_enabled: false
backup_retention_days: 3
# SSL
ssl_enabled: false
domain_name: staging.myapp.example.com
Production All Vars
# inventories/production/group_vars/all.yml
# Ortam kimliği
environment_name: production
environment_color: red
# Uygulama ayarları
app_version: "{{ lookup('env', 'APP_VERSION') | default('1.0.0') }}"
app_debug: false
app_log_level: warning
# Veritabanı
db_host: prod-db-primary
db_port: 5432
db_name: myapp_production
db_pool_size: 20
# Web sunucusu
nginx_worker_processes: auto
nginx_worker_connections: 2048
# Monitoring
enable_monitoring: true
datadog_enabled: true
# Backup
backup_enabled: true
backup_retention_days: 30
# SSL
ssl_enabled: true
domain_name: myapp.example.com
Bu yapıda aynı deploy.yml playbook’u çalıştırdığınızda, staging’de debug açık, monitoring kapalı, küçük pool size’larla çalışırken; production’da tam tersi konfigürasyon devreye girer.
Ansible Vault ile Gizli Bilgileri Yönetmek
Şifreler, API anahtarları ve sertifikalar hiçbir zaman düz metin olarak repository’e girmemeli. Her ortam için ayrı vault dosyası kullanmak en doğru yaklaşım.
# Staging vault dosyası oluşturma
ansible-vault create inventories/staging/group_vars/vault.yml
# Production vault dosyası oluşturma
ansible-vault encrypt inventories/production/group_vars/vault.yml
Vault dosyalarının içeriği şöyle görünmeli (encrypt edilmeden önce):
# inventories/staging/group_vars/vault.yml (şifrelenmemiş hali)
vault_db_password: "staging-sifre-123"
vault_db_user: "myapp_staging"
vault_redis_password: "redis-staging-pass"
vault_app_secret_key: "staging-secret-key-buraya"
vault_datadog_api_key: "" # Staging'de kullanılmıyor
# inventories/production/group_vars/vault.yml (şifrelenmemiş hali)
vault_db_password: "{{ uretim_sifreniz }}"
vault_db_user: "myapp_prod"
vault_redis_password: "guclu-redis-sifresi"
vault_app_secret_key: "production-secret-key-buraya"
vault_datadog_api_key: "dd-api-anahtariniz"
Vault şifrelerini ortam değişkenlerinden okumak için:
# .vault_pass_staging dosyasına staging vault şifresi
# .vault_pass_production dosyasına production vault şifresi
# Her ikisi de .gitignore'a eklenmeli!
echo "staging-vault-password" > .vault_pass_staging
echo "production-vault-password" > .vault_pass_production
chmod 600 .vault_pass_staging .vault_pass_production
# ansible.cfg içinde
[defaults]
inventory = inventories/staging
vault_password_file = .vault_pass_staging
roles_path = roles
retry_files_enabled = False
stdout_callback = yaml
Production Koruyucu Mekanizmalar
En kritik nokta burasıdır. Production’ı yanlışlıkla değiştirmek için ekstra engeller koymalısınız.
Playbook Seviyesinde Koruma
# playbooks/deploy.yml
---
- name: Deployment öncesi kontrol
hosts: localhost
gather_facts: false
tasks:
- name: Ortam değişkenini kontrol et
fail:
msg: |
HATA: Production deployment için onay gerekli!
Lütfen DEPLOY_CONFIRM=yes ortam değişkenini set edin.
Örnek: DEPLOY_CONFIRM=yes ansible-playbook ...
when:
- hostvars[groups['all'][0]]['env'] == 'production'
- lookup('env', 'DEPLOY_CONFIRM') != 'yes'
- name: Production deployment uyarısı
pause:
prompt: |
*** PRODUCTION DEPLOYMENT ***
Ortam: {{ hostvars[groups['all'][0]]['env'] }}
Versiyon: {{ hostvars[groups['all'][0]]['app_version'] }}
Devam etmek için ENTER'a basın, iptal için Ctrl+C
when: hostvars[groups['all'][0]]['env'] == 'production'
- name: Uygulama deployment
hosts: webservers
become: true
roles:
- common
- webserver
- application
Role İçinde Ortam Kontrolü
# roles/database/tasks/main.yml
---
- name: Veritabanı rol kontrolü
block:
- name: Staging veritabanı işlemleri
include_tasks: staging_db.yml
when: env == 'staging'
- name: Production veritabanı işlemleri
include_tasks: production_db.yml
when: env == 'production'
- name: Production'da migration onayı
pause:
prompt: "Migration çalıştırılacak. Onaylıyor musunuz? (yes/no)"
register: migration_confirm
when: env == 'production'
- name: Migration onayı kontrol
fail:
msg: "Migration iptal edildi."
when:
- env == 'production'
- migration_confirm.user_input != 'yes'
Makefile ile Komut Standardizasyonu
Farklı geliştiricilerin farklı komutlar çalıştırması kaosa yol açar. Makefile, tüm ekibin aynı dili konuşmasını sağlar:
# Makefile
STAGING_INVENTORY = inventories/staging
PRODUCTION_INVENTORY = inventories/production
STAGING_VAULT = .vault_pass_staging
PRODUCTION_VAULT = .vault_pass_production
.PHONY: help staging-deploy production-deploy staging-check production-check
help:
@echo "Kullanılabilir komutlar:"
@echo " make staging-check - Staging dry-run"
@echo " make staging-deploy - Staging'e deploy et"
@echo " make production-check - Production dry-run"
@echo " make production-deploy - Production'a deploy et (onay gerekir)"
@echo " make staging-ping - Staging sunucularını kontrol et"
@echo " make production-ping - Production sunucularını kontrol et"
staging-ping:
ansible all -i $(STAGING_INVENTORY) --vault-password-file $(STAGING_VAULT) -m ping
production-ping:
ansible all -i $(PRODUCTION_INVENTORY) --vault-password-file $(PRODUCTION_VAULT) -m ping
staging-check:
ansible-playbook -i $(STAGING_INVENTORY)
--vault-password-file $(STAGING_VAULT)
--check --diff
playbooks/site.yml
staging-deploy:
ansible-playbook -i $(STAGING_INVENTORY)
--vault-password-file $(STAGING_VAULT)
playbooks/site.yml
production-check:
ansible-playbook -i $(PRODUCTION_INVENTORY)
--vault-password-file $(PRODUCTION_VAULT)
--check --diff
playbooks/site.yml
production-deploy:
@echo "Production deployment başlatılıyor..."
DEPLOY_CONFIRM=yes ansible-playbook -i $(PRODUCTION_INVENTORY)
--vault-password-file $(PRODUCTION_VAULT)
playbooks/site.yml
Artık ekipteki herkes make staging-deploy veya make production-deploy yazarak standart bir şekilde deployment yapabilir.
Gerçek Dünya Senaryosu: Nginx Konfigürasyonu
Aynı Nginx rolünün iki ortamda nasıl farklı davrandığını görelim:
# roles/webserver/templates/nginx.conf.j2
user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
multi_accept on;
}
http {
# Temel ayarlar
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
# Loglama - ortama göre değişir
{% if env == 'production' %}
access_log /var/log/nginx/access.log combined buffer=16k;
error_log /var/log/nginx/error.log warn;
{% else %}
access_log /var/log/nginx/access.log combined;
error_log /var/log/nginx/error.log debug;
{% endif %}
# SSL - sadece production'da zorunlu
{% if ssl_enabled %}
server {
listen 80;
server_name {{ domain_name }};
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ domain_name }};
ssl_certificate /etc/ssl/certs/{{ domain_name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ domain_name }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app_upstream;
}
}
{% else %}
server {
listen 80;
server_name {{ domain_name }};
# Staging banner
add_header X-Environment "STAGING" always;
location / {
proxy_pass http://app_upstream;
}
}
{% endif %}
}
Bu template, staging’de HTTP üzerinden çalışırken ve response header’a X-Environment: STAGING eklerken, production’da HTTPS’e yönlendirme yapıyor.
CI/CD Pipeline ile Entegrasyon
GitLab CI örneği üzerinden ortam ayrımının pipeline’a nasıl yansıdığını görelim:
# .gitlab-ci.yml
stages:
- test
- staging-deploy
- production-deploy
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
# Syntax kontrolü her branch'te
ansible-lint:
stage: test
image: cytopia/ansible:latest
script:
- ansible-lint playbooks/
- ansible-playbook --syntax-check -i inventories/staging playbooks/site.yml
# Staging: develop branch'e merge edilince otomatik
deploy-staging:
stage: staging-deploy
image: cytopia/ansible:latest
environment:
name: staging
url: https://staging.myapp.example.com
only:
- develop
before_script:
- echo "$STAGING_VAULT_PASS" > .vault_pass_staging
- chmod 600 .vault_pass_staging
script:
- ansible-playbook -i inventories/staging
--vault-password-file .vault_pass_staging
playbooks/deploy.yml
-e "app_version=$CI_COMMIT_SHA"
after_script:
- rm -f .vault_pass_staging
# Production: main branch'e merge edilince manuel onay ile
deploy-production:
stage: production-deploy
image: cytopia/ansible:latest
environment:
name: production
url: https://myapp.example.com
only:
- main
when: manual # Manuel tetikleme zorunlu
before_script:
- echo "$PRODUCTION_VAULT_PASS" > .vault_pass_production
- chmod 600 .vault_pass_production
script:
- DEPLOY_CONFIRM=yes ansible-playbook
-i inventories/production
--vault-password-file .vault_pass_production
playbooks/deploy.yml
-e "app_version=$CI_COMMIT_TAG"
after_script:
- rm -f .vault_pass_production
Bu pipeline’da staging otomatik deploy edilirken, production için GitLab UI’dan manuel onay gerekiyor. when: manual satırı, birinin kasıtlı olarak “Deploy” butonuna basmasını zorunlu kılıyor.
Ortam Doğrulama Playbook’u
Deployment sonrası her şeyin doğru çalıştığını kontrol eden bir validation playbook’u çok işe yarar:
# playbooks/validate.yml
---
- name: Ortam doğrulama
hosts: webservers
gather_facts: true
tasks:
- name: Nginx servis durumu
service_facts:
- name: Nginx çalışıyor mu?
assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
fail_msg: "HATA: Nginx çalışmıyor!"
success_msg: "OK: Nginx aktif"
- name: Uygulama portu açık mı?
wait_for:
port: "{{ app_port | default(8080) }}"
host: "{{ ansible_host }}"
timeout: 10
msg: "Uygulama portu kapalı!"
- name: HTTP health check
uri:
url: "http://{{ ansible_host }}/health"
status_code: 200
return_content: true
register: health_check
retries: 3
delay: 5
- name: Ortam bilgisi doğrulama
assert:
that:
- "'{{ environment_name }}' in health_check.content"
fail_msg: "Ortam bilgisi response'da bulunamadı!"
when: env == 'staging'
- name: Validation özeti
debug:
msg: |
Doğrulama tamamlandı
Ortam: {{ environment_name }}
Sunucu: {{ inventory_hostname }}
Durum: BAŞARILI
Sık Yapılan Hatalar
Ortam yönetimi konusunda sahada sıkça karşılaştığım hataları paylaşayım:
- Tek inventory dosyasında
[staging]ve[production]grupları kullanmak: Vault şifrelerini ayırmak imkansız hale geliyor, değişken çakışmaları kaçınılmaz oluyor.
ansible.cfgiçinde sabit inventory yolu tanımlamak: Birisi varsayılan komutu çalıştırdığında hangi ortamın etkileneceği belirsiz kalıyor.
- Vault dosyalarını tek bir merkezi yerde tutmak: Her ortamın kendi vault’u olmalı. Staging ekibinin production vault’una erişimi olmamalı.
--checkmodunu production öncesinde atlamamak: Dry-run, production’da sürprizleri önlemenin en basit yolu. Makefile’dakiproduction-checkhedefini bir alışkanlık haline getirin.
envdeğişkenini role’lerde kontrol etmemek: Kritik işlemler için (veri silme, migration) her zaman ortam kontrolü ekleyin.
Sonuç
Ansible ile çoklu ortam yönetimi aslında birkaç temel prensibe dayanıyor: ayrı inventory dizinleri, ortam bazlı group_vars, vault ile gizli bilgi yönetimi ve koruyucu mekanizmalar. Bu yapıyı bir kez kurduğunuzda, ekibinizdeki her kişi güvenle staging’de deneme yapabilir, production’a ise ancak kasıtlı olarak geçilebilir.
Makefile yaklaşımı küçük bir detay gibi görünse de ekip standardizasyonu için kritik. Yeni bir ekip üyesi geldiğinde make help çalıştırıp ne yapacağını anlayabilmeli.
CI/CD pipeline entegrasyonuyla bu yapı tamamlanıyor: develop’a merge edilince staging’e otomatik git, main’e merge edilince production için birisinin kasıtlı onay vermesini zorunlu kıl. Bu basit fark, gece yarısı “staging’de test ettim production’da da çalışır” diye yapılan hatalı deployment’ların önüne geçer.
Son olarak, validation playbook’unu deployment sürecinize mutlaka ekleyin. Deploy ettikten sonra otomatik kontrol çalışırsa, sorunları kullanıcılar bulmadan siz bulursunuz. Bu fark, iyi bir sysadmin’i mükemmel bir sysadmin’den ayıran şeylerden biridir.