Apache APISIX คือ API Gateway แบบโอเพนซอร์สในโครงการ Apache Software Foundation ทำหน้าที่เป็นจุดรับคำขอก่อนส่งต่อไปยัง API ภายใน ช่วยรวมการกำหนดเส้นทาง การยืนยันตัวตน การจำกัดจำนวนคำขอ และการกระจายโหลดไว้ในชั้นกลาง [1]

บทความนี้อธิบายแนวคิดและสร้างห้องทดลองที่มี APISIX หนึ่งตัวกับ Backend API สองตัว ผู้เรียนจะได้ทดสอบทั้งกรณีสำเร็จและกรณีถูกปฏิเสธ เหมาะสำหรับนำไปต่อยอดเป็นบทเรียน API Testing หรือ Microservices

#1. APISIX ช่วยแก้ปัญหาอะไร

เมื่อระบบมีหลาย API เช่น ผู้ใช้ สินค้า และคำสั่งซื้อ การใส่โค้ดตรวจ API Key และจำกัดคำขอแยกทุกบริการทำให้การดูแลนโยบายซ้ำซ้อน APISIX ช่วยบังคับใช้นโยบายเหล่านี้ก่อนที่คำขอจะถึงบริการปลายทาง

ตัวอย่างเช่น Client เรียก /api/users ที่ APISIX จากนั้น Gateway ตรวจ API Key ตรวจโควตา เปลี่ยนเส้นทางเป็น /users และเลือก Backend ที่จะรับคำขอ ส่วน Backend ยังคงรับผิดชอบตรรกะธุรกิจและการตรวจสิทธิ์ระดับข้อมูล เช่น ผู้ใช้คนใดดูข้อมูลของใครได้

ความสามารถ การนำไปใช้
Routing จับคู่ Path, Host หรือ HTTP Method กับบริการ
Authentication ตรวจ API Key หรือเชื่อมระบบยืนยันตัวตนผ่าน Plugin ที่เหมาะสม
Traffic control จำกัดจำนวนคำขอหรือควบคุมปริมาณงาน
Load balancing กระจายคำขอไปยัง Backend หลายตัว
Observability เชื่อม Metrics, Logs และ Traces
Plugins เพิ่มนโยบายให้ Route หรือทรัพยากรอื่นตามขอบเขตที่รองรับ

APISIX พัฒนาบน NGINX และ ngx_lua โดยใช้ LuaJIT พร้อมระบบ Plugin [2] ส่วน Apache HTTP Server เป็นคนละโครงการ ไม่ใช่ชื่อเต็มหรือรุ่นหนึ่งของ APISIX

#2. คำศัพท์สำคัญ

คำศัพท์ ความหมายในห้องทดลอง
Route กฎรับคำขอ เช่น GET /api/users
Upstream กลุ่ม Backend ที่ Gateway ส่งคำขอต่อไป
Consumer ตัวตนของผู้เรียก เช่น student
Plugin นโยบาย เช่น key-auth และ limit-count
Service ทรัพยากรสำหรับใช้การตั้งค่าร่วมกันระหว่างหลาย Route; ห้องทดลองนี้ยังไม่จำเป็นต้องใช้
Data plane ส่วนที่รับและประมวลผลคำขอจาก Client
Control plane ส่วนจัดการและส่งต่อการตั้งค่าให้ระบบ

#3. โหมดการติดตั้ง

APISIX รองรับหลายรูปแบบ [3]

รูปแบบ แนวทางใช้งาน
Traditional รวมบทบาทรับ Traffic และจัดการ Configuration ใช้ etcd ในรูปแบบทั่วไป
Decoupled แยก Control plane และ Data plane
Standalone แบบ File-driven อ่าน Configuration จากไฟล์ เช่น YAML โดยไม่ต้องใช้ etcd

Workshop นี้เลือก Standalone แบบ File-driven โดยใช้ role: data_plane และ config_provider: yaml การแก้ Route จึงทำผ่านไฟล์ ไม่ใช่การส่งคำสั่ง CRUD ไปยัง Admin API ทั้งนี้เอกสารรุ่นปัจจุบันยังมี Standalone แบบ API-driven ซึ่งเป็นอีกแนวทางหนึ่งและไม่ได้ใช้ในบทความนี้

#4. เป้าหมายและสิ่งที่ต้องเตรียม

ใช้เวลาประมาณ 45–60 นาที เตรียม Docker Engine หรือ Docker Desktop พร้อม Docker Compose v2, Terminal แบบ Bash และ curl หากใช้ Windows แนะนำ WSL2 สำหรับคำสั่งในบทความ

เมื่อทำเสร็จจะสามารถ:

  1. เรียก API ผ่าน Gateway
  2. ทดสอบ API Key ที่ถูกต้องและไม่ถูกต้อง
  3. ตรวจพบ HTTP 429 เมื่อเกินโควตา
  4. สังเกตคำขอที่ไปถึง Backend สองตัว
  5. แยกแยะ Gateway 404 กับ Backend 404
docker --version
docker compose version
mkdir apisix-workshop
cd apisix-workshop
mkdir conf

สร้างไฟล์ตามตารางต่อไปนี้ในโฟลเดอร์เดียวกัน

ไฟล์ หน้าที่
compose.yaml เริ่ม APISIX และ Backend
backend.py API ตัวอย่างด้วย Python standard library
conf/config.yaml โหมดการทำงานของ APISIX
conf/apisix.yaml Routes, Upstreams, Consumers และ Plugins

#5. สร้าง Backend API

สร้าง backend.py ตัวอย่างนี้เขียนขึ้นสำหรับห้องทดลอง ใช้ Python standard library โดยไม่ต้องติดตั้งแพ็กเกจเพิ่ม

import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit

SERVER_NAME = os.environ.get("SERVER_NAME", "backend")

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/health":
            status, body = 200, {"status": "ok", "server": SERVER_NAME}
        elif path == "/users":
            status, body = 200, {
                "server": SERVER_NAME,
                "users": [{"id": 1, "name": "Student"}],
                "apikey_received": self.headers.get("apikey") is not None,
            }
        else:
            status, body = 404, {"error": "backend route not found"}
        payload = json.dumps(body).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

ฟิลด์ server ช่วยระบุว่า Backend ตัวใดตอบ ส่วน apikey_received ใช้ตรวจว่า Gateway ลบ API Key ก่อนส่งต่อแล้วหรือไม่ โดยไม่แสดงค่า Key จริง

#6. ตั้งค่าโหมด APISIX

สร้าง conf/config.yaml

apisix:
  node_listen: 9080
  enable_admin: false

deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml

ไฟล์นี้กำหนดให้รับ HTTP ที่พอร์ต 9080 และอ่านทรัพยากรจาก conf/apisix.yaml ภายใน Container การเปลี่ยนไฟล์ตั้งค่าหลักอาจต้อง Reload หรือ Restart ต่างจากการเปลี่ยน Route แบบ File-driven ที่รองรับ Hot update [3]

#7. สร้าง Routes และนโยบาย

สร้าง conf/apisix.yaml โดยคงบรรทัด #END ไว้ท้ายไฟล์

upstreams:
  - id: backend-pool
    type: roundrobin
    nodes:
      "backend1:8000": 1
      "backend2:8000": 1

consumers:
  - username: student
    plugins:
      key-auth:
        key: workshop-key-change-me

routes:
  - id: public-health
    uri: /health
    methods: [GET]
    upstream_id: backend-pool

  - id: users-api
    uri: /api/*
    methods: [GET]
    upstream_id: backend-pool
    plugins:
      key-auth:
        hide_credentials: true
      limit-count:
        count: 5
        time_window: 60
        key_type: var
        key: consumer_name
        policy: local
        rejected_code: 429
        show_limit_quota_header: true
      proxy-rewrite:
        regex_uri: ["^/api/(.*)", "/$1"]
#END

คำอธิบายค่าที่สำคัญ:

  • backend-pool มีสอง Backend น้ำหนักเท่ากัน
  • /health เปิดให้เรียกโดยไม่ใช้ Key เฉพาะการตรวจห้องทดลอง
  • /api/* ต้องผ่าน key-auth ก่อน โดย Key ของ Consumer เป็นค่าตัวอย่างสำหรับ Local lab
  • hide_credentials: true ลบ Credential ก่อนส่งไป Backend [4]
  • limit-count ใช้โควตา 5 คำขอต่อหน้าต่างเวลา 60 วินาทีต่อ Consumer บน Gateway ตัวนี้ ไม่ใช่ 5 คำขอต่อวินาที [5]
  • policy: local เก็บ Counter ภายใน APISIX instance นี้ เมื่อเพิ่มหลาย instance โควตาจะไม่ได้รวมทั่วระบบโดยอัตโนมัติ
  • proxy-rewrite แปลง /api/users เป็น /users ก่อนส่งต่อ เป็นการ Rewrite ภายใน ไม่ใช่ HTTP Redirect ให้ Browser เปลี่ยน URL [6]
  • ลำดับ Plugin ที่เขียนใน YAML ไม่ใช่ตัวกำหนดลำดับทำงานทั้งหมด APISIX ใช้ Phase และ Priority ของ Plugin

File-driven YAML ต้องมี #END ท้ายไฟล์เพื่อให้ APISIX โหลดกฎ การตั้งค่าในไฟล์นี้เป็น Native APISIX standalone configuration ไม่ใช่ไฟล์ ADC ซึ่งอาจใช้โครงสร้างต่างกัน [3]

#8. เริ่มระบบด้วย Docker Compose

สร้าง compose.yaml

services:
  backend1:
    image: python:3.12-alpine
    working_dir: /app
    command: ["python", "-u", "backend.py"]
    environment:
      SERVER_NAME: backend-1
    volumes:
      - ./backend.py:/app/backend.py:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
      interval: 5s
      timeout: 3s
      retries: 10

  backend2:
    image: python:3.12-alpine
    working_dir: /app
    command: ["python", "-u", "backend.py"]
    environment:
      SERVER_NAME: backend-2
    volumes:
      - ./backend.py:/app/backend.py:ro
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"]
      interval: 5s
      timeout: 3s
      retries: 10

  apisix:
    image: apache/apisix:3.19.0-debian
    ports:
      - "127.0.0.1:9080:9080"
    volumes:
      - ./conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
      - ./conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml:ro
    depends_on:
      backend1:
        condition: service_healthy
      backend2:
        condition: service_healthy

Backend ไม่มีการ Publish port ออกมายัง Host การทดลองทั้งหมดเข้าผ่าน APISIX ที่ 127.0.0.1:9080 ชื่อ backend1 และ backend2 เป็นชื่อบริการที่ค้นหาได้บนเครือข่าย Compose

docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail=80 apisix
curl -i http://127.0.0.1:9080/health

เมื่อพร้อมควรได้ HTTP 200 และ JSON ที่มี status: ok หากเพิ่งเริ่ม Container ให้รอ Gateway พร้อมแล้วลองซ้ำ Image tag ของ APISIX อ้างอิงจาก Repository Docker ทางการ [7] ก่อนนำไปใช้จริงควรเลือกเวอร์ชันที่องค์กรรองรับและตรึง Digest ตามกระบวนการของทีม

#9. ทดสอบการยืนยันตัวตน

#9.1 ไม่ส่ง API Key

curl -i http://127.0.0.1:9080/api/users

คาดหวัง HTTP 401 เพราะ Route บังคับ Authentication

#9.2 ส่ง API Key ผิด

curl -i -H 'apikey: wrong-key' http://127.0.0.1:9080/api/users

คาดหวัง HTTP 401 เช่นกัน

#9.3 ส่ง API Key ถูกต้อง

curl -i -H 'apikey: workshop-key-change-me' http://127.0.0.1:9080/api/users

คาดหวัง HTTP 200 หากยังไม่เกินโควตา และ JSON ตัวอย่าง:

{"server":"backend-1","users":[{"id":1,"name":"Student"}],"apikey_received":false}

ค่า server อาจเป็น backend-2 ได้ ส่วน apikey_received: false แสดงว่า Header apikey ไม่ได้ถูกส่งถึง Backend ในคำขอนี้

#10. ทดสอบ Rate Limiting

ถ้าเพิ่งทดสอบคำขอที่ใช้ Key ให้รออย่างน้อย 61 วินาทีโดยไม่ส่งคำขอที่ใช้ Consumer นี้ แล้วรันทั้งชุดให้เสร็จภายในหน้าต่างเดียว

for i in {1..7}; do
  printf 'Request %s: ' "$i"
  curl -s -o /dev/null -w '%{http_code}\n' \
    -H 'apikey: workshop-key-change-me' \
    http://127.0.0.1:9080/api/users
done

ในหน้าต่างเวลาที่เริ่มใหม่ คาดหวังห้าคำขอแรกได้ 200 และสองคำขอถัดไปได้ 429 หากเริ่มด้วย Counter ที่ถูกใช้ไปแล้วหรือข้ามหน้าต่างเวลา จำนวนที่สำเร็จอาจต่างออกไป

consumer_name ช่วยแยกโควตาตาม Consumer ที่ผ่าน Authentication หากเปลี่ยนเป็น remote_addr โควตาจะผูกกับ IP ที่ APISIX มองเห็น ซึ่งอาจเป็น IP ของ Reverse Proxy หรือ NAT แทน Client จริง

#11. สังเกต Load Balancing

ใช้ /health เพื่อไม่ใช้โควตาของ Route ที่ป้องกันไว้

for i in {1..12}; do
  curl -s http://127.0.0.1:9080/health
  printf '\n'
done

ควรสังเกตได้ว่ามีทั้ง backend-1 และ backend-2 รับคำขอ การทดสอบจำนวนน้อยไม่จำเป็นต้องสลับแบบ 1–2 อย่างเคร่งครัดทุกครั้ง และตัวอย่างนี้ยังไม่ได้ตั้งค่า Active health check ของ APISIX

Healthcheck ใน Compose ใช้ช่วยตรวจความพร้อมของ Backend ก่อนเริ่ม APISIX ไม่ได้เท่ากับการกำหนดนโยบาย Health check, Retry หรือ Failover ให้ Upstream ของ Gateway

#12. ตารางกรณีทดสอบสำหรับใช้สอน

กรณี คำขอ/เงื่อนไข ผลที่คาดหวัง
TC01 GET /health 200
TC02 GET /api/users ไม่มี Key 401
TC03 Key ไม่ถูกต้อง 401
TC04 Key ถูกต้องและมีโควตา 200 และมี users
TC05 คำขอสำเร็จที่ส่ง Key ผ่าน Header apikey_received เป็น false
TC06 ใช้โควตาเกินกำหนดในหน้าต่างเดียวกัน 429
TC07 GET /unknown 404 จาก Gateway เพราะไม่ตรง Route
TC08 GET /api/unknown พร้อม Key และมีโควตา 404 จาก Backend พร้อมข้อความ backend route not found
TC09 เรียก /health ซ้ำหลายครั้ง สังเกต Backend ได้ทั้งสองตัว

แยกชุดทดสอบ Rate limit ออกจากกรณีที่ต้องการ HTTP 200 เพื่อไม่ให้ Counter จากกรณีก่อนหน้ารบกวนผล สามารถนำตารางนี้ไปเขียนด้วย Playwright APIRequestContext, Postman/Newman หรือ pytest ได้

#13. แก้ไข Configuration และแก้ปัญหา

APISIX ตรวจการเปลี่ยนแปลงของไฟล์ Standalone ตามรอบที่กำหนด ซึ่งค่าเริ่มต้นคือประมาณหนึ่งวินาที [3] ลองเพิ่ม count เป็น 10 แล้วเรียกซ้ำเมื่อ Counter เก่าหมดอายุ

หาก Editor บันทึกไฟล์ด้วยการแทนที่ไฟล์แบบ Atomic rename การ Bind mount รายไฟล์อาจยังอ้างถึงไฟล์เดิม เพื่อให้แน่ใจว่านำไฟล์ใหม่เข้า Container แล้ว ให้ใช้:

docker compose up -d --force-recreate apisix
docker compose logs --tail=100 apisix

การ Recreate ทำให้ Counter แบบ Local เริ่มใหม่ และไม่ใช่แนวทาง Deploy แบบไม่หยุดบริการสำหรับ Production

อาการ จุดที่ควรตรวจ
Connection refused Container, Logs และ Port 9080
Gateway ตอบ 404 ทุก Route YAML, #END และตำแหน่ง Mount
401 แม้ส่ง Key ชื่อ Header, ค่า Key และ Consumer
429 เร็วกว่าที่คิด Counter จากคำขอเดิมในหน้าต่างเดียวกัน
502/503 Backend, DNS ภายใน Compose และ Upstream port
ตั้งค่าใหม่ไม่เกิดผล การบันทึกไฟล์, Logs และการ Bind mount
Image pull ไม่สำเร็จ Network, Registry, Tag และสถาปัตยกรรมเครื่องที่ Image รองรับ

#14. แนวทางต่อยอด

สำหรับระบบที่ต้องจำกัด IP สามารถเพิ่ม Plugin ip-restriction โดยเลือก Allowlist หรือ Denylist ตามงาน [8] หากมี Load Balancer อยู่ด้านหน้า ต้องตั้งค่า Trusted proxy และ Real client IP ให้ถูกต้องก่อนใช้ IP เป็นเงื่อนไข อย่าเชื่อ X-Forwarded-For ที่ผู้เรียกส่งมาเองโดยไม่มีขอบเขต Trust

สำหรับ Production ควรออกแบบ TLS, การเก็บและหมุนเวียน Secret, ความพร้อมใช้งานของ Gateway และการกระจาย Configuration หลาย instance เพิ่มเติม โควตารวมหลาย Gateway ต้องใช้ Counter backend ที่เหมาะสม เช่น Redis ตาม Policy ที่ Plugin รองรับ ส่วนสิทธิ์ระดับข้อมูลยังต้องตรวจใน Backend

ตัวอย่าง Python HTTP server และ Key ที่แสดงในบทความใช้เพื่อการเรียนรู้ ไม่ใช่บริการและ Credential สำหรับเปิดให้ใช้งานจริง

#15. ปิดห้องทดลอง

docker compose down

คำสั่งนี้หยุดและนำ Container กับเครือข่ายของ Compose ชุดนี้ออก ไฟล์ที่สร้างไว้บนเครื่องยังคงอยู่

#แบบฝึกหัด

  1. เพิ่ม Consumer ชื่อ teacher ด้วย Key อีกค่า แล้วตรวจว่ามี Counter แยกจาก student
  2. เพิ่ม Endpoint /products ใน Backend แล้วทดลองเรียก /api/products
  3. เปลี่ยนน้ำหนัก Backend เป็น 3:1 และเก็บผลจากคำขอจำนวนมากขึ้น
  4. เขียน Automated test สำหรับ TC01–TC08 โดยจัดการเวลาของ Rate-limit test ให้ชัดเจน
  5. อธิบายว่าเหตุใดการได้ 404 จาก Gateway และ Backend จึงควรแยกกันใน Test report

#เอกสารอ้างอิง