Workshop: ใช้ Ansible ตั้งค่า Load Balancer + Multiple Backend Servers

การตั้งค่า Load Balancer ด้วยมือบนหลาย Backend Server ใช้เวลานานและมีโอกาสพลาดในขั้นตอน configuration โดยเฉพาะเมื่อต้องทำซ้ำบน Server หลายเครื่องพร้อมกัน Ansible ช่วยให้กระบวนการนี้เป็นอัตโนมัติ ตั้งค่า Nginx Load Balancer และ Backend Servers ได้พร้อมกันด้วย Playbook เดียว

Workshop นี้จะสร้าง Infrastructure ที่ประกอบด้วย Nginx Load Balancer 1 เครื่องพร้อม Backend Application Servers อีก 2 เครื่อง โดยใช้ Ansible จัดการ Configuration ทั้งหมดแบบ Idempotent

โครงสร้าง Infrastructure และ Project

Infrastructure ที่เราจะสร้างประกอบด้วย 3 เซิร์ฟเวอร์: Load Balancer 1 ตัว และ Backend 2 ตัว

Infrastructure:
  lb01 (192.168.1.10) — Nginx Load Balancer
  backend01 (192.168.1.11) — App Server #1
  backend02 (192.168.1.12) — App Server #2

Project Structure:
load-balancer/
├── inventory/
│   └── hosts.ini
├── group_vars/
│   ├── all.yml
│   ├── loadbalancers.yml
│   └── backends.yml
├── roles/
│   ├── loadbalancer/
│   │   ├── tasks/main.yml
│   │   ├── templates/
│   │   │   └── nginx-lb.conf.j2
│   │   └── handlers/main.yml
│   └── backend/
│       ├── tasks/main.yml
│       ├── templates/
│       │   └── app.conf.j2
│       └── handlers/main.yml
└── site.yml

Inventory: แยก Group ตามบทบาท

การแยก Host ออกเป็น Group ตามบทบาทช่วยให้ Apply Role ที่เหมาะสมกับแต่ละกลุ่มได้:

[loadbalancers]
lb01 ansible_host=192.168.1.10 ansible_user=ubuntu

[backends]
backend01 ansible_host=192.168.1.11 ansible_user=ubuntu
backend02 ansible_host=192.168.1.12 ansible_user=ubuntu

[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python3

Variables: กำหนด Backend Pool

กำหนด Variables สำหรับ Load Balancer ใน group_vars/loadbalancers.yml — Ansible จะรวม IP ของ Backend servers อัตโนมัติจาก Inventory:

# group_vars/loadbalancers.yml
lb_method: least_conn           # round_robin (ค่าเริ่มต้น), least_conn, ip_hash
lb_port: 80
backend_port: 8080

# รวม backend IPs จาก inventory group อัตโนมัติ
backend_servers: "{{ groups['backends'] | map('extract', hostvars, 'ansible_host') | list }}"

กำหนด Variables สำหรับ Backend servers ใน group_vars/backends.yml:

# group_vars/backends.yml
app_port: 8080
app_user: www-data
app_root: /var/www/app
server_id: "{{ inventory_hostname }}"   # ใช้ชื่อ host เป็น Server ID ในการ debug

Role: Load Balancer

สร้าง roles/loadbalancer/tasks/main.yml:

---
- name: Install Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes

- name: Deploy Load Balancer configuration
  template:
    src: nginx-lb.conf.j2
    dest: /etc/nginx/conf.d/loadbalancer.conf
    owner: root
    group: root
    mode: "0644"
  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/loadbalancer/templates/nginx-lb.conf.j2 — Load Balancer config ที่ดึง Backend IPs จาก Variables:

upstream backend_pool {
    {{ lb_method }};
{% for server_ip in backend_servers %}
    server {{ server_ip }}:{{ backend_port }};
{% endfor %}
    keepalive 32;
}

server {
    listen {{ lb_port }};
    server_name _;

    access_log /var/log/nginx/lb.access.log;
    error_log  /var/log/nginx/lb.error.log;

    location / {
        proxy_pass         http://backend_pool;
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 5s;
        proxy_read_timeout    60s;
    }

    # Health check endpoint สำหรับ monitoring
    location /lb-health {
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }
}

Jinja2 loop {% for server_ip in backend_servers %} จะขยายเป็น upstream entries ตามจำนวน Backend จริงใน Inventory โดยอัตโนมัติ ทำให้เพิ่ม Backend ได้เพียงแค่เพิ่ม Host ใน hosts.ini

Role: Backend Application Server

สร้าง roles/backend/tasks/main.yml — ติดตั้ง Nginx ฝั่ง Backend และ Deploy Application:

---
- name: Install Nginx for backend
  apt:
    name: nginx
    state: present
    update_cache: yes

- name: Create application directory
  file:
    path: "{{ app_root }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: "0755"

- name: Deploy application config (listen on backend port)
  template:
    src: app.conf.j2
    dest: /etc/nginx/conf.d/app.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload Nginx

- name: Remove default site
  file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: Reload Nginx

- name: Deploy application (index page identifying this backend)
  copy:
    content: |
      <!DOCTYPE html>
      <html>
      <head><title>Backend: {{ server_id }}</title></head>
      <body>
        <h1>Response from: {{ server_id }}</h1>
        <p>IP: {{ ansible_host }}</p>
      </body>
      </html>
    dest: "{{ app_root }}/index.html"
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: "0644"

- name: Ensure Nginx is started and enabled
  service:
    name: nginx
    state: started
    enabled: yes

สร้าง roles/backend/templates/app.conf.j2:

server {
    listen {{ app_port }};
    server_name _;
    root {{ app_root }};
    index index.html index.htm;

    access_log /var/log/nginx/app.access.log;
    error_log  /var/log/nginx/app.error.log;

    location / {
        try_files $uri $uri/ =404;
    }

    # Health check สำหรับ Load Balancer ตรวจสอบ
    location /health {
        return 200 "{{ server_id }} OK\n";
        add_header Content-Type text/plain;
    }
}

Main Playbook: site.yml

สร้าง site.yml ที่ Apply Role ต่างกันสำหรับ Group ต่างกัน:

---
# Deploy Backend servers ก่อน Load Balancer
- name: Configure Backend Application Servers
  hosts: backends
  become: yes
  roles:
    - backend

# Deploy Load Balancer หลังจาก Backend พร้อมแล้ว
- name: Configure Load Balancer
  hosts: loadbalancers
  become: yes
  roles:
    - loadbalancer

  post_tasks:
    - name: Wait for Load Balancer to be ready
      wait_for:
        host: "{{ ansible_host }}"
        port: "{{ lb_port }}"
        delay: 2
        timeout: 30
      delegate_to: localhost
      become: no

    - name: Verify Load Balancer responds
      uri:
        url: "http://{{ ansible_host }}/lb-health"
        status_code: 200
      delegate_to: localhost
      become: no
      register: lb_check

    - name: Show Load Balancer health check result
      debug:
        msg: "Load Balancer health: {{ lb_check.status }}"

รัน Playbook และตรวจสอบ Load Balancing

# Deploy ทั้งหมด
ansible-playbook -i inventory/hosts.ini site.yml

# ทดสอบ Load Balancing — รัน curl หลายครั้งควรได้ response จาก Backend ต่างกัน
for i in $(seq 1 6); do
    curl -s http://192.168.1.10/ | grep "Response from"
done

ผลที่ควรเห็น — Load Balancer กระจาย Request ไปยัง Backend ทั้งสอง:

Response from: backend01
Response from: backend02
Response from: backend01
Response from: backend02
Response from: backend01
Response from: backend02

ตรวจสอบ Backend Health แต่ละตัว

# ตรวจสอบ health endpoint ของ backend แต่ละตัว
ansible backends -i inventory/hosts.ini -m uri \
  -a "url=http://localhost:8080/health" --become

# ดู Nginx upstream status
ansible loadbalancers -i inventory/hosts.ini -m command \
  -a "nginx -T" --become | grep upstream

เพิ่ม Backend Server โดยไม่แก้ Playbook

จุดเด่นของ Design นี้คือการ Scale โดยเพิ่มแค่ Inventory:

# เพิ่ม backend03 ใน hosts.ini
[backends]
backend01 ansible_host=192.168.1.11 ansible_user=ubuntu
backend02 ansible_host=192.168.1.12 ansible_user=ubuntu
backend03 ansible_host=192.168.1.13 ansible_user=ubuntu  # เพิ่มใหม่

# รัน Playbook ใหม่ — Ansible จะ:
# 1. Configure backend03 ด้วย Role "backend"
# 2. Update nginx-lb.conf บน lb01 ให้รวม backend03:8080 ใน upstream pool อัตโนมัติ
ansible-playbook -i inventory/hosts.ini site.yml

เนื่องจาก template ใช้ {% for server_ip in backend_servers %} และ backend_servers ดึงมาจาก groups['backends'] ใน Inventory การเพิ่ม Host ใหม่จึงอัพเดต Load Balancer config โดยอัตโนมัติ

สรุป

Workshop นี้สร้าง Load Balancer + Multiple Backend Infrastructure ด้วย Ansible โดยมี Design สำคัญสองจุด ได้แก่ การใช้ Jinja2 Loop ใน Template ทำให้ upstream pool อัพเดตอัตโนมัติตาม Inventory และการแยก Playbook ให้ Deploy Backend ก่อน Load Balancer เพื่อให้ระบบพร้อมรับ Traffic ตั้งแต่แรก

การ Scale เพิ่ม Backend เพียงแค่เพิ่ม Host ใน Inventory แล้วรัน Playbook ซ้ำ โดยไม่ต้องแก้ไข Template หรือ Task ใดๆ ทำให้ Infrastructure ขยายตัวได้อย่างปลอดภัยและรวดเร็ว