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 นั้นแทนเสมอ

