Ansible Modules เจาะลึก: Command, Shell, Script และ Custom Modules

Ansible มี modules หลายร้อยตัวสำหรับงานที่แตกต่างกัน แต่ modules กลุ่ม command execution คือพื้นฐานที่ใช้บ่อยที่สุด ไม่ว่าจะเป็น command, shell, script และสำหรับงานที่ไม่มี module รองรับ ก็ยังสร้าง custom module ขึ้นมาเองได้

บทความนี้อธิบายความแตกต่างระหว่าง command และ shell, วิธีใช้ script รัน local script บน remote host, การ register output, และวิธีสร้าง custom module ด้วย Python เบื้องต้น

command Module: รัน Command ปลอดภัยที่สุด

command เป็น module พื้นฐานสำหรับรัน command บน remote host โดยไม่ผ่าน shell ทำให้ปลอดภัยจาก shell injection และ predictable กว่า

---
- name: Using command module
  hosts: all
  tasks:
    - name: Check OS version
      command: cat /etc/os-release
      register: os_info

    - name: Print OS info
      debug:
        var: os_info.stdout_lines

    - name: Create directory
      command: mkdir -p /opt/myapp/data

    - name: Run command with arguments
      command:
        cmd: df -h /
        chdir: /tmp       # เปลี่ยน working directory ก่อนรัน

    - name: Run command only if file exists
      command: cat /etc/app.conf
      args:
        creates: /opt/app/initialized   # ข้ามถ้า file นี้มีอยู่แล้ว

command module ไม่รองรับ shell features เช่น pipes (|), redirects (>), wildcards (*) หรือ environment variable expansion เช่น $HOME — ถ้าต้องการสิ่งเหล่านี้ต้องใช้ shell module แทน

shell Module: รัน Command ผ่าน Shell

shell module ส่ง command ไปรันผ่าน /bin/sh บน remote host ทำให้ใช้ shell features ได้ครบ ทั้ง pipes, redirects, wildcards และ subshell

---
- name: Using shell module
  hosts: all
  tasks:
    - name: Get process count
      shell: ps aux | grep nginx | grep -v grep | wc -l
      register: nginx_procs

    - name: Check if service is active
      shell: systemctl is-active nginx && echo "running" || echo "stopped"
      register: nginx_status
      ignore_errors: yes

    - name: Write output to file
      shell: df -h > /tmp/disk_usage.txt

    - name: Use environment variables
      shell: echo "Home is $HOME, User is $USER"
      register: env_output

    - name: Multi-line shell command
      shell: |
        cd /opt/myapp
        git pull origin main
        pip install -r requirements.txt
      register: deploy_output

ใช้ shell เฉพาะเมื่อจำเป็นต้องใช้ shell features จริง ๆ เพราะอาจเกิด shell injection ได้ถ้า input มาจากผู้ใช้ ถ้า command ง่ายและไม่ต้องการ pipes ให้ใช้ command เสมอ

command vs shell: เมื่อไรใช้อะไร

ความแตกต่างหลักระหว่าง command และ shell คือวิธีที่ command ถูกส่งไปรัน ซึ่งส่งผลต่อความปลอดภัยและความสามารถ

# ใช้ command (ปลอดภัยกว่า — ไม่ผ่าน shell)
- command: /usr/bin/python3 /opt/scripts/check.py    # ✅ ไม่มี shell features

# ใช้ shell (ต้องการ shell features)
- shell: cat /var/log/app.log | grep ERROR | tail -20   # ✅ ต้องการ pipe
- shell: echo "{{ item }}" >> /tmp/list.txt              # ✅ ต้องการ redirect
- shell: rm -f /tmp/old_*                                # ✅ ต้องการ wildcard

# อย่าใช้ shell เมื่อไม่จำเป็น
- shell: ls /etc/nginx                                   # ❌ ไม่มี shell features — ใช้ command แทน
- shell: /usr/bin/python3 /opt/scripts/check.py          # ❌ ใช้ command แทนได้

script Module: รัน Local Script บน Remote Host

script module คัดลอก script จาก control node ไปรันบน remote host โดยตรง ไม่ต้อง copy ก่อนแล้วค่อยรัน เหมาะสำหรับ scripts ที่ซับซ้อนหรือมีอยู่แล้ว

---
- name: Using script module
  hosts: all
  tasks:
    - name: Run local bash script on remote
      script: scripts/setup.sh
      register: setup_result

    - name: Run script with arguments
      script: scripts/configure.sh --env production --port 8080

    - name: Run Python script from local
      script: scripts/health_check.py
      args:
        executable: /usr/bin/python3

    - name: Run script only if not already done
      script: scripts/init.sh
      args:
        creates: /opt/myapp/.initialized   # ข้ามถ้า file นี้มีอยู่บน remote แล้ว

script module ไม่ตรวจ idempotency โดยอัตโนมัติ ต้องใช้ creates หรือ removes argument เพื่อควบคุม หรือใช้ร่วมกับ when condition

Register: เก็บ Output จาก Command

register เก็บผลลัพธ์จาก module ไว้ใน variable สำหรับใช้ใน task ถัดไป ตัวแปรที่ได้จะมี fields หลัก ๆ คือ stdout, stderr, rc, และ stdout_lines

---
- name: Register examples
  hosts: all
  tasks:
    - name: Check disk usage
      command: df -h /
      register: disk_result

    - name: Show stdout
      debug:
        msg: "{{ disk_result.stdout }}"

    - name: Show as lines
      debug:
        msg: "{{ disk_result.stdout_lines }}"

    - name: Check return code
      debug:
        msg: "Return code: {{ disk_result.rc }}"

    - name: Run health check
      shell: curl -s -o /dev/null -w "%{http_code}" http://localhost/health
      register: health_code

    - name: Fail if health check bad
      fail:
        msg: "Health check failed: {{ health_code.stdout }}"
      when: health_code.stdout != "200"

    - name: Parse output with conditions
      shell: free -m | awk 'NR==2{print $4}'
      register: free_mem

    - name: Warn if low memory
      debug:
        msg: "Warning: Free memory is {{ free_mem.stdout }}MB"
      when: free_mem.stdout | int < 512

raw Module: สำหรับ Host ที่ไม่มี Python

raw module ส่ง command ไปยัง remote host ผ่าน SSH โดยตรงโดยไม่ต้องการ Python บน remote host เหมาะสำหรับ bootstrap เครื่องใหม่หรือ network devices ที่ไม่มี Python

---
- name: Bootstrap new server
  hosts: new_servers
  gather_facts: false    # ปิด facts gathering เพราะยังไม่มี Python
  tasks:
    - name: Install Python (required for Ansible)
      raw: apt-get install -y python3 python3-apt
      register: python_install

    - name: Check Python installed
      raw: python3 --version
      register: python_version

    - name: Show Python version
      debug:
        var: python_version.stdout

หลังจาก bootstrap แล้ว ให้ใช้ module ปกติแทน raw เสมอ เพราะ raw ไม่มี idempotency, ไม่มี error handling และไม่ return structured data

สร้าง Custom Module ด้วย Python

เมื่อไม่มี module ที่ตรงกับความต้องการ สามารถสร้าง custom module ขึ้นมาเองได้ วาง Python script ไว้ใน directory library/ ข้าง Playbook หรือใน Role

# library/check_port.py — Custom module ตรวจสอบว่า port เปิดอยู่หรือไม่
#!/usr/bin/python3

from ansible.module_utils.basic import AnsibleModule
import socket

def run_module():
    # กำหนด arguments ที่ module รับ
    module_args = dict(
        host=dict(type='str', required=True),
        port=dict(type='int', required=True),
        timeout=dict(type='int', default=5)
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    host = module.params['host']
    port = module.params['port']
    timeout = module.params['timeout']

    # ถ้า check mode ไม่ต้องทำอะไรจริง
    if module.check_mode:
        module.exit_json(changed=False, msg="Check mode: no action taken")

    # ลอง connect port
    try:
        sock = socket.create_connection((host, port), timeout=timeout)
        sock.close()
        module.exit_json(
            changed=False,
            open=True,
            msg=f"Port {port} on {host} is open"
        )
    except (socket.timeout, ConnectionRefusedError, OSError) as e:
        module.exit_json(
            changed=False,
            open=False,
            msg=f"Port {port} on {host} is closed: {str(e)}"
        )

if __name__ == '__main__':
    run_module()
---
# ใช้ custom module ใน Playbook
- name: Check services with custom module
  hosts: all
  tasks:
    - name: Check if web server port is open
      check_port:
        host: "{{ inventory_hostname }}"
        port: 80
        timeout: 3
      register: web_port

    - name: Report port status
      debug:
        msg: "Port 80: {{ web_port.msg }}"

    - name: Check database port
      check_port:
        host: "{{ db_host }}"
        port: 5432
      register: db_port

    - name: Fail if database unreachable
      fail:
        msg: "Database port 5432 is not open!"
      when: not db_port.open

Custom module ต้อง return JSON ผ่าน module.exit_json() (สำเร็จ) หรือ module.fail_json() (ล้มเหลว) เสมอ และควรรองรับ check_mode เพื่อให้ใช้ร่วมกับ --check flag ได้

Module ที่ควรรู้จัก

นอกจาก command/shell/script ยังมี modules พื้นฐานอีกหลายตัวที่ใช้บ่อยใน Playbook ทั่วไป ได้แก่ debug สำหรับ print ข้อความหรือ variable, fail สำหรับหยุด play และแสดงข้อความ error, set_fact สำหรับกำหนด variable ใหม่ระหว่าง play, pause สำหรับหยุดรอ input หรือรอเวลา, wait_for สำหรับรอจนกว่า port หรือ file จะพร้อม และ assert สำหรับตรวจสอบ condition ก่อนดำเนินต่อ

---
- name: Utility modules demo
  hosts: all
  tasks:
    - name: Set computed variable
      set_fact:
        app_url: "http://{{ inventory_hostname }}:{{ app_port }}"

    - name: Wait for port to be open
      wait_for:
        host: "{{ inventory_hostname }}"
        port: "{{ app_port }}"
        delay: 5
        timeout: 60

    - name: Assert service is running
      assert:
        that:
          - app_port is defined
          - app_port | int > 0
          - app_port | int < 65536
        fail_msg: "Invalid app_port: {{ app_port }}"
        success_msg: "app_port {{ app_port }} is valid"

    - name: Pause for manual verification
      pause:
        prompt: "Please verify the deployment at {{ app_url }}. Press Enter to continue"

สรุป

การเลือกใช้ module ที่เหมาะสมช่วยให้ Playbook ปลอดภัยและดูแลรักษาง่าย หลักการง่าย ๆ คือ: ใช้ command เป็น default เมื่อต้องรัน command, ใช้ shell เมื่อต้องการ pipes หรือ shell features, ใช้ script เมื่อมี script ที่ซับซ้อนอยู่แล้วและไม่ต้องการ rewrite เป็น Ansible tasks, และใช้ raw เฉพาะตอน bootstrap เครื่องที่ยังไม่มี Python

สำหรับงานที่ไม่มี built-in module รองรับ การสร้าง custom module ด้วย Python ไม่ซับซ้อน เพียงใช้ AnsibleModule class รับ arguments และ return JSON ก็สามารถใช้งานได้เหมือน built-in module ทุกประการ