- 1. APISIX ช่วยแก้ปัญหาอะไร
- 2. คำศัพท์สำคัญ
- 3. โหมดการติดตั้ง
- 4. เป้าหมายและสิ่งที่ต้องเตรียม
- 5. สร้าง Backend API
- 6. ตั้งค่าโหมด APISIX
- 7. สร้าง Routes และนโยบาย
- 8. เริ่มระบบด้วย Docker Compose
- 9. ทดสอบการยืนยันตัวตน
- 10. ทดสอบ Rate Limiting
- 11. สังเกต Load Balancing
- 12. ตารางกรณีทดสอบสำหรับใช้สอน
- 13. แก้ไข Configuration และแก้ปัญหา
- 14. แนวทางต่อยอด
- 15. ปิดห้องทดลอง
- แบบฝึกหัด
- เอกสารอ้างอิง
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 สำหรับคำสั่งในบทความ
เมื่อทำเสร็จจะสามารถ:
- เรียก API ผ่าน Gateway
- ทดสอบ API Key ที่ถูกต้องและไม่ถูกต้อง
- ตรวจพบ HTTP 429 เมื่อเกินโควตา
- สังเกตคำขอที่ไปถึง Backend สองตัว
- แยกแยะ 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 labhide_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 ชุดนี้ออก ไฟล์ที่สร้างไว้บนเครื่องยังคงอยู่
#แบบฝึกหัด
- เพิ่ม Consumer ชื่อ
teacherด้วย Key อีกค่า แล้วตรวจว่ามี Counter แยกจากstudent - เพิ่ม Endpoint
/productsใน Backend แล้วทดลองเรียก/api/products - เปลี่ยนน้ำหนัก Backend เป็น 3:1 และเก็บผลจากคำขอจำนวนมากขึ้น
- เขียน Automated test สำหรับ TC01–TC08 โดยจัดการเวลาของ Rate-limit test ให้ชัดเจน
- อธิบายว่าเหตุใดการได้ 404 จาก Gateway และ Backend จึงควรแยกกันใน Test report