เขียน Ansible Playbook ติดตั้ง Web Server (Nginx) บน Cloud VPS

การติดตั้ง 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 เดิม — ทุกอย่างตั้งค่าและตรวจสอบให้ครบโดยอัตโนมัติ