Ansible template Module: Deploy Dynamic Configuration Files

Ansible template module ใช้สร้างไฟล์บน remote server จาก template ที่มี Jinja2 syntax โดยแทนค่า variables และ expressions ก่อน deploy ต่างจาก copy module ที่ส่งไฟล์ static ตรงๆ template ช่วยให้ config file เดียวสามารถปรับเปลี่ยนได้ตามค่า inventory, facts, หรือ variables ของแต่ละ host

บทความนี้อธิบายการใช้ template module ตั้งแต่ Jinja2 syntax พื้นฐาน, variables และ filters, conditionals, loops, ไปจนถึง pattern สำหรับ multi-environment deployment ที่ใช้ template เดียวรองรับหลาย environment

Template พื้นฐานและ Jinja2 Syntax

ไฟล์ template ใช้นามสกุล .j2 โดย convention และเก็บไว้ใน templates/ directory ของ Playbook หรือ role ใช้ {{ variable }} แทนค่า, {% %} สำหรับ control flow, {# #} สำหรับ comment

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

    root {{ web_root }};
    index index.html;

    {# เพิ่ม SSL ถ้ากำหนดไว้ #}
    {% if ssl_enabled %}
    listen {{ ssl_port | default(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;
}
---
- name: Deploy nginx config from template
  hosts: webservers
  become: true
  vars:
    server_name: "myapp.example.com"
    web_root: "/var/www/html"
    nginx_port: 80
    ssl_enabled: true
    ssl_port: 443
    ssl_cert_path: "/etc/ssl/certs/myapp.crt"
    ssl_key_path: "/etc/ssl/private/myapp.key"
  tasks:
    - name: Deploy nginx virtualhost config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/myapp
        owner: root
        mode: '0644'
      notify: reload nginx

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

Variables และ Jinja2 Filters

Jinja2 มี filters สำหรับแปลงค่า variables ใน template ใช้ | (pipe) ต่อกับชื่อ filter เช่น upper, lower, default, join

# templates/app.conf.j2
[application]
name = {{ app_name | upper }}
version = {{ app_version | default('1.0.0') }}
debug = {{ debug_mode | lower }}

[database]
host = {{ db_host }}
port = {{ db_port | default(5432) }}
name = {{ db_name }}
# password ไม่แสดงใน template ให้ใช้ vault แทน

[server]
workers = {{ ansible_processor_vcpus * 2 }}
hostname = {{ inventory_hostname }}
ip = {{ ansible_default_ipv4.address }}

[logging]
level = {{ log_level | default('INFO') | upper }}
path = /var/log/{{ app_name | lower }}/app.log

Ansible facts เช่น ansible_processor_vcpus และ ansible_default_ipv4.address ใช้ได้ตรงใน template เหมาะสำหรับ config ที่ขึ้นอยู่กับ spec ของ server จริง

Conditionals ใน Template

{% if %} ... {% elif %} ... {% else %} ... {% endif %} ใช้สำหรับเนื้อหาที่แสดงเฉพาะเมื่อเงื่อนไขตรง เหมาะสำหรับ config ที่แตกต่างกันตาม environment หรือ OS

# templates/php-fpm.conf.j2
[www]
user = {{ web_user | default('www-data') }}
group = {{ web_user | default('www-data') }}

{% if php_fpm_socket_type == 'unix' %}
listen = /run/php/php{{ php_version }}-fpm.sock
listen.owner = {{ web_user | default('www-data') }}
listen.group = {{ web_user | default('www-data') }}
listen.mode = 0660
{% else %}
listen = 127.0.0.1:{{ php_fpm_port | default(9000) }}
{% endif %}

pm = dynamic
pm.max_children = {{ php_fpm_max_children | default(10) }}
pm.start_servers = {{ php_fpm_start_servers | default(3) }}

{% if environment == 'production' %}
php_admin_value[error_log] = /var/log/php-fpm/error.log
php_admin_flag[log_errors] = on
php_admin_value[display_errors] = Off
{% else %}
php_admin_value[display_errors] = On
php_admin_flag[log_errors] = on
{% endif %}

Loops ใน Template

{% for item in list %} ... {% endfor %} ใช้ generate เนื้อหาซ้ำๆ เช่น list ของ virtual hosts, database users, หรือ firewall rules

# templates/hosts.j2
# Managed by Ansible — do not edit manually
127.0.0.1 localhost

{% for host in groups['all'] %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }} {{ host }}
{% endfor %}

{% for entry in custom_hosts | default([]) %}
{{ entry.ip }} {{ entry.hostname }}
{% endfor %}
# templates/nginx-upstreams.conf.j2
upstream {{ upstream_name }} {
    {% for server in backend_servers %}
    server {{ server.host }}:{{ server.port | default(8080) }}{% if server.weight is defined %} weight={{ server.weight }}{% endif %};
    {% endfor %}
    keepalive 32;
}

Multi-Environment Deployment

pattern ที่พบบ่อยคือใช้ template เดียวกับ variables ที่แตกต่างกันตาม environment (dev/staging/production) โดยแยก variables ไว้ใน group_vars/ หรือ host_vars/

# group_vars/production/app.yml
environment: production
app_name: myapp
db_host: db-prod-01.internal
db_port: 5432
debug_mode: false
log_level: WARNING
php_fpm_max_children: 50
ssl_enabled: true

# group_vars/staging/app.yml
environment: staging
app_name: myapp
db_host: db-staging.internal
db_port: 5432
debug_mode: true
log_level: DEBUG
php_fpm_max_children: 5
ssl_enabled: false
---
- name: Deploy application config
  hosts: appservers
  become: true
  tasks:
    - name: Deploy app config from template
      template:
        src: templates/app.conf.j2
        dest: /etc/myapp/app.conf
        owner: "{{ app_user }}"
        mode: '0640'
        backup: yes
      notify: restart app

    - name: Deploy PHP-FPM config
      template:
        src: templates/php-fpm.conf.j2
        dest: /etc/php/{{ php_version }}/fpm/pool.d/www.conf
        owner: root
        mode: '0644'
      notify: restart php-fpm

  handlers:
    - name: restart app
      service:
        name: myapp
        state: restarted

    - name: restart php-fpm
      service:
        name: "php{{ php_version }}-fpm"
        state: restarted

Validate Config หลัง Template

เหมือนกับ copy module template รองรับ validate parameter เพื่อทดสอบ syntax ก่อน commit ไฟล์จริง ลด risk จากการ deploy config ที่มีข้อผิดพลาด

---
- name: Deploy configs with validation
  hosts: webservers
  become: true
  tasks:
    - name: Deploy nginx config with validation
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/myapp
        validate: nginx -t -c %s
        backup: yes

    - name: Deploy Apache config with validation
      template:
        src: templates/apache.conf.j2
        dest: /etc/apache2/sites-available/myapp.conf
        validate: apache2ctl -t -f %s

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

สรุป

template module เป็นเครื่องมือหลักสำหรับ deploy configuration files ที่ต้องปรับค่าตาม environment ใน Ansible Pattern ที่ควรจำ: เก็บ template ไว้ใน templates/ directory พร้อมนามสกุล .j2, แยก variables ตาม environment ด้วย group_vars/, ใช้ default() filter สำหรับค่า fallback, และใช้ validate parameter เสมอสำหรับ config ที่มี syntax checker

ความแตกต่างหลักระหว่าง template และ copy คือ template ประมวลผล Jinja2 ก่อน deploy ทำให้ config file เดียวรองรับหลาย host และหลาย environment ได้ ส่วน copy ส่งไฟล์ตามที่เป็นโดยไม่แปลงค่าใดๆ