การตั้งค่า Web Server Stack แบบ Nginx + PHP-FPM + MySQL ด้วยมือบนเซิร์ฟเวอร์ใหม่ทุกครั้งใช้เวลานานและเสี่ยงต่อข้อผิดพลาด ไม่ว่าจะเป็นการลืม configuration บางขั้น หรือ version ที่ไม่ตรงกันระหว่าง environment Ansible ช่วยให้กระบวนการนี้กลายเป็น Playbook ที่รันได้ซ้ำแล้วซ้ำเล่าอย่างสม่ำเสมอ
Workshop นี้จะพาคุณสร้าง Ansible Project ตั้งแต่ศูนย์สำหรับติดตั้งและตั้งค่า Nginx + PHP-FPM + MySQL พร้อม Virtual Host, Database, และ User บนเซิร์ฟเวอร์ Ubuntu โดยใช้ Role Structure ที่ Reusable และ production-ready
โครงสร้าง Project
เริ่มจากสร้าง directory structure ตาม Ansible Best Practice ที่แยก Role ออกเป็นส่วนๆ เพื่อให้ดูแลรักษาง่าย
webserver-stack/
├── inventory/
│ └── hosts.ini
├── group_vars/
│ └── all.yml
├── roles/
│ ├── nginx/
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ ├── templates/
│ │ │ ├── nginx.conf.j2
│ │ │ └── vhost.conf.j2
│ │ └── handlers/
│ │ └── main.yml
│ ├── php/
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ └── templates/
│ │ └── php-fpm.conf.j2
│ └── mysql/
│ ├── tasks/
│ │ └── main.yml
│ └── handlers/
│ └── main.yml
└── site.yml
Inventory และ Variables
กำหนด Host ที่ต้องการตั้งค่าใน inventory/hosts.ini:
[webservers]
web01 ansible_host=192.168.1.10 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa
[webservers:vars]
ansible_python_interpreter=/usr/bin/python3
กำหนด Variables ที่ใช้ร่วมกันทั้ง Project ใน group_vars/all.yml:
# Nginx
nginx_user: www-data
server_name: example.com
web_root: /var/www/{{ server_name }}
# PHP
php_version: "8.2"
php_packages:
- php8.2-fpm
- php8.2-mysql
- php8.2-mbstring
- php8.2-xml
- php8.2-curl
- php8.2-zip
# MySQL
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_db_name: myapp
mysql_db_user: appuser
mysql_db_password: "{{ vault_mysql_db_password }}"
Password ที่ sensitive ให้เก็บใน Ansible Vault (group_vars/all_vault.yml) และ encrypt ด้วย ansible-vault encrypt group_vars/all_vault.yml
Role: Nginx
สร้าง roles/nginx/tasks/main.yml สำหรับติดตั้งและตั้งค่า Nginx:
---
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Create web root directory
file:
path: "{{ web_root }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: "0755"
- name: Deploy Nginx main config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify: Reload Nginx
- name: Deploy Virtual Host config
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ server_name }}"
owner: root
group: root
mode: "0644"
notify: Reload Nginx
- name: Enable Virtual Host
file:
src: "/etc/nginx/sites-available/{{ server_name }}"
dest: "/etc/nginx/sites-enabled/{{ server_name }}"
state: link
notify: Reload Nginx
- name: Remove default site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Reload Nginx
- name: Ensure Nginx is started and enabled
service:
name: nginx
state: started
enabled: yes
สร้าง roles/nginx/templates/vhost.conf.j2 — Virtual Host template:
server {
listen 80;
server_name {{ server_name }} www.{{ server_name }};
root {{ web_root }};
index index.php index.html;
access_log /var/log/nginx/{{ server_name }}.access.log;
error_log /var/log/nginx/{{ server_name }}.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php{{ php_version }}-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
}
สร้าง roles/nginx/handlers/main.yml:
---
- name: Reload Nginx
service:
name: nginx
state: reloaded
Role: PHP-FPM
สร้าง roles/php/tasks/main.yml สำหรับติดตั้ง PHP-FPM และ Extensions ที่จำเป็น:
---
- name: Add PHP repository (ondrej/php)
apt_repository:
repo: ppa:ondrej/php
state: present
update_cache: yes
- name: Install PHP-FPM and extensions
apt:
name: "{{ php_packages }}"
state: present
update_cache: yes
- name: Set PHP-FPM pool configuration
template:
src: php-fpm.conf.j2
dest: "/etc/php/{{ php_version }}/fpm/pool.d/www.conf"
owner: root
group: root
mode: "0644"
notify: Restart PHP-FPM
- name: Ensure PHP-FPM is started and enabled
service:
name: "php{{ php_version }}-fpm"
state: started
enabled: yes
สร้าง roles/php/handlers/main.yml:
---
- name: Restart PHP-FPM
service:
name: "php{{ php_version }}-fpm"
state: restarted
Role: MySQL
สร้าง roles/mysql/tasks/main.yml สำหรับติดตั้ง MySQL และสร้าง Database พร้อม User:
---
- name: Install MySQL server and Python MySQL library
apt:
name:
- mysql-server
- python3-pymysql
state: present
update_cache: yes
- name: Ensure MySQL is started and enabled
service:
name: mysql
state: started
enabled: yes
- name: Set MySQL root password
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
host: localhost
state: present
no_log: true
- name: Create .my.cnf for root
template:
src: my.cnf.j2
dest: /root/.my.cnf
owner: root
group: root
mode: "0600"
no_log: true
- name: Create application database
mysql_db:
name: "{{ mysql_db_name }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Create application database user
mysql_user:
name: "{{ mysql_db_user }}"
password: "{{ mysql_db_password }}"
priv: "{{ mysql_db_name }}.*:ALL"
host: localhost
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
no_log: true
- name: Remove anonymous MySQL users
mysql_user:
name: ''
host_all: yes
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
- name: Remove MySQL test database
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
Main Playbook: site.yml
รวม Roles ทั้งหมดใน site.yml พร้อม pre-task ตรวจสอบ OS ก่อนเริ่ม:
---
- name: Deploy Web Server Stack (Nginx + PHP + MySQL)
hosts: webservers
become: yes
pre_tasks:
- name: Verify Ubuntu OS
assert:
that:
- ansible_distribution == "Ubuntu"
- ansible_distribution_major_version | int >= 20
fail_msg: "This playbook requires Ubuntu 20.04 or later"
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
roles:
- mysql
- php
- nginx
post_tasks:
- name: Deploy test PHP info page
copy:
content: "<?php phpinfo(); ?>"
dest: "{{ web_root }}/info.php"
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: "0644"
- name: Verify Nginx is responding
uri:
url: "http://{{ ansible_host }}"
status_code: 200
delegate_to: localhost
become: no
รัน Playbook และตรวจสอบผล
ก่อนรันจริง ทดสอบด้วย --check (dry run) เพื่อดูว่า Ansible จะทำอะไรบ้างโดยไม่เปลี่ยนแปลงระบบ:
# Dry run — ไม่เปลี่ยนแปลงระบบจริง
ansible-playbook -i inventory/hosts.ini site.yml --check --diff \
--ask-vault-pass
# รันจริง
ansible-playbook -i inventory/hosts.ini site.yml --ask-vault-pass
# ตรวจสอบ syntax ก่อนรัน
ansible-playbook -i inventory/hosts.ini site.yml --syntax-check
หลังรันสำเร็จ ตรวจสอบผลด้วยคำสั่ง ad-hoc:
# ตรวจสอบ Nginx status
ansible webservers -i inventory/hosts.ini -m service_facts -a "name=nginx" --become
# ตรวจสอบ PHP version
ansible webservers -i inventory/hosts.ini -m command -a "php --version" --become
# ตรวจสอบ MySQL สามารถ login ได้
ansible webservers -i inventory/hosts.ini -m command \
-a "mysql -u{{ mysql_db_user }} -p{{ mysql_db_password }} -e 'SHOW DATABASES;'" \
--become
ผลลัพธ์ที่คาดหวัง
เมื่อ Playbook รันสำเร็จ เซิร์ฟเวอร์จะมี:
- Nginx ทำงานบน port 80 พร้อม Virtual Host สำหรับ
example.com - PHP-FPM 8.2 รันเป็น Unix socket พร้อม Extensions ครบ
- MySQL พร้อม Database
myappและ Userappuserที่มี permission เฉพาะ DB นั้น - Web root ที่
/var/www/example.comพร้อม owner ถูกต้อง
Tips สำหรับ Production
Workshop นี้วางรากฐานสำหรับ production deployment ได้ทันที โดย extend เพิ่มเติมได้ดังนี้:
- SSL/TLS: เพิ่ม Role
certbotสำหรับ Let’s Encrypt certificate อัตโนมัติ — ใช้community.crypto.acme_certificatemodule - Firewall: เพิ่ม UFW Role เปิดเฉพาะ port 80, 443, และ 22 — ใช้
community.general.ufwmodule - Backup: เพิ่ม MySQL dump task รันผ่าน cron ทุกคืน — ใช้
cronmodule พร้อมchanged_when: false - Multiple environments: สร้าง inventory แยก เช่น
inventory/production/และinventory/staging/ใช้ Playbook เดียวกัน - Health check: เพิ่ม post_task ทดสอบ MySQL connection จาก PHP ด้วย
urimodule
สรุป
Workshop นี้สร้าง Ansible Project สำหรับ deploy Nginx + PHP-FPM + MySQL ครบวงจร ตั้งแต่ Directory Structure ที่แยก Role ชัดเจน, Variables ที่ใช้ Vault เก็บ Secrets, Handler สำหรับ reload/restart service เมื่อ config เปลี่ยน ไปจนถึง pre_task ตรวจสอบ OS และ post_task ยืนยันว่าระบบทำงานได้จริง
จุดสำคัญคือ Role Structure ที่แยก concern ออกจากกัน ทำให้ทีมสามารถแก้ไข Nginx config โดยไม่ต้องแตะ MySQL Role และในทางกลับกัน นอกจากนี้การใช้ Template แทน hardcode ทำให้ Playbook เดียวใช้ตั้งค่า server_name หรือ PHP version ที่แตกต่างกันได้ผ่าน Variables โดยไม่ต้องแก้ไข Task ใดๆ

