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:
{{ ... }}(Expressions): Digunakan untuk mencetak nilai variabel atau hasil dari suatu ekspresi ke dalam file output. Contoh:{{ db_port }}.{% ... %}(Statements): Digunakan untuk mengontrol alur logika (control flow) seperti perulangan (loops), pencabangan kondisi (conditionals), dan pemanggilan makro. Contoh:{% if ssl_enabled %}.{# ... #}(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: Bernilaitruejika perulangan berada di elemen pertama.loop.last: Bernilaitruejika 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
| boolsaat 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.lastuntuk mendeteksi elemen terakhir perulangan guna menghindari penulisan koma atau pemisah yang merusak sintaksis JSON/YAML.- Filter IP Address — Gunakan filter
| ipaddruntuk memvalidasi keabsahan data alamat IP yang dimasukkan sebelum menulisnya ke konfigurasi server.- Akses Lintas-Host — Gunakan variabel global
hostvarsuntuk menyusun konfigurasi load balancer secara dinamis berdasarkan fakta data yang dikumpulkan dari server backend.- Validasi Sintaksis Template — Selalu sertakan opsi
validateyang sesuai dengan perkakas target (sepertinginx -t -c %satauhaproxy -c -f %s) untuk memastikan stabilitas server produksi.