Ansible Block: จัดกลุ่ม Tasks และ Apply Block-level Handler

Ansible block ใช้จัดกลุ่ม tasks หลาย ๆ task เข้าด้วยกัน และ apply directive ร่วมกันทั้งกลุ่มได้ในครั้งเดียว — เช่น กำหนด when, become, tags, หรือ notify ระดับ block ให้ทุก task ภายในได้รับผลพร้อมกัน แทนที่จะเขียนซ้ำในแต่ละ task

บทความนี้ครอบคลุมการใช้ block เพื่อจัดกลุ่ม tasks, การ apply when ระดับ block, การ apply become เฉพาะกลุ่ม, การใช้ tags กับ block, การ notify handler จาก block, การซ้อน blocks และ pattern สำหรับ multi-stage deployment

Block พื้นฐาน — จัดกลุ่ม Tasks

block เป็น keyword ระดับเดียวกับ name ใน task — tasks ทั้งหมดที่อยู่ภายใน block จะทำงานตามลำดับเหมือน tasks ปกติ แต่ได้รับประโยชน์จาก directive ที่กำหนดไว้ในระดับ block

---
- name: Block basic grouping
  hosts: all
  tasks:
    - name: Install and configure Nginx
      block:
        - name: Install Nginx
          apt:
            name: nginx
            state: present
            update_cache: true

        - name: Create web root
          file:
            path: /var/www/mysite
            state: directory
            owner: www-data
            mode: '0755'

        - name: Deploy index page
          copy:
            content: "Hello from Ansible"
            dest: /var/www/mysite/index.html
            owner: www-data

        - name: Start and enable Nginx
          service:
            name: nginx
            state: started
            enabled: true

Block กับ when — Apply เงื่อนไขทั้งกลุ่ม

แทนที่จะเพิ่ม when ในทุก task สามารถกำหนด when ที่ระดับ block เพียงครั้งเดียว — ถ้าเงื่อนไขเป็น false ทุก task ใน block จะถูก skip พร้อมกัน

---
- name: Block with when condition
  hosts: all
  become: true
  vars:
    install_monitoring: true
    env: production

  tasks:
    # ทุก task ใน block นี้จะรันเฉพาะเมื่อ install_monitoring เป็น true
    - name: Install monitoring stack
      block:
        - name: Install Prometheus
          apt:
            name: prometheus
            state: present

        - name: Install Grafana
          apt:
            name: grafana
            state: present

        - name: Configure Prometheus
          template:
            src: prometheus.yml.j2
            dest: /etc/prometheus/prometheus.yml

        - name: Start services
          service:
            name: "{{ item }}"
            state: started
            enabled: true
          loop:
            - prometheus
            - grafana-server
      when: install_monitoring | bool   # apply เงื่อนไขนี้กับทุก task ใน block

    # block สำหรับ production เท่านั้น
    - name: Production-only hardening
      block:
        - name: Disable root SSH
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^PermitRootLogin'
            line: 'PermitRootLogin no'

        - name: Set password policy
          command: /opt/scripts/set-password-policy.sh

        - name: Enable audit logging
          service:
            name: auditd
            state: started
            enabled: true
      when: env == "production"

Block กับ become — เปลี่ยน Privilege เฉพาะกลุ่ม

become ที่ระดับ block ช่วยจำกัดการใช้ root privilege เฉพาะ tasks ที่จำเป็นจริง ๆ โดยไม่ต้องเปิด become: true ทั้ง play — เป็น principle of least privilege ที่ดี

---
- name: Block with become privilege separation
  hosts: all
  # ไม่มี become ระดับ play

  tasks:
    # tasks ส่วนนี้รันในฐานะ user ปกติ (ไม่ต้องการ root)
    - name: Read application config
      slurp:
        src: /home/appuser/app.conf
      register: app_config

    - name: Check application health
      uri:
        url: http://localhost:8080/health
      register: health

    # tasks ที่ต้องการ root — ใช้ become ระดับ block
    - name: System-level configuration
      block:
        - name: Install system packages
          apt:
            name:
              - libssl-dev
              - build-essential
            state: present

        - name: Configure kernel parameters
          sysctl:
            name: net.core.somaxconn
            value: '1024'
            state: present

        - name: Set open file limits
          pam_limits:
            domain: appuser
            limit_type: '-'
            limit_item: nofile
            value: '65536'
      become: true           # apply become เฉพาะ block นี้
      become_user: root

    # tasks หลัง block กลับไปเป็น user ปกติ
    - name: Verify configuration
      command: /home/appuser/verify.sh
      changed_when: false

Block กับ tags — Tag ทั้งกลุ่มพร้อมกัน

กำหนด tags ที่ระดับ block เพื่อให้ทุก task ใน block ได้รับ tag นั้น — เมื่อรัน ansible-playbook --tags install จะรันเฉพาะ tasks ที่อยู่ใน block ที่ tag เป็น install ทั้งหมด

---
- name: Block with tags for selective execution
  hosts: all
  become: true

  tasks:
    # รัน: ansible-playbook site.yml --tags install
    - name: Installation tasks
      block:
        - name: Install application
          apt:
            name: myapp
            state: present

        - name: Install dependencies
          apt:
            name:
              - libmysqlclient-dev
              - redis-tools
            state: present
      tags: [install]

    # รัน: ansible-playbook site.yml --tags configure
    - name: Configuration tasks
      block:
        - name: Deploy app config
          template:
            src: app.conf.j2
            dest: /etc/myapp/app.conf

        - name: Set environment variables
          lineinfile:
            path: /etc/environment
            line: "APP_ENV={{ env }}"

        - name: Configure log rotation
          template:
            src: logrotate.conf.j2
            dest: /etc/logrotate.d/myapp
      tags: [configure]

    # รัน: ansible-playbook site.yml --tags restart
    - name: Service restart tasks
      block:
        - name: Restart application
          service:
            name: myapp
            state: restarted

        - name: Verify application started
          uri:
            url: http://localhost:8080/health
            status_code: 200
          retries: 3
          delay: 5
      tags: [restart]

Block กับ notify — Trigger Handler จาก Block

notify ที่ระดับ block จะ trigger handler เมื่อ task ใด task หนึ่งใน block เกิด changed — ไม่ต้องเพิ่ม notify ในทุก task ที่ควร trigger handler เดียวกัน

---
- name: Block with notify handler
  hosts: all
  become: true

  tasks:
    # เมื่อมี task ใดใน block นี้ "changed" → trigger Reload Nginx
    - name: Nginx configuration changes
      block:
        - name: Deploy main nginx config
          template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf

        - name: Deploy virtual host configs
          template:
            src: "{{ item.src }}"
            dest: "{{ item.dest }}"
          loop:
            - { src: site1.conf.j2, dest: /etc/nginx/conf.d/site1.conf }
            - { src: site2.conf.j2, dest: /etc/nginx/conf.d/site2.conf }

        - name: Deploy SSL certificates
          copy:
            src: "certs/{{ item }}"
            dest: "/etc/nginx/certs/{{ item }}"
          loop:
            - mysite.crt
            - mysite.key
      notify: Reload Nginx   # trigger handler ถ้า config หรือ cert เปลี่ยน

    # block สำหรับ application config
    - name: Application configuration changes
      block:
        - name: Deploy application config
          template:
            src: app.conf.j2
            dest: /etc/myapp/app.conf

        - name: Deploy database config
          template:
            src: db.conf.j2
            dest: /etc/myapp/db.conf
      notify: Restart Application   # trigger handler เมื่อ app config เปลี่ยน

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

    - name: Restart Application
      service:
        name: myapp
        state: restarted

Nested Blocks — Block ซ้อน Block

สามารถซ้อน block ภายใน block ได้ เพื่อแบ่งกลุ่มตาม stage หรือ responsibility — directive ของ outer block ไม่ถ่ายทอดลง inner block อัตโนมัติ แต่สามารถกำหนด directive แยกในแต่ละระดับได้

---
- name: Nested blocks example
  hosts: all
  become: true
  vars:
    deploy_version: "2.5.0"
    env: production

  tasks:
    - name: Full deployment process
      block:
        # Inner block 1 — prerequisites (เฉพาะ production)
        - name: Pre-deployment checks
          block:
            - name: Check disk space
              command: df -h /opt
              changed_when: false

            - name: Verify database connectivity
              command: /opt/myapp/bin/check-db.sh
              changed_when: false

            - name: Check external service availability
              uri:
                url: "{{ external_api_url }}/health"
                status_code: 200
          when: env == "production"

        # Inner block 2 — deployment (ทุก env)
        - name: Deploy application
          block:
            - name: Pull new version
              git:
                repo: "{{ app_repo }}"
                dest: /opt/myapp/current
                version: "{{ deploy_version }}"

            - name: Install dependencies
              command: /opt/myapp/current/bin/install-deps.sh
              args:
                chdir: /opt/myapp/current

            - name: Run migrations
              command: /opt/myapp/current/bin/migrate.sh
              register: migration_result
              changed_when: '"Applied" in migration_result.stdout'

        # Inner block 3 — service restart
        - name: Restart services
          block:
            - name: Restart application
              service:
                name: myapp
                state: restarted

            - name: Verify health
              uri:
                url: http://localhost:8080/health
                status_code: 200
              retries: 5
              delay: 3
          notify: Send Deployment Notification

      rescue:
        - name: Handle deployment failure
          debug:
            msg: "Deployment failed at step: {{ ansible_failed_task.name }}"

  handlers:
    - name: Send Deployment Notification
      debug:
        msg: "Deployment of {{ deploy_version }} completed successfully"

Pattern: Multi-Stage Deployment ด้วย Blocks

ตัวอย่าง Playbook deploy application แบบ multi-stage โดยใช้ blocks แยก stage ออกจากกัน — แต่ละ stage มี when, become, tags เป็นของตัวเอง และ notify handler ที่เหมาะสม

---
- name: Multi-stage deployment with blocks
  hosts: appservers
  vars:
    app_name: myapp
    deploy_version: "{{ target_version | default('latest') }}"
    env: "{{ deployment_env | default('staging') }}"

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded
      become: true

    - name: Restart Application
      service:
        name: "{{ app_name }}"
        state: restarted
      become: true

  tasks:
    # Stage 1: System preparation
    - name: Stage 1 - System preparation
      block:
        - name: Update package cache
          apt:
            update_cache: true
            cache_valid_time: 3600

        - name: Ensure required packages
          apt:
            name:
              - curl
              - git
              - unzip
            state: present
      become: true
      tags: [prepare, always]

    # Stage 2: Deploy application code
    - name: Stage 2 - Deploy application
      block:
        - name: Create app directories
          file:
            path: "{{ item }}"
            state: directory
            owner: "{{ app_name }}"
            mode: '0755'
          loop:
            - "/opt/{{ app_name }}/releases"
            - "/opt/{{ app_name }}/shared/config"
            - "/opt/{{ app_name }}/shared/logs"

        - name: Deploy release
          git:
            repo: "{{ app_repo }}"
            dest: "/opt/{{ app_name }}/releases/{{ deploy_version }}"
            version: "{{ deploy_version }}"

        - name: Link shared directories
          file:
            src: "/opt/{{ app_name }}/shared/{{ item }}"
            dest: "/opt/{{ app_name }}/releases/{{ deploy_version }}/{{ item }}"
            state: link
          loop:
            - config
            - logs

        - name: Update current symlink
          file:
            src: "/opt/{{ app_name }}/releases/{{ deploy_version }}"
            dest: "/opt/{{ app_name }}/current"
            state: link
      become: true
      become_user: "{{ app_name }}"
      tags: [deploy]
      notify: Restart Application

    # Stage 3: Configure web server
    - name: Stage 3 - Web server configuration
      block:
        - name: Deploy Nginx virtual host
          template:
            src: "nginx-{{ app_name }}.conf.j2"
            dest: "/etc/nginx/conf.d/{{ app_name }}.conf"

        - name: Validate Nginx config
          command: nginx -t
          changed_when: false
      become: true
      tags: [nginx, configure]
      notify: Reload Nginx
      when: configure_nginx | default(true) | bool

    # Stage 4: Post-deploy verification
    - name: Stage 4 - Verify deployment
      block:
        - name: Wait for application to be ready
          uri:
            url: "http://localhost:8080/health"
            status_code: 200
          retries: 10
          delay: 5

        - name: Run smoke tests
          command: "/opt/{{ app_name }}/current/bin/smoke-test.sh"
          changed_when: false

        - name: Log successful deployment
          lineinfile:
            path: "/var/log/{{ app_name }}-deployments.log"
            line: "{{ ansible_date_time.iso8601 }} version={{ deploy_version }} env={{ env }} status=success"
            create: true
      become: true
      tags: [verify]

สรุป

block ช่วยจัดกลุ่ม tasks ที่เกี่ยวข้องกันเข้าไว้ด้วยกัน ทำให้ Playbook อ่านง่ายและลดความซ้ำซ้อน — directive ที่ apply ที่ระดับ block ได้แก่ when, become, tags, notify, ignore_errors, และ rescue/always สำหรับ error handling

Pattern ที่ควรจำ: ใช้ when ระดับ block แทนการเขียนซ้ำในทุก task, ใช้ become ระดับ block เพื่อจำกัด privilege เฉพาะส่วนที่จำเป็น, ใช้ tags ระดับ block เพื่อเปิด/ปิด stage การทำงาน, ใช้ notify ระดับ block เพื่อ trigger handler เมื่อมีการเปลี่ยนแปลงในกลุ่ม และใช้ nested blocks เพื่อแบ่ง deployment ออกเป็น stages ที่ชัดเจน