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_port yerine kendi role’ünüzde myapp_nginx_port gibi
  • Production’a dokunmadan önce mutlaka --check ile dry-run yapın
  • Playbook’larınızı idempotent yazın, ikinci çalıştırmada “changed” sayısı sıfır olmalı
  • ansible-lint kullanarak playbook’larınızı kod kalitesi açısından kontrol edin
  • Hassas çıktıları loglara yazmamak için no_log: true kullanı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.

Bir yanıt yazın

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