Ansible Templates (Jinja2): สร้าง Configuration Files อัตโนมัติ

Template module ใน Ansible ใช้ Jinja2 engine สร้าง configuration files แบบ dynamic โดยแทรกค่าจาก variables และ facts ลงในไฟล์ต้นแบบ (.j2) ก่อน deploy ไปยัง remote host ทำให้ config file ชุดเดียวรองรับหลาย environment ได้โดยไม่ต้องดูแลหลายไฟล์แยกกัน

บทความนี้อธิบายการใช้ template module ตั้งแต่โครงสร้างไฟล์ .j2 พื้นฐาน, การใช้ conditionals และ loops ใน template, การจัดการ whitespace, ไปจนถึง pattern ที่ใช้บ่อยในทางปฏิบัติสำหรับ nginx, systemd และ app config

ใช้งาน Template Module

Module template ทำงานคล้าย copy แต่ประมวลผล Jinja2 expressions ในไฟล์ต้นแบบก่อนส่งไปยัง host

---
- name: Deploy config with template
  hosts: webservers
  vars:
    server_name: "example.com"
    app_port: 8080
  tasks:
    - name: Deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/conf.d/app.conf
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

ไฟล์ต้นแบบ (.j2) วางไว้ในไดเรกทอรี templates/ ภายใน playbook หรือ role เสมอ Ansible จะค้นหา path นี้โดยอัตโนมัติ

โครงสร้างไฟล์ .j2 พื้นฐาน

Jinja2 ใช้ delimiters 3 แบบ: {{ }} สำหรับ expressions (แทรกค่า), {% %} สำหรับ statements (control flow), และ {# #} สำหรับ comments

{# templates/nginx.conf.j2 #}
server {
    listen {{ app_port | default(80) }};
    server_name {{ server_name }};

    root {{ web_root | default('/var/www/html') }};
    index index.html index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    {% if enable_ssl is defined and enable_ssl %}
    listen 443 ssl;
    ssl_certificate     {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}

    access_log /var/log/nginx/{{ server_name }}_access.log;
    error_log  /var/log/nginx/{{ server_name }}_error.log;
}

Conditionals ใน Template

ใช้ {% if %} เพื่อเปิด/ปิด section ของ config ตามค่า variable ทำให้ template เดียวรองรับหลาย configuration

{# templates/app.conf.j2 #}
[app]
name = {{ app_name }}
port = {{ app_port }}

{% if db_host is defined %}
[database]
host     = {{ db_host }}
port     = {{ db_port | default(5432) }}
name     = {{ db_name }}
user     = {{ db_user }}
password = {{ db_password }}
{% endif %}

{% if cache_enabled | default(false) %}
[cache]
backend = {{ cache_backend | default('redis') }}
host    = {{ cache_host | default('localhost') }}
port    = {{ cache_port | default(6379) }}
{% endif %}

[logging]
level  = {{ log_level | default('info') }}
{% if log_level == 'debug' %}
verbose = true
{% else %}
verbose = false
{% endif %}

Loops ใน Template

{% for %} ใช้สร้าง config ซ้ำหลาย block จาก list variable เหมาะสำหรับ upstream server, allowed IPs, หรือ vhost หลายตัว

{# templates/haproxy.cfg.j2 #}
frontend web_frontend
    bind *:80
    default_backend web_servers

backend web_servers
    balance roundrobin
{% for server in backend_servers %}
    server {{ server.name }} {{ server.ip }}:{{ server.port | default(80) }} check
{% endfor %}

{# ตัวอย่างการใช้ loop กับ dict items #}
[allowed_hosts]
{% for host in allowed_hosts %}
{{ loop.index }}. {{ host }}
{% endfor %}

ตัวแปรพิเศษที่ใช้ได้ใน for loop: loop.index (เริ่มจาก 1), loop.index0 (เริ่มจาก 0), loop.first, loop.last, และ loop.length ช่วยให้ควบคุม output ได้ละเอียด

Whitespace Control

Jinja2 จะ render newlines รอบ {% %} blocks ออกมาด้วยซึ่งอาจทำให้ config มีบรรทัดว่างเกิน ใช้ - เพื่อตัด whitespace

{# ปัญหา: มีบรรทัดว่างเกิน #}
server_list:
{% for s in servers %}
  - {{ s }}
{% endfor %}

{# แก้ด้วย whitespace control #}
server_list:
{%- for s in servers %}
  - {{ s }}
{%- endfor %}

{# ตัด whitespace ทั้งก่อนและหลัง block #}
{% if feature_enabled -%}
feature = on
{%- endif %}

ใช้ Facts ใน Template

Template เข้าถึง facts และ variables ทั้งหมดที่มีใน play ได้โดยตรง ทำให้ปรับ config ตาม hardware จริงได้อัตโนมัติ

{# templates/nginx.conf.j2 — ปรับตาม CPU และ RAM จริง #}
user nginx;
worker_processes {{ ansible_processor_vcpus }};
worker_rlimit_nofile {{ [ansible_processor_vcpus * 1024, 8192] | max }};

events {
    worker_connections {{ [ansible_memtotal_mb // 4, 1024] | max }};
    multi_accept on;
}

http {
    server_tokens off;

    {% if ansible_memtotal_mb >= 4096 %}
    open_file_cache          max=10000 inactive=30s;
    open_file_cache_valid    60s;
    {% else %}
    open_file_cache          max=1000  inactive=20s;
    open_file_cache_valid    30s;
    {% endif %}
}

Template สำหรับ systemd Service

Pattern ที่ใช้บ่อยคือสร้าง systemd unit file จาก template เพื่อ deploy application service พร้อม configuration ที่ถูกต้องในทีเดียว

{# templates/myapp.service.j2 #}
[Unit]
Description={{ app_name }} Service
After=network.target
{% if db_host is defined %}
After=mysql.service
Requires=mysql.service
{% endif %}

[Service]
Type=simple
User={{ app_user | default('www-data') }}
WorkingDirectory={{ app_dir }}
ExecStart={{ app_dir }}/bin/{{ app_name }} --port {{ app_port }}
Restart=on-failure
RestartSec=5

{% if app_env_vars is defined %}
Environment={% for key, value in app_env_vars.items() %}"{{ key }}={{ value }}" {% endfor %}

{% endif %}
[Install]
WantedBy=multi-user.target
---
- name: Deploy app service
  hosts: appservers
  vars:
    app_name: myservice
    app_dir: /opt/myservice
    app_port: 8080
    app_env_vars:
      NODE_ENV: production
      LOG_LEVEL: info
  tasks:
    - name: Deploy systemd unit
      template:
        src: templates/myapp.service.j2
        dest: /etc/systemd/system/{{ app_name }}.service
        mode: '0644'
      notify:
        - reload systemd
        - restart service

  handlers:
    - name: reload systemd
      systemd:
        daemon_reload: yes

    - name: restart service
      service:
        name: "{{ app_name }}"
        state: restarted
        enabled: yes

validate: ตรวจ Config ก่อน Deploy

Parameter validate ของ template module รัน command เพื่อตรวจสอบความถูกต้องของ config ก่อนเขียนลง destination จริง ป้องกัน config พังทำ service ล่ม

---
- name: Deploy and validate nginx config
  hosts: webservers
  tasks:
    - name: Deploy nginx config with validation
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        validate: nginx -t -c %s
      notify: reload nginx

    - name: Deploy sshd config with validation
      template:
        src: templates/sshd_config.j2
        dest: /etc/ssh/sshd_config
        validate: sshd -t -f %s
      notify: restart sshd

Ansible จะเขียน template ลงไฟล์ชั่วคราวก่อน แล้วรัน validate command โดยแทนที่ %s ด้วย path ของไฟล์ชั่วคราว ถ้า command return non-zero exit code จะไม่เขียนทับ destination

สรุป

Template module เป็นเครื่องมือหลักสำหรับ configuration management ที่แท้จริง Pattern ที่ควรจำ: ใช้ | default() กับ variable ทุกตัวที่อาจไม่ได้กำหนด, ใช้ whitespace control (-) เมื่อ config format มีความสำคัญ, และใส่ validate parameter กับ config ของ service สำคัญเสมอเพื่อป้องกัน deploy config ที่พัง

Facts จาก remote host เช่น CPU count และ RAM ทำให้ template ปรับ config ตาม hardware จริงได้โดยอัตโนมัติ ไม่ต้องดูแล config file แยกสำหรับแต่ละ server spec