สร้าง Ansible Role จาก Scratch: Directory Structure และ Files

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 ถัดไป