Ç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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir