Jinja2 Şablonları ile Dinamik Yapılandırma Dosyaları

Bir Ansible playbook yazarken aynı yapılandırma dosyasını onlarca farklı sunucu için elle düzenlemek zorunda kaldıysanız, ne kadar sıkıcı ve hata prone bir iş olduğunu biliyorsunuzdur. İşte tam bu noktada Jinja2 şablonları hayat kurtarıcı oluyor. Ansible’ın içine gömülü bu şablon motoru sayesinde, tek bir .j2 dosyasıyla yüzlerce farklı sunucuya özgü yapılandırma üretebiliyorsunuz. Bu yazıda Jinja2’yi gerçek dünya senaryolarıyla, sık yapılan hatalarla ve pratik ipuçlarıyla ele alacağız.

Jinja2 Nedir ve Ansible ile İlişkisi Nedir?

Jinja2, Python ekosisteminde doğmuş bir şablon motorudur. Flask gibi web frameworkleri tarafından HTML üretmek için kullanılır, ama Ansible onu tamamen farklı bir amaçla benimsedi: sistem yapılandırma dosyaları üretmek için.

Ansible, bir şablonu işlerken şu adımları takip eder: önce ilgili host’a ait değişkenleri toplar (inventory değişkenleri, group_vars, host_vars, register çıktıları), ardından bu değişkenleri .j2 uzantılı şablon dosyanıza enjekte ederek hedef sunucuya nihai dosyayı yazar. Bu süreçte siz ne raw copy modülüyle uğraşırsınız ne de lineinfile ile satır satır değişiklik yaparsınız.

Şablon sözdiziminde üç temel yapı var:

  • {{ degisken }} : Değişken değerini yazdırır
  • {% if/for %} : Kontrol yapıları (if, for, block vb.)
  • {# yorum #} : Şablon yorumları, çıktıya yansımaz

İlk Gerçek Dünya Senaryosu: Nginx Sanal Host Yapılandırması

Diyelim ki elinizde 20 farklı web uygulaması var ve her biri için Nginx sanal host dosyası oluşturmanız gerekiyor. Domain adları, port numaraları, upstream sunucular hepsi farklı. El ile yapmak yerine bir şablon yazalım.

Önce group_vars/webservers.yml dosyamıza değişkenleri tanımlayalım:

# group_vars/webservers.yml
nginx_worker_processes: 4
nginx_keepalive_timeout: 65
nginx_gzip: true

vhosts:
  - name: api.sirketim.com
    port: 80
    upstream_port: 8080
    ssl_enabled: false
    proxy_pass: http://127.0.0.1:8080
  - name: panel.sirketim.com
    port: 443
    upstream_port: 8443
    ssl_enabled: true
    ssl_cert: /etc/ssl/panel.sirketim.com.crt
    ssl_key: /etc/ssl/panel.sirketim.com.key
    proxy_pass: http://127.0.0.1:8443

Ardından templates/nginx_vhost.conf.j2 dosyamızı yazalım:

# templates/nginx_vhost.conf.j2
# Bu dosya Ansible tarafından otomatik üretilmistir.
# Manuel degisiklik YAPMAYIN. Degisiklikler bir sonraki çalıştırmada ezilecektir.
# Yonetici: {{ ansible_user }} | Tarih: {{ ansible_date_time.date }}

{% for vhost in vhosts %}
server {
    listen {{ vhost.port }}{% if vhost.ssl_enabled %} ssl{% endif %};
    server_name {{ vhost.name }};

{% if vhost.ssl_enabled %}
    ssl_certificate     {{ vhost.ssl_cert }};
    ssl_certificate_key {{ vhost.ssl_key }};
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
{% endif %}

    access_log /var/log/nginx/{{ vhost.name }}_access.log;
    error_log  /var/log/nginx/{{ vhost.name }}_error.log;

    location / {
        proxy_pass         {{ vhost.proxy_pass }};
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

{% endfor %}

Playbook’ta bu şablonu kullanmak için:

# playbooks/nginx_config.yml
---
- name: Nginx yapilandirmasini uygula
  hosts: webservers
  become: true
  tasks:
    - name: Nginx vhost sablonunu yukle
      ansible.builtin.template:
        src: templates/nginx_vhost.conf.j2
        dest: /etc/nginx/conf.d/vhosts.conf
        owner: root
        group: root
        mode: '0644'
        backup: yes
      notify: nginx reload

    - name: Nginx syntax kontrolu yap
      ansible.builtin.command: nginx -t
      changed_when: false

  handlers:
    - name: nginx reload
      ansible.builtin.service:
        name: nginx
        state: reloaded

backup: yes parametresine dikkat edin. Şablon her değiştirildiğinde önceki sürümü .bak uzantısıyla saklar. Bir hata yaptığınızda geri dönüş yapabilmek için bu çok değerli.

Filtreler: Jinja2’nin Gizli Gücü

Jinja2 filtreleri, değişkenler üzerinde dönüşüm işlemleri yapmanızı sağlar. | karakteriyle kullanılır ve Ansible bunlara bir sürü kendi özel filtresini de eklemiştir.

Temel Filtreler

# templates/app_config.j2

# String filtreleri
app_name: {{ uygulama_adi | upper }}
log_prefix: {{ uygulama_adi | lower | replace(' ', '_') }}

# Default deger - degisken tanimli degilse fallback kullan
timeout: {{ connection_timeout | default(30) }}
max_conn: {{ max_connections | default(100) }}

# Sayi formatlama
max_memory_mb: {{ total_memory_mb | int }}
disk_threshold: {{ disk_usage_percent | float | round(2) }}

# Liste filtreleri
allowed_hosts: {{ web_servers | join(', ') }}
first_master: {{ master_nodes | first }}
node_count: {{ all_nodes | length }}

# Kosullu default - None veya bos string icin
db_port: {{ database_port | default(5432, true) }}

Önemli Ansible Özel Filtreleri

# templates/security_config.j2

# Sifre hashleme (htpasswd icin)
# admin_password degiskenini SHA512 ile hashle
hashed_pw: {{ admin_password | password_hash('sha512') }}

# IP adresi dogrulama ve manipulasyon
# ansible_default_ipv4.address: "192.168.1.100"
network_cidr: {{ ansible_default_ipv4.address | ipaddr('network/prefix') }}
is_private: {{ ansible_default_ipv4.address | ipaddr('private') }}

# JSON/YAML donusumu
config_json: {{ app_config | to_json }}
config_yaml: {{ app_config | to_nice_yaml(indent=2) }}

# Tarih filtreleri
generated_at: {{ '%Y-%m-%d %H:%M:%S' | strftime }}
cert_expiry: {{ (ansible_date_time.epoch | int + 86400 * 365) | strftime('%Y-%m-%d') }}

İkinci Senaryo: MySQL / MariaDB Yapılandırması

Farklı rollerdeki veritabanı sunucularınız var: biri master, diğerleri slave. Her birinin my.cnf dosyası farklı olmalı. İşte burada Jinja2’nin koşullu mantığı işe yarıyor.

# templates/my.cnf.j2
[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = {{ mysql_port | default(3306) }}
datadir         = {{ mysql_datadir | default('/var/lib/mysql') }}

# Bellek ayarlari - toplam RAM'in yüzdesine gore hesapla
innodb_buffer_pool_size     = {{ (ansible_memtotal_mb * 0.7) | int }}M
innodb_log_file_size        = {{ (ansible_memtotal_mb * 0.1) | int }}M
key_buffer_size             = {{ (ansible_memtotal_mb * 0.05) | int }}M
max_connections             = {{ mysql_max_connections | default(150) }}

# Replikasyon ayarlari
{% if mysql_role == 'master' %}
server-id               = {{ mysql_server_id }}
log_bin                 = /var/log/mysql/mysql-bin.log
binlog_format           = ROW
binlog_expire_logs_secs = {{ mysql_binlog_expire | default(604800) }}
sync_binlog             = 1

# Sadece su veritabanlarini replike et
{% if mysql_replicate_dbs is defined %}
{% for db in mysql_replicate_dbs %}
binlog_do_db = {{ db }}
{% endfor %}
{% endif %}

{% elif mysql_role == 'slave' %}
server-id               = {{ mysql_server_id }}
relay-log               = /var/log/mysql/mysql-relay.log
read_only               = ON
slave_parallel_workers  = {{ mysql_slave_workers | default(4) }}
slave_parallel_type     = LOGICAL_CLOCK

{% else %}
# Standalone mod - replikasyon yok
server-id               = 1
{% endif %}

# Karakter seti
character-set-server    = utf8mb4
collation-server        = utf8mb4_unicode_ci

# Slow query log
slow_query_log          = {{ 'ON' if mysql_slow_log | default(false) else 'OFF' }}
{% if mysql_slow_log | default(false) %}
slow_query_log_file     = /var/log/mysql/slow.log
long_query_time         = {{ mysql_slow_query_time | default(2) }}
{% endif %}

[client]
port    = {{ mysql_port | default(3306) }}
socket  = /var/run/mysqld/mysqld.sock

Bu şablonu kullanan inventory yapısı şöyle görünür:

# inventory/production/host_vars/db-master-01.yml
mysql_role: master
mysql_server_id: 1
mysql_max_connections: 300
mysql_slow_log: true
mysql_slow_query_time: 1
mysql_replicate_dbs:
  - uygulama_db
  - kullanici_db
  - analitik_db

# inventory/production/host_vars/db-slave-01.yml
mysql_role: slave
mysql_server_id: 2
mysql_max_connections: 200
mysql_slave_workers: 8

Döngüler ve Karmaşık Veri Yapıları

Jinja2 döngüleri sadece basit listelerle değil, iç içe sözlüklerle de çalışır. Örneğin bir firewall kural şablonu düşünelim:

# templates/iptables_rules.j2
#!/bin/bash
# Ansible tarafından üretildi - {{ ansible_date_time.iso8601 }}
# Host: {{ inventory_hostname }}

# Mevcut kurallari temizle
iptables -F
iptables -X
iptables -t nat -F

# Varsayilan politikalar
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Loopback izin ver
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Established baglantilara izin ver
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

{% for rule in firewall_rules %}
# Kural: {{ rule.description | default('Aciklama yok') }}
{% if rule.source is defined %}
iptables -A INPUT -s {{ rule.source }} -p {{ rule.protocol | default('tcp') }} 
    --dport {{ rule.port }} -j {{ rule.action | default('ACCEPT') }}
{% else %}
iptables -A INPUT -p {{ rule.protocol | default('tcp') }} 
    --dport {{ rule.port }} -j {{ rule.action | default('ACCEPT') }}
{% endif %}

{% endfor %}

{% if ansible_hostname.startswith('web') %}
# Web sunucularina ozel kurallar
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
{% endif %}

# Kurallari kaydet
iptables-save > /etc/iptables/rules.v4

Şablon Testleri: is Operatörü

Jinja2’de is operatörüyle değişken tipini ve durumunu test edebilirsiniz. Bu özellikle isteğe bağlı değişkenlerle çalışırken çok işe yarar:

# templates/app_env.j2

# Ortam degiskeni dosyasi - {{ app_name }}

APP_ENV={{ app_environment | default('production') }}
APP_PORT={{ app_port }}

{% if database_url is defined and database_url is string %}
DATABASE_URL={{ database_url }}
{% elif db_host is defined %}
DATABASE_URL=postgresql://{{ db_user }}:{{ db_password }}@{{ db_host }}:{{ db_port | default(5432) }}/{{ db_name }}
{% else %}
# UYARI: Veritabani baglantisi tanimlanmamis!
DATABASE_URL=
{% endif %}

{% if redis_host is defined %}
REDIS_URL=redis://{% if redis_password is defined %}:{{ redis_password }}@{% endif %}{{ redis_host }}:{{ redis_port | default(6379) }}/{{ redis_db | default(0) }}
{% endif %}

{% if app_features is defined and app_features is iterable %}
# Ozellik bayraklari
{% for feature, enabled in app_features.items() %}
FEATURE_{{ feature | upper }}={{ enabled | string | upper }}
{% endfor %}
{% endif %}

LOG_LEVEL={{ log_level | default('INFO') | upper }}
SECRET_KEY={{ app_secret_key }}

Block ve Include ile Modüler Şablonlar

Büyük şablonları küçük parçalara bölmek, bakımı kolaylaştırır. Ansible’da include Jinja2 direktifiyle şablon parçaları çağırabilirsiniz:

# templates/haproxy.cfg.j2
# HAProxy Ana Yapılandırması
# Uretim tarihi: {{ ansible_date_time.date }}

global
    log         /dev/log local0
    log         /dev/log local1 notice
    chroot      /var/lib/haproxy
    user        haproxy
    group       haproxy
    daemon
    maxconn     {{ haproxy_maxconn | default(50000) }}
    nbthread    {{ ansible_processor_vcpus }}

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect {{ haproxy_timeout_connect | default('5s') }}
    timeout client  {{ haproxy_timeout_client | default('30s') }}
    timeout server  {{ haproxy_timeout_server | default('30s') }}

{% for backend_group in haproxy_backends %}
#-------------------------------
# Frontend: {{ backend_group.name }}
#-------------------------------
frontend {{ backend_group.name }}_front
    bind *:{{ backend_group.frontend_port }}
{% if backend_group.ssl | default(false) %}
    bind *:443 ssl crt {{ backend_group.ssl_cert }}
    redirect scheme https if !{ ssl_fc }
{% endif %}
    default_backend {{ backend_group.name }}_back

backend {{ backend_group.name }}_back
    balance {{ backend_group.balance | default('roundrobin') }}
    option  httpchk GET {{ backend_group.health_check | default('/health') }}
{% for server in backend_group.servers %}
    server {{ server.name }} {{ server.ip }}:{{ server.port }} check weight {{ server.weight | default(1) }}{% if server.backup | default(false) %} backup{% endif %}

{% endfor %}

{% endfor %}

Şablon Doğrulama ve Hata Ayıklama

En sık yapılan hata, şablonu doğrudan production’a uygulamak ve sonra hata almak. Bunun yerine şu yöntemi kullanın:

# Sadece sablonu test et, degisiklik yapma
ansible-playbook playbooks/nginx_config.yml 
  --check 
  --diff 
  -l web-server-01

# Tek bir sunucu icin sablon ciktisini goster
ansible web-server-01 -m template 
  -a "src=templates/nginx_vhost.conf.j2 dest=/tmp/test_nginx.conf" 
  --check --diff

# Debug modulu ile sablon degiskenini ekrana bas
ansible web-server-01 -m debug 
  -a "msg={{ lookup('template', 'templates/nginx_vhost.conf.j2') }}"

Playbook içinde şablon çıktısını debug etmek için:

# playbooks/debug_template.yml
---
- name: Sablon ciktisini test et
  hosts: web-server-01
  tasks:
    - name: Sablon icerigini degiskene al
      ansible.builtin.set_fact:
        rendered_config: "{{ lookup('template', 'templates/nginx_vhost.conf.j2') }}"

    - name: Uretilen konfigurasyonu goster
      ansible.builtin.debug:
        var: rendered_config

    - name: Uretilen konfigurasyon satirlarini say
      ansible.builtin.debug:
        msg: "Toplam satir: {{ rendered_config.split('n') | length }}"

Sık Yapılan Hatalar ve Çözümleri

Jinja2 ile çalışırken birkaç yaygın sorunla karşılaşabilirsiniz:

Tanımsız değişken hatası: {{ degisken }} kullandığınızda değişken tanımlı değilse Ansible hata verir. Her zaman | default() filtresini kullanın ya da vars bölümünde varsayılan değer tanımlayın.

Whitespace sorunları: Jinja2 blokları bazen fazladan boş satır bırakır. Bunu önlemek için blok açılış ve kapanışlarında - kullanın:

# templates/whitespace_ornek.j2
# Fazla bosluk olusturur:
{% for item in liste %}
{{ item }}
{% endfor %}

# Boslugu temizler:
{%- for item in liste %}
{{ item }}
{%- endfor %}

Özel karakterler ve escaping: Bir değişkende {% gibi Jinja2 sözdizimi geçmesi gerekiyorsa {% raw %} bloğu kullanın:

# templates/prometheus_alert.j2
# Prometheus kural dosyasi
groups:
  - name: {{ alert_group_name }}
    rules:
{% for alert in alerts %}
      - alert: {{ alert.name }}
        expr: {% raw %}{{ alert.expression }}{% endraw %}
        for: {{ alert.duration }}
        labels:
          severity: {{ alert.severity }}
{% endfor %}

İleri Seviye: Şablondan Şablon Üretmek

Bazen şablonun kendisinin dinamik olması gerekir. Örneğin, ortama göre farklı şablon dosyaları seçmek:

# playbooks/dynamic_template.yml
---
- name: Ortama gore sablon sec
  hosts: all
  vars:
    template_file: "{{ 'templates/app_' + app_environment + '.conf.j2' }}"
  tasks:
    - name: Ortam bilgisini goster
      ansible.builtin.debug:
        msg: "Kullanilan sablon: {{ template_file }}"

    - name: Ortama ozgu yapılandırmayı uygula
      ansible.builtin.template:
        src: "{{ template_file }}"
        dest: /etc/myapp/config.conf
        mode: '0640'
        owner: appuser
        group: appgroup
      when: app_environment in ['production', 'staging', 'development']

Bu yaklaşım production, staging ve development ortamları için ayrı şablon dosyaları tutmanıza olanak tanır, üstelik kod tekrarı olmadan.

Performans İpucu: loop_control ve Şablonlar

Büyük loop’larda her iterasyonda şablon dosyasını diske yazmak yerine, tüm veriyi tek seferde işlemek daha verimlidir:

# playbooks/toplu_config.yml
---
- name: Tum uygulama configlerini olustur
  hosts: appservers
  tasks:
    - name: Uygulama konfigurasyon dizinini olustur
      ansible.builtin.file:
        path: /etc/apps/{{ item.name }}
        state: directory
        mode: '0755'
      loop: "{{ applications }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Her uygulama icin config dosyasini olustur
      ansible.builtin.template:
        src: templates/app_config.j2
        dest: /etc/apps/{{ item.name }}/config.ini
        mode: '0644'
      loop: "{{ applications }}"
      loop_control:
        loop_var: app_item
        label: "{{ app_item.name }}"
      vars:
        current_app: "{{ app_item }}"

Sonuç

Jinja2 şablonları, Ansible’ın en güçlü ve en az kullanılan özelliklerinden biri. Düzgün kullanıldığında hem zamandan kazandırır hem de insan hatasını minimuma indirir. Bir şablon dosyası yazma zahmetine katlandığınızda, onlarca sunucuya aynı anda, tutarlı ve doğrulanmış yapılandırma yayabilirsiniz.

Pratik önerilerim şunlar: şablonlarınıza her zaman “bu dosya otomatik üretilmiştir, manuel değişiklik yapmayın” uyarısı koyun, default() filtresini alışkanlık haline getirin, ve her yeni şablonu --check --diff ile test etmeyi unutmayın. Karmaşık şablon mantığını olabildiğince değişken tanımlarına taşıyın; şablon ne kadar sade olursa, bakımı o kadar kolay olur.

Bir sonraki adım olarak Ansible’ın vars_prompt, vault entegrasyonu ve şablon içinde lookup eklentilerini araştırmanızı öneririm. Özellikle hassas verileri şablon içinde güvenli şekilde yönetmek için Ansible Vault ile Jinja2’yi birlikte kullanmak başlı başına bir konu.

Yorum yapın