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 ที่ต้องการได้โดยไม่ต้องเรียนรู้ใหม่ตั้งแต่ต้น

