Ansible shell Module และ command Module: รัน Commands บน Server

Ansible มี 2 module สำหรับรัน commands บน remote server คือ command และ shell ความแตกต่างหลักอยู่ที่ command รันคำสั่งโดยตรงโดยไม่ผ่าน shell ทำให้ปลอดภัยกว่าแต่ไม่รองรับ shell features เช่น pipe (|), redirect (>), หรือ environment variables ส่วน shell รันผ่าน /bin/sh จึงรองรับ shell syntax ครบถ้วนแต่ต้องระวัง injection risks

บทความนี้อธิบายการใช้ทั้ง 2 module ครอบคลุม syntax พื้นฐาน, การเลือกใช้ให้ถูกกรณี, changed_when และ failed_when สำหรับควบคุม idempotency, การใช้ register รับ output กลับมาประมวลผล และ pattern สำหรับ system health check

command Module พื้นฐาน

command module รันคำสั่งโดยตรงโดยไม่ผ่าน shell interpreter เหมาะสำหรับคำสั่งง่ายๆ ที่ไม่ต้องการ shell features ปลอดภัยกว่า shell เพราะไม่เสี่ยง shell injection

---
- name: Basic command module usage
  hosts: all
  become: true
  tasks:
    - name: Check disk usage
      command: df -h /

    - name: Check memory info
      command: free -m

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

    - name: Run application binary
      command: /opt/myapp/bin/myapp --config /etc/myapp/app.conf

    - name: Check if service is running
      command: systemctl is-active nginx
      register: nginx_status
      failed_when: false

command module ไม่รองรับ |, &, >, <, ;, $() — ถ้าต้องการ shell features เหล่านี้ต้องใช้ shell module แทน

shell Module พื้นฐาน

shell module รันคำสั่งผ่าน /bin/sh รองรับ pipe, redirect, glob patterns, และ environment variables เหมาะสำหรับคำสั่งที่ต้องการ shell syntax

---
- name: shell module examples
  hosts: all
  become: true
  tasks:
    - name: Check process count
      shell: ps aux | grep nginx | grep -v grep | wc -l

    - name: Get last 10 lines of log
      shell: tail -n 10 /var/log/nginx/error.log > /tmp/nginx_errors.txt

    - name: Find and delete old log files
      shell: find /var/log/myapp -name "*.log" -mtime +30 -exec rm {} \;

    - name: Get current memory usage percentage
      shell: free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}'
      register: mem_usage

    - name: Run script with environment variable
      shell: APP_ENV=production /opt/myapp/bin/migrate.sh
      environment:
        DB_HOST: "{{ db_host }}"
        DB_PASS: "{{ db_password }}"

command vs shell — เลือกใช้อย่างไร

หลักการเลือกง่ายๆ คือ ใช้ command เป็น default และเปลี่ยนไป shell เฉพาะเมื่อจำเป็น การใช้ shell โดยไม่จำเป็นเพิ่ม risk และทำให้ debug ยากขึ้น

---
- name: command vs shell comparison
  hosts: all
  tasks:
    # ใช้ command: ไม่ต้องการ shell features
    - name: Get hostname (use command)
      command: hostname -f

    - name: Check file exists (use command)
      command: test -f /etc/app/app.conf
      register: conf_exists
      failed_when: false

    # ใช้ shell: ต้องการ pipe หรือ redirect
    - name: Count failed logins (use shell - needs grep+wc)
      shell: grep "Failed password" /var/log/auth.log | wc -l
      register: failed_logins

    - name: Get top memory processes (use shell - needs sort+head)
      shell: ps aux --sort=-%mem | head -5

    # ใช้ shell: ต้องการ environment variable expansion
    - name: Run script using $HOME (use shell)
      shell: $HOME/scripts/cleanup.sh

    # ใช้ command: path ชัดเจนไม่ต้องการ shell
    - name: Run script with absolute path (use command)
      command: /home/deploy/scripts/cleanup.sh

changed_when และ failed_when

ปัญหาของ command และ shell คือ Ansible ถือว่าทุก execution “changed” เสมอ แม้ว่าคำสั่งไม่ได้เปลี่ยนแปลง state ใดๆ ใช้ changed_when และ failed_when เพื่อควบคุม idempotency

---
- name: Idempotency with changed_when and failed_when
  hosts: all
  tasks:
    # คำสั่ง read-only — ไม่มีการเปลี่ยนแปลง
    - name: Check service status (never changes)
      command: systemctl is-active mysql
      register: mysql_status
      changed_when: false    # ไม่ใช่ changed เพราะแค่อ่านสถานะ
      failed_when: false     # ไม่ fail แม้ service หยุด

    # ตรวจ output เพื่อตัดสินว่า changed หรือไม่
    - name: Run database migration
      command: /opt/myapp/bin/migrate --check-and-run
      register: migrate_result
      changed_when: "'Applied' in migrate_result.stdout"

    # custom failure condition
    - name: Check disk usage
      shell: df / | tail -1 | awk '{print $5}' | tr -d '%'
      register: disk_usage
      changed_when: false
      failed_when: disk_usage.stdout | int > 85

    # ตรวจ return code
    - name: Check if user exists
      command: id deploy
      register: user_check
      changed_when: false
      failed_when: user_check.rc not in [0, 1]

register — รับ Output มาใช้ต่อ

register เก็บ output ของคำสั่งไว้ใน variable สามารถนำไปใช้ใน task ถัดไปได้ ข้อมูลที่ได้รับมีทั้ง stdout, stderr, rc (return code), และ stdout_lines

---
- name: Using register to capture output
  hosts: all
  tasks:
    - name: Get OS version
      command: cat /etc/os-release
      register: os_info
      changed_when: false

    - name: Print OS info
      debug:
        msg: "{{ os_info.stdout }}"

    - name: Get list of running services
      shell: systemctl list-units --type=service --state=running --no-pager --no-legend | awk '{print $1}'
      register: running_services
      changed_when: false

    - name: Check if specific service is running
      debug:
        msg: "nginx is running"
      when: "'nginx.service' in running_services.stdout_lines"

    - name: Get application version
      command: /opt/myapp/bin/myapp --version
      register: app_version
      changed_when: false
      failed_when: app_version.rc != 0

    - name: Store version as fact
      set_fact:
        myapp_version: "{{ app_version.stdout | trim }}"

args: chdir และ creates/removes

chdir เปลี่ยน working directory ก่อนรันคำสั่ง ส่วน creates และ removes ช่วยสร้าง idempotency โดยข้ามคำสั่งถ้าไฟล์ที่ระบุมีอยู่แล้ว (หรือยังไม่มี)

---
- name: chdir and creates/removes examples
  hosts: all
  become: true
  tasks:
    # chdir: รัน command จาก directory ที่กำหนด
    - name: Run make install from source directory
      command: make install
      args:
        chdir: /usr/local/src/myapp-2.0

    # creates: ข้ามถ้าไฟล์นี้มีอยู่แล้ว (idempotent)
    - name: Extract archive only if not already extracted
      command: tar -xzf /tmp/myapp.tar.gz -C /opt/
      args:
        creates: /opt/myapp/bin/myapp

    # removes: ข้ามถ้าไฟล์นี้ไม่มี
    - name: Remove lock file if exists
      command: rm /var/run/myapp.lock
      args:
        removes: /var/run/myapp.lock

    # ใช้ shell กับ chdir
    - name: Run npm install in project directory
      shell: npm install --production
      args:
        chdir: /opt/webapp

Pattern: System Health Check

ตัวอย่าง Playbook ใช้ command และ shell ตรวจสอบสถานะระบบก่อน deploy โดยรวบรวม facts จากหลายคำสั่งแล้วแสดงสรุป

---
- name: System Health Check
  hosts: all
  gather_facts: true
  tasks:
    - name: Check disk usage
      shell: df / | tail -1 | awk '{print $5}' | tr -d '%'
      register: disk_pct
      changed_when: false

    - name: Check memory usage
      shell: free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}'
      register: mem_pct
      changed_when: false

    - name: Check CPU load (1 min avg)
      shell: cat /proc/loadavg | awk '{print $1}'
      register: cpu_load
      changed_when: false

    - name: Check critical services
      command: systemctl is-active "{{ item }}"
      register: service_checks
      changed_when: false
      failed_when: false
      loop:
        - nginx
        - mysql
        - redis

    - name: Fail if disk usage over 85%
      fail:
        msg: "Disk usage critical: {{ disk_pct.stdout }}%"
      when: disk_pct.stdout | int > 85

    - name: Warn if memory usage over 80%
      debug:
        msg: "WARNING: Memory usage at {{ mem_pct.stdout }}%"
      when: mem_pct.stdout | int > 80

    - name: Print health summary
      debug:
        msg:
          - "Host: {{ inventory_hostname }}"
          - "Disk: {{ disk_pct.stdout }}%"
          - "Memory: {{ mem_pct.stdout }}%"
          - "Load: {{ cpu_load.stdout }}"

สรุป

command module เป็นตัวเลือกที่ปลอดภัยกว่าสำหรับรันคำสั่งที่ไม่ต้องการ shell features ส่วน shell เหมาะเมื่อต้องใช้ pipe, redirect, หรือ shell syntax Pattern ที่ควรจำ: ใช้ changed_when: false กับคำสั่ง read-only ทุกตัว, ใช้ register เพื่อรับ output มาตรวจสอบหรือส่งต่อ และใช้ args: creates เพื่อสร้าง idempotency กับคำสั่งที่ติดตั้งหรือ extract ไฟล์

ข้อควรระวัง: ทั้ง command และ shell รัน task ทุกครั้งโดย default (ไม่ idempotent) ดังนั้นควรใช้ changed_when, creates, หรือ when condition เพื่อหลีกเลี่ยงการรันซ้ำที่ไม่จำเป็น ถ้า Ansible มี dedicated module สำหรับงานนั้น (เช่น apt, copy, file) ให้ใช้ module นั้นแทนเสมอ