Template (Jinja2)

Template (Jinja2) #

Dalam pengembangan infrastruktur otomatis, fleksibilitas adalah kunci utama. Menulis berkas konfigurasi statis untuk setiap server tidaklah praktis karena setiap node sering kali memerlukan pengaturan unik, seperti alamat IP yang berbeda, alokasi memori yang disesuaikan dengan kapasitas RAM, atau lisensi yang bervariasi. Ansible memecahkan masalah ini dengan menyediakan modul ansible.builtin.template yang memanfaatkan mesin pembuat template (templating engine) Jinja2. Dengan Jinja2, kita dapat menulis satu berkas template konfigurasi induk dan secara dinamis menghasilkan berkas konfigurasi akhir yang disesuaikan secara presisi untuk setiap server target kita.


Prinsip Dasar dan Sintaksis Templating Jinja2 #

Modul template bekerja dengan membaca file sumber di control node (biasanya berkas teks dengan ekstensi .j2), merender seluruh variabel dan ekspresi logika di dalamnya menggunakan data dari inventory, facts, atau variables, lalu mengirimkan file teks hasil render tersebut ke managed node.

Mesin templating Jinja2 menggunakan tiga jenis pembatas (delimiters) utama untuk memisahkan kode template dari teks biasa:

  1. {{ ... }} (Expressions): Digunakan untuk mencetak nilai variabel atau hasil dari suatu ekspresi ke dalam file output. Contoh: {{ db_port }}.
  2. {% ... %} (Statements): Digunakan untuk mengontrol alur logika (control flow) seperti perulangan (loops), pencabangan kondisi (conditionals), dan pemanggilan makro. Contoh: {% if ssl_enabled %}.
  3. {# ... #} (Comments): Digunakan untuk menulis komentar internal template. Segala sesuatu di dalam pembatas ini tidak akan diproses dan tidak akan muncul di dalam berkas konfigurasi akhir di server target.

Penting untuk kita ingat bahwa rendering Jinja2 terjadi sepenuhnya di komputer lokal kita (control node) sebelum file dikirim via SSH. Hal ini berarti kita dapat menggunakan seluruh data yang dikumpulkan selama fase Gathering Facts untuk memengaruhi hasil akhir berkas konfigurasi.


Kontrol Whitespace dan Spasi Kosong #

Secara default, Jinja2 akan mempertahankan setiap baris baru (newline) dan spasi yang berada di sekitar blok statement {% ... %}. Hal ini sering kali menghasilkan file konfigurasi akhir yang memiliki banyak baris kosong atau indentasi yang berantakan, terutama setelah kita mengeksekusi perulangan for atau percabangan if.

Untuk mengatasi masalah visual ini, Jinja2 menyediakan mekanisme kontrol whitespace menggunakan karakter minus (-). Kita bisa menyisipkan minus di awal statement ({%-) atau di akhir statement (-%}) untuk menghapus spasi kosong dan baris baru di sebelah kiri atau kanan blok tersebut.

Mari kita bandingkan perbedaan hasil output dari penulisan yang salah dan benar:

{# ANTI-PATTERN: Loop tanpa kontrol whitespace menghasilkan baris-baris kosong yang merusak file konfigurasi #}
upstream backend_app {
{% for host in groups['webservers'] %}
    server {{ host }}:8080;
{% endfor %}
}

{# BENAR: Menggunakan minus (-) untuk menghapus whitespace di awal/akhir blok perulangan #}
upstream backend_app {
{%- for host in groups['webservers'] %}
    server {{ host }}:8080;
{%- endfor %}
}

Jika kita menggunakan pendekatan anti-pattern di atas, berkas akhir kita akan terlihat berantakan seperti ini:

upstream backend_app {

    server web-01:8080;

    server web-02:8080;

}

Namun, dengan menggunakan kontrol whitespace {%- dan -%}, Jinja2 akan merapatkan spasi kosong di sekitar statement dan menghasilkan output yang bersih dan standar:

upstream backend_app {
    server web-01:8080;
    server web-02:8080;
}

Ekspresi Kondisional dalam Jinja2 #

Pencabangan kondisi menggunakan if, elif, dan else memungkinkan kita membuat konfigurasi yang adaptif berdasarkan spesifikasi hardware server atau jenis lingkungan deployment (staging vs production).

Berikut adalah contoh konfigurasi Nginx vhost yang secara dinamis mengaktifkan SSL dan menyesuaikan performa berdasarkan memori server target:

server {
    listen {{ nginx_port | default(80) }};
    server_name {{ domain_name }};

    {# Mengaktifkan konfigurasi SSL secara kondisional #}
    {% if ssl_enabled | bool %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}

    {# Menyesuaikan koneksi database berdasarkan memori RAM server #}
    {% if ansible_memtotal_mb > 8000 %}
    # Konfigurasi untuk server berkapasitas besar (> 8GB RAM)
    pm.max_children = 50
    pm.start_servers = 10
    {% elif ansible_memtotal_mb > 2000 %}
    # Konfigurasi untuk server menengah (2GB - 8GB RAM)
    pm.max_children = 20
    pm.start_servers = 5
    {% else %}
    # Konfigurasi minimal untuk micro server (< 2GB RAM)
    pm.max_children = 5
    pm.start_servers = 2
    {% endif %}
}

Dalam contoh di atas, data ansible_memtotal_mb yang merupakan fakta bawaan sistem Linux dibaca langsung oleh Jinja2 untuk menghitung konfigurasi PHP-FPM secara otomatis tanpa campur tangan manual.


Perulangan dan Variabel Loop Khusus #

Perulangan for digunakan untuk menghasilkan baris konfigurasi yang berulang dari sebuah data list atau dictionary. Di dalam blok perulangan for, Jinja2 menyediakan variabel pembantu khusus bernama loop yang menyimpan status perulangan saat ini:

  • loop.index: Angka indeks saat ini, dimulai dari 1.
  • loop.index0: Angka indeks saat ini, dimulai dari 0.
  • loop.first: Bernilai true jika perulangan berada di elemen pertama.
  • loop.last: Bernilai true jika perulangan berada di elemen terakhir.
  • loop.length: Jumlah total elemen dalam perulangan.

Variabel loop.last sangat membantu ketika kita membuat file format JSON, YAML, atau baris konfigurasi yang memisahkan elemen menggunakan tanda koma, di mana kita ingin memastikan elemen terakhir tidak diakhiri dengan tanda koma guna menghindari error sintaksis.

{# Menghasilkan file konfigurasi JSON tanpa koma menggantung di akhir array #}
{
  "database_nodes": [
    {%- for host in groups['dbservers'] %}
    {
      "hostname": "{{ host }}",
      "ip": "{{ hostvars[host]['ansible_default_ipv4']['address'] }}"
    }{% if not loop.last %},{% endif %}
    {%- endfor %}
  ]
}

Tanpa pengecekan {% if not loop.last %},{% endif %}, Jinja2 akan menambahkan koma setelah elemen terakhir, yang mana akan memicu error parser JSON karena format JSON standar melarang trailing comma.


Filter Jinja2: default, ipaddr, join, dan bool #

Filter adalah fungsi utilitas yang digunakan untuk mengubah format atau memproses nilai variabel sebelum dicetak. Kita menggunakan karakter pipa (|) untuk memanggil filter. Berikut adalah empat filter paling penting dalam konfigurasi Ansible:

1. Filter default #

Digunakan untuk menentukan nilai cadangan (fallback) jika sebuah variabel tidak didefinisikan. Jika kita ingin memaksa kegagalan jika variabel kosong, kita bisa menggunakan filter kustom mandatory.

# Jika app_port tidak ditentukan di inventory, gunakan port 8000
port = {{ app_port | default(8000) }}

# Jika db_password tidak ditentukan, hentikan render dengan error yang jelas
db_password = {{ db_password | mandatory }}

2. Filter bool #

Mengonversi representasi string (seperti "yes", "true", "1", atau "on") menjadi tipe data boolean Python (True atau False). Ini sangat krusial karena Ansible sering kali membaca nilai input dari user sebagai string biasa.

# Memaksa evaluasi boolean yang aman
{% if enable_debug | default('false') | bool %}
log_level = DEBUG
{% else %}
log_level = WARNING
{% endif %}

3. Filter join #

Menggabungkan elemen-elemen dalam sebuah list menjadi satu string tunggal dengan pemisah tertentu.

# Mengonversi ['10.0.1.1', '10.0.1.2'] menjadi "10.0.1.1,10.0.1.2"
allowed_hosts = "{{ acl_hosts | join(',') }}"

4. Filter ipaddr #

Filter yang sangat kuat untuk memvalidasi dan memanipulasi alamat IP dan subnet mask. Filter ini membutuhkan pustaka Python netaddr terinstal di control node kita.

# Memeriksa apakah variabel berisi alamat IP yang valid
{% if my_ip | ipaddr %}
ip_address = {{ my_ip | ipaddr('address') }}
subnet_mask = {{ my_ip | ipaddr('netmask') }}
network_range = {{ my_ip | ipaddr('network') }}/{{ my_ip | ipaddr('prefix') }}
{% endif %}

Berikut adalah ringkasan filter penting beserta contoh kasus penggunaannya:

Nama Filter Input Variabel Sintaks Filter Hasil Output Kasus Penggunaan
default undefined {{ port | default(80) }} 80 Cadangan port default
bool "yes" {% if debug | bool %} True (Boolean) Evaluasi kondisi aman
join ['a', 'b'] {{ list | join(':') }} "a:b" Pemisah daftar host
ipaddr "192.168.1.1/24" {{ ip | ipaddr('netmask') }} "255.255.255.0" Penghitungan subnet mask

Integrasi Lintas-Host dengan hostvars #

Salah satu fitur paling kuat dari Ansible adalah kemampuan untuk mengakses data dari server lain di tengah-tengah proses rendering template untuk server saat ini. Kita melakukan ini menggunakan variabel global hostvars.

Sebagai contoh, ketika kita men-deploy load balancer HAProxy, kita perlu mengetahui alamat IP dari seluruh backend server web kita. Kita dapat melakukan perulangan pada kelompok server webservers di inventory dan membaca variabel IP dari masing-masing server tersebut menggunakan hostvars.

flowchart TD
    subgraph "Managed Nodes (Backends)"
        W1["web-01 (IP: 10.0.1.10)"]
        W2["web-02 (IP: 10.0.1.11)"]
    end
    subgraph "Control Node"
        A["Ansible gathers facts from web-01 & web-02"] --> B["Ansible renders haproxy.cfg.j2 using hostvars"]
    end
    subgraph "Load Balancer"
        C["HAProxy Server"]
    end
    B -- "template deploy" --> C
    C -->|"Route Traffic"| W1
    C -->|"Route Traffic"| W2

Namun, perlu kita ingat: hostvars hanya akan berisi data jika fakta-fakta server target sudah dikumpulkan. Pastikan playbook kita menyertakan seluruh host terkait dalam target play atau kita secara eksplisit menjalankan task gathering facts untuk seluruh host di awal playbook:

# templates/haproxy.cfg.j2
backend app_nodes
    balance roundrobin
    option httpchk GET /health
    {% for host in groups['webservers'] -%}
    server {{ host }} {{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_port | default(8080) }} check
    {% endfor %}

Studi Kasus: Konfigurasi Dinamis HAProxy dan Web Server #

Mari kita satukan seluruh materi ini ke dalam sebuah studi kasus nyata. Kita akan menulis template konfigurasi HAProxy dinamis (templates/haproxy.cfg.j2) yang secara dinamis mendaftarkan seluruh web server di kelompok inventory kita, menyaring IP eksternal yang tidak valid, menggunakan kontrol whitespace agar filenya rapi, dan memvalidasi konfigurasi HAProxy tersebut sebelum disimpan.

Berikut adalah isi dari file template HAProxy kustom kita (templates/haproxy.cfg.j2):

{# templates/haproxy.cfg.j2 #}
global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    user haproxy
    group haproxy
    daemon
    maxconn {{ haproxy_max_connections | default(4096) | int }}

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

frontend http_in
    bind *:80
    {% if haproxy_ssl_enabled | default('false') | bool -%}
    bind *:443 ssl crt {{ haproxy_ssl_cert_path | mandatory }}
    redirect scheme https code 301 if !{ ssl_fc }
    {% endif -%}
    default_backend web_servers

backend web_servers
    balance {{ haproxy_balance_algorithm | default('roundrobin') }}
    option forwardfor
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }
    option httpchk HEAD {{ haproxy_health_check_path | default('/health') }} HTTP/1.1\r\nHost:\ localhost
    
    {# Loop menggunakan hostvars untuk mengidentifikasi backend IP #}
    {%- for host in groups['webservers'] %}
    {%- set ip = hostvars[host]['ansible_default_ipv4']['address'] %}
    {%- if ip | ipaddr %}
    server {{ host }} {{ ip }}:{{ hostvars[host]['app_port'] | default(8080) }} check inter 2s fall 3 rise 2
    {%- endif %}
    {%- endfor %}

Dan ini adalah playbook utama (playbooks/deploy-haproxy.yml) yang bertugas men-deploy template tersebut ke server load balancer, lengkap dengan validasi command:

# playbooks/deploy-haproxy.yml
---
- name: Kumpulkan fakta backend terlebih dahulu
  hosts: webservers
  gather_facts: true  # Wajib untuk memastikan hostvars backend terisi fakta IP

- name: Deploy Load Balancer HAProxy
  hosts: loadbalancers
  become: true
  
  vars:
    haproxy_max_connections: 5000
    haproxy_ssl_enabled: "true"
    haproxy_ssl_cert_path: "/etc/ssl/certs/haproxy.pem"
    haproxy_balance_algorithm: "leastconn"
    haproxy_health_check_path: "/api/v1/health"

  tasks:
    - name: Pastikan haproxy terinstal
      apt:
        name: haproxy
        state: present
        update_cache: true
      when: ansible_os_family == "Debian"

    - name: Deploy sertifikat dummy untuk validasi SSL jika diaktifkan
      copy:
        content: "DUMMY CERTIFICATE"
        dest: "{{ haproxy_ssl_cert_path }}"
        mode: '0600'
        force: false # Jangan timpa jika sertifikat asli sudah ada
      when: haproxy_ssl_enabled | bool

    - name: Deploy konfigurasi HAProxy dinamis dari template
      template:
        src: templates/haproxy.cfg.j2
        dest: /etc/haproxy/haproxy.cfg
        owner: root
        group: root
        mode: '0644'
        backup: true
        # Perintah validasi sintaksis bawaan HAProxy
        validate: 'haproxy -c -f %s'
      notify: Restart HAProxy

  handlers:
    - name: Restart HAProxy
      systemd:
        name: haproxy
        state: restarted

Dalam studi kasus ini, template HAProxy dikonfigurasi secara sangat dinamis. Jika kita menambahkan server web baru ke kelompok webservers di inventory kita, kita cukup menjalankan ulang playbook ini. HAProxy akan membaca perubahan jumlah backend, membangun kembali daftar server di berkas haproxy.cfg, memvalidasi sintaksisnya secara aman, dan merestart layanan load balancer tanpa merusak layanan aktif jika terjadi kesalahan penulisan IP.


Ringkasan #

  • Tiga Sintaks Jinja2 — Gunakan pembatas {{ }} untuk mencetak variabel, {% %} untuk blok logika seperti if dan for, serta {# #} untuk komentar internal.
  • Kontrol Whitespace — Sisipkan karakter minus (-) pada statement statement Jinja2 ({%- dan -%}) untuk menghapus baris kosong yang tidak diinginkan di output konfigurasi.
  • Filter Bool untuk String — Selalu gunakan filter | bool saat mengevaluasi kondisi variabel string agar terhindar dari kesalahan logika kebenaran data.
  • Filter Default & Mandatory — Atur nilai fallback menggunakan filter | default(value) atau paksa kegagalan rendering jika data kosong menggunakan filter | mandatory.
  • Variabel Loop Last — Manfaatkan variabel internal loop.last untuk mendeteksi elemen terakhir perulangan guna menghindari penulisan koma atau pemisah yang merusak sintaksis JSON/YAML.
  • Filter IP Address — Gunakan filter | ipaddr untuk memvalidasi keabsahan data alamat IP yang dimasukkan sebelum menulisnya ke konfigurasi server.
  • Akses Lintas-Host — Gunakan variabel global hostvars untuk menyusun konfigurasi load balancer secara dinamis berdasarkan fakta data yang dikumpulkan dari server backend.
  • Validasi Sintaksis Template — Selalu sertakan opsi validate yang sesuai dengan perkakas target (seperti nginx -t -c %s atau haproxy -c -f %s) untuk memastikan stabilitas server produksi.

← Sebelumnya: Copy Module   Berikutnya: Drift Handling →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact