Ansible Best Practices: Performance Tuning และ Optimization

Ansible รัน tasks บน hosts จำนวนมากได้เร็วหรือช้าขึ้นอยู่กับการตั้งค่า — ค่า default ของ Ansible อนุรักษ์นิยมและใช้ SSH connections มากกว่าที่จำเป็น การ tune ค่าเพียงไม่กี่จุดสามารถลดเวลา execution จาก 30 นาทีเหลือ 5 นาทีโดยไม่ต้องเปลี่ยน playbook

บทความนี้อธิบายการเพิ่ม forks สำหรับ parallel execution, เปิด pipelining ลด SSH overhead, ใช้ fact caching ไม่ต้อง gather facts ซ้ำ, async tasks สำหรับ long-running operations, strategy: free, และ profile_tasks callback สำหรับหาจุดที่ช้าที่สุด

Forks — Parallel Execution

forks กำหนดจำนวน hosts ที่ Ansible รัน task พร้อมกัน — ค่า default คือ 5 ซึ่งช้ามากสำหรับ infrastructure ขนาดใหญ่ เพิ่มเป็น 20-50 ขึ้นอยู่กับ resource ของ control node

# ansible.cfg — เพิ่ม forks สำหรับ parallel execution
[defaults]
forks = 20          # default คือ 5, เพิ่มได้ถึง 50+ ตาม CPU/RAM

# หรือระบุใน command line
ansible-playbook site.yml -f 20

# ผลกระทบ:
# forks=5  : รัน 100 hosts ใน 20 รอบ (100/5)
# forks=20 : รัน 100 hosts ใน 5 รอบ (100/20)
# forks=50 : รัน 100 hosts ใน 2 รอบ

# serial — จำกัด hosts ต่อ batch ใน play (แตกต่างจาก forks)
- name: Rolling deployment
  hosts: webservers
  serial: 5          # รัน 5 hosts ต่อ batch (ไม่ขนานกันระหว่าง batch)
  tasks:
    - name: Deploy app
      ...

# serial กับ percentage
  serial: "20%"      # 20% ของ hosts ต่อ batch

# serial กับ list (escalating batches)
  serial:
    - 1              # batch แรก: 1 host
    - 5              # batch ที่สอง: 5 hosts
    - "50%"          # batch ที่สาม: 50% ที่เหลือ

SSH Pipelining — ลด Connection Overhead

SSH Pipelining ลดจำนวน SSH connections ที่ต้องเปิดต่อ task — แทนที่จะเปิด connection ใหม่ทุกครั้ง Ansible ส่ง module ผ่าน connection เดิม ลด overhead ได้มากโดยเฉพาะเมื่อมี tasks จำนวนมาก

# ansible.cfg — เปิด SSH pipelining
[ssh_connection]
pipelining = True

# ข้อกำหนด: ต้อง disable requiretty ใน /etc/sudoers บน target hosts
# เพิ่มใน /etc/sudoers หรือ /etc/sudoers.d/ansible:
# Defaults:ansible !requiretty

# ทำผ่าน Ansible task:
- name: Disable requiretty for ansible user
  ansible.builtin.lineinfile:
    path: /etc/sudoers.d/ansible
    line: "Defaults:ansible !requiretty"
    create: true
    validate: "visudo -cf %s"

# ControlMaster — reuse SSH connections
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%r@%h:%p

# ผลลัพธ์: task ที่ใช้เวลา 30s อาจลดเหลือ 15s จาก SSH overhead ที่หายไป

Fact Caching — ไม่ Gather Facts ซ้ำ

Gather facts เป็นขั้นตอนที่ช้ามาก — Ansible ต้องเชื่อมต่อและดึงข้อมูล host facts ก่อนทุก play การ cache facts ช่วยให้รัน playbook ครั้งถัดไปข้ามขั้นตอนนี้ได้ถ้า facts ยังไม่ expire

# ansible.cfg — เปิด fact caching ด้วย jsonfile backend
[defaults]
gathering = smart          # smart = ใช้ cache ถ้ามี, gather ถ้าไม่มี/หมดอายุ
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
fact_caching_timeout = 3600  # cache อยู่ได้ 1 ชั่วโมง

# หรือใช้ Redis backend (สำหรับ multi-node control)
fact_caching = redis
fact_caching_connection = localhost:6379:0
# ต้องติดตั้ง: pip install redis

# ปิด fact gathering บน play ที่ไม่ต้องการ facts
- name: Deploy app (no facts needed)
  hosts: webservers
  gather_facts: false
  tasks:
    - name: Copy application
      ansible.builtin.copy:
        src: app/
        dest: /opt/app/

# gather_facts เฉพาะ subset ที่ต้องการ
- name: Play with minimal facts
  hosts: webservers
  gather_facts: true
  gather_subset:
    - "!all"           # ไม่เก็บ facts ทั้งหมด
    - "!min"           # ไม่เก็บ minimal set
    - network          # เก็บเฉพาะ network facts
    - hardware         # เก็บเฉพาะ hardware facts

async และ poll — Non-blocking Tasks

async รัน task แบบ background โดยไม่รอ response — เหมาะสำหรับ tasks ที่ใช้เวลานาน เช่น yum update, package compilation, หรือ database migration ที่ไม่ต้องการ block tasks ถัดไป

# async task — รันใน background, ไม่รอผลทันที
- name: Run long database migration
  ansible.builtin.command: python manage.py migrate
  async: 600        # timeout สูงสุด 600 วินาที
  poll: 0           # ไม่รอ, ทำ task ถัดไปได้เลย
  register: migration_job

# ทำ tasks อื่นระหว่างรอ
- name: Update static assets
  ansible.builtin.command: python manage.py collectstatic --no-input

# รอผลลัพธ์ของ async task
- name: Wait for migration to complete
  ansible.builtin.async_status:
    jid: "{{ migration_job.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 30
  delay: 20

# รัน tasks แบบ parallel บน host เดียว
# (ปกติ tasks รันทีละ task ต่อ host)
- name: Download package A (async)
  ansible.builtin.get_url:
    url: https://example.com/package-a.tar.gz
    dest: /tmp/package-a.tar.gz
  async: 120
  poll: 0
  register: download_a

- name: Download package B (async)
  ansible.builtin.get_url:
    url: https://example.com/package-b.tar.gz
    dest: /tmp/package-b.tar.gz
  async: 120
  poll: 0
  register: download_b

- name: Wait for all downloads
  ansible.builtin.async_status:
    jid: "{{ item.ansible_job_id }}"
  loop: ["{{ download_a }}", "{{ download_b }}"]
  register: downloads
  until: downloads.finished
  retries: 12
  delay: 10

Strategy: free — ไม่รอ Slowest Host

Strategy default ของ Ansible คือ “linear” — รอให้ทุก host ทำ task เดียวกันเสร็จก่อนไป task ถัดไป Strategy “free” ให้แต่ละ host รัน tasks ต่อไปได้ทันทีโดยไม่รอ host อื่น

# strategy: free — แต่ละ host รันเร็วที่สุดที่ทำได้
- name: Deploy to all servers
  hosts: webservers
  strategy: free      # ไม่รอ host อื่น
  tasks:
    - name: Install packages
      ansible.builtin.apt:
        name: nginx
        state: present
    - name: Start service
      ansible.builtin.systemd:
        name: nginx
        state: started

# strategy: host_pinned — คล้าย free แต่ไม่เกิน forks ต่อ host
- name: Deploy
  hosts: webservers
  strategy: host_pinned

# กำหนด default strategy ใน ansible.cfg
[defaults]
strategy = free

# เปรียบเทียบ linear vs free กับ 3 hosts:
# linear: H1-task1, H2-task1, H3-task1, [wait all], H1-task2, H2-task2...
# free:   H1-task1, H1-task2, H1-task3 (H1 เร็วกว่า H2,H3 ไม่ต้องรอ)

profile_tasks — หา Bottleneck

profile_tasks callback แสดงเวลาที่ใช้ต่อ task — ใช้หา tasks ที่ช้าที่สุดเพื่อ optimize ก่อน แทนที่จะ guess สิ่งที่ต้อง optimize

# ansible.cfg — เปิด profile_tasks callback
[defaults]
callbacks_enabled = profile_tasks, profile_roles, timer

# หรือใช้ ANSIBLE_CALLBACKS_ENABLED environment variable
ANSIBLE_CALLBACKS_ENABLED=profile_tasks ansible-playbook site.yml

# ตัวอย่าง output หลัง playbook รันเสร็จ:
# Thursday 16 April 2026  06:00:00 +0700 (0:00:00.050) 0:02:35.123
# ========================================================
# Install packages --------------------------------------- 45.23s
# Run database migrations -------------------------------- 32.10s
# Pull docker images ------------------------------------- 28.50s
# Configure nginx ---------------------------------------- 5.23s
# Start services ----------------------------------------- 2.10s

# timer callback — แสดงเวลารวมท้าย playbook
# profile_roles — แสดงเวลาต่อ role

# ดู task timing เฉพาะ task ที่สนใจ
- name: Measure package installation time
  ansible.builtin.apt:
    name: "{{ packages }}"
    state: present
  register: install_result

- name: Show installation time
  ansible.builtin.debug:
    msg: "Installation took {{ install_result.delta }}"

Mitogen — Drop-in Accelerator

Mitogen เป็น third-party strategy plugin ที่เร่งความเร็ว Ansible อย่างมีนัยสำคัญ — ใช้ Python multiplexing แทน SSH ทำให้ tasks รันเร็วขึ้น 1.5-7x โดยไม่ต้องเปลี่ยน playbook

# ติดตั้ง Mitogen
pip install mitogen --break-system-packages

# ตรวจสอบ path ของ Mitogen
python3 -c "import mitogen; print(mitogen.__file__)"
# /usr/local/lib/python3.10/dist-packages/mitogen/__init__.py

# ansible.cfg — ใช้ Mitogen strategy
[defaults]
strategy_plugins = /usr/local/lib/python3.10/dist-packages/ansible_mitogen/plugins/strategy
strategy = mitogen_linear   # แทน linear ปกติ

# หรือใช้ mitogen_free
strategy = mitogen_free     # แทน free ปกติ

# ข้อควรระวัง Mitogen:
# - ไม่รองรับ connection plugins บางตัว (เช่น network devices)
# - อาจ conflict กับบาง modules
# - ตรวจสอบ compatibility กับ Ansible version ก่อนใช้ใน production
# - ทดสอบด้วย --check ก่อนเสมอ

# เปรียบเทียบ benchmark (ตัวอย่าง 50 hosts, 20 tasks):
# standard linear : ~8 นาที
# mitogen_linear  : ~2 นาที (เร็วขึ้น ~4x)

Configuration Optimization รวม

ตัวอย่าง ansible.cfg ที่ optimize ทุกตัวเลือกหลักพร้อมกัน — เหมาะสำหรับ production environment ที่ต้องการ performance สูงสุด

# ansible.cfg — production-optimized configuration
[defaults]
# Parallel execution
forks = 20

# Fact caching
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400  # 24 ชั่วโมง

# Callbacks สำหรับ profiling
callbacks_enabled = profile_tasks, timer

# Reduce output noise
display_skipped_hosts = false
display_ok_hosts = false  # ซ่อน OK tasks (แสดงเฉพาะ changed/failed)

# Retry files
retry_files_enabled = false  # ไม่สร้าง .retry files

[ssh_connection]
# SSH optimization
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%r@%h:%p
control_path_dir = /tmp/ansible-ssh

# Connection timeout
timeout = 30

# ผลลัพธ์ที่คาดหวัง:
# playbook 100 hosts, 50 tasks:
# ก่อน optimize : ~25 นาที
# หลัง optimize : ~5-8 นาที (ลดลง 3-5x)

สรุป

Performance tuning ใน Ansible เริ่มที่ forks (เพิ่มจาก 5 เป็น 20+) และ SSH pipelining — สองตัวนี้ให้ผลมากที่สุดโดยไม่ต้องเปลี่ยน playbook เลย จากนั้นเปิด fact caching ลด gather facts overhead, ใช้ async tasks สำหรับ long-running operations, strategy: free เพื่อไม่รอ slow hosts และ profile_tasks หา bottleneck

ลำดับการ optimize: วัดก่อน (profile_tasks) → หา bottleneck → แก้ที่ใหญ่ก่อน (forks, pipelining, fact caching) → วัดอีกครั้ง — อย่า optimize สิ่งที่ไม่ช้า