Kamal Proxy พร็อกซี HTTP ขนาดเล็กสำหรับการดีพลอยแบบไม่มีดาวน์ไทม์
(github.com/basecamp)- Kamal Proxy เป็น HTTP proxy ขนาดเล็กที่ทำงานอยู่หน้าเว็บแอปพลิเคชัน ออกแบบมาเพื่อสลับการดีพลอยไปยังอินสแตนซ์ใหม่โดยไม่ตัดทราฟฟิกที่กำลังดำเนินอยู่
- อินสแตนซ์ใหม่จะถูกลงทะเบียนด้วย
kamal-proxy deployและพร็อกซีจะ route ทราฟฟิกใหม่ไปยังอินสแตนซ์นั้นก็ต่อเมื่อผ่าน HTTP health check แล้วเท่านั้น - คำสั่งดีพลอยจะรอจนกว่าทราฟฟิกของอินสแตนซ์เดิมจะ drain ออกหมด ดังนั้นหลังจากคำสั่งคืนค่าสำเร็จแล้ว จึงสามารถลบอินสแตนซ์เดิมได้ โดยไม่ขัดจังหวะ request ที่กำลังดำเนินอยู่
- เพื่อให้รันหลายแอปพลิเคชันบนพร็อกซีตัวเดียวได้ มีฟีเจอร์ host-based routing, path-based routing, TLS อัตโนมัติ และการระบุใบรับรอง TLS แบบ manual
- Kamal Proxy ถูกออกแบบมาเป็นส่วนหนึ่งของเครื่องมือดีพลอย Kamal แต่ก็สามารถใช้แบบ standalone หรือเป็นคอมโพเนนต์ของเครื่องมือดีพลอยอื่นได้เช่นกัน
Kamal Proxy ทำอะไร
- Kamal Proxy เป็น HTTP proxy ขนาดเล็กสำหรับทำให้การประสานงานดีพลอยแบบไม่มีดาวน์ไทม์ง่ายขึ้น
- เมื่อรันเว็บแอปพลิเคชันไว้หลัง Kamal Proxy จะสามารถดีพลอยการเปลี่ยนแปลงได้โดยไม่หยุดทราฟฟิกที่กำลังดำเนินอยู่
- พฤติกรรมนี้ไม่ต้องอาศัยความร่วมมือพิเศษใด ๆ จากฝั่งแอปพลิเคชัน
- Kamal Proxy ถูกออกแบบมาให้ทำงานเป็นส่วนหนึ่งของ Kamal ซึ่งครอบคลุมการทำ container packaging และ provisioning
- สามารถใช้แบบ standalone หรือใช้เป็นส่วนหนึ่งของเครื่องมือดีพลอยอื่นได้
การรันและโฟลว์การดีพลอย
- อินสแตนซ์พร็อกซีรันด้วยคำสั่ง
kamal-proxy run - ไม่มีไฟล์ตั้งค่า และสามารถระบุ option ได้เมื่อค่า default ไม่เหมาะกับแอปพลิเคชัน
- พอร์ต HTTP default คือ
80 - หากต้องการรันบนพอร์ตอื่น ให้ใช้
kamal-proxy run --http-port 8080 - ดู option ทั้งหมดได้ที่
kamal-proxy help run
- พอร์ต HTTP default คือ
- หากต้องการส่งทราฟฟิกไปยังเว็บแอปพลิเคชัน ให้ deploy อินสแตนซ์แอปพลิเคชันไปยังพร็อกซี
- อินสแตนซ์เป้าหมายสำหรับดีพลอยระบุในรูปแบบ
hostname:port- ตัวอย่าง:
kamal-proxy deploy service1 --target web-1:3000 - คำสั่งนี้ลงทะเบียน
web-1:3000ภายใต้ชื่อ serviceservice1
- ตัวอย่าง:
วิธีสลับแบบไม่มีดาวน์ไทม์
- เมื่อมีการลงทะเบียนอินสแตนซ์ใหม่ Kamal Proxy จะรัน HTTP health check ทันทีเพื่อตรวจสอบว่าสามารถเข้าถึงและทำงานได้หรือไม่
- หาก health check สำเร็จ จะเริ่ม route ทราฟฟิกใหม่ไปยังอินสแตนซ์ใหม่
- หากอินสแตนซ์ใหม่ไม่เข้าสู่สถานะ healthy ภายในเวลาที่เหมาะสม คำสั่ง
deployจะหยุดการดีพลอยและคืน exit code ที่ไม่ใช่ 0 - การดีพลอยแต่ละครั้งจะย้ายทราฟฟิกทั้งหมดจากอินสแตนซ์ที่ดีพลอยไว้ก่อนหน้าไปยังอินสแตนซ์ใหม่
- คำสั่ง
deployจะรอจนกว่าทราฟฟิกจะออกจากอินสแตนซ์เดิมหมดแล้วจึงคืนค่า- หลังจากคืนค่าสำเร็จแล้ว สามารถลบอินสแตนซ์เดิมได้โดยไม่ตัด request ที่กำลังดำเนินอยู่
- เนื่องจากอินสแตนซ์ใหม่จะรับทราฟฟิกเฉพาะเมื่ออยู่ในสถานะ healthy และอินสแตนซ์เดิมจะถูกลบหลังจาก drain สมบูรณ์แล้ว การดีพลอยจึงดำเนินไปแบบ ไม่มีดาวน์ไทม์
การตั้งค่า health check
- health check default จะส่ง request
GETไปที่/upวินาทีละ 1 ครั้งสำหรับแต่ละ service - ถือว่า response
200เป็นสถานะ healthy - หากต้องการปรับ health check ให้เหมาะกับแอปพลิเคชัน ให้ใช้ flag ของ
deploy--health-check-path--health-check-port--health-check-timeout--health-check-interval
- ตัวอย่างการเปลี่ยน path สำหรับ health check:
kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
- กรณี expose health endpoint บนพอร์ตที่ต่างจาก service หลัก:
kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080
ฟีเจอร์ routing
-
Host-based routing
- การใช้ host-based routing ทำให้สามารถ route ทราฟฟิกจาก Kamal Proxy อินสแตนซ์เดียวไปยังหลายแอปพลิเคชันบนเซิร์ฟเวอร์เดียวกันได้
- ระบุ host เฉพาะขณะดีพลอยได้
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com- อินสแตนซ์ที่ดีพลอยด้วยวิธีนี้จะรับเฉพาะทราฟฟิกของ host ที่ระบุเท่านั้น
- หากระบุ host แยกให้แต่ละแอปพลิเคชัน ก็สามารถรันหลายแอปพลิเคชันบนเซิร์ฟเวอร์เดียวกันได้โดยไม่เกิด port conflict
- host หนึ่ง ๆ สามารถ route ไปยัง service ได้เพียงหนึ่งรายการในเวลาเดียวกัน
- หาก
service1ใช้app1.example.comอยู่แล้ว เมื่อservice2ถูกดีพลอยด้วย host เดียวกันจะคืน error - หลังจากลบ
service1แล้ว จึงสามารถดีพลอยservice2ไปยัง host เดียวกันได้
-
Path-based routing
- path-based routing ใช้กับแอปพลิเคชันที่แบ่งทราฟฟิกไปยัง service ต่าง ๆ ตาม path ของ request
- service สามารถ mount อยู่ใต้ path prefix ที่แตกต่างกันได้
- ตัวอย่างการส่ง request ที่ขึ้นต้นด้วย
/apiไปยังweb-1:kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api - ตัวอย่างการส่งส่วนที่เหลือไปยัง
web-2:kamal-proxy deploy service2 --target web-2:3000 - โดย default path prefix จะถูกลบออกก่อนส่งต่อไปยัง upstream
- request
/api/users/123จะถูกส่งไปยังweb-1เป็น/users/123 - หากต้องการส่งต่อ path เดิมตามเดิม ให้ระบุ
--strip-path-prefix=false
การรองรับ TLS
- Kamal Proxy สามารถขอรับและต่ออายุใบรับรอง TLS สำหรับแอปพลิเคชันได้โดยอัตโนมัติ
- เปิดใช้ TLS อัตโนมัติได้โดยเพิ่ม flag
--tlsตอนดีพลอยkamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
- TLS อัตโนมัติต้องมีการระบุ host
- เป็นเงื่อนไขเพื่อป้องกันไม่ให้มีการร้องขอใบรับรองสำหรับ hostname ใด ๆ โดยเจตนาร้าย
- ใน path-based routing ต้องตั้งค่า option TLS ที่ root path
- service ที่ดีพลอยไปยัง path อื่นของ host เดียวกันจะใช้การตั้งค่า TLS ที่ระบุไว้ที่ root path
- หากต้องติดตั้งใบรับรอง TLS ที่ขอรับมาแบบ manual, certificate authority ของตนเอง หรือ Cloudflare origin certificate สามารถระบุ path ของไฟล์ใบรับรองและ private key ได้โดยตรง
--tls-certificate-path cert.pem--tls-private-key-path key.pem
Environment variable และการ build
- ใน environment เช่นการรัน Docker container สามารถระบุ option ของ
runผ่าน environment variable ได้ kamal-proxy runจะอ่านค่าของแต่ละ option จาก environment variable หากมีการตั้งค่าไว้kamal-proxy run --http-port 8080HTTP_PORT=8080 kamal-proxy run
- หากชื่อ environment variable ชนกับ environment เดิม สามารถเพิ่ม prefix
KAMAL_PROXY_เพื่อแยกความแตกต่างได้KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
- การ build ในเครื่องทำด้วย
makeบน environment Go ที่ใช้งานได้ - หากต้องการ build ด้วย Docker container ให้ใช้
make docker - การตั้งค่า Docker Compose สำหรับลองใช้คำสั่งพร็อกซีอยู่ในโฟลเดอร์
example
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
การเลือกชื่อการกระทำว่า deploy ดูแปลก
คำนี้ถูกใช้จนมีความหมายล้นอยู่แล้ว น่าจะทำให้สับสนแบบว่า “แอปถูก deploy แล้วเหรอ? หรือ deploy ไปที่พร็อกซีกันแน่?”
น่าจะใช้คำอย่าง
bind,interceptหรือแค่proxyก็ได้ แต่ไม่เข้าใจว่าทำไมต้องเป็นdeployทุกวันนี้ทำ การ deploy แบบไม่หยุดให้บริการ ด้วยเซิร์ฟเวอร์แบบอื่น ๆ ก็ทำได้ง่าย เลยน่าสนใจที่เลือกทำสิ่งนี้เป็นแอปแยกทั้งตัว
เช่น แอป+เว็บพร็อกซีที่รองรับ Unix socket สามารถทำแบบไม่หยุดให้บริการได้เพียงแค่ย้ายไฟล์
เป็นแบบ atomic และยังส่งคำขอวอร์มอัปด้วย
curlได้ด้วยการทำไปถึงระบบลงทะเบียนดูจะเกินจำเป็น
Kamal เป็นเครื่องมือ deploy ที่ใช้ตอนย้ายจากคลาวด์ไปยังฮาร์ดแวร์ของตัวเอง และช่วยประหยัดเงินได้หลายล้านดอลลาร์(https://basecamp.com/cloud-exit)
ถ้ามีบทความอ้างอิงก็อยากเรียนรู้เพิ่มเติม
ดูเหมือนว่าสิ่งนี้ตั้งใจจัดการปัญหาพื้นฐานที่ทราฟฟิกสะดุดเมื่อสลับคอนเทนเนอร์ของบริการใน Docker Swarm เป็นหลัก
ตอนทำเซิร์ฟเวอร์ JAMStack ที่ Cloud 66 ก็เคยเจอปัญหาเดียวกัน และใช้ Caddy แทนการทำพร็อกซีเอง ส่วน Traefik ก็น่าจะเหมาะพอเช่นกัน
ไม่รู้ว่าทำไม Kamal ถึงเลือก Swarm แทน k8s หรือ k3s แต่ความซับซ้อนต้องไปอยู่ที่ไหนสักแห่งและซ่อนไม่ได้ สุดท้ายเลยเหมือนจะไปจบที่พร็อกซีของตัวเอง
แม้ยังไม่เคยลองใช้เอง แต่ค่อนข้างกังขา เพราะน่าจะต้องคอยวิ่งตามการรองรับ WebSockets, SSE, HTTP/3 รวมถึงการบีบอัดและการเข้ารหัสสารพัดแบบไปเรื่อย ๆ
ดูเป็นตัวอย่างของ reverse proxy ที่เริ่มต้นแบบเรียบง่าย แต่สุดท้ายค่อย ๆ ดูดซับความซับซ้อนที่ตั้งใจหลีกเลี่ยง
ผมคิดว่าระบบรันไทม์ที่กว้างและขยายได้ควรมีคู่แข่ง แต่ Kamal ดูเหมือนเป็นงานที่ถอยกลับเข้าสู่ความซับซ้อนที่พยายามหลีกเลี่ยง
กลับยิ่งทำให้ความสามารถและความยืดหยุ่นของ Kubernetes ในฐานะเฟรมเวิร์ก จัดการสถานะที่ต้องการ เด่นชัดขึ้น
คุณยังใช้ Docker ต่อไปได้และใช้ Caddy, Traefik, Envoy เป็นพร็อกซีได้
ใน Kubernetes เองสุดท้ายก็มักใช้สิ่งเหล่านี้เป็น ingress controller อยู่ดี
ดูแล้วไม่น่าจะใช้ Swarm
docker runธรรมดา และไม่มี Swarm หรือ orchestrationโดยพื้นฐานแล้วเป็นเครื่องมือง่าย ๆ ที่มาแทน Capistrano
พื้นฐานของบริการ container orchestration คือการใช้ ingress controller เพื่อจัดการการเชื่อมต่อและการ deploy แบบ blue/green
พูดให้แม่นกว่านั้น ingress controller คือชื่อบทบาทที่มอบให้ reverse proxy จำนวนมาก ซึ่งรับมือ use case นี้มาหลายปี หรือยาวไปถึงหลายสิบปีแล้ว
ถ้าอธิบายสั้น ๆ ได้ว่าโดยทั่วไป การ deploy แบบไม่หยุดให้บริการ ทำงานอย่างไรจะดีมาก
เดาว่าเป็นโครงสร้างที่แอปสองเวอร์ชันรันพร้อมกัน และทราฟฟิกใหม่ถูก route ไปยังเวอร์ชันใหม่
แต่ถ้าใช้ฐานข้อมูลเดียว และแอปเวอร์ชันใหม่มีการเปลี่ยน schema สคริปต์ migration ตอนเริ่มต้นก็คงเปลี่ยน schema แล้วแอปเวอร์ชันเก่าก็คาดหวัง schema แบบเดิม
เลยสงสัยว่าจัดการเรื่องนี้อย่างไร
หลายเฟรมเวิร์กรัน migration ตอน deploy โค้ด แต่ในทางปฏิบัติควรควบคุมเวลารันด้วยตัวเองจะดีกว่า
โค้ดแต่ละเวอร์ชันต้องทำงานได้ทั้งกับ schema ปัจจุบันและ schema หลัง migration ในอนาคต จึงกลายเป็นโค้ดที่ backward compatible โดยปริยาย
โฟลว์จะเป็น “deploy โค้ดใหม่ที่ทำงานได้ทั้งกับ schema ปัจจุบัน/อนาคต → ตรวจสอบ → รัน migration → ตรวจสอบ → เก็บกวาดโค้ดที่รองรับ schema ที่หายไปแล้ว”
นี่คือต้นทุนเพิ่มเติมที่ต้องจ่ายหากต้องการ deploy แบบไม่หยุดให้บริการหรือ rolling deploy และบริษัทใหญ่ ๆ โดยทั่วไปก็ทำแบบนี้
โดยเฉพาะถ้ากลุ่มผู้ใช้เป็นระดับภูมิภาค และสามารถเลือก deploy ในช่วงเวลาที่เงียบได้ ก็ยิ่งเป็นแบบนั้น
แอปสองเวอร์ชันสามารถรันพร้อมกันได้
https://xata.io/blog/multi-version-schema-migrations
load balancer จะเริ่มรับการเชื่อมต่อที่ Server2 และไม่รับการเชื่อมต่อใหม่ที่ Server1
จากนั้นเมื่อการเชื่อมต่อทั้งหมดของ Server1 ปิดลงแล้ว ก็ถอด Server1 ออก
อาจเป็นคนละเซิร์ฟเวอร์กัน หรือเป็นหลาย worker ในเซิร์ฟเวอร์เดียวกันก็ได้
ในช่วงเวลานั้น migration ต้อง backward compatible ตามที่คำตอบอื่น ๆ บอกไว้
Kamal 2 ตอนนี้เป็น RC (https://github.com/basecamp/kamal/releases) และรองรับ SSL อัตโนมัติ ทำให้น่าตั้งตารอ เพราะจะทำให้รันหลายแอปด้วย Kamal บนเซิร์ฟเวอร์เดียวได้ง่ายขึ้น
autocertซึ่งแทบจะเป็นการใช้งานแบบขั้นต่ำสุดเปราะบาง และเพราะข้อจำกัดการออกใบรับรองของ Let's Encrypt กับข้อจำกัดใบรับรองพร้อมกัน จึงทำให้ขยายอินสแตนซ์พร็อกซีในแนวนอนได้ยาก
Caddy/Certmagic แก้ปัญหานี้ด้วยการเขียนข้อมูลลง shared storage เพื่อออกใบรับรองเพียงใบเดียว แล้วให้อินสแตนซ์ทั้งหมดนำกลับมาใช้และประสานงานกันผ่าน storage นั้น
ไม่มีการสลับผู้ออกใบรับรอง, การเลี่ยงข้อจำกัดการออกใบรับรอง, หรือการรองรับ ARI
ส่วนการค้าง request ไว้จนกว่า upstream จะพร้อม Caddy ก็จัดการได้ดีถ้าตั้ง
try_durationและtry_intervalในreverse_proxyการจัดการ proxy header ก็ไม่คำนึงถึง IP ที่เชื่อถือได้ ดังนั้นถ้าเปิดใช้ ใครก็สามารถตั้ง
X-Forwarded-Forเพื่อปลอม IP ได้โชคดีที่ค่าเริ่มต้นปิดอยู่ แต่ก็ไม่มีคำเตือน
เข้าใจว่าเป้าหมายคือความเรียบง่าย แต่ในสภาพปัจจุบันยังไม่พร้อม และมีตัวเลือกที่ดีกว่าเยอะจนไม่อยากใช้จริง
ยังไม่ค่อยเข้าใจว่าจะใช้สิ่งนี้อย่างไร และเหมือนผมอาจพลาดอะไรไป
ตัวอย่างเริ่ม replica ของบริการ
web4 ตัวถ้าสามารถ deploy ไปยัง replica หนึ่งตัว เช่น
example-web-1เพื่อสร้างบริการได้ แล้ว replica อีก 3 ตัวทำอะไรอยู่ก็ไม่รู้ถ้าจะอัปเดต
webเพื่อ deploy แบบไม่หยุดให้บริการ น่าจะต้องรันคำสั่ง build บนบริการwebแล้วเริ่มบริการนี้ด้วยวิธีใดวิธีหนึ่ง จากนั้นค่อยรัน deploy ไปยัง target ใหม่แต่ถ้ารัน
docker compose up --build --force-recreate webreplica เดิมก็จะถูกปิดลง ทำให้ทั้งหมดไม่มีความหมายยังไม่ได้ตรวจว่าพร็อกซีนี้กระจายโหลดไปยังหลาย record ที่คืนจาก DNS request หรือไม่ แต่อย่างน้อยถ้าเป็น load balancing แบบ VIP ก็น่าจะทำงานตามที่คาดไว้
ส่วนเรื่องอัปเดตข้อที่สองยังไม่ชัดเจนนัก
อาจคาดหวังให้รันบริการชื่ออื่นใน network เดียวกันแล้วใช้
kamal-proxy deployอาจเป็นแนวทางใส่เลขเวอร์ชันไว้ในชื่อบริการ และถ้าต้องการ rollback อย่างรวดเร็ว การคงเวอร์ชันก่อนหน้าไว้แบบพร้อมใช้งานก็สมเหตุสมผล
[0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
k8s rollout restart deploymentหรือจะสลับ DNS หรือ router ระหว่าง backend สองตัวก็ได้
สงสัยว่านี่เป็นการทำแพตเทิร์น หยุดทราฟฟิกชั่วคราว หรือเปล่า
เป็นวิธีที่พร็อกซีหยุดทราฟฟิกไว้จริง ๆ สักไม่กี่วินาที ทำให้ request ที่เข้ามาดูเหมือนใช้เวลานานกว่าปกติไม่กี่วินาที แต่เสร็จหลังหน่วงสั้น ๆ
ในช่วงไม่กี่วินาทีนั้นสามารถรัน DB migration เล็ก ๆ หรือการเปลี่ยนแปลงโครงสร้างพื้นฐานแบบ blocking ที่ซับซ้อนขึ้นเล็กน้อยแต่จบได้ภายใน 5 วินาที
ฟังดูค่อนข้างเสี่ยง และแม้จะเคยทำงานในบริษัทแอปเซิร์ฟเวอร์มาหลายปี แต่เพิ่งเคยได้ยินแพตเทิร์นนี้ครั้งแรก
อย่างไรก็ตาม ถ้าเป็นผู้ร่วมก่อตั้ง Django ก็น่าจะเคยเห็นการ deploy มาเยอะ จึงยากจะมองข้ามไปเฉย ๆ
จำได้ว่าเคยคุยกันเรื่องนี้มาก่อน
สงสัยว่ามีอธิบายไว้ที่ไหนว่าทำไมถึงเลือกสร้าง พร็อกซีของตัวเอง แทนที่จะใช้ Traefik หรือเครื่องมืออื่นที่พิสูจน์แล้ว
ใน PR มีบริบทว่าทำไมถึงเปลี่ยนและทำเอง
https://github.com/basecamp/kamal/pull/940
แต่ดูเหมือนจะไม่เคยมีการเปรียบเทียบกับตัวอื่นนอกจาก Traefik อย่างเป็นทางการ
สงสัยว่ามีวิธีตั้งค่า timeout ไหม
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://blog.cloudflare.com/exposing-go-on-the-internet/
เป็น NIH ไม่มีอะไรจะเสริมแล้ว