เขียน Ansible Playbook ติดตั้ง Database (MySQL/PostgreSQL) บน VPS

Database เป็น component ที่ต้องการความระมัดระวังในการติดตั้งและตั้งค่ามากกว่า web server เพราะเกี่ยวข้องกับข้อมูลโดยตรง การใช้ Ansible จัดการ database server ช่วยให้ทุกขั้นตอนตั้งแต่ติดตั้ง package, สร้าง database, กำหนดสิทธิ์ user, ไปจนถึง hardening ถูก document ไว้ในรูป playbook ที่ตรวจสอบและทำซ้ำได้ ลดความเสี่ยงจากการตั้งค่าผิดพลาดที่เกิดจากการทำด้วยมือ

ความท้าทายของการ automate database setup ต่างจาก service ทั่วไปตรงที่หลาย operation ไม่ได้ idempotent โดยธรรมชาติ เช่น การตั้ง root password ครั้งแรก หรือการ grant สิทธิ์ให้ user ต้องอาศัย module เฉพาะที่ออกแบบมาเพื่อรับมือกับสถานะต่าง ๆ ของ database ซึ่ง Ansible มี collection เฉพาะทางสำหรับงานนี้ที่ครอบคลุม operation ที่จำเป็นทั้งหมด ช่วยให้ playbook รัน idempotent ได้อย่างถูกต้องแม้จะรันซ้ำหลายครั้ง

บทความนี้แสดง playbook สำหรับติดตั้งและตั้งค่า MySQL และ PostgreSQL บน Ubuntu/Debian พร้อม best practices ด้านความปลอดภัย การสร้าง database user ที่มีสิทธิ์เฉพาะที่จำเป็น และการ verify หลัง deploy เพื่อให้แน่ใจว่า database พร้อมรับ connection ครอบคลุมตั้งแต่ขั้นตอนแรกของการติดตั้งไปจนถึงการตั้งค่า backup และ monitoring เพื่อให้ database server พร้อมรองรับ workload จริงในสภาพแวดล้อม production

เตรียม Variables สำหรับ Database

ข้อมูล database เช่น password ไม่ควรเก็บไว้ใน playbook หรือ group_vars แบบ plaintext ควรใช้ Ansible Vault encrypt ก่อนเสมอ ในตัวอย่างนี้จะแสดงโครงสร้าง variable ที่แนะนำ โดยแยกค่าปกติออกจากค่าที่เป็น secret

การตั้งชื่อ variable ที่มี prefix vault_ เป็น convention ที่ช่วยให้รู้ทันทีว่าค่าใดมาจาก vault และค่าใดเป็น plaintext ทำให้ code review ทำได้ง่ายขึ้นและลดความเสี่ยงที่จะ commit secret โดยไม่ตั้งใจ

# group_vars/dbservers/main.yml — ค่าทั่วไป
db_port: 3306
db_bind_address: "127.0.0.1"   # bind เฉพาะ localhost ถ้าไม่ต้องการ remote access
db_name: myapp
db_user: myapp_user
db_charset: utf8mb4
db_collation: utf8mb4_unicode_ci

# group_vars/dbservers/vault.yml — encrypt ด้วย ansible-vault
# vault_db_root_password: "strong_root_password_here"
# vault_db_password: "strong_app_password_here"

# group_vars/dbservers/main.yml — อ้างอิง vault variables
db_root_password: "{{ vault_db_root_password }}"
db_password: "{{ vault_db_password }}"

Playbook ติดตั้ง MySQL

การติดตั้ง MySQL ต้องการ Python library สำหรับให้ Ansible communicate กับ MySQL ได้ โดย module community.mysql ต้องการ PyMySQL หรือ mysqlclient ติดตั้งอยู่ใน Python ของ managed host ก่อน tasks database จะทำงานได้

ขั้นตอนหลังติดตั้งที่สำคัญมากคือการ set root password และ remove anonymous user ทันที เพราะ server ที่ติดตั้งใหม่บางเวอร์ชันมี anonymous user ที่ไม่มี password ซึ่งเป็นช่องโหว่ความปลอดภัย tasks เหล่านี้ควรรันก่อนสร้าง database หรือ user ใด ๆ

# mysql.yml
---
- name: Install and configure MySQL database server
  hosts: dbservers
  become: true

  tasks:
    - name: Update apt cache
      apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Install MySQL server
      apt:
        name:
          - mysql-server
          - python3-pymysql
        state: present

    - name: Ensure MySQL service is started and enabled
      service:
        name: mysql
        state: started
        enabled: true

    - name: Set MySQL root password
      community.mysql.mysql_user:
        name: root
        password: "{{ db_root_password }}"
        login_unix_socket: /var/run/mysqld/mysqld.sock
        host: localhost
        state: present
      no_log: true

    - name: Remove anonymous MySQL users
      community.mysql.mysql_user:
        name: ''
        host_all: true
        state: absent
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Remove MySQL test database
      community.mysql.mysql_db:
        name: test
        state: absent
        login_user: root
        login_password: "{{ db_root_password }}"

สร้าง Database และ User สำหรับ Application

หลักการ least privilege สำคัญมากในการสร้าง database user — user ที่ใช้กับ application ควรมีสิทธิ์เฉพาะ database ที่ต้องการเท่านั้น ไม่ใช่ GRANT ALL ON *.* ซึ่งให้สิทธิ์ทุก database ทุก operation การจำกัดสิทธิ์ช่วยลดความเสียหายหาก application ถูก compromise

การตั้ง host: "{{ db_allowed_host | default('localhost') }}" ทำให้ control ได้ว่า user จะ connect จาก host ใดได้บ้าง สำหรับ application ที่รันบน server เดียวกัน ให้ใช้ localhost เสมอ แต่ถ้าเป็น multi-tier architecture ที่ web server และ database server อยู่คนละเครื่อง ก็สามารถระบุ IP ของ web server ได้

    - name: Create application database
      community.mysql.mysql_db:
        name: "{{ db_name }}"
        encoding: "{{ db_charset }}"
        collation: "{{ db_collation }}"
        state: present
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Create application database user
      community.mysql.mysql_user:
        name: "{{ db_user }}"
        password: "{{ db_password }}"
        priv: "{{ db_name }}.*:SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,INDEX,ALTER"
        host: "{{ db_allowed_host | default('localhost') }}"
        state: present
        login_user: root
        login_password: "{{ db_root_password }}"
      no_log: true

ตั้งค่า MySQL สำหรับ Production

ระบบที่ติดตั้งใหม่มักใช้ค่า default ที่ไม่เหมาะกับ production การตั้งค่าเพิ่มเติมผ่าน config template ช่วยให้ปรับ buffer size, connection limit, และ slow query log ได้ตาม spec ของ server แต่ละเครื่อง ทำให้ performance สอดคล้องกับขนาดและ workload จริงของแต่ละ server

# templates/mysql.cnf.j2
[mysqld]
bind-address            = {{ db_bind_address }}
port                    = {{ db_port }}

# Performance tuning
innodb_buffer_pool_size = {{ db_innodb_buffer_pool | default('256M') }}
max_connections         = {{ db_max_connections | default(150) }}
query_cache_type        = 0

# Slow query log
slow_query_log          = 1
slow_query_log_file     = /var/log/mysql/slow.log
long_query_time         = 2

# Character set
character-set-server    = {{ db_charset }}
collation-server        = {{ db_collation }}
    - name: Deploy MySQL config
      template:
        src: mysql.cnf.j2
        dest: /etc/mysql/conf.d/custom.cnf
        owner: root
        group: root
        mode: '0644'
      notify: restart mysql

  handlers:
    - name: restart mysql
      service:
        name: mysql
        state: restarted

Playbook ติดตั้ง PostgreSQL

PostgreSQL มีโครงสร้างแตกต่างจาก MySQL ในแง่การจัดการ authentication โดยใช้ไฟล์ pg_hba.conf กำหนดว่า user ใด จาก host ใด สามารถ connect กับ database ใดได้ผ่าน authentication method ใด Ansible มี module community.postgresql สำหรับจัดการ PostgreSQL โดยเฉพาะ

ข้อแตกต่างสำคัญคือ PostgreSQL ใช้ become_user: postgres ในบาง task เพราะการจัดการ database ต้องทำในฐานะ postgres system user เพื่อ authenticate ผ่าน peer authentication ก่อนที่จะตั้ง password-based authentication ได้

# postgresql.yml
---
- name: Install and configure PostgreSQL database server
  hosts: dbservers
  become: true

  tasks:
    - name: Install PostgreSQL
      apt:
        name:
          - postgresql
          - postgresql-contrib
          - python3-psycopg2
        state: present
        update_cache: true

    - name: Ensure PostgreSQL service is started and enabled
      service:
        name: postgresql
        state: started
        enabled: true

    - name: Create application database
      community.postgresql.postgresql_db:
        name: "{{ db_name }}"
        encoding: UTF-8
        lc_collate: en_US.UTF-8
        lc_ctype: en_US.UTF-8
        state: present
      become_user: postgres

    - name: Create application database user
      community.postgresql.postgresql_user:
        name: "{{ db_user }}"
        password: "{{ db_password }}"
        db: "{{ db_name }}"
        priv: ALL
        state: present
      become_user: postgres
      no_log: true

    - name: Grant database privileges
      community.postgresql.postgresql_privs:
        database: "{{ db_name }}"
        role: "{{ db_user }}"
        objs: ALL_IN_SCHEMA
        privs: SELECT,INSERT,UPDATE,DELETE
        state: present
      become_user: postgres

ตั้งค่า PostgreSQL pg_hba.conf

ไฟล์ pg_hba.conf ควบคุม client authentication ของ PostgreSQL การตั้งค่า default อนุญาตเฉพาะ local connection ผ่าน peer authentication ซึ่งเหมาะสำหรับ local access แต่ถ้า application ต้องการ connect ผ่าน TCP ต้องเพิ่ม entry ที่อนุญาต password authentication ด้วย

    - name: Configure PostgreSQL authentication
      community.postgresql.postgresql_pg_hba:
        dest: /etc/postgresql/{{ pg_version | default('14') }}/main/pg_hba.conf
        contype: host
        databases: "{{ db_name }}"
        users: "{{ db_user }}"
        source: "{{ db_allowed_host | default('127.0.0.1/32') }}"
        method: scram-sha-256
        state: present
      notify: restart postgresql

    - name: Configure PostgreSQL listen address
      community.postgresql.postgresql_set:
        name: listen_addresses
        value: "{{ pg_listen_addresses | default('localhost') }}"
      notify: restart postgresql
      become_user: postgres

  handlers:
    - name: restart postgresql
      service:
        name: postgresql
        state: restarted

ตรวจสอบ Database หลัง Deploy

การตรวจสอบหลัง deploy สำหรับ database ต่างจาก web server ตรงที่ต้องตรวจสอบทั้ง service state, port availability, และการ connect จริงด้วย credentials ที่สร้างไว้ เพื่อให้แน่ใจว่า application สามารถใช้งาน database ได้ทันที ไม่ใช่แค่ service รันอยู่เท่านั้น

การทดสอบ connect จริงด้วย application user เป็น verification ที่ครอบคลุมกว่า เพราะจะจับปัญหาทั้ง password ผิด, สิทธิ์ไม่ครบ, และ firewall block ได้พร้อมกัน ทำให้ playbook fail เร็วและชัดเจนถ้า database ยังไม่พร้อมรับ connection จาก application

    # Verify MySQL
    - name: Verify MySQL is accepting connections
      community.mysql.mysql_info:
        login_user: "{{ db_user }}"
        login_password: "{{ db_password }}"
        login_db: "{{ db_name }}"
        filter: version
      register: mysql_info
      when: db_engine | default('mysql') == 'mysql'

    - name: Show MySQL version
      debug:
        msg: "MySQL {{ mysql_info.version.full }} is ready on port {{ db_port }}"
      when: db_engine | default('mysql') == 'mysql'

    # Verify PostgreSQL
    - name: Verify PostgreSQL is accepting connections
      community.postgresql.postgresql_ping:
        db: "{{ db_name }}"
        login_user: "{{ db_user }}"
        login_password: "{{ db_password }}"
      when: db_engine | default('mysql') == 'postgresql'

รัน Playbook และ Tips การใช้งาน

เมื่อ playbook พร้อมแล้ว ให้รันผ่าน command line โดยระบุ vault password เพื่อ decrypt credentials ที่เข้ารหัสไว้ การแยก playbook ออกเป็นไฟล์ตามระบบ (mysql.yml และ postgresql.yml) ทำให้เลือกรันเฉพาะ engine ที่ต้องการได้โดยไม่กระทบกัน

# รัน playbook สำหรับ database server
ansible-playbook -i inventory/production mysql.yml --vault-password-file ~/.vault_pass

# รัน dry-run ก่อนจริง
ansible-playbook -i inventory/production mysql.yml --check --vault-password-file ~/.vault_pass

# รันเฉพาะ tag ที่ต้องการ
ansible-playbook -i inventory/production mysql.yml --tags backup --vault-password-file ~/.vault_pass

ควรทดสอบ playbook บน staging server ก่อนเสมอ เพราะ operation บางอย่างเช่นการตั้ง root password หรือ remove test database อาจมีผลที่ irreversible ถ้ารันผิดพลาดบน production สำหรับ server ที่มีข้อมูลอยู่แล้ว ควรใช้ tag แบ่งการรันเพื่อ apply เฉพาะส่วนที่ต้องการโดยไม่ต้อง re-install ทั้งหมด

Backup Configuration

การตั้งค่า automated backup เป็นส่วนที่ควรอยู่ใน playbook ติดตั้ง database ด้วย ไม่ใช่ทำแยกต่างหาก เพราะถ้า deploy ระบบฐานข้อมูลโดยไม่มี backup strategy ไว้ล่วงหน้า มักจะลืมตั้งค่าในภายหลัง script backup ง่าย ๆ ผ่าน cron job เป็นจุดเริ่มต้นที่ดีก่อนจะ scale ไปใช้ solution ที่ซับซ้อนกว่า backup ที่ทำโดย cron ควรเก็บไว้ใน directory ที่แยกจาก data directory และมีระบบ rotate ไฟล์เก่าออกด้วยเพื่อป้องกัน disk เต็ม

ตัวอย่าง tasks ด้านล่างสร้าง backup directory, deploy shell script จาก template ที่กำหนด credentials และ retention policy, และตั้ง cron ให้รันทุกคืนเวลา 02:30 น. ซึ่งเป็นช่วง low-traffic ของ workload ส่วนใหญ่ ผู้ดูแลระบบสามารถปรับ schedule และ retention ได้ตาม variable โดยไม่ต้องแก้ไข playbook โดยตรง การทำ backup ผ่าน Ansible ยังช่วยให้ consistent ว่าทุก server ใช้ script เดียวกันและมี log ในที่เดียวกัน ง่ายต่อการตรวจสอบและ troubleshoot เมื่อต้องการ restore

    - name: Create backup directory
      file:
        path: /var/backups/mysql
        state: directory
        owner: root
        group: root
        mode: '0750'

    - name: Deploy MySQL backup script
      template:
        src: mysql_backup.sh.j2
        dest: /usr/local/bin/mysql-backup.sh
        mode: '0750'
        owner: root

    - name: Schedule daily MySQL backup
      cron:
        name: "MySQL daily backup"
        minute: "30"
        hour: "2"
        job: "/usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1"
        user: root
        state: present

Security Hardening เพิ่มเติม

นอกจาก tasks ที่กล่าวมาแล้ว ควรพิจารณา hardening เพิ่มเติมตามนโยบายองค์กร เช่น การปิด remote root login, การตั้งค่า firewall ให้เปิด port 3306 หรือ 5432 เฉพาะ IP ของ application server, การตั้งค่า audit log เพื่อบันทึกการ login และ query สำคัญ และการหมุนเวียน credentials ตามรอบที่กำหนด

สำหรับ environment ที่ต้องการความปลอดภัยสูง ควรพิจารณาใช้ SSL/TLS สำหรับ connection ระหว่าง application กับ database server ทั้งคู่รองรับการเข้ารหัส connection ด้วย certificate ซึ่งสามารถจัดการผ่าน playbook ได้เช่นกัน โดยใช้ Ansible จัดการ certificate, config file, และ restart service ในขั้นตอนเดียว ทำให้กระบวนการ certificate rotation ที่เคยต้องทำด้วยมือกลายเป็น operation ที่ automated และ reproducible

การตั้งค่า firewall ร่วมกับ playbook ก็เป็นแนวทางที่แนะนำ โดยสามารถใช้ module community.general.ufw หรือ ansible.posix.firewalld เพื่อเปิด port เฉพาะสำหรับ IP ที่อนุญาต และปิด port อื่นทั้งหมด การรวม firewall rules ไว้ใน playbook เดียวกันกับการติดตั้งระบบฐานข้อมูลทำให้ทีมมั่นใจได้ว่า server ทุกเครื่องที่ deploy ด้วย playbook นี้จะมี network policy ที่เหมือนกันทุกครั้ง ลดความเสี่ยงจากการลืม lock down port หลัง deploy

Monitoring และ Alerting

การติดตั้งระบบฐานข้อมูลที่สมบูรณ์ควรรวม monitoring ไว้ด้วย ไม่ว่าจะเป็น slow query log ที่เปิดไว้ใน config template, การส่ง metrics ไปยัง Prometheus ด้วย exporter, หรือการตั้ง alert เมื่อ connection pool ใกล้เต็ม การรวม monitoring setup ไว้ใน playbook เดียวกันช่วยให้ทีม ops มีทัศนวิสัยตั้งแต่วันแรกที่ deploy โดยไม่ต้องรอให้มีปัญหาก่อนแล้วค่อยตั้งค่า

ตัวอย่าง exporter ที่นิยมใช้คู่กับ Prometheus ได้แก่ mysqld_exporter สำหรับ engine ที่เป็นตระกูล MySQL และ postgres_exporter สำหรับ PostgreSQL ทั้งคู่สามารถติดตั้งและตั้งค่าได้ผ่าน Ansible โดยใช้ pattern เดียวกับ playbook ที่เขียนในบทความนี้

สรุป

playbook ติดตั้ง database ที่ดีต้องครอบคลุมมากกว่าแค่ติดตั้ง package — ต้องตั้งค่าความปลอดภัยพื้นฐาน สร้าง database และ user ด้วย least privilege, deploy config ที่ tuning ตาม spec, ตรวจสอบ connection จริง และตั้งค่า backup อัตโนมัติ การใช้ Ansible Vault เก็บ credentials ทำให้ playbook ปลอดภัยพอที่จะเก็บใน version control ได้ ซึ่งช่วยให้ทีมมีหลักฐานว่า server ทุกเครื่องถูกตั้งค่าอย่างเป็นมาตรฐานเดียวกัน ไม่ว่าจะใช้ engine ใดก็สามารถ apply pattern เดียวกันได้

pattern ที่ใช้ในบทความนี้ — ตั้งค่าความปลอดภัยก่อน จากนั้นสร้าง database และ user, deploy config, verify, และตั้ง backup — สามารถนำไปปรับใช้กับ engine อื่น ๆ ได้โดยเปลี่ยนเพียง collection และ parameter ที่เกี่ยวข้อง โครงสร้าง playbook และ principle เรื่อง security, idempotency, และ verification ยังคงเดิมในทุก engine ทำให้ทีมที่ใช้ Ansible อยู่แล้วสามารถนำ playbook นี้ไปเป็นฐานสำหรับ engine ที่ต้องการได้โดยไม่ต้องเรียนรู้ใหม่ตั้งแต่ต้น