Workshop: สร้าง Ansible Playbook ตั้งค่า Web Server Stack (Nginx + PHP + MySQL)

การตั้งค่า 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 และ User appuser ที่มี 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_certificate module
  • Firewall: เพิ่ม UFW Role เปิดเฉพาะ port 80, 443, และ 22 — ใช้ community.general.ufw module
  • Backup: เพิ่ม MySQL dump task รันผ่าน cron ทุกคืน — ใช้ cron module พร้อม changed_when: false
  • Multiple environments: สร้าง inventory แยก เช่น inventory/production/ และ inventory/staging/ ใช้ Playbook เดียวกัน
  • Health check: เพิ่ม post_task ทดสอบ MySQL connection จาก PHP ด้วย uri module

สรุป

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 ใดๆ