การติดตั้ง web server ด้วย Ansible แทนการ SSH เข้า server แล้วพิมพ์คำสั่งทีละบรรทัดช่วยให้ deploy ได้เร็วกว่า ลดความผิดพลาด และทำซ้ำได้บน server ทุกเครื่องโดยไม่ต้องกังวลว่าจะลืมขั้นตอนใด playbook ที่ดีสำหรับ web server ไม่ใช่แค่ติดตั้ง package แต่ต้องจัดการ config, virtual host, SSL certificate, และ service state ได้ครบในที่เดียว
บทความนี้แสดง playbook สมบูรณ์สำหรับติดตั้งและตั้งค่า web server บน Ubuntu/Debian พร้อมรายละเอียดแต่ละ task และ template ที่สามารถนำไปปรับใช้กับ production server ได้ทันที โดยครอบคลุมตั้งแต่การเตรียม inventory, deploy config ผ่าน Jinja2 template, จัดการ virtual host, ไปจนถึงการตรวจสอบหลัง deploy ตัวอย่างทั้งหมดทดสอบบน Ubuntu 22.04 และใช้ได้กับ Debian-based distributions ทั่วไป
เตรียม Inventory และ Variables
ก่อนเขียน playbook ต้องกำหนด inventory และ variables ให้พร้อม โดยแยก web server ออกเป็น group เฉพาะ และเก็บค่า config ไว้ใน group_vars เพื่อให้ปรับได้ง่ายโดยไม่ต้องแก้ playbook หลัก ค่าที่ควรกำหนดไว้ล่วงหน้าได้แก่ port ที่ใช้รับ connection, ชื่อ domain, path ของ document root, และ performance tuning parameters เช่น จำนวน worker process และ worker connection
การแยก variables ออกจาก playbook ทำให้ใช้ playbook เดิมกับ environment ต่างกันได้โดยเพียงเปลี่ยนค่าใน group_vars หรือ host_vars เท่านั้น เช่น staging อาจใช้ port 8080 ขณะที่ production ใช้ port 80 และมี worker_processes สูงกว่า โดยไม่ต้องเขียน playbook สองชุดแยกกัน ตัวอย่างด้านล่างแสดง inventory สำหรับ web server 2 เครื่องพร้อม variables พื้นฐานที่ใช้ทั่วทั้ง group
# inventories/production/hosts.ini
[webservers]
web-01 ansible_host=203.0.113.10 ansible_user=ubuntu
web-02 ansible_host=203.0.113.11 ansible_user=ubuntu
# inventories/production/group_vars/webservers.yml
web_port: 80
server_name: example.com
document_root: /var/www/html
worker_processes: auto
worker_connections: 1024
Playbook ติดตั้ง Web Server
โครงสร้าง playbook หลักแบ่งเป็น 4 ส่วน ได้แก่ ติดตั้ง package, deploy config, จัดการ virtual host, และตรวจสอบ service การกำหนด handlers แยกไว้ตั้งแต่ต้นช่วยให้ reload หรือ restart เกิดขึ้นเฉพาะเมื่อ config เปลี่ยนแปลงจริง ไม่ใช่ทุกครั้งที่รัน playbook ซึ่งสำคัญมากสำหรับ production server ที่รับ traffic อยู่ตลอดเวลา
การใช้ cache_valid_time ใน apt module ช่วยลดเวลารัน playbook โดยไม่ update apt cache ซ้ำถ้า cache ยังใหม่ไม่ถึง 1 ชั่วโมง ส่วน state: present ทำให้ task idempotent — ถ้า package ติดตั้งไว้แล้วจะข้ามโดยอัตโนมัติ ไม่มีการ reinstall โดยไม่จำเป็น การตั้ง enabled: true ในทุก service task ทำให้ service เริ่มขึ้นอัตโนมัติหลัง server reboot โดยไม่ต้องทำซ้ำ
# nginx.yml
---
- name: Install and configure Nginx web server
hosts: webservers
become: true
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
enabled: true
- name: reload nginx
service:
name: nginx
state: reloaded
tasks:
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
- name: Install nginx
apt:
name: nginx
state: present
- name: Ensure nginx is started and enabled
service:
name: nginx
state: started
enabled: true
Deploy Config ผ่าน Jinja2 Template
แทนที่จะ hardcode ค่าใน config file ให้ใช้ Jinja2 template เพื่อดึงค่าจาก variables มาใส่อัตโนมัติ ทำให้ config แต่ละ server แตกต่างกันได้โดยไม่ต้องแก้ template ไฟล์ template จะถูก render ด้วยค่า variable ของแต่ละ host ก่อน copy ไปยัง server จริง
ไฟล์ template เก็บไว้ใน directory templates/ ของ playbook โดยใช้นามสกุล .j2 ตามธรรมเนียม ค่าตัวแปรจะถูกแทนที่ด้วย syntax {{ variable_name }} ซึ่ง Ansible จะ resolve ก่อน upload ขึ้น server ทำให้ server แต่ละเครื่องได้ค่า config ที่ถูกต้องตาม variables ที่กำหนดไว้
# templates/nginx.conf.j2
user www-data;
worker_processes {{ worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ worker_connections }};
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
# templates/virtualhost.conf.j2
server {
listen {{ web_port }};
server_name {{ server_name }};
root {{ document_root }};
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
access_log /var/log/nginx/{{ server_name }}_access.log;
error_log /var/log/nginx/{{ server_name }}_error.log;
}
Tasks สำหรับ deploy config ใช้ template module แทน copy module เพื่อให้ Ansible render ค่า variable ก่อน upload การตั้ง backup: true จะสำรองไฟล์ config เดิมไว้อัตโนมัติ ซึ่งช่วยให้ rollback ได้ง่ายหากเกิดปัญหา ส่วน notify: reload nginx จะเรียก handler ให้ reload config เฉพาะเมื่อไฟล์เปลี่ยนแปลง ไม่ใช่ทุกครั้งที่รัน playbook
- name: Deploy nginx main config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: true
notify: reload nginx
- name: Ensure document root exists
file:
path: "{{ document_root }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Deploy virtual host config
template:
src: virtualhost.conf.j2
dest: /etc/nginx/sites-available/{{ server_name }}.conf
owner: root
group: root
mode: '0644'
notify: reload nginx
- name: Enable virtual host
file:
src: /etc/nginx/sites-available/{{ server_name }}.conf
dest: /etc/nginx/sites-enabled/{{ server_name }}.conf
state: link
notify: reload nginx
- name: Remove default virtual host
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
จัดการ Virtual Host และ Document Root
การสร้าง virtual host ผ่าน template และ symlink ไปยัง sites-enabled เป็น pattern มาตรฐานที่ช่วยให้จัดการหลาย domain บน server เดียวกันได้สะดวก แต่ละ domain มีไฟล์ config แยกใน sites-available และเปิดใช้งานโดย symlink ใน sites-enabled ทำให้ disable domain ได้ง่ายเพียงลบ symlink โดยไม่ต้องลบ config จริง
การลบ default virtual host ออกเป็นขั้นตอนที่มักถูกลืม แต่สำคัญมากใน production เพราะ default site จะรับ request ที่ไม่ match กับ virtual host ใด ๆ ซึ่งอาจเปิดเผยข้อมูลที่ไม่ต้องการต่อ internet task state: absent จะลบ symlink นั้นออกอย่าง idempotent หมายความว่าถ้าไม่มีไฟล์อยู่แล้วก็จะข้ามโดยไม่ error
ติดตั้ง SSL Certificate ด้วย Let’s Encrypt
สำหรับ server ที่ต้องการ HTTPS สามารถเพิ่ม tasks สำหรับติดตั้ง certbot และออก certificate ได้โดยตรงใน playbook เดิม ใช้ conditional when: ssl_enabled | default(false) | bool เพื่อให้ tasks เหล่านี้รันเฉพาะเมื่อกำหนด variable ไว้ ทำให้ playbook เดิมรองรับได้ทั้ง HTTP และ HTTPS โดยไม่ต้องแยกเป็น playbook คนละชุด
argument creates: ใน command module ทำให้ task idempotent — ถ้า certificate มีอยู่แล้วจะข้ามการออก certificate ซ้ำ ป้องกันการชน rate limit ของ Let’s Encrypt ที่จำกัดจำนวนการออก certificate ต่อ domain ต่อสัปดาห์ ซึ่งสำคัญมากในระหว่าง development ที่อาจรัน playbook หลายรอบ
- name: Install certbot
apt:
name:
- certbot
- python3-certbot-nginx
state: present
when: ssl_enabled | default(false) | bool
- name: Obtain SSL certificate
command: >
certbot --nginx
-d {{ server_name }}
--non-interactive
--agree-tos
--email {{ ssl_email }}
--redirect
args:
creates: /etc/letsencrypt/live/{{ server_name }}/fullchain.pem
when: ssl_enabled | default(false) | bool
notify: reload nginx
ตรวจสอบหลัง Deploy
การตรวจสอบหลัง deploy เป็นส่วนสำคัญที่หลายคนมักข้ามไป แต่จริง ๆ แล้วช่วยให้รู้ทันทีว่าการ deploy สำเร็จหรือไม่โดยไม่ต้องเปิด browser ตรวจ task nginx -t ตรวจ syntax ของ config ก่อน และ wait_for รอให้ port พร้อมรับ connection จากนั้น uri module ส่ง HTTP request จริงไปยัง server เพื่อยืนยันว่าทำงานถูกต้อง
การ register ผล HTTP check ไว้ใน variable ทำให้ debug task แสดงผลสรุปได้ชัดเจน และยังใช้ในการตัดสินใจใน task ถัดไปได้ เช่น ส่ง notification หรือ rollback อัตโนมัติถ้า response code ไม่ถูกต้อง การ validate config ก่อน reload ช่วยป้องกันไม่ให้ service หยุดทำงานเพราะ config syntax ผิดพลาด
การใช้ wait_for module ร่วมกับ uri ให้ผลดีกว่า command: curl เพราะ Ansible จัดการ retry และ timeout ให้เองโดยอัตโนมัติ และ fail ด้วย error message ที่อ่านเข้าใจง่ายเมื่อเกินเวลาที่กำหนด แทนที่จะต้องเขียน logic retry เองใน playbook ซึ่งทำให้ code ซับซ้อนโดยไม่จำเป็น
- name: Validate nginx config syntax
command: nginx -t
changed_when: false
- name: Wait for web server to be ready
wait_for:
host: "{{ ansible_host }}"
port: "{{ web_port }}"
delay: 2
timeout: 30
- name: Check HTTP response
uri:
url: "http://{{ ansible_host }}:{{ web_port }}"
status_code: [200, 301, 302]
timeout: 10
register: http_check
failed_when: http_check.status not in [200, 301, 302]
- name: Show deployment result
debug:
msg: "Web server is running on {{ ansible_host }}:{{ web_port }} — status {{ http_check.status }}"
เพิ่ม Tags เพื่อรัน Subset ของ Tasks
การใส่ tags ใน tasks ช่วยให้รัน subset ของ playbook ได้โดยไม่ต้องรันทั้งหมด เช่น หลังจากติดตั้ง package ไปแล้วและต้องการแก้เฉพาะ config สามารถใช้ --tags config เพื่อข้ามขั้นตอน package installation ได้ทันที ลดเวลาการรัน playbook อย่างมีนัยสำคัญในระหว่าง development และ troubleshooting
tag ที่ดีควรสะท้อน “ประเภทงาน” ไม่ใช่ชื่อ task เช่น ใช้ packages, config, vhost, verify แทนการใส่ชื่อ task โดยตรง ทำให้เข้าใจได้ทันทีว่า --tags config หมายถึงรัน task ทั้งหมดที่เกี่ยวกับ configuration และ --skip-tags packages หมายถึงข้ามการติดตั้ง package ทั้งหมด ซึ่งสะดวกมากเมื่อต้องการ apply เฉพาะส่วนใดส่วนหนึ่งในระหว่าง incident response
- name: Install nginx
apt:
name: nginx
state: present
tags: [packages]
- name: Deploy nginx main config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
backup: true
notify: reload nginx
tags: [config]
- name: Deploy virtual host config
template:
src: virtualhost.conf.j2
dest: /etc/nginx/sites-available/{{ server_name }}.conf
mode: '0644'
notify: reload nginx
tags: [config, vhost]
- name: Enable virtual host
file:
src: /etc/nginx/sites-available/{{ server_name }}.conf
dest: /etc/nginx/sites-enabled/{{ server_name }}.conf
state: link
notify: reload nginx
tags: [config, vhost]
- name: Validate config syntax
command: nginx -t
changed_when: false
tags: [verify]
- name: Check HTTP response
uri:
url: "http://{{ ansible_host }}:{{ web_port }}"
status_code: [200, 301, 302]
tags: [verify]
# รันทั้งหมด
ansible-playbook -i inventories/production nginx.yml
# รันเฉพาะส่วน config (ข้ามการติดตั้ง package)
ansible-playbook -i inventories/production nginx.yml --tags config
# รันเฉพาะส่วนตรวจสอบ
ansible-playbook -i inventories/production nginx.yml --tags verify
# Dry-run ดูว่าจะเปลี่ยนอะไรบ้าง
ansible-playbook -i inventories/production nginx.yml --check
โครงสร้างไฟล์ทั้งหมด
การจัดโครงสร้างไฟล์อย่างเป็นระบบตั้งแต่ต้นทำให้ทีมเพิ่มเติมหรือแก้ไขได้ง่าย โดยแต่ละไฟล์มีหน้าที่ชัดเจน inventory อยู่แยกจาก templates และ group_vars ซึ่งช่วยให้ค้นหาได้รวดเร็วเมื่อโปรเจกต์ขยายใหญ่ขึ้น การแยก staging และ production inventory ออกจากกันป้องกันไม่ให้ deploy ผิด environment โดยไม่ตั้งใจ
การวาง templates ไว้ใน directory templates/ ที่ระดับเดียวกับ playbook หลักทำให้ Ansible ค้นหาไฟล์ได้อัตโนมัติโดยไม่ต้องระบุ path เต็ม ถ้าโปรเจกต์มีหลาย role สามารถย้ายไปใช้ Ansible roles structure แทนได้โดยไม่ต้องแก้ logic ของ playbook มากนัก เพียงแค่จัดไฟล์ใหม่ตามโครงสร้าง roles/webserver/templates/ ก็พร้อมใช้งาน
web-project/
├── nginx.yml # playbook หลัก
├── inventories/
│ ├── production/
│ │ ├── hosts.ini
│ │ └── group_vars/
│ │ └── webservers.yml
│ └── staging/
│ ├── hosts.ini
│ └── group_vars/
│ └── webservers.yml
└── templates/
├── nginx.conf.j2 # main config template
└── virtualhost.conf.j2 # virtual host template
สรุป
playbook ติดตั้ง web server ที่ดีควรครอบคลุมตั้งแต่ติดตั้ง package, deploy config ผ่าน Jinja2 template, จัดการ virtual host, ไปจนถึงตรวจสอบหลัง deploy การใช้ handlers ทำให้ reload config เฉพาะเมื่อมีการเปลี่ยนแปลง และ tags ช่วยให้รันเฉพาะส่วนที่ต้องการได้โดยไม่ต้องรัน playbook ทั้งหมดทุกครั้ง การแยก variables ออกมาไว้ใน group_vars ทำให้ใช้ playbook เดิมกับ environment ต่าง ๆ ได้โดยไม่ต้องแก้ logic หลัก โครงสร้างนี้ scale ได้ดีตั้งแต่ server เครื่องเดียวไปจนถึงหลายร้อยเครื่อง
ขั้นตอนสำคัญที่ไม่ควรข้ามคือการ validate config syntax ก่อน reload และการ check HTTP response หลัง deploy เสร็จ steps เหล่านี้ช่วยให้จับปัญหาได้เร็วก่อนที่จะกระทบผู้ใช้จริง และยังทำให้ทีมมั่นใจได้ว่าทุก deployment ที่ผ่าน playbook จะอยู่ในสถานะที่ถูกต้องเสมอ ไม่ใช่เพียงสั่งติดตั้งแล้วหวังว่าจะทำงานได้เอง เมื่อเพิ่ม server ใหม่เข้า inventory สิ่งที่ต้องทำคือ run playbook เดิม — ทุกอย่างตั้งค่าและตรวจสอบให้ครบโดยอัตโนมัติ

