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:
yesyapı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.