Pelajari cara membuat file konfigurasi dinamis menggunakan mesin templating Jinja2, mulai dari syntax dasar expression dan statement, modul ansible.builtin.template, hingga penggunaan filters untuk memanipulasi data di dalam template.

Setelah di episode 7 sebelumnya kita membahas variables & facts, kalian sekarang punya banyak bahan: variabel yang tersimpan rapi di group_vars dan host_vars, serta fakta sistem yang dikumpulkan otomatis dari setiap server. Tapi apa gunanya bahan-bahan tersebut jika kita tidak punya cara untuk mengubahnya menjadi file konfigurasi yang benar-benar dipakai oleh service?
Di episode 8 ini, kita akan membahas Jinja2 Templating & Filters, dua kemampuan yang membuat playbook Ansible terasa seperti "program sungguhan". Mari kita mulai dengan sebuah pertanyaan: apa masalah terbesar dari file konfigurasi statis?
Bayangkan kalian punya 50 server NGINX. Server di data center Jakarta membutuhkan IP dan domain yang berbeda dengan server di Singapura. Kalau kalian menulis file konfigurasi secara statis, kalian butuh 50 file berbeda yang di-maintain satu per satu. Kalau ada perubahan satu parameter, kalian harus mengubah 50 file. Inilah yang disebut configuration drift: file-file yang menyimpang satu sama lain seiring waktu karena dikelola secara manual.
Jinja2 menyelesaikan masalah ini dengan konsep yang sangat sederhana: satu template, banyak hasil. Template adalah file "cetakan" yang berisi placeholder, dan nilainya diisi saat render menggunakan variabel dan facts. Satu template NGINX yang sama bisa menghasilkan konfigurasi yang berbeda untuk setiap server, hanya dengan mengubah inputnya.
Teknologi ini sebenarnya bukan milik Ansible. Jinja2 adalah templating engine yang ditulis dalam bahasa Python dan sudah digunakan di berbagai framework seperti Flask dan Django. Ansible memakainya sebagai bahasa templating bawaan, jadi skill yang kalian pelajari di episode ini juga berguna di luar konteks Ansible.
Jinja2 bekerja dengan cara sederhana: ia membaca file template, mengevaluasi sintaks khusus yang ada di dalamnya, lalu menghasilkan output akhir berupa teks murni. Di Ansible, output ini biasanya berupa file konfigurasi yang ditaruh di managed node.
Ada tiga sintaks dasar Jinja2 yang wajib kalian kuasai:
| Sintaks | Nama | Fungsi | Contoh |
|---|---|---|---|
{{ variabel }} | Expression | Mencetak nilai variabel / hasil evaluasi | {{ nginx_port }} |
{% if kondisi %} | Statement | Logika kondisional, loop, pengendali alur | {% if enable_https %}...{% endif %} |
{# komentar #} | Comment | Komentar, tidak dirender ke output | {# ini tidak muncul di file hasil #} |
Tidak seperti komentar di kebanyakan bahasa pemrograman, komentar Jinja2 benar-benar dihapus dari output. Jadi jika kalian menaruh komentar di tengah template, komentar itu tidak akan pernah muncul di file konfigurasi hasil render.
Note
Aturan praktis: gunakan {{ }} ketika kalian ingin menampilkan nilai, dan {% %} ketika kalian ingin mengendalikan alur logika. Mencampur keduanya secara keliru adalah salah satu kesalahan sintaks paling umum di Jinja2.
.j2Template di Ansible biasanya diberi ekstensi .j2 agar mudah dikenali, meskipun secara teknis ekstensi tidak terlalu berpengaruh. Mari kita buat contoh template sederhana untuk NGINX. Perhatikan bagaimana variabel dan statement dipakai di dalamnya:
{# Template virtual host NGINX untuk server {{ server_name }} #}
server {
listen {{ nginx_port }};
server_name {{ server_name }};
{% if enable_https %}
listen 443 ssl;
ssl_certificate /etc/ssl/certs/{{ server_name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ server_name }}.key;
{% endif %}
root {{ docroot }};
index index.html index.htm;
access_log /var/log/nginx/{{ server_name }}.access.log;
}Mari kita bedah template di atas:
{{ nginx_port }}, {{ server_name }}, dan {{ docroot }} adalah expression yang akan diganti nilainya saat render.{% if enable_https %} ... {% endif %} adalah statement yang membuat blok SSL hanya dirender jika variabel enable_https bernilai true.{# ... #} adalah komentar yang tidak akan muncul di file hasil.Perhatikan pola pentingnya: template ini satu file, tapi bisa menghasilkan konfigurasi dengan atau tanpa blok SSL tergantung nilai variabelnya. Inilah kekuatan utamanya.
ansible.builtin.templateTemplate tidak berguna tanpa cara untuk merendernya. Di Ansible, modul yang bertugas adalah ansible.builtin.template. Modul ini membaca file .j2 dari control node, merender-nya dengan variabel dan facts yang tersedia, lalu menyalin hasilnya ke dest di managed node.
Struktur direktori proyek kita kali ini:
ansible-jinja2/
├── inventory.yml
├── group_vars/
│ └── webservers.yml
├── templates/
│ └── nginx-vhost.conf.j2
└── playbook-template-nginx.ymlIni playbook lengkap yang merender template virtual host dan mengaktifkannya sebagai site NGINX:
---
- name: Deploy konfigurasi NGINX dari template
hosts: webservers
become: true
vars:
server_name: blog.example.com
nginx_port: 8080
docroot: /var/www/blog
enable_https: true
tasks:
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Render template virtualhost
ansible.builtin.template:
src: templates/nginx-vhost.conf.j2
dest: /etc/nginx/sites-available/blog.conf
owner: root
group: root
mode: "0644"
notify: Reload Nginx
- name: Aktifkan site
ansible.builtin.file:
src: /etc/nginx/sites-available/blog.conf
dest: /etc/nginx/sites-enabled/blog.conf
state: link
notify: Reload Nginx
handlers:
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloadedPerhatikan kombinasi yang indah dengan materi episode 6: task yang me-render template me-notify handler Reload Nginx. Karena modul template bersifat idempotent, handler hanya terpicu jika hasil render berbeda dari file yang sudah ada di server. Kalian mengganti nilai nginx_port → hasil render berubah → NGINX di-reload. Kalian menjalankan ulang dengan nilai sama → tidak ada perubahan → NGINX tetap tenang.
Important
Satu-satunya kelemahan kecil template untuk idempotency adalah whitespace. Jika template kalian memiliki baris kosong di ujung (trailing whitespace/newline) yang tidak konsisten antar render, file hasil akan dianggap "berubah" setiap kali dan handler akan terus terpicu. Gunakan ansible-lint dan cek --check --diff untuk mendeteksi masalah ini.
Hal menarik dari template adalah ia memiliki akses ke semua variabel yang tersedia, termasuk ansible facts yang kita pelajari di episode 7. Ini membuat template menjadi sangat dinamis. Kalian bisa men-generate konfigurasi yang menyesuaikan diri dengan spesifikasi tiap server.
Contohnya, kita bisa mengatur jumlah worker process NGINX berdasarkan jumlah CPU server, dan menggunakan IP address server sebagai nilai di konfigurasi:
{# Template nginx.conf yang adaptif terhadap spesifikasi server #}
user www-data;
worker_processes {{ ansible_facts['processor_vcpus'] }};
events {
worker_connections 1024;
}
http {
access_log /var/log/nginx/{{ ansible_facts['hostname'] }}.access.log;
server {
listen {{ nginx_port }} default_server;
server_name {{ server_name }};
location / {
proxy_pass http://{{ ansible_facts['default_ipv4']['address'] }}:{{ backend_port }};
}
}
}Perhatikan: tidak ada angka magic di template ini. Jumlah worker mengikuti CPU, IP dan hostname mengikuti fakta server. Template yang sama dipakai untuk 50 server, dan setiap server mendapat konfigurasi yang tepat untuk dirinya.
Selain facts, Jinja2 di Ansible juga mendukung lookup untuk menarik data dari sumber lain. Contoh yang berguna:
server_name: {{ lookup('env', 'DOMAIN_NAME') | default('example.com') }}Tip
Untuk kasus yang lebih kompleks, Ansible juga menyediakan lookup seperti pipe (menjalankan command), url (mengambil konten HTTP), dan vars (mengakses variabel di host lain). Gunakan secukupnya, karena lookup yang berlebihan membuat playbook sulit di-debug.
Sekarang mari kita gabungkan semuanya dalam satu skenario utuh. Kita akan men-deploy virtual host NGINX yang dinamis untuk beberapa server dengan domain berbeda. Satu template, beberapa hasil.
Template virtual host yang kita buat sebelumnya akan kita pakai, lalu kita render dengan variabel yang berbeda untuk setiap server. Variabel server_name, nginx_port, docroot, dan enable_https kita isi lewat group_vars dan host_vars:
---
nginx_port: 80
docroot: /var/www/html
enable_https: true---
server_name: blog.example.com---
server_name: api.example.comJalankan playbook-nya:
ansible-playbook -i inventory.yml playbook-template-nginx.ymlKarena web01 dan web02 punya server_name yang berbeda, template yang sama menghasilkan dua file konfigurasi yang berbeda. Inilah hasil render untuk web01:
server {
listen 80;
server_name blog.example.com;
listen 443 ssl;
ssl_certificate /etc/ssl/certs/blog.example.com.crt;
ssl_certificate_key /etc/ssl/private/blog.example.com.key;
root /var/www/html;
index index.html index.htm;
access_log /var/log/nginx/blog.example.com.access.log;
}Perhatikan bahwa blok SSL dirender karena enable_https: true. Jika suatu saat kalian set enable_https: false untuk satu server, blok SSL di server tersebut otomatis hilang tanpa menyentuh file template maupun playbook.
Warning
Saat memverifikasi hasil render, gunakan kombinasi flag --check dan --diff agar Ansible menampilkan perbedaan file tanpa benar-benar mengubahnya. Ini sangat berguna sebelum menerapkan template baru ke produksi:
ansible-playbook -i inventory.yml playbook.yml --check --diff
Sekarang kita masuk ke bagian kedua episode ini: filters. Filter adalah fungsi yang memanipulasi nilai variabel di dalam template. Di Jinja2 dan Ansible, filter ditulis setelah tanda pipe |, seperti {{ variabel | filter_name }}. Beberapa filter bisa dirantai sekaligus, misalnya {{ nama | lower | trim }}.
Bayangkan filter seperti alat di dapur: variabel adalah bahan mentah, dan filter adalah cara kita memotong, memanaskan, atau membentuk bahan tersebut sebelum disajikan. Ansible menyediakan ratusan filter bawaan. Berikut tabel filter yang paling sering dipakai dalam pengelolaan konfigurasi:
| Filter | Fungsi | Contoh | Hasil |
|---|---|---|---|
default(value) | Memberi nilai fallback jika variabel kosong/tidak terdefinisi | {{ port | default(80) }} | 80 (jika port tidak ada) |
lower / upper | Mengubah teks menjadi huruf kecil / besar | {{ "Web" | lower }} | web |
trim | Menghapus spasi di awal dan akhir | {{ " devops " | trim }} | devops |
length | Menghitung panjang list/string | {{ [1,2,3] | length }} | 3 |
join(', ') | Menggabungkan list menjadi string | {{ ["a","b"] | join(', ') }} | a, b |
to_json / to_nice_json | Mengubah objek menjadi JSON | {{ data | to_json }} | {"port": 80} |
to_yaml / to_nice_yaml | Mengubah objek menjadi YAML | {{ data | to_yaml }} | port: 80 |
combine | Menggabungkan dictionary | {{ a | combine(b) }} | dictionary gabungan |
regex_replace | Mengganti teks dengan regex | {{ s | regex_replace('^www\\.', '') }} | teks tanpa www. |
b64encode / b64decode | Encoding / decoding Base64 | {{ "admin" | b64encode }} | YWRtaW4= |
Note
Filter to_nice_json dan to_nice_yaml menghasilkan output yang lebih mudah dibaca manusia (dengan indentasi), cocok untuk file konfigurasi yang akan di-review manusia. Versi "non-nice" (to_json, to_yaml) menghasilkan output yang lebih kompak.
Mari kita lihat filter-filter penting dalam kode nyata, mulai dari yang paling sering dipakai.
defaultFilter ini wajib kalian kuasai karena ia mencegah error "undefined variable". Perhatikan perbedaan kedua parameter berikut:
# Jika nginx_port undefined → pakai 80
server {
listen {{ nginx_port | default(80) }};
}# Kombinasi dengan ansible_facts (episode 7)
worker_processes {{ ansible_facts['processor_vcpus'] | default(2) }};to_json dan to_yamlFilter ini sangat berguna ketika kalian perlu menyisipkan data terstruktur ke dalam file konfigurasi, misalnya mendefinisikan upstream NGINX atau daftar server dari sebuah variabel list/dict:
# Dalam template aplikasi yang butuh konfigurasi JSON
CONFIG_BACKEND='{{ backend_config | to_json }}'# group_vars/webservers.yml
upstream_servers:
- server: 10.10.10.21
weight: 3
- server: 10.10.10.22
weight: 2combineFilter combine digunakan untuk menggabungkan dua dictionary menjadi satu. Ini sangat berguna ketika kalian ingin menerapkan "default" lalu menimpa dengan "override":
# Default konfigurasi
default_config:
worker_processes: 4
keepalive_timeout: 65
# Override khusus server
override_config:
worker_processes: 8
# Hasil combine (recursive=true untuk menggabungkan nested dict)
final_config: {{ default_config | combine(override_config, recursive=true) }}Hasil akhir final_config akan menjadi:
final_config:
worker_processes: 8
keepalive_timeout: 65Perhatikan bahwa worker_processes diambil dari override_config (nilai 8 menimpa 4), sedangkan keepalive_timeout tetap dari default_config. Teknik ini adalah fondasi dari pola "default + override" yang banyak digunakan untuk mengelola konfigurasi multi-environment.
regex_replaceFilter ini menggunakan Regular Expression (regex) untuk mencari dan mengganti pola teks. Contoh paling umum adalah membersihkan domain dari subdomain www, atau mengambil bagian tertentu dari sebuah string:
# domain: www.example.com → example.com
server_name {{ server_name | regex_replace('^www\\.', '') }};Tip
Ingat aturan escaping di Jinja2: karena backslash \ itu sendiri adalah karakter khusus, untuk menulis regex \. di template kalian harus menulis \\. (dengan dua backslash). Lupa menulis double backslash adalah salah satu bug regex yang paling sering terjadi di template.
Supaya kalian tidak terjebak di lapangan, berikut rangkuman kesalahan umum yang paling sering muncul ketika bekerja dengan Jinja2 di Ansible:
1. Menggunakan {{ }} untuk logika, bukan untuk nilai. Statement seperti if harus memakai {% %}. Menulis {{ if enable_https }} akan menghasilkan error.
2. Melupakan filter default untuk variabel opsional. Jika sebuah variabel tidak terdefinisi dan dipakai di template tanpa default, playbook akan gagal dengan error 'xxx' is undefined.
3. Salah escaping backslash di regex. Di Jinja2, tulis \\. untuk regex \., atau error sintaks akan muncul.
4. Whitespace yang tidak konsisten. Baris kosong atau spasi berlebih di akhir template membuat modul template melaporkan changed setiap kali, sehingga handler reload terus terpicu. Bersihkan trailing whitespace, atau pertimbangkan untuk menyimpan file yang sudah dirender sebagai referensi.
5. Menempatkan template di lokasi yang salah. Modul template mencari src relatif terhadap direktori playbook (atau role). Memindahkan file .j2 tanpa memperbaiki path adalah penyebab error could not locate file yang sangat umum.
Untuk memastikan template tidak error di salah satu host, kita bisa menambahkan task validasi konfigurasi NGINX sebelum me-reload. Teknik ini akan berguna juga untuk error handling yang akan dibahas di episode 10:
---
- name: Render template dengan validasi
hosts: webservers
become: true
vars:
server_name: blog.example.com
nginx_port: 8080
docroot: /var/www/blog
enable_https: false
tasks:
- name: Render dan validasi konfigurasi NGINX
ansible.builtin.template:
src: templates/nginx-vhost.conf.j2
dest: /etc/nginx/sites-available/blog.conf
owner: root
group: root
mode: "0644"
validate: nginx -t -c %s
notify: Reload Nginx
handlers:
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloadedParameter validate menjalankan perintah nginx -t -c %s terhadap file hasil render sebelum file tersebut benar-benar dipasang. Jika konfigurasi hasil template tidak valid, playbook akan gagal sebelum file rusak menggantikan konfigurasi yang sudah berjalan. Ini adalah praktik yang sangat disarankan untuk file konfigurasi yang kritis.
Caution
Selalu gunakan validate untuk file konfigurasi yang kritis seperti NGINX, Apache, dan sshd. File konfigurasi yang rusak bisa membuat service tidak bisa dijalankan, dan dalam kasus sshd, bisa membuat kalian kehilangan akses remote ke server sama sekali.
Pada episode 8 ini, kita telah menjelajahi salah satu kemampuan terkuat Ansible: Jinja2 templating. Kalian sekarang memahami tiga sintaks dasar Jinja2, yaitu expression {{ }}, statement {% %} (terutama if), dan comment {# #}. Kita juga sudah praktik menggunakan modul ansible.builtin.template untuk merender file konfigurasi dinamis, mulai dari template virtual host NGINX yang menyesuaikan IP dan domain setiap server, hingga template yang memanfaatkan ansible facts seperti jumlah CPU dan hostname. Terakhir, kita membahas filters: default, lower, upper, to_json, to_yaml, combine, regex_replace, beserta contoh dan jebakan umumnya.
Poin kunci yang perlu kalian bawa pulang:
.j2 bisa menghasilkan banyak file konfigurasi yang berbeda, cukup dengan mengubah variabel input.{{ }} untuk menampilkan nilai, {% %} untuk logika, {# #} untuk komentar.template bersifat idempotent dan bisa dikombinasikan dengan handler dari episode 6.default, to_json, to_yaml, combine, dan regex_replace adalah senjata utama untuk memanipulasi data.validate untuk melindungi service dari konfigurasi hasil template yang salah.Di episode 9 selanjutnya kita akan membahas topik Control Flow (Conditionals & Loops). Kita akan belajar cara membuat playbook yang benar-benar "berpikir", mulai dari percabangan dengan when (plus and, or, not), perulangan modern dengan loop, hingga teknik retry until. Semua yang kalian pelajari hari ini tentang variabel, facts, dan template akan menjadi fondasi yang sangat berguna di episode tersebut. Pastikan tetap semangat!