Ansible ile Dosya ve Şablon Dağıtımı

Bir sistem yöneticisinin en çok zaman harcadığı işlerden biri, onlarca veya yüzlerce sunucuya aynı konfigürasyon dosyalarını dağıtmaktır. “Bu sunucuya nginx.conf’u kopyaladım mı?”, “Hangi sürücüde hangi sysctl ayarı vardı?” gibi sorular günlük rutinin parçası haline gelir. Ansible’ın copy ve template modülleri bu kabustan kurtulmak için tasarlanmıştır. Bu yazıda, gerçek dünya senaryolarıyla birlikte dosya ve şablon dağıtımını enine boyuna ele alacağız.

Temel Kavramlar: copy vs template

Ansible’da dosya dağıtımı için iki ana yöntem var. Bunları doğru anlamak, hangi durumda ne kullanacağını bilmek demek.

copy modülü: Kaynak dosyayı olduğu gibi hedefe kopyalar. İçerikte hiçbir değişiklik yapılmaz. Sabit bir sertifika dosyası, binary bir script ya da her sunucuda aynı olmasını istediğin bir konfigürasyon için idealdir.

template modülü: Jinja2 şablon motoru kullanır. Dosya içinde {{ değişken }} gibi ifadeler bulunabilir ve Ansible bunları çalışma zamanında gerçek değerlerle doldurur. Her sunucu için özelleştirilmiş konfigürasyon dosyaları üretmek istiyorsan bu modül tam sana göre.

Şablon dosyaları geleneksel olarak .j2 uzantısıyla kaydedilir, ancak bu zorunlu değil. Ansible community’de bu konvansiyona uymak, dosyaların şablon olduğunu hemen anlamayı sağlar.

copy Modülü ile Çalışmak

En basit kullanım senaryosundan başlayalım. Bir SSH daemon konfigürasyonunu tüm sunuculara dağıtmak istiyoruz:

- name: SSH konfigürasyonunu dağıt
  copy:
    src: files/sshd_config
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    backup: yes
  notify: restart sshd

Buradaki parametrelere bakalım:

  • src: Ansible control node üzerindeki kaynak dosyanın yolu. files/ dizini rol veya playbook dizinine göreli olarak aranır
  • dest: Hedef sistemdeki tam dosya yolu
  • owner: Dosyanın sahibi olacak kullanıcı
  • group: Dosyanın grup sahibi
  • mode: Dosya izinleri. Başındaki sıfır oktal formatı belirtir
  • backup: yes yapılırsa, dosya zaten varsa önce yedeklenir
  • notify: Handler tetikleme, konfigürasyon değişince servisi yeniden başlatır

Eğer içeriği doğrudan playbook içinde yazmak istiyorsan content parametresini kullanabilirsin:

- name: Motd dosyasını oluştur
  copy:
    content: |
      ############################################
      # Bu sunucu merkezi yönetim altındadır     #
      # Yetkisiz erişim yasaktır                 #
      ############################################
    dest: /etc/motd
    owner: root
    group: root
    mode: '0644'

Bu yöntem özellikle kısa ve sabit içerikler için kullanışlıdır. Dosyayı ayrıca oluşturup yönetmek zorunda kalmıyorsun.

Dizin Kopyalama

copy modülü sadece tek dosya değil, dizin de kopyalayabilir:

- name: Web uygulama statik dosyalarını kopyala
  copy:
    src: files/webapp/static/
    dest: /var/www/html/static/
    owner: www-data
    group: www-data
    mode: preserve
    directory_mode: '0755'

Dikkat etmeni gereken önemli bir nokta var: src sonundaki / karakteri davranışı değiştirir. Eğer files/webapp/static/ yazarsan dizinin içindekiler kopyalanır. files/webapp/static yazarsan static dizininin kendisi hedef konuma kopyalanır. Bu klasik bir rsync davranışı ve başlangıçta kafa karıştırabilir.

template Modülü ile Dinamik Konfigürasyon

Gerçek gücün ortaya çıktığı yer burası. Diyelim ki 50 farklı web sunucusuna nginx konfigürasyonu dağıtıyorsun, ama her birinin farklı server_name, farklı worker_processes ve farklı upstream tanımları var. Template olmadan ya 50 ayrı dosya tutarsın ya da her seferinde elle düzenleme yaparsın.

Önce şablon dosyamızı oluşturalım (templates/nginx.conf.j2):

user {{ nginx_user | default('www-data') }};
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log {{ nginx_log_level | default('warn') }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;

    {% if nginx_enable_gzip | default(true) %}
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    {% endif %}

    server {
        listen {{ nginx_port | default(80) }};
        server_name {{ nginx_server_name }};
        root {{ nginx_doc_root | default('/var/www/html') }};

        {% for location in nginx_locations | default([]) %}
        location {{ location.path }} {
            {% if location.proxy_pass is defined %}
            proxy_pass {{ location.proxy_pass }};
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            {% else %}
            try_files $uri $uri/ =404;
            {% endif %}
        }
        {% endfor %}
    }
}

Şimdi bu şablonu kullanan playbook:

- name: Nginx konfigürasyonunu dağıt
  template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    validate: /usr/sbin/nginx -t -c %s
  notify: reload nginx

Burada validate parametresi altın değerinde. Dosyayı hedef konuma yazmadan önce belirtilen komutla doğrulama yapar. %s geçici dosyanın yoluna yerleştirilir. Eğer nginx konfigürasyon sözdizimi hatalıysa Ansible değişikliği uygulamaz ve hata verir. Production ortamında bu parametreyi kullanmak neredeyse zorunludur.

Değişkenleri Organize Etmek

Şablon değişkenlerini group_vars ve host_vars üzerinden yönetmek en iyi pratiktir:

# group_vars/webservers.yml
nginx_user: www-data
nginx_worker_connections: 2048
nginx_enable_gzip: true
nginx_log_level: warn

# host_vars/web01.example.com.yml
nginx_server_name: app1.example.com
nginx_port: 443
nginx_doc_root: /var/www/app1
nginx_locations:
  - path: /api
    proxy_pass: http://backend01:8080
  - path: /static

Bu yapıyla web01 sunucusuna özel konfigürasyon otomatik olarak uygulanır. Grup genelinde geçerli varsayılanlar var, sunucu özelinde gerekli olan değerler host_vars’ta tanımlanıyor.

Jinja2 Filtreleri ile Güçlü Şablonlar

Jinja2’nin filtre sistemi şablonları çok daha güçlü kılar. Sysadmin olarak en çok kullanacağın filtrelere bakalım:

# templates/app.conf.j2

# Değişken yoksa varsayılan değer kullan
database_host={{ db_host | default('localhost') }}

# Büyük/küçük harf dönüşümü
environment={{ app_env | upper }}

# Liste elemanlarını birleştir
allowed_ips={{ trusted_ips | join(', ') }}

# Şifre hash'leme (shadow password formatı)
# NOT: Gerçek şifreler vault ile şifrelenmelidir
admin_hash={{ admin_password | password_hash('sha512') }}

# Boşluk silme ve string işlemleri
log_prefix={{ app_name | trim | lower | replace(' ', '_') }}

# Koşullu ifade
debug_mode={{ 'on' if app_env == 'development' else 'off' }}

# Sayısal işlemler
max_connections={{ (ansible_memtotal_mb / 4) | int }}

Özellikle ansible_memtotal_mb gibi fact değişkenleri kullanmak çok değerlidir. Sunucunun toplam RAM’ine göre otomatik konfigürasyon hesaplaması yapmak seni manuel hesaplama zahmetinden kurtarır.

Gerçek Dünya Senaryosu: Çok Katmanlı Uygulama Konfigürasyonu

Üretim ortamında bir Java uygulamasının konfigürasyonunu yönettiğimizi düşünelim. Uygulama farklı ortamlarda (dev, staging, prod) çalışıyor ve her ortamın farklı veritabanı bağlantıları, farklı log seviyeleri var.

# templates/application.properties.j2
# Bu dosya Ansible tarafından yönetilmektedir
# Manuel değişiklik yapmayın - {{ ansible_date_time.iso8601 }}

# Uygulama Ayarları
spring.application.name={{ app_name }}
server.port={{ app_port | default(8080) }}
spring.profiles.active={{ spring_profile }}

# Veritabanı Bağlantısı
spring.datasource.url=jdbc:postgresql://{{ db_host }}:{{ db_port | default(5432) }}/{{ db_name }}
spring.datasource.username={{ db_user }}
spring.datasource.password={{ db_password }}
spring.datasource.hikari.maximum-pool-size={{ db_pool_size | default(10) }}

# Cache Ayarları
{% if redis_enabled | default(false) %}
spring.cache.type=redis
spring.redis.host={{ redis_host }}
spring.redis.port={{ redis_port | default(6379) }}
{% else %}
spring.cache.type=simple
{% endif %}

# Logging
logging.level.root={{ log_level | default('INFO') }}
logging.level.com.example={{ app_log_level | default(log_level) | default('INFO') }}
logging.file.name=/var/log/{{ app_name }}/application.log

# Actuator
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details={{ 'always' if app_env == 'production' else 'when_authorized' }}

Bu şablonu kullanan role yapısı:

# roles/java-app/tasks/main.yml
- name: Uygulama konfigürasyon dizinini oluştur
  file:
    path: /etc/{{ app_name }}
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: '0750'

- name: Application properties şablonunu dağıt
  template:
    src: application.properties.j2
    dest: /etc/{{ app_name }}/application.properties
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: '0640'
  notify: restart application

- name: Systemd servis dosyasını dağıt
  template:
    src: app.service.j2
    dest: /etc/systemd/system/{{ app_name }}.service
    owner: root
    group: root
    mode: '0644'
  notify:
    - reload systemd
    - restart application

Hassas Dosyaları Ansible Vault ile Korumak

Konfigürasyon dosyalarında şifre ve API anahtarı gibi hassas bilgiler kaçınılmazdır. Bunları şifreli tutmak için Ansible Vault kullanmalısın.

# Vault şifreli değişken dosyası oluştur
ansible-vault create group_vars/production/vault.yml

# İçeriği şöyle görünür (düz metin, vault şifreler)
# vault_db_password: SuperSecret123!
# vault_api_key: sk-prod-abc123xyz

# Ana değişken dosyasında vault değişkenini referans al
# group_vars/production/vars.yml
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

Şablonda bu değişkenleri normal gibi kullanırsın:

# templates/database.conf.j2
[database]
host={{ db_host }}
port={{ db_port | default(5432) }}
name={{ db_name }}
user={{ db_user }}
password={{ db_password }}

Playbook’u çalıştırırken vault şifresini sağlaman gerekir:

ansible-playbook site.yml --ask-vault-pass
# veya dosyadan oku
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Dosya Değişikliklerini İzlemek: diff ve check Modları

Production’a bir şeyler göndermeden önce ne değişecek bunu görmek isteyebilirsin. Ansible’ın --check ve --diff modları tam bu iş için:

# Sadece ne değişeceğini göster, gerçekten değiştirme
ansible-playbook -i inventory/production site.yml --check --diff

# Sadece belirli bir tag için
ansible-playbook -i inventory/production site.yml 
  --check --diff 
  --tags nginx-config

--diff çıktısı sana unified diff formatında tam olarak neyin değişeceğini gösterir. Bir konfigürasyon dosyasında tek bir satır değişmişse bile bunu açıkça görürsün. Bu özellikle bir şablon değişkeni güncellendiğinde ne etki yaratacağını anlamak için paha biçilmezdir.

lineinfile ve blockinfile ile Cerrahi Müdahale

Bazen bir dosyanın tamamını değiştirmek yerine sadece belirli satırları veya blokları düzenlemek istersin. lineinfile modülü tam bu iş için:

- name: Kernel parametre ayarla
  lineinfile:
    path: /etc/sysctl.conf
    regexp: '^net.ipv4.ip_forward'
    line: 'net.ipv4.ip_forward = 1'
    state: present
    backup: yes

- name: Eski NTP sunucusunu kaldır
  lineinfile:
    path: /etc/chrony.conf
    regexp: '^server old-ntp.example.com'
    state: absent

Birden fazla satır eklemek için blockinfile daha uygun:

- name: SSH hardening ayarlarını ekle
  blockinfile:
    path: /etc/ssh/sshd_config
    marker: "# {mark} ANSIBLE MANAGED BLOCK - SSH Hardening"
    block: |
      Protocol 2
      PermitRootLogin no
      PasswordAuthentication no
      PubkeyAuthentication yes
      AllowUsers {{ ssh_allowed_users | join(' ') }}
      MaxAuthTries 3
      ClientAliveInterval 300
      ClientAliveCountMax 2
    backup: yes
  notify: restart sshd

marker parametresi çok önemli. Ansible bu marker’ı kullanarak bloğun başını ve sonunu işaretler. Böylece playbook tekrar çalıştığında bloğu günceller veya state: absent ile tamamen kaldırabilir. Marker olmadan her çalıştırmada aynı blok tekrar tekrar eklenir.

assemble Modülü ile Parça Parça Konfigürasyon

Büyük ve modüler konfigürasyonlar için assemble modülü var. Birden fazla parça dosyayı tek bir hedefe birleştirmek için kullanılır. Örneğin her rol kendi SSH ayar parçasını koyar ve hepsi tek bir dosyada birleşir:

# tasks/main.yml
- name: SSH config parçalarını kopyala
  copy:
    src: "{{ item }}"
    dest: "/etc/ssh/sshd_config.d/{{ item | basename }}"
    mode: '0600'
  with_fileglob:
    - files/sshd_config.d/*.conf

- name: Parça dosyaları birleştir
  assemble:
    src: /etc/ssh/sshd_config.d/
    dest: /etc/ssh/sshd_config
    delimiter: "# --- Sonraki Bölüm ---n"
    mode: '0600'
    validate: /usr/sbin/sshd -t -f %s
  notify: restart sshd

Idempotency ve En İyi Pratikler

Ansible’da en önemli prensip idempotency’dir. Yani aynı playbook birden fazla kez çalıştırılabilir ve her seferinde aynı sonucu verir. Dosya dağıtımında bunu bozmamak için dikkat etmen gereken noktalar:

Checksum kontrolü: copy ve template modülleri otomatik olarak dosya içeriğini karşılaştırır. Dosya zaten doğru içerikteyse hiçbir şey yazmaz, changed göstermez.

Sahiplik ve izinler: Her zaman owner, group ve mode tanımla. Tanımlamasan bile mevcut dosyanın izinleri değişmez ama yeni dosya oluşturulurken beklenmedik izinler olabilir.

Yedekleme: Kritik konfigürasyon dosyaları için backup: yes kullan. Ansible timestamp ekleyerek yedek oluşturur. Bu özellikle başlangıç aşamasında hayat kurtarır.

Handler kullanımı: Konfigürasyon değiştiğinde servisi yeniden başlatmak için mutlaka handler kullan, doğrudan service modülünü task içinde çağırma. Handler’lar yalnızca bir değişiklik olduğunda ve play sonunda bir kez çalışır.

# handlers/main.yml
- name: restart nginx
  service:
    name: nginx
    state: restarted

- name: reload nginx
  service:
    name: nginx
    state: reloaded

- name: reload systemd
  systemd:
    daemon_reload: yes

Template başlıkları: Her şablon dosyasının başına bir yorum eklemek iyi alışkanlıktır:

# templates/nginx.conf.j2
# BU DOSYA ANSIBLE TARAFINDAN YÖNETİLMEKTEDİR
# Kaynak: {{ role_path }}/templates/nginx.conf.j2
# Son güncelleme: {{ ansible_date_time.iso8601 }}
# Manuel değişiklikleriniz bir sonraki Ansible çalışmasında silinecektir

Bu yorum, sunucuya bağlanan başka bir yöneticinin dosyayı elle değiştirmeye çalışmasını önler.

Sonuç

Ansible ile dosya ve şablon dağıtımı, altyapı yönetiminin temel taşlarından biri. copy modülüyle sabit dosyaları, template modülüyle dinamik konfigürasyonları yönetmek; lineinfile ve blockinfile ile cerrahi müdahaleler yapmak seni hem zamandan hem de operasyonel hatalardan kurtarır.

Önemli olan şu: Bu araçları kullanmaya başlamak için mükemmel bir yapıya sahip olmak gerekmez. Küçük başla, önce en çok acı çektiğin konfigürasyon dosyasını Ansible’a al. Zamanla şablon yazmak, değişkenleri doğru organize etmek sezgisel hale gelir. Validate parametresini ihmal etme, vault kullanmayı alışkanlık haline getir ve her zaman --check --diff ile production’a göndermeden önce ne yapacağına bak. Konfigürasyon yönetimi kültürünü yerleştirdikten sonra “Ben bu sunucuya ne kopyalamıştım?” sorusu artık hayatından çıkar.

Yorum yapın