Ansible Role มีโครงสร้าง directory ที่กำหนดไว้แน่นอน — แต่ละ directory มีหน้าที่เฉพาะตัว เช่น tasks/ สำหรับ task logic, templates/ สำหรับ Jinja2 templates, handlers/ สำหรับ handlers และ defaults/ สำหรับ default variables การเข้าใจโครงสร้างนี้ทำให้สร้างและใช้ roles ได้อย่างถูกต้อง
บทความนี้ครอบคลุมการสร้าง role ด้วย ansible-galaxy init, โครงสร้าง directory และหน้าที่ของแต่ละไฟล์, การเขียน tasks/main.yml แบบแยก task files, การใช้ templates/ และ files/, การเขียน handlers และ pattern สำหรับ complete role
สร้าง Role ด้วย ansible-galaxy init
ansible-galaxy init สร้าง directory structure มาตรฐานครบถ้วนพร้อมไฟล์ว่างเปล่าให้อัตโนมัติ — ทำให้ไม่ต้องสร้าง directory เองและมั่นใจว่าโครงสร้างถูกต้องตาม Ansible standard
# สร้าง role ใหม่ชื่อ nginx_app
ansible-galaxy init nginx_app
# สร้างใน path เฉพาะ
ansible-galaxy init roles/nginx_app
# ผลลัพธ์ directory structure:
nginx_app/
├── README.md
├── defaults/
│ └── main.yml # default variables (priority ต่ำสุด, override ได้)
├── files/
│ └── (static files ที่ copy ไปยัง host)
├── handlers/
│ └── main.yml # handlers สำหรับ restart/reload
├── meta/
│ └── main.yml # galaxy info, dependencies, platform support
├── tasks/
│ └── main.yml # entry point ของ tasks ทั้งหมด
├── templates/
│ └── (Jinja2 templates ที่ render แล้ว copy)
├── tests/
│ ├── inventory
│ └── test.yml
└── vars/
└── main.yml # internal variables (priority สูง, ไม่ควร override)
tasks/main.yml — จัดโครงสร้าง Task Files
สำหรับ role ที่ซับซ้อน แทนที่จะเขียน tasks ทั้งหมดใน tasks/main.yml ไฟล์เดียว ให้แยกเป็นไฟล์ตาม function แล้วใช้ import_tasks หรือ include_tasks ใน main.yml
# tasks/main.yml — entry point
---
- name: Validate required variables
assert:
that:
- app_repo != ""
- app_domain != ""
fail_msg: "app_repo and app_domain must be set"
# import_tasks: รันทุกครั้ง, ไม่รองรับ dynamic vars
- import_tasks: install.yml
- import_tasks: configure.yml
- import_tasks: service.yml
# include_tasks: รันตามเงื่อนไข, รองรับ dynamic vars
- include_tasks: "ssl.yml"
when: app_enable_ssl | bool
# tasks/install.yml
---
- name: Create application user
user:
name: "{{ app_user }}"
system: true
shell: /bin/false
home: "{{ app_install_dir }}"
create_home: false
tags: [install]
- name: Create required directories
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0755'
loop:
- "{{ app_install_dir }}"
- "{{ app_log_dir }}"
- "{{ app_config_dir }}"
tags: [install]
- name: Install required system packages
package:
name: "{{ app_packages }}"
state: present
tags: [install]
- name: Deploy application from git
git:
repo: "{{ app_repo }}"
dest: "{{ app_install_dir }}/current"
version: "{{ app_version }}"
force: true
become_user: "{{ app_user }}"
tags: [install, deploy]
notify: Restart application
# tasks/configure.yml
---
- name: Deploy application configuration
template:
src: app.conf.j2
dest: "{{ app_config_dir }}/app.conf"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0640'
notify: Restart application
tags: [configure]
- name: Deploy environment file
template:
src: environment.j2
dest: "{{ app_config_dir }}/environment"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0640'
notify: Restart application
tags: [configure]
- name: Deploy systemd service unit
template:
src: app.service.j2
dest: "/etc/systemd/system/{{ app_name }}.service"
owner: root
group: root
mode: '0644'
notify:
- Reload systemd
- Restart application
tags: [configure]
templates/ — Jinja2 Templates
ไฟล์ใน templates/ จะถูก render ผ่าน Jinja2 ก่อน copy ไปยัง host — ใช้ role variables ทั้งหมดใน template ได้โดยตรง ตามชื่อ convention ใช้ suffix .j2
{# templates/app.conf.j2 #}
# {{ app_name }} configuration
# Managed by Ansible — do not edit manually
[server]
host = 0.0.0.0
port = {{ app_port }}
workers = {{ app_workers }}
timeout = {{ app_timeout }}
[database]
host = {{ app_db_host }}
port = {{ app_db_port }}
name = {{ app_db_name }}
user = {{ app_db_user }}
[logging]
level = {{ app_log_level | default('info') }}
file = {{ app_log_dir }}/app.log
{% if app_enable_cache %}
[cache]
enabled = true
backend = redis
url = {{ app_cache_url | default('redis://localhost:6379') }}
{% endif %}
{# templates/app.service.j2 #}
[Unit]
Description={{ app_name }} service
After=network.target
{% if app_requires_db %}
After=postgresql.service
Requires=postgresql.service
{% endif %}
[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_install_dir }}/current
EnvironmentFile={{ app_config_dir }}/environment
ExecStart={{ app_install_dir }}/current/bin/{{ app_name }} start
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
files/ — Static Files
ไฟล์ใน files/ จะถูก copy ไปยัง host โดยตรง (ไม่ผ่าน Jinja2 render) — เหมาะสำหรับ binary files, certificates, หรือ config ที่ไม่ต้องการ variable substitution
# tasks/configure.yml — copy static files
---
# ใช้ copy module กับ files ใน files/
- name: Deploy SSL certificate
copy:
src: ssl/mysite.crt # อ้างอิงจาก files/ อัตโนมัติ
dest: /etc/nginx/certs/mysite.crt
owner: root
mode: '0644'
tags: [configure, ssl]
- name: Deploy SSL private key
copy:
src: ssl/mysite.key
dest: /etc/nginx/certs/mysite.key
owner: root
mode: '0600'
tags: [configure, ssl]
- name: Deploy custom error pages
copy:
src: "error_pages/{{ item }}"
dest: "/var/www/errors/{{ item }}"
owner: www-data
loop:
- 404.html
- 500.html
- 503.html
tags: [configure]
handlers/main.yml — Restart และ Reload
handlers รันเมื่อ task ส่ง notify มาและมีสถานะ changed — Ansible จะรัน handler ที่ถูก notify เพียงครั้งเดียวหลัง tasks ทั้งหมดในส่วนนั้นจบ แม้จะถูก notify หลายครั้ง
# handlers/main.yml
---
- name: Reload systemd
systemd:
daemon_reload: true
- name: Restart application
service:
name: "{{ app_name }}"
state: restarted
listen: "Restart application" # listen ทำให้ notify ด้วยชื่อนี้ได้
- name: Reload nginx
service:
name: nginx
state: reloaded
- name: Restart nginx
service:
name: nginx
state: restarted
# flush_handlers ในกรณีที่ต้องการรัน handlers ทันทีก่อน task ถัดไป
# ใช้ใน tasks/service.yml เพื่อให้ service restart ก่อน verify health
Pattern: Complete Role สำหรับ Web Application
ตัวอย่าง role ครบถ้วนสำหรับ deploy web application — แสดงการใช้ไฟล์ทุก type ใน role structure และ best practices ที่ควรทำใน production role
# roles/webapp/defaults/main.yml
---
webapp_name: webapp
webapp_user: webapp
webapp_group: webapp
webapp_install_dir: "/opt/{{ webapp_name }}"
webapp_config_dir: "/etc/{{ webapp_name }}"
webapp_log_dir: "/var/log/{{ webapp_name }}"
webapp_port: 8080
webapp_workers: 4
webapp_log_level: info
webapp_enable_ssl: false
webapp_enable_metrics: false
webapp_repo: ""
webapp_version: main
webapp_packages:
- curl
- git
# roles/webapp/tasks/main.yml
---
- name: Validate required variables
assert:
that:
- webapp_repo | length > 0
fail_msg: "webapp_repo must be set in group_vars or host_vars"
- import_tasks: install.yml
tags: [webapp, install]
- import_tasks: configure.yml
tags: [webapp, configure]
- import_tasks: service.yml
tags: [webapp, service]
- name: Include SSL tasks
include_tasks: ssl.yml
when: webapp_enable_ssl | bool
tags: [webapp, ssl]
- name: Include metrics tasks
include_tasks: metrics.yml
when: webapp_enable_metrics | bool
tags: [webapp, metrics]
# roles/webapp/tasks/service.yml
---
- name: Enable and start webapp service
systemd:
name: "{{ webapp_name }}"
state: started
enabled: true
daemon_reload: true
- name: Flush handlers before health check
meta: flush_handlers
- name: Verify webapp is responding
uri:
url: "http://localhost:{{ webapp_port }}/health"
status_code: 200
retries: 5
delay: 3
changed_when: false
- name: Record installation date
lineinfile:
path: "{{ webapp_install_dir }}/.installed"
line: "{{ ansible_date_time.iso8601 }}"
create: true
owner: "{{ webapp_user }}"
# ใช้ role ใน Playbook
---
- name: Deploy webapp to production
hosts: webservers
become: true
vars:
webapp_repo: "https://github.com/myteam/webapp.git"
webapp_version: "v2.5.0"
webapp_enable_ssl: true
webapp_workers: 8
webapp_log_level: warning
roles:
- role: webapp
สรุป
Ansible Role มีโครงสร้าง directory มาตรฐานที่ชัดเจน — tasks/ สำหรับ logic, templates/ สำหรับ Jinja2 files, files/ สำหรับ static files, handlers/ สำหรับ restart/reload, defaults/ สำหรับ overridable variables และ meta/ สำหรับ dependencies
Pattern ที่ควรจำ: ใช้ ansible-galaxy init เสมอเพื่อสร้างโครงสร้างที่ถูกต้อง, แยก tasks ออกเป็นไฟล์ตาม function แล้วใช้ import_tasks/include_tasks, ใช้ assert ตรวจ required variables ก่อนรัน tasks, ตั้งชื่อ template ด้วย .j2 เสมอ และใช้ meta: flush_handlers เมื่อต้องการให้ handlers รันทันทีก่อน task ถัดไป

