Workshop: Ansible สำหรับ Server Maintenance: Backup, Updates, Monitoring

Server Maintenance เป็นงานที่ต้องทำสม่ำเสมอทุกเซิร์ฟเวอร์ ไม่ว่าจะเป็น Backup ข้อมูล, อัปเดต System Package, หรือตรวจสอบ Disk Usage แต่เมื่อมีเซิร์ฟเวอร์หลายสิบหรือหลายร้อยเครื่อง การทำงานเหล่านี้ด้วยมือกลายเป็นเรื่องที่ใช้เวลามากและเสี่ยงต่อการตกหล่น

Workshop นี้สร้าง Ansible Playbook สำหรับงาน Server Maintenance ที่ครอบคลุม 3 ด้านหลัก ได้แก่ การ Backup ไฟล์สำคัญและ Database, การอัปเดต System Package อย่างปลอดภัย, และการติดตั้ง Monitoring Agent บนเซิร์ฟเวอร์ทุกเครื่องพร้อมกัน

โครงสร้าง Project

server-maintenance/
├── inventory/
│   └── hosts.ini
├── group_vars/
│   └── all.yml
├── roles/
│   ├── backup/
│   │   └── tasks/main.yml
│   ├── update/
│   │   └── tasks/main.yml
│   └── monitoring/
│       ├── tasks/main.yml
│       └── templates/
│           └── node_exporter.service.j2
├── maintenance.yml      # Playbook หลัก
└── backup-only.yml      # Playbook เฉพาะ Backup

Inventory และ Variables

[production]
web01 ansible_host=192.168.1.10 ansible_user=ubuntu server_type=web
web02 ansible_host=192.168.1.11 ansible_user=ubuntu server_type=web
db01  ansible_host=192.168.1.20 ansible_user=ubuntu server_type=db

[staging]
stage01 ansible_host=192.168.1.30 ansible_user=ubuntu server_type=web

[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python3

กำหนด Variables ใน group_vars/all.yml:

# Backup settings
backup_dir: /var/backups/ansible
backup_retention_days: 30
backup_timestamp: "{{ ansible_date_time.date }}"

# Paths to backup (สามารถ override ต่อ host ได้)
backup_paths:
  - /etc/nginx
  - /etc/mysql
  - /var/www

# Update settings
update_reboot_allowed: false      # ตั้งเป็น true ถ้าต้องการ reboot อัตโนมัติ
update_cache_valid_time: 3600

# Monitoring
node_exporter_version: "1.7.0"
node_exporter_port: 9100

Role: Backup

สร้าง roles/backup/tasks/main.yml — Backup ไฟล์ Config และ MySQL Database:

---
- name: Ensure backup directory exists
  file:
    path: "{{ backup_dir }}/{{ backup_timestamp }}"
    state: directory
    owner: root
    group: root
    mode: "0700"

- name: Backup configuration directories
  archive:
    path: "{{ backup_paths }}"
    dest: "{{ backup_dir }}/{{ backup_timestamp }}/configs.tar.gz"
    format: gz
    exclude_path:
      - "*/cache"
      - "*/tmp"
  register: backup_result
  ignore_errors: yes

- name: Show backup size
  debug:
    msg: "Backup size: {{ backup_result.expanded_size | default('unknown') }} bytes"

- name: Backup MySQL databases (only on db servers)
  shell: |
    mysqldump --all-databases \
      --single-transaction \
      --quick \
      --lock-tables=false \
      | gzip > {{ backup_dir }}/{{ backup_timestamp }}/mysql-all.sql.gz
  args:
    creates: "{{ backup_dir }}/{{ backup_timestamp }}/mysql-all.sql.gz"
  when: server_type == "db"
  no_log: true

- name: Remove backups older than retention period
  find:
    paths: "{{ backup_dir }}"
    age: "{{ backup_retention_days }}d"
    file_type: directory
  register: old_backups

- name: Delete old backup directories
  file:
    path: "{{ item.path }}"
    state: absent
  loop: "{{ old_backups.files }}"
  when: old_backups.files | length > 0

Role: System Update

สร้าง roles/update/tasks/main.yml — อัปเดต Package อย่างปลอดภัยพร้อม Reboot ถ้าจำเป็น:

---
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: "{{ update_cache_valid_time }}"

- name: List packages to be upgraded
  apt:
    upgrade: yes
    autoremove: yes
    autoclean: yes
  check_mode: yes
  register: apt_upgrade_check

- name: Show packages to be updated
  debug:
    msg: "Packages to update: {{ apt_upgrade_check.stdout_lines | length }} packages"
  when: apt_upgrade_check.stdout_lines is defined

- name: Apply security updates only
  apt:
    upgrade: safe
    autoremove: yes
    autoclean: yes
  register: update_result

- name: Show update summary
  debug:
    msg: "Updated: {{ update_result.stdout | regex_findall('upgraded') | length }} packages"
  when: update_result.stdout is defined

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

- name: Reboot if required and allowed
  reboot:
    reboot_timeout: 300
    pre_reboot_delay: 10
    msg: "Rebooting for system updates"
  when:
    - reboot_required.stat.exists
    - update_reboot_allowed | bool

- name: Notify if reboot is needed but not allowed
  debug:
    msg: "⚠️ Reboot required on {{ inventory_hostname }} but update_reboot_allowed=false — please reboot manually"
  when:
    - reboot_required.stat.exists
    - not (update_reboot_allowed | bool)

Role: Monitoring (Node Exporter)

สร้าง roles/monitoring/tasks/main.yml — ติดตั้ง Prometheus Node Exporter สำหรับ Monitoring:

---
- name: Check if node_exporter is already installed
  stat:
    path: /usr/local/bin/node_exporter
  register: node_exporter_binary

- name: Check installed version
  command: /usr/local/bin/node_exporter --version
  register: installed_version
  changed_when: false
  failed_when: false
  when: node_exporter_binary.stat.exists

- name: Download Node Exporter
  get_url:
    url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz"
    dest: /tmp/node_exporter.tar.gz
    mode: "0644"
  when: >
    not node_exporter_binary.stat.exists or
    node_exporter_version not in (installed_version.stdout | default(''))

- name: Extract and install Node Exporter
  unarchive:
    src: /tmp/node_exporter.tar.gz
    dest: /tmp
    remote_src: yes
  when: >
    not node_exporter_binary.stat.exists or
    node_exporter_version not in (installed_version.stdout | default(''))

- name: Copy node_exporter binary
  copy:
    src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
    dest: /usr/local/bin/node_exporter
    owner: root
    group: root
    mode: "0755"
    remote_src: yes
  notify: Restart node_exporter
  when: >
    not node_exporter_binary.stat.exists or
    node_exporter_version not in (installed_version.stdout | default(''))

- name: Deploy systemd service for node_exporter
  template:
    src: node_exporter.service.j2
    dest: /etc/systemd/system/node_exporter.service
    owner: root
    group: root
    mode: "0644"
  notify:
    - Reload systemd
    - Restart node_exporter

- name: Ensure node_exporter is started and enabled
  service:
    name: node_exporter
    state: started
    enabled: yes

- name: Verify node_exporter is responding
  uri:
    url: "http://localhost:{{ node_exporter_port }}/metrics"
    status_code: 200
  retries: 3
  delay: 5

Main Playbook: maintenance.yml

รวม 3 Roles ด้วย Tags เพื่อให้รันเลือกบางส่วนได้:

---
- name: Server Maintenance — Backup, Update, Monitoring
  hosts: all
  become: yes
  serial: 1          # รันทีละ 1 เครื่อง ป้องกัน downtime พร้อมกัน

  pre_tasks:
    - name: Check disk space before maintenance
      command: df -h /var
      register: disk_space
      changed_when: false

    - name: Alert if disk space is low (< 20%)
      fail:
        msg: "Low disk space on {{ inventory_hostname }}: {{ disk_space.stdout }}"
      when: >
        disk_space.stdout | regex_search('(\d+)%') | int > 80

  roles:
    - role: backup
      tags: [backup]

    - role: update
      tags: [update]

    - role: monitoring
      tags: [monitoring]

  post_tasks:
    - name: Record maintenance completion
      copy:
        content: "Maintenance completed: {{ ansible_date_time.iso8601 }}\n"
        dest: /var/log/ansible-maintenance.log
      changed_when: false

การใช้งาน: รัน Maintenance Tasks

# รัน Maintenance ทุกอย่างบน production
ansible-playbook -i inventory/hosts.ini maintenance.yml --limit production

# รันเฉพาะ Backup บนทุก Server
ansible-playbook -i inventory/hosts.ini maintenance.yml --tags backup

# รันเฉพาะ Update บน staging ก่อน production
ansible-playbook -i inventory/hosts.ini maintenance.yml \
  --limit staging --tags update
ansible-playbook -i inventory/hosts.ini maintenance.yml \
  --limit production --tags update

# รันเฉพาะ Monitoring บน servers ที่ยังไม่มี node_exporter
ansible-playbook -i inventory/hosts.ini maintenance.yml --tags monitoring

# Dry run ดูว่าจะทำอะไร
ansible-playbook -i inventory/hosts.ini maintenance.yml --check --diff

ตรวจสอบ Disk Space และ Backup ทั้งหมดด้วย Ad-hoc

# ตรวจสอบ disk usage บนทุก server พร้อมกัน
ansible all -i inventory/hosts.ini -m command -a "df -h /" --become

# ตรวจสอบ backup ล่าสุด
ansible all -i inventory/hosts.ini -m find \
  -a "paths=/var/backups/ansible age=1d file_type=directory" --become

# ตรวจสอบ node_exporter metrics
ansible all -i inventory/hosts.ini -m uri \
  -a "url=http://localhost:9100/metrics" --become

ตั้ง Cron ให้รัน Maintenance อัตโนมัติ

เพิ่ม Cron Job บน Ansible Control Node เพื่อให้ Maintenance รันอัตโนมัติทุกสัปดาห์:

# เพิ่มใน crontab บน Ansible Control Node
# รัน Backup ทุกคืน 02:00 น.
0 2 * * * cd /opt/server-maintenance && \
  ansible-playbook -i inventory/hosts.ini maintenance.yml --tags backup \
  >> /var/log/ansible-backup.log 2>&1

# รัน Update ทุกวันจันทร์ 03:00 น.
0 3 * * 1 cd /opt/server-maintenance && \
  ansible-playbook -i inventory/hosts.ini maintenance.yml --tags update \
  --limit staging >> /var/log/ansible-update.log 2>&1

สรุป

Workshop นี้สร้าง Ansible Maintenance Playbook ที่ครอบคลุม Backup, System Update, และ Monitoring ในไฟล์เดียว โดยใช้ Tags ให้รันเลือกบางส่วนได้ ใช้ serial: 1 ป้องกัน downtime พร้อมกัน และมี pre_task ตรวจสอบ disk space ก่อนเริ่มทุกครั้ง

การกำหนด server_type ใน Inventory ทำให้ Task บางอย่างเช่น MySQL dump จะรันเฉพาะบน Database server โดยใช้ when: server_type == "db" โดยไม่ต้องสร้าง Playbook แยก ทำให้ดูแลรักษา Codebase เดียวได้ง่ายขึ้น