Jinja2 Lanjutan

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.
  • namespace untuk akumulator dalam loop — variabel biasa yang di-set di 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 task assert.
  • 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.

← Sebelumnya: Collection   Berikutnya: Strategy & Serial →

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