Jinja2 Lanjutan #
Sebagian besar pengguna Ansible menguasai dasar Jinja2 — variabel {{ var }}, kondisi {% if %}, loop {% for %}. Tapi Jinja2 jauh lebih ekspresif dari itu. Macro yang bisa dipanggil ulang, namespace untuk mutasi variabel dalam loop, filter chaining yang elegan — teknik-teknik ini mengubah template yang kompleks menjadi kode yang mudah dibaca dan di-maintain. Artikel ini membahas fitur Jinja2 yang sering diabaikan tapi sangat berguna.
Macro: Fungsi yang Bisa Dipanggil Ulang #
Macro adalah cara mendefinisikan blok template yang bisa dipanggil berulang kali seperti fungsi:
{# templates/nginx.conf.j2 #}
{# Definisikan macro untuk server block #}
{% macro server_block(server_name, port, root, ssl=false) %}
server {
listen {{ port }}{% if ssl %} ssl{% endif %};
server_name {{ server_name }};
root {{ root }};
{% if ssl %}
ssl_certificate /etc/ssl/certs/{{ server_name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ server_name }}.key;
{% endif %}
location / {
try_files $uri $uri/ =404;
}
}
{% endmacro %}
{# Gunakan macro untuk setiap vhost #}
{% for vhost in nginx_vhosts %}
{{ server_block(vhost.name, vhost.port | default(80), vhost.root) }}
{% if vhost.ssl | default(false) %}
{{ server_block(vhost.name, 443, vhost.root, ssl=true) }}
{% endif %}
{% endfor %}
Namespace: Variabel yang Bisa Dimutasi dalam Loop #
Masalah umum: kamu perlu akumulator dalam loop, tapi set biasa di dalam loop tidak bisa diakses di luar loop. namespace menyelesaikan ini:
{# ANTI-PATTERN: variabel dalam loop tidak bisa diubah dan dibaca di luar #}
{% set found = false %}
{% for server in servers %}
{% if server.role == 'primary' %}
{% set found = true %} {# Ini TIDAK mengubah 'found' di luar loop! #}
{% endif %}
{% endfor %}
{{ found }} {# Masih false! #}
{# BENAR: gunakan namespace #}
{% set ns = namespace(found=false, primary_server='') %}
{% for server in servers %}
{% if server.role == 'primary' %}
{% set ns.found = true %}
{% set ns.primary_server = server.hostname %}
{% endif %}
{% endfor %}
{# Sekarang ns.found dan ns.primary_server berisi nilai yang benar #}
Primary server: {{ ns.primary_server }}
Found: {{ ns.found }}
Contoh praktis — generate konfigurasi upstream nginx dengan weight:
{# templates/upstream.conf.j2 #}
{% set ns = namespace(total_weight=0) %}
{% for server in upstream_servers %}
{% set ns.total_weight = ns.total_weight + server.weight | default(1) %}
{% endfor %}
# Total weight: {{ ns.total_weight }}
upstream {{ upstream_name }} {
{% for server in upstream_servers %}
server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }};
{% endfor %}
}
Filter Chaining yang Ekspresif #
Ansible menyediakan banyak filter bawaan yang bisa di-chain untuk transformasi data yang kompleks:
vars:
servers:
- {name: web-01, role: webserver, env: production, ip: 10.0.1.1}
- {name: web-02, role: webserver, env: production, ip: 10.0.1.2}
- {name: db-01, role: database, env: production, ip: 10.0.2.1}
- {name: web-03, role: webserver, env: staging, ip: 10.1.1.1}
tasks:
# Ambil hanya production webservers, ekstrak IP, sort
- debug:
msg: >-
{{ servers
| selectattr('env', 'equalto', 'production')
| selectattr('role', 'equalto', 'webserver')
| map(attribute='ip')
| sort
| list }}
# Output: ['10.0.1.1', '10.0.1.2']
# Group by role, hitung per role
- debug:
msg: "{{ servers | groupby('role') | map('first') | list }}"
# Buat dictionary dari list: hostname → ip
- debug:
msg: >-
{{ servers
| items2dict(key_name='name', value_name='ip') }}
# Output: {web-01: 10.0.1.1, web-02: 10.0.1.2, ...}
# Flatten dan deduplikasi
- debug:
msg: >-
{{ servers
| map(attribute='role')
| unique
| sort
| list }}
# Output: ['database', 'webserver']
Custom Test #
Selain filter, Jinja2 mendukung test — fungsi boolean yang digunakan dengan is:
# filter_plugins/custom_tests.py
def is_private_ip(ip):
"""Test apakah IP adalah private address."""
import ipaddress
try:
addr = ipaddress.ip_address(ip)
return addr.is_private
except ValueError:
return False
def is_valid_hostname(hostname):
"""Test apakah hostname valid (huruf, angka, dan tanda hubung)."""
import re
pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
return bool(re.match(pattern, hostname))
class FilterModule:
def filters(self):
return {}
def tests(self):
return {
'private_ip': is_private_ip,
'valid_hostname': is_valid_hostname,
}
{# Gunakan custom test dalam template #}
{% for server in servers %}
{% if server.ip is private_ip %}
# Server internal: {{ server.name }}
{% endif %}
{% endfor %}
{# Dalam task Ansible #}
- name: Validasi hostname
assert:
that:
- inventory_hostname is valid_hostname
fail_msg: "Hostname '{{ inventory_hostname }}' tidak valid!"
Menangani Data Hierarkis #
Template untuk konfigurasi dengan struktur data nested yang kompleks:
{# templates/haproxy.cfg.j2 #}
{# Data input:
services:
- name: api
backends:
- host: 10.0.1.1
port: 8080
weight: 2
- host: 10.0.1.2
port: 8080
weight: 1
health_check:
path: /health
interval: 5s
#}
{% for service in services %}
frontend {{ service.name }}_front
bind *:{{ service.port | default(80) }}
default_backend {{ service.name }}_back
backend {{ service.name }}_back
balance roundrobin
{% if service.health_check is defined %}
option httpchk GET {{ service.health_check.path | default('/health') }}
{% endif %}
{% for backend in service.backends %}
server {{ service.name }}_{{ loop.index }}
{{- ' ' + backend.host + ':' + backend.port | string }}
{{- ' weight=' + backend.weight | default(1) | string }}
{{- ' check inter ' + service.health_check.interval | default('10s') if service.health_check is defined else '' }}
{% endfor %}
{% endfor %}
Jinja2 dalam Variabel (tidak hanya template) #
Jinja2 tidak hanya untuk file template — bisa juga dalam nilai variabel:
# group_vars/all.yml
app_log_dir: "/var/log/{{ app_name }}"
db_url: "postgresql://{{ db_user }}:{{ vault_db_password }}@{{ db_host }}:{{ db_port }}/{{ db_name }}"
backup_filename: "backup-{{ inventory_hostname }}-{{ ansible_date_time.date }}.tar.gz"
# Ekspresi kondisional dalam variabel
nginx_worker_processes: "{{ ansible_processor_vcpus * 2 }}"
max_open_files: >-
{{ '65536' if ansible_memtotal_mb > 8192 else '32768' }}
Ringkasan #
- Macro untuk blok template yang berulang — definisikan sekali, panggil berkali-kali dengan parameter berbeda, persis seperti fungsi.
namespaceuntuk akumulator dalam loop — variabel biasa yang di-setdi dalam loop tidak bisa diakses setelah loop berakhir.- Filter chaining dengan
selectattr,map,groupby,unique,sort— transformasi list yang kompleks bisa ditulis dalam satu ekspresi yang ekspresif.- Custom test (
is private_ip,is valid_hostname) untuk logika boolean yang reusable di template dan taskassert.- Gunakan
-(dash) untuk menghapus whitespace di Jinja2:{{- expr }}atau{%- block -%}— menghasilkan output yang lebih bersih terutama untuk konfigurasi yang sensitif terhadap whitespace.- Jinja2 bisa digunakan dalam nilai variabel, bukan hanya file template — sangat berguna untuk membangun string dinamis dari beberapa variabel.