Ansible Playbook สำหรับ System Updates และ Package Management

การอัปเดตระบบปฏิบัติการและจัดการ Package เป็นงานที่ Sysadmin ต้องทำเป็นประจำ โดยเฉพาะเมื่อมีเซิร์ฟเวอร์หลายสิบหรือหลายร้อยเครื่อง การทำด้วยมือทีละเครื่องนั้นเสียเวลาและเสี่ยงต่อความผิดพลาด เช่น ลืม server บางเครื่อง หรือใช้คำสั่งไม่ถูกต้อง

บทความนี้จะอธิบายวิธีสร้าง Playbook สำหรับงาน System Updates และ Package Management ทั้งบน Ubuntu/Debian (apt) และ CentOS/RHEL (yum/dnf) พร้อมเทคนิคการอัปเดตแบบ Rolling Update ที่ไม่กระทบ production service

ทำไมต้องจัดการ System Updates ด้วย Automation

การอัปเดตระบบด้วยตนเองบนหลายเครื่องพร้อมกันมีความเสี่ยงสูง เพราะยากต่อการควบคุมและตรวจสอบ ปัญหาที่พบบ่อยได้แก่ server บางเครื่องได้รับ patch ล่าช้า, version ของ package ไม่สม่ำเสมอข้ามเซิร์ฟเวอร์, และไม่มี log บันทึกการเปลี่ยนแปลง การใช้ Automation ช่วยแก้ปัญหาเหล่านี้ได้โดยตรง

  • ความสม่ำเสมอ: ทุกเซิร์ฟเวอร์ได้รับ package version เดียวกันในเวลาเดียวกัน
  • ตรวจสอบได้: มี log บันทึกว่า host ใดอัปเดตสำเร็จหรือล้มเหลว
  • ปลอดภัยกว่า: ทดสอบด้วย dry-run ก่อนรันจริงบน production ได้
  • ประหยัดเวลา: รัน update 100 เครื่องพร้อมกันในเวลาเดียวกับการอัปเดต 1 เครื่องด้วยตนเอง

Playbook สำหรับ Ubuntu/Debian (apt)

ระบบที่ใช้ Debian-based distribution เช่น Ubuntu ใช้ apt เป็น package manager Ansible มี module apt ที่ทำงานเหมือนคำสั่ง apt-get แต่มีความสามารถเพิ่มเติมหลายอย่าง เช่น ตรวจสอบ cache validity และ autoremove ได้ในคำสั่งเดียว

อัปเดต Cache และ Upgrade ทั้งระบบ

---
- name: System Updates on Ubuntu/Debian
  hosts: ubuntu_servers
  become: true

  tasks:
    - name: Update apt package cache
      ansible.builtin.apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Upgrade all packages to latest version
      ansible.builtin.apt:
        upgrade: dist
        autoremove: yes

    - name: Check if reboot is required
      ansible.builtin.stat:
        path: /var/run/reboot-required
      register: reboot_required

    - name: Reboot if required
      ansible.builtin.reboot:
        reboot_timeout: 300
      when: reboot_required.stat.exists

Playbook นี้ทำตามลำดับดังนี้: อัปเดต cache (โดยข้ามถ้า cache ยังไม่เก่าเกิน 1 ชั่วโมง), upgrade ทุก package พร้อมลบ dependency ที่ไม่จำเป็น, ตรวจสอบว่าต้อง reboot หรือเปล่า แล้ว reboot อัตโนมัติถ้าจำเป็น

ติดตั้งและลบ Package เฉพาะ

- name: Install essential packages
  ansible.builtin.apt:
    name:
      - curl
      - wget
      - vim
      - htop
      - unzip
    state: present
    update_cache: yes

- name: Remove insecure packages
  ansible.builtin.apt:
    name:
      - telnet
      - ftp
    state: absent
    purge: yes

ใช้ state: present สำหรับติดตั้ง และ state: absent สำหรับลบ การใส่ purge: yes จะลบทั้ง package และ configuration files ออกจากระบบ

Playbook สำหรับ CentOS/RHEL (yum/dnf)

ระบบที่ใช้ Red Hat-based distribution เช่น CentOS, Rocky Linux, AlmaLinux ใช้ yum หรือ dnf เป็น package manager โครงสร้าง Playbook คล้ายกัน แต่เปลี่ยน module เป็น yum แทน

---
- name: System Updates on CentOS/RHEL
  hosts: rhel_servers
  become: true

  tasks:
    - name: Update all packages
      ansible.builtin.yum:
        name: "*"
        state: latest
        update_only: yes

    - name: Install essential packages
      ansible.builtin.yum:
        name:
          - epel-release
          - curl
          - wget
          - vim
          - htop
        state: present

    - name: Clean yum cache
      ansible.builtin.command:
        cmd: yum clean all
      changed_when: false

บน CentOS 8 หรือ Rocky Linux 9 ขึ้นไป สามารถเปลี่ยนไปใช้ module dnf แทน yum ได้เลย เนื่องจาก dnf เป็น package manager หลักของระบบรุ่นใหม่และมีประสิทธิภาพสูงกว่า

ควบคุม Version ของ Package ให้สม่ำเสมอ

ใน production environment บางครั้งจำเป็นต้อง pin version ของ package เฉพาะเจาะจง เพื่อป้องกันการ upgrade โดยไม่ตั้งใจที่อาจทำให้แอปพลิเคชันทำงานผิดปกติ

# ติดตั้ง nginx version เฉพาะ
- name: Install specific nginx version
  ansible.builtin.apt:
    name: "nginx=1.24.*"
    state: present
    update_cache: yes

# Lock nginx ไม่ให้ถูก upgrade
- name: Hold nginx at current version
  ansible.builtin.dpkg_selections:
    name: nginx
    selection: hold

# ตรวจสอบ version ที่ติดตั้ง
- name: Check installed nginx version
  ansible.builtin.command:
    cmd: nginx -v
  changed_when: false
  register: nginx_version

- name: Show nginx version
  ansible.builtin.debug:
    msg: "{{ nginx_version.stderr }}"

การใช้ dpkg_selections: hold จะป้องกันไม่ให้ package ถูก upgrade โดยอัตโนมัติ ซึ่งมีประโยชน์มากใน environment ที่ต้องการความเสถียรสูง

อัปเดต Security Patches เฉพาะ (ไม่ Upgrade ทั้งหมด)

หลายองค์กรต้องการ apply เฉพาะ security patches โดยไม่อัปเดต feature packages ทั้งหมด เพื่อลดความเสี่ยงจากการ breaking change

---
- name: Apply security patches only
  hosts: all
  become: true

  tasks:
    - name: Install unattended-upgrades (Ubuntu)
      ansible.builtin.apt:
        name: unattended-upgrades
        state: present
      when: ansible_os_family == "Debian"

    - name: Run security updates only
      ansible.builtin.command:
        cmd: unattended-upgrade --dry-run -v
      changed_when: false
      register: upgrade_output
      when: ansible_os_family == "Debian"

    - name: Show pending security updates
      ansible.builtin.debug:
        var: upgrade_output.stdout_lines
      when: ansible_os_family == "Debian"

    - name: Apply security updates on RHEL
      ansible.builtin.yum:
        name: "*"
        state: latest
        security: yes
      when: ansible_os_family == "RedHat"

การใช้ ansible_os_family fact ทำให้สามารถเขียน Playbook เดียวที่รองรับได้ทั้ง Ubuntu และ CentOS โดยอัตโนมัติ

Rolling Update: อัปเดตแบบไม่กระทบ Service

การอัปเดตเซิร์ฟเวอร์ทั้งหมดพร้อมกันอาจทำให้ service หยุดชะงักได้ กลยุทธ์ Rolling Update คือการอัปเดตทีละกลุ่มเล็ก ๆ โดย load balancer ยังคงรับ traffic ได้ระหว่างนั้น

---
- name: Rolling Update - Web Servers
  hosts: webservers
  become: true
  serial: 2               # อัปเดตทีละ 2 เครื่อง
  max_fail_percentage: 25 # หยุดทั้งหมดถ้าล้มเหลวเกิน 25%

  pre_tasks:
    - name: Remove server from load balancer
      ansible.builtin.uri:
        url: "http://lb01/api/disable/{{ inventory_hostname }}"
        method: POST
      delegate_to: lb01

    - name: Wait for connections to drain
      ansible.builtin.pause:
        seconds: 10

  tasks:
    - name: Update all packages
      ansible.builtin.apt:
        upgrade: dist
        update_cache: yes

    - name: Restart web service
      ansible.builtin.service:
        name: nginx
        state: restarted

    - name: Verify service is running
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:80/health"
        status_code: 200

  post_tasks:
    - name: Re-add server to load balancer
      ansible.builtin.uri:
        url: "http://lb01/api/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: lb01

serial: 2 บอกให้รันทีละ 2 host ก่อนเริ่ม batch ถัดไป ส่วน pre_tasks และ post_tasks ใช้นำ server ออกจาก load balancer ก่อนอัปเดต และนำกลับเข้าหลังจากตรวจสอบแล้วว่า service ทำงานปกติ

Dry-run และ Limit: ทดสอบก่อนรันจริง

ก่อนรัน update บน production ควรทดสอบด้วย --check flag เสมอ เพื่อดูว่าจะมีการเปลี่ยนแปลงอะไรบ้างโดยไม่แตะระบบจริง

# Dry-run ดูว่าจะเปลี่ยนแปลงอะไร
ansible-playbook update-systems.yml --check --diff

# รันเฉพาะ server กลุ่ม staging ก่อน
ansible-playbook update-systems.yml --limit staging_servers

# รันเฉพาะ task ที่มี tag "update"
ansible-playbook update-systems.yml --tags update

# ตรวจสอบ package ที่จะถูก upgrade บน Ubuntu
ansible ubuntu_servers -m apt -a "upgrade=dist update_cache=yes" --check -b

แนะนำให้ทดสอบบน staging environment ก่อนเสมอ โดยใช้ --limit กำหนดเฉพาะกลุ่มที่ต้องการ จากนั้นค่อย apply บน production เมื่อมั่นใจแล้ว

สรุป

การจัดการ System Updates ด้วย Automation ช่วยให้ infrastructure ของคุณมีความสม่ำเสมอ ปลอดภัย และตรวจสอบได้ ไม่ว่าจะเป็น Ubuntu (apt) หรือ CentOS/RHEL (yum/dnf) สามารถจัดการด้วย Playbook ชุดเดียว และขยายไปรองรับ server ใหม่ได้โดยแค่เพิ่ม host ใน inventory

หัวใจสำคัญคือ: ใช้ --check ก่อนรันจริง, ใช้ rolling update สำหรับ production เพื่อไม่ให้ service หยุดชะงัก, และใส่ reboot check เพื่อให้ security patch มีผลครบถ้วน การวางแผนและทดสอบอย่างรอบคอบก่อน deploy จริงจะช่วยลดความเสี่ยงได้มาก