Ansible Loops: repeat Tasks หลาย ๆ ครั้งกับ Loop Data

Ansible loop ใช้รัน task ซ้ำหลายครั้งด้วยข้อมูลที่ต่างกัน — เช่น ติดตั้ง packages หลายตัว, สร้างหลาย users, หรือ deploy หลาย config files ในคำสั่งเดียว แทนที่จะเขียน task แยกสำหรับแต่ละรายการ

บทความนี้ครอบคลุม syntax พื้นฐานของ loop, การวนซ้ำบน list, dictionary, และ nested data, การใช้ loop_control เพื่อควบคุม output, การใช้ register กับ loop และ pattern สำหรับ bulk provisioning

loop พื้นฐาน — วนซ้ำบน List

ใช้ loop กับ list ของ string — แต่ละ item จะถูกเข้าถึงผ่าน {{ item }} ใน task loop เป็น syntax มาตรฐานที่แนะนำใน Ansible 2.5+ แทน with_items รุ่นเก่า

---
- name: Basic loop usage
  hosts: all
  become: true

  tasks:
    # วนซ้ำบน list ของ string
    - name: Install required packages
      apt:
        name: "{{ item }}"
        state: present
        update_cache: true
      loop:
        - nginx
        - curl
        - git
        - unzip
        - python3-pip

    # สร้างหลาย directories
    - name: Create application directories
      file:
        path: "{{ item }}"
        state: directory
        owner: appuser
        mode: '0755'
      loop:
        - /opt/myapp
        - /opt/myapp/bin
        - /opt/myapp/config
        - /opt/myapp/logs
        - /opt/myapp/data

    # ลบหลาย files
    - name: Remove old log files
      file:
        path: "{{ item }}"
        state: absent
      loop:
        - /var/log/myapp/old.log
        - /var/log/myapp/backup.log
        - /tmp/myapp.pid

วนซ้ำบน List of Dictionaries

เมื่อต้องการส่งหลาย parameters ต่อรายการ ใช้ list of dictionaries — เข้าถึงแต่ละ field ด้วย {{ item.field_name }}

---
- name: Loop over dictionaries
  hosts: all
  become: true

  tasks:
    # สร้างหลาย users พร้อม properties
    - name: Create system users
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.groups }}"
        shell: "{{ item.shell | default('/bin/bash') }}"
        state: present
      loop:
        - { name: appuser, uid: 1001, groups: "www-data", shell: /bin/bash }
        - { name: deploy,  uid: 1002, groups: "deploy",   shell: /bin/bash }
        - { name: monitor, uid: 1003, groups: "monitor",  shell: /bin/false }

    # Deploy หลาย config files จาก templates
    - name: Deploy configuration files
      template:
        src: "{{ item.src }}"
        dest: "{{ item.dest }}"
        owner: "{{ item.owner | default('root') }}"
        mode: "{{ item.mode | default('0644') }}"
      loop:
        - { src: nginx.conf.j2,    dest: /etc/nginx/nginx.conf }
        - { src: app.conf.j2,      dest: /etc/myapp/app.conf, owner: appuser, mode: '0640' }
        - { src: logrotate.conf.j2, dest: /etc/logrotate.d/myapp }

    # เพิ่ม firewall rules
    - name: Open firewall ports
      ufw:
        rule: allow
        port: "{{ item.port }}"
        proto: "{{ item.proto | default('tcp') }}"
      loop:
        - { port: "22",  proto: tcp }
        - { port: "80",  proto: tcp }
        - { port: "443", proto: tcp }
        - { port: "8080" }

วนซ้ำบน Variable List

แทนที่จะ hardcode list ใน task สามารถอ่านจาก variable ที่ define ใน vars, inventory หรือ group vars ทำให้ Playbook ยืดหยุ่นและ reusable มากขึ้น

---
- name: Loop with variables
  hosts: all
  become: true
  vars:
    required_packages:
      - nginx
      - nodejs
      - postgresql-client
    app_users:
      - name: appuser
        groups: www-data
      - name: deploy
        groups: deploy
    vhosts:
      - name: mysite
        domain: mysite.example.com
        port: 80
      - name: api
        domain: api.example.com
        port: 8080

  tasks:
    - name: Install packages from variable
      apt:
        name: "{{ item }}"
        state: present
      loop: "{{ required_packages }}"

    - name: Create users from variable
      user:
        name: "{{ item.name }}"
        groups: "{{ item.groups }}"
        state: present
      loop: "{{ app_users }}"

    - name: Deploy virtual hosts
      template:
        src: vhost.conf.j2
        dest: "/etc/nginx/conf.d/{{ item.name }}.conf"
      loop: "{{ vhosts }}"
      notify: Reload Nginx

loop กับ register — เก็บผลทุก Iteration

เมื่อใช้ register กับ loop ผลลัพธ์จะถูกเก็บเป็น list ใน .results property — แต่ละ item ใน list มี .item บอกว่าผลนั้นมาจาก iteration ไหน

---
- name: register with loop
  hosts: all
  tasks:
    # ตรวจสถานะหลาย services
    - name: Check service status
      command: "systemctl is-active {{ item }}"
      register: service_status
      ignore_errors: true
      changed_when: false
      loop:
        - nginx
        - postgresql
        - myapp
        - redis

    # แสดงสถานะแต่ละ service
    - name: Show service status
      debug:
        msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}"
      loop: "{{ service_status.results }}"

    # ตรวจ files หลายไฟล์
    - name: Check if config files exist
      stat:
        path: "{{ item }}"
      register: config_checks
      loop:
        - /etc/nginx/nginx.conf
        - /etc/myapp/app.conf
        - /etc/postgresql/14/main/postgresql.conf

    - name: Report missing configs
      debug:
        msg: "MISSING: {{ item.item }}"
      loop: "{{ config_checks.results }}"
      when: not item.stat.exists

loop_control — ควบคุม Label และ Pause

loop_control ใช้ปรับพฤติกรรมของ loop เช่น กำหนด label ที่แสดงใน output แทน item ทั้งหมด, หยุดรอระหว่าง iterations หรือเปลี่ยนชื่อตัวแปร loop variable

---
- name: loop_control examples
  hosts: all
  tasks:
    # label: แสดงแค่ชื่อแทน item ทั้งหมด
    - name: Deploy user configurations
      template:
        src: user.conf.j2
        dest: "/home/{{ item.name }}/.myapp.conf"
        owner: "{{ item.name }}"
        mode: '0600'
      loop:
        - { name: alice, role: admin,    quota: 10000 }
        - { name: bob,   role: developer, quota: 5000 }
        - { name: carol, role: viewer,    quota: 1000 }
      loop_control:
        label: "{{ item.name }}"   # แสดงแค่ชื่อใน output แทน dict ทั้งก้อน

    # pause: หน่วงเวลาระหว่าง iterations
    - name: Restart services with delay
      service:
        name: "{{ item }}"
        state: restarted
      loop:
        - nginx
        - myapp
        - worker
      loop_control:
        pause: 5   # รอ 5 วินาทีระหว่าง service restart

    # loop_var: เปลี่ยนชื่อจาก item เป็นชื่ออื่น (สำหรับ nested loops)
    - name: Process servers
      debug:
        msg: "Processing: {{ server.name }}"
      loop: "{{ servers }}"
      loop_control:
        loop_var: server

with_dict — วนซ้ำบน Dictionary

ใช้ loop กับ dict2items filter เพื่อวนซ้ำบน dictionary — แต่ละ iteration จะมี item.key และ item.value

---
- name: Loop over dictionary
  hosts: all
  vars:
    app_env_vars:
      DATABASE_URL: "postgresql://localhost/myapp"
      REDIS_URL: "redis://localhost:6379"
      LOG_LEVEL: "info"
      MAX_WORKERS: "4"

    nginx_params:
      worker_processes: auto
      worker_connections: 1024
      keepalive_timeout: 65

  tasks:
    # วนซ้ำบน dict ด้วย dict2items filter
    - name: Set environment variables
      lineinfile:
        path: /etc/myapp/environment
        regexp: "^{{ item.key }}="
        line: "{{ item.key }}={{ item.value }}"
        create: true
      loop: "{{ app_env_vars | dict2items }}"
      loop_control:
        label: "{{ item.key }}"

    # ใช้ key และ value ใน template logic
    - name: Show config entries
      debug:
        msg: "Setting {{ item.key }} = {{ item.value }}"
      loop: "{{ nginx_params | dict2items }}"

Pattern: Bulk User และ SSH Key Provisioning

ตัวอย่าง Playbook สร้าง users, directories และ SSH authorized keys จาก variable list — pattern ที่ใช้บ่อยใน user onboarding automation

---
- name: Bulk user provisioning
  hosts: all
  become: true
  vars:
    dev_users:
      - name: alice
        uid: 2001
        groups: ["sudo", "docker"]
        ssh_key: "ssh-ed25519 AAAA... alice@workstation"
      - name: bob
        uid: 2002
        groups: ["docker"]
        ssh_key: "ssh-ed25519 AAAA... bob@workstation"
      - name: carol
        uid: 2003
        groups: ["sudo", "docker", "deploy"]
        ssh_key: "ssh-ed25519 AAAA... carol@workstation"

  tasks:
    # สร้าง users
    - name: Create developer accounts
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.groups }}"
        append: true
        shell: /bin/bash
        create_home: true
        state: present
      loop: "{{ dev_users }}"
      loop_control:
        label: "{{ item.name }}"

    # สร้าง .ssh directory
    - name: Create .ssh directories
      file:
        path: "/home/{{ item.name }}/.ssh"
        state: directory
        owner: "{{ item.name }}"
        group: "{{ item.name }}"
        mode: '0700'
      loop: "{{ dev_users }}"
      loop_control:
        label: "{{ item.name }}"

    # เพิ่ม SSH authorized keys
    - name: Add authorized SSH keys
      authorized_key:
        user: "{{ item.name }}"
        key: "{{ item.ssh_key }}"
        state: present
      loop: "{{ dev_users }}"
      loop_control:
        label: "{{ item.name }}"

    # สร้าง sudoers rules
    - name: Configure sudo access
      lineinfile:
        path: "/etc/sudoers.d/{{ item.name }}"
        line: "{{ item.name }} ALL=(ALL) NOPASSWD:ALL"
        create: true
        mode: '0440'
        validate: 'visudo -cf %s'
      loop: "{{ dev_users }}"
      when: '"sudo" in item.groups'
      loop_control:
        label: "{{ item.name }}"

สรุป

loop ทำให้ Playbook กระชับและ reusable โดยไม่ต้องเขียน task ซ้ำ รองรับ list, list of dicts, dictionary ผ่าน dict2items และ variable list จาก inventory หรือ vars

Pattern ที่ควรจำ: ใช้ loop_control.label เสมอเมื่อ item เป็น dict เพื่อให้ output อ่านง่าย, ใช้ register กับ loop แล้วเข้าถึงผลผ่าน .results, ใช้ when ร่วมกับ loop เพื่อ skip เฉพาะ item ที่ไม่ตรงเงื่อนไข และใช้ loop_control.pause เมื่อ restart services เพื่อให้ระบบ stabilize ระหว่าง iterations