การใช้งาน Multi-tenancy ด้วย PostgreSQL: เรียนรู้ผ่านตัวอย่างง่ายๆ ในโลกจริง
เรียนรู้วิธีการใช้สถาปัตยกรรม multi-tenant กับ PostgreSQL Row-Level Security (RLS) และบทบาทฐานข้อมูล ผ่านตัวอย่างจริงเพื่อการแยกข้อมูลที่ปลอดภัยระหว่างผู้เช่าแต่ละราย
เรียนรู้วิธีการใช้สถาปัตยกรรม multi-tenant กับ PostgreSQL Row-Level Security (RLS) และบทบาทฐานข้อมูล ผ่านตัวอย่างจริงเพื่อการแยกข้อมูลที่ปลอดภัยระหว่างผู้เช่าแต่ละราย
ในบางบทความก่อนหน้า เราเจาะลึกถึงแนวคิดของ multi-tenancy และการประยุกต์ใช้ในผลิตภัณฑ์และสถานการณ์ธุรกิจในโลกจริง
ในบทความนี้ เราจะสำรวจวิธีการใช้สถาปัตยกรรม multi-tenant สำหรับแอปพลิเคชันของคุณโดยใช้ PostgreSQL จากมุมมองทางเทคนิค
สถาปัตยกรรม single-tenant หมายถึงสถาปัตยกรรมซอฟต์แวร์ที่แต่ละลูกค้ามี instance ของแอปพลิเคชันและฐานข้อมูลที่แยกเป็นของตนเอง
ในสถาปัตยกรรมนี้ ข้อมูลและทรัพยากรของผู้เช่าแต่ละรายจะแยกออกจากกันอย่างสิ้นเชิง

สถาปัตยกรรม multi-tenant คือสถาปัตยกรรมซอฟต์แวร์ที่มีผู้ใช้หลายคน (ผู้เช่า) ใช้แอปพลิเคชัน instance และโครงสร้างพื้นฐานเดียวกัน แต่ข้อมูลที่ใช้ยังคงแยกกันอยู่ ในสถาปัตยกรรมนี้ ซอฟต์แวร์ instance หนึ่งจะให้บริการแก่ผู้เช่าหลายราย โดยแต่ละรายจะรักษาข้อมูลที่แยกจากกันผ่านกลไกแยกต่าง ๆ

สถาปัตยกรรม single-tenant และ multi-tenant แตกต่างกันในแง่ต่าง ๆ เช่น การแยกข้อมูล การใช้ทรัพยากร การขยายตัว การจัดการและบำรุงรักษา และความปลอดภัย
ในสถาปัตยกรรม single-tenant แต่ละลูกค้ามีพื้นที่ข้อมูลที่แยกเป็นของตนเอง ทำให้การใช้งานทรัพยากรต่ำ แต่เป็นที่นิยมในการปรับแต่ง ในการใช้งานซอฟต์แวร์ single-tenant มักจะปรับให้ตรงกับความต้องการของลูกค้า เช่น ระบบจัดการสินค้าสำหรับผู้จำหน่ายผ้าหรือแอพบล็อกส่วนตัว สิ่งที่คล้ายกันคือแต่ละลูกค้ามี instance ของเซอร์วิสแอพที่แยกออก ทำให้สะดวกในการปรับแต่งตามความต้องการเฉพาะ
ในสถาปัตยกรรม multi-tenant, ผู้เช่าหลายรายใช้ทรัพยากรร่วมกัน, ทำให้การใช้ทรัพยากรสูงขึ้น อย่างไรก็ตาม จำเป็นต้องมั่นใจในเรื่องของการแยกและความปลอดภัยของข้อมูล
สถาปัตยกรรม multi-tenant มักเป็นที่ต้องการเมื่อนักให้บริการเสนอเซอร์วิสมาตรฐานไปยังลูกค้าต่าง ๆ ซึ่งเซอร์วิสมักจะมีการปรับแต่งน้อย และลูกค้าทั้งหมดใช้แอพพลิเคชั่น instance เดียวกัน เมื่อแอพต้องการอัปเดต การอัปเดต instance แอพพลิเคชั่นเพียง instance เดียวเท่ากับการอัปเดตแอพสำหรับลูกค้าทั้งหมด เช่น ระบบ CRM (Customer Relationship Management) ซึ่งเป็นการเปรียบเทียบมาตรฐาน ความต้องการของระบบเหล่านี้คือการใช้สถาปัตยกรรม multi-tenant เพื่อให้บริการเดียวกันแก่ผู้เช่าทั้งหมด
ในสถาปัตยกรรม multi-tenant ผู้เช่าทุกคนใช้ทรัพยากรพื้นฐานร่วมกัน ทำให้การแยกทรัพยากรระหว่างผู้เช่าเป็นเรื่องสำคัญ การแยกนี้ไม่จำเป็นต้องเป็นการแยกทางกายภาพ เพียงแต่ต้องมั่นใจว่าทรัพยากรระหว่างผู้เช่าไม่ได้มีความสามารถในการมองเห็นหรือเข้าถึงกัน
ในออกแบบสถาปัตยกรรม สามารถทำการแยกระดับทรัพยากรระหว่างผู้เช่าได้หลายระดับ:

โดยทั่วไปแล้ว ยิ่งแชร์ทรัพยากรระหว่างผู้เช่ามากเท่าใด ค่าใช้จ่ายในการพัฒนาระบบและการบำรุงรักษาก็จะต่ำลง ในทางตรงกันข้าม ยิ่งแชร์ทรัพยากรน้อย ค่าใช้จ่ายก็จะสูงขึ้น
ในบทความนี้ เราจะใช้ระบบ CRM เป็นตัวอย่างเพื่อแนะนำสถาปัตยกรรม multi-tenant ที่เรียบง่ายแต่มีความสามารถ
เราตระหนักดีว่าผู้เช่าทั้งหมดใช้เซอร์วิสมาตรฐานเดียวกัน ดังนั้นเราจึงตัดสินใจให้ผู้เช่าทั้งหมดแชร์ทรัพยากรพื้นฐานเดียวกัน และเราจะทำการแยกข้อมูลระหว่างผู้เช่าต่าง ๆ ที่ระดับฐานข้อมูลโดยใช้ Row-Level Security ของ PostgreSQL
นอกจากนี้ เรายังจะสร้างการเชื่อมต่อข้อมูลแยกเป็นสำหรับแต่ละผู้เช่าเพื่อการจัดการสิทธิ์การใช้งานของผู้เช่าได้ดียิ่งขึ้น
ต่อไป เราจะอธิบายวิธีการใช้งานสถาปัตยกรรม multi-tenant นี้
ในระบบ CRM ของเรา จะมีทรัพยากรมากมายและเก็บในตารางต่าง ๆ เช่น ข้อมูลลูกค้าเก็บอยู่ในตาราง customers
ก่อนการใช้งาน multi-tenancy ทรัพยากรเหล่านี้ไม่ได้เชื่อมโยงกับผู้เช่าใด ๆ
เพื่อแยกความแตกต่างของผู้เช่าซึ่งเป็นเจ้าของทรัพยากรต่างๆ เราแนะนำให้มีตาราง tenants เพื่อเก็บข้อมูลผู้เช่า (ที่ db_user และ db_user_password ใช้เก็บข้อมูลการเชื่อมต่อฐานข้อมูลของแต่ละผู้เช่า จะอธิบายรายละเอียดด้านล่าง) นอกจากนี้เรายังเพิ่มฟิลด์ tenant_id ในแต่ละทรัพยากรเพื่อระบุว่ามันเป็นของผู้เช่ารายใด:
ตอนนี้ แต่ละทรัพยากรมีการเชื่อมโยงกับ tenant_id ซึ่งในทางทฤษฎีทำให้เราสามารถเพิ่มคำสั่ง where ไปยังคำสั่ง query เพื่อจำกัดการเข้าถึงทรัพยากรของแต่ละผู้เช่า:
ในตอนแรก ดูเหมือนง่ายและเป็นไปได้ แต่จะมีปัญหาอื่นๆ ที่จะเกิดขึ้น:
where นี้ ทำให้โค้ดยุ่งเหยิงและยากต่อการบำรุงรักษา โดยเฉพาะเมื่อเขียนคำสั่ง join ที่ซับซ้อนwhere นี้ได้ง่ายๆดังนั้นเราจะไม่ใช้วิธีนี้ แต่จะใช้ PostgreSQL' Row Level Security เพื่อจัดการกับข้อกังวลเหล่านี้ อย่างไรก็ตาม ก่อนดำเนินการต่อ เราจะสร้างบัญชีฐานข้อมูลแยกเฉพาะผู้เช่าแต่ละคนเท่านั้นเพื่อเข้าถึงฐานข้อมูลที่ร่วมกันนี้
มันเป็นการปฏิบัติที่ดีที่จะกำหนดบทบาทฐานข้อมูลให้กับผู้ใช้แต่ละคนที่สามารถเชื่อมต่อกับฐานข้อมูลได้ สิ่งนี้อนุญาตให้มีการควบคุมการเข้าถึงของผู้ใช้แต่ละคนได้ดีขึ้น สิ่งนี้ช่วยให้มีการแยกการดำเนินการระหว่างผู้ใช้ต่าง ๆ และปรับปรุงความมั่นคงและความปลอดภัยของระบบ
เพราะผู้เช่าทั้งหมดมีสิทธิ์การดำเนินการพื้นฐานของฐานข้อมูลเหมือนกัน เราสามารถสร้างบทบาทพื้นฐานเพื่อจัดการสิทธิ์เหล่านี้ได้:
จากนั้นเพื่อต่างบทย่อยจากแต่ละบทที่สืบทอดจากบทบาทพื้นฐาน จะมีบทบาทที่สืบทอดจากบทบาทพื้นฐานพร้อมการระบุของผู้เช่าเมื่อมีการสร้าง:
ถัดไป ข้อมูลการเชื่อมต่อฐานข้อมูลสำหรับผู้เช่าแต่ละคนจะถูกเก็บในตาราง tenants:
| id | db_user | db_user_password |
|---|---|---|
| x2euic | crm_tenant_x2euic | pa55w0rd |
กลไกนี้ให้ผู้เช่าแต่ละคนมีบทบาทฐานข้อมูลของตัวเอง และบทบาทเหล่านี้จะใช้สิทธิ์ที่มอบให้กับบทบาท crm_tenant
เราสามารถกำหนดขอบเขตของสิทธิ์สำหรับผู้เช่าผ่านบทบาท `crm_tenant':
systems)tenants และควรเห็นเฉพาะฟิลด์ id และ db_user เท่านั้นเพื่อใช้สอบถามเมื่อทำการดำเนินการฐานข้อมูลเมื่อบทบาทของผู้เช่าถูกตั้งค่าแล้ว เมื่อผู้เช่าต้องการเข้าสู่บริการ เราสามารถติดต่อฐานข้อมูลโดยใช้บทบาทฐานข้อมูลที่แสดงถึงผู้เช่านั้น ๆ ได้
หลังจากนี้ เราได้กำหนดบทบาทฐานข้อมูลสำหรับผู้เช่าแต่ละคนแล้ว แต่สิ่งนี้ยังไม่จำกัดการเข้าถึงข้อมูลระหว่างผู้เช่า ตอนนี้เราจะใช้คุณสมบัติของ PostgreSQL คือ Row-Level Security เพื่อจำกัดการเข้าถึงข้อมูลของผู้เช่าแต่ละคนเฉพาะข้อมูลของตัวเอง
ใน PostgreSQL ตารางสามารถมี นโยบายความปลอดภัยระดับแถว ที่ควบคุมแถวที่สามารถเข้าถึงได้โดยคำสั่ง query หรือสามารถแก้ไขได้โดยคำสั่งการจัดการข้อมูล ฟีเจอร์นี้ยังรู้จักกันในชื่อ RLS (Row-Level Security)
โดยปกติแล้ว ตารางไม่มีนโยบายความปลอดภัยระดับแถว หากต้องการใช้ RLS คุณจำเป็นต้องเปิดใช้งานมันสำหรับตารางและสร้างนโยบายความปลอดภัยที่จะดำเนินการทุกครั้งเมื่อตารางถูกเข้าถึง
ตัวอย่างที่ใช้ตาราง customers ในระบบ CRM เราจะเปิดใช้งาน RLS และสร้างนโยบายความปลอดภัยเพื่อจำกัดการเข้าถึงข้อมูลของลูกค้าเฉพาะข้อมูลของผู้เช่าเองเท่านั้น:
ในคำสั่งสร้างนโยบายความปลอดภัย:
สำหรับทั้งหมด (ไม่จำเป็น) ระบุนโยบายการเข้าถึงนี้จะถูกใช้สำหรับการปฏิบัติการ select, insert, update, และ delete บนตาราง คุณสามารถระบุให้ for ตามด้วยคำหลักของคำสั่งเพื่อใช้นโยบายการเข้าถึงสำหรับการปฏิบัติการเฉพาะถึง crm_tenant ระบุนโยบายนี้ใช้กับผู้ใช้ที่มีบทบาทฐานข้อมูล crm_tenant ซึ่งหมายถึงผู้เช่าทุกคนแบบ restrict ระบุโหมดการบังคับของนโยบาย หมายถึงการเข้าถึงควรถูกจำกัดอย่างเข้มงวด ค่าเริ่มต้น ตารางสามารถมีนโยบายได้โดยที่หลายนโยบายจะถูกรวมเข้ากับความสัมพันธ์ของ OR สำหรับผู้ใช้ที่อยู่ในระบบ CRMusing กำหนดความจำเป็นในการเข้าถึงจริง โดยจำกัดผู้ใช้ฐานข้อมูลที่ทำการ query ปัจจุบันให้ดูเฉพาะข้อมูลที่เป็นของผู้เช่าเดียวกัน ข้อจำกัดนี้ใช้กับแถวข้อมูลที่ถูกเลือกโดยคำสั่ง (select, update, หรือ delete)with check กำหนดข้อจำกัดที่จำเป็นเมื่อต้องแก้ไขแถวข้อมูล (insert หรือ update), ประกันให้ผู้เช่าสามารถเพิ่มหรืออัพเดตข้อมูลได้แต่เพียงของตนเองการใช้ RLS ในการจำกัดการเข้าถึงของผู้เช่าในตารางทรัพยากรของเรามีข้อดีหลายประการ:
where tenant_id = (select id from tenants where db_user = current_user) ลงในปฏิบัติการ query ทั้งหมด (select, update, หรือ delete) ได้อย่างมีประสิทธิภาพ
สำหรับตัวอย่าง เมื่อต้องใช้ select * from customers, มันเท่ากับการใช้ select * from customers where tenant_id = (select id from tenants where db_user = current_user) สิ่งนี้ขจัดความจำเป็นในการเพิ่มเงื่อนไข where ในโค้ดของแอปพลิเคชัน ลดความซับซ้อนของมันและลดความเสี่ยงของข้อผิดพลาดอย่างไรก็ตาม มีจุดที่ต้องคำนึงถึง:
tenant_id โดยอัตโนมัติระหว่างการใช้งาน insert. มันแค่จำกัดให้ผู้เช่าใส่ข้อมูลของตนเองเท่านั้น นี่หมายความว่าเมื่อต้องใส่ข้อมูลใหม่ เรายังคงต้องให้ ID ผู้เช่าซึ่งไม่ได้สอดคล้องกับกระบวนการ query และอาจทำให้เกิดความสับสนในระหว่างการพัฒนา เพิ่มโอกาสของข้อผิดพลาด (สิ่งนี้จะได้รับการแก้ไขในขั้นตอนถัดไป)นอกจากตาราง customers แล้ว เรายังต้องนำกระบวนการเดียวกันนี้มาใช้กับทรัพยากรของระบบ CRM ทั้งหมด (ขั้นตอนนี้อาจทำให้รู้สึกวุ่นวาย แต่เราสามารถเขียนโปรแกรมเพื่อตั้งค่าได้ในระหว่างการเริ่มต้นตาราง) เพื่อแยกแยะข้อมูลจากผู้เช่าที่ต่างกัน
ตามที่กล่าวถึงก่อนหน้านี้ RLS (Row-Level Security) อนุญาตให้เราดำเนินการ query โดยไม่ต้องกังวลเกี่ยวกับการมีอยู่ของ tenant_id, เพราะฐานข้อมูลจัดการให้เอง อย่างไรก็ตาม สำหรับการใส่ข้อมูล (insert operations) เรายังต้องระบุ tenant_id ให้ชัดเจน
เพื่อให้สะดวกเหมือนความสามารถของ RLS สำหรับการใส่ข้อมูล เรายังต้องการให้ฐานข้อมูลสามารถจัดการ tenant_id โดยอัตโนมัติในระหว่างการใส่ข้อมูล
สิ่งนี้จะมีข้อได้เปรียบที่ชัดเจน: ในระดับการพัฒนาแอปพลิเคชัน เราไม่ต้องพิจารณาว่าข้อมูลเป็นของผู้เช่าใด ซึ่งจะลดโอกาสของข้อผิดพลาดและลดภาระจิตใจของเราเมื่อพัฒนาแอปพลิเคชัน multi-tenant
โชคดี PostgreSQL ให้ความสามารถของฟังก์ชันทริกเกอร์ที่มีความสามารถ
ทริกเกอร์เป็นฟังก์ชันพิเศษที่เชื่อมต่อกับตารางที่ดำเนินการเฉพาะเมื่อการกระทำ (เช่น การใส่, การอัพเดต, หรือการลบ) ถูกรับทำบนตาราง การกระทำเหล่านี้สามารถเรียกใช้ในระดับแถว (สำหรับแต่ละแถว) หรือระดับคำสั่ง (สำหรับแถลงการณ์ทั้งหมด) ด้วยทริกเกอร์ เราสามารถดำเนินการลอจิกกำหนดเองก่อนหรือหลังการปฏิบัติการฐานข้อมูลเฉพาะ ทำให้เราสามารถบรรลุเป้าหมายได้ง่าย
ขั้นแรก ลองสร้างฟังก์ชันทริกเกอร์ set_tenant_id เพื่อดำเนินการก่อนการใส่ข้อมูลแต่ละครั้ง:
จากนั้นเชื่อมโยงฟังก์ชันทริกเกอร์นี้กับตาราง customers สำหรับการใส่ข้อมูล (เหมือนกับการเปิดใช้งาน RLS สำหรับตาราง จะต้องเชื่อมโยงฟังก์ชันทริกเกอร์นี้กับตารางที่เกี่ยวข้องทั้งหมด):
ทริกเกอร์นี้สร้างความมั่นใจว่าข้อมูลที่จะใส่มี tenant_id ที่ถูกต้อง หากข้อมูลใหม่มีอยู่แล้วใน tenant_id, ฟังก์ชันทริกเกอร์จะไม่ทำอะไร มิฉะนั้น มันจะเติม field tenant_id อัตโนมัติตามข้อมูลของผู้ใช้ปัจจุบันในตาราง tenants
ด้วยวิธีนี้ เราจะบรรลุการจัดการ tenant_id อัตโนมัติในระดับฐานข้อมูลในระหว่างการใส่ข้อมูลโดยผู้เช่า
ในบทความนี้ เราได้เจาะลึกการใช้จริงของสถาปัตยกรรม multi-tenant โดยใช้ระบบ CRM เป็นตัวอย่างเพื่อแสดงทางออกที่ใช้งานได้โดยใช้ฐานข้อมูล PostgreSQL
เราได้พูดถึงการจัดการบทบาทฐานข้อมูล การควบคุมการเข้าถึง และฟีเจอร์ Row-Level Security ของ PostgreSQL เพื่อมั่นใจถึงการแยกข้อมูลระหว่างผู้เช่า นอกจากนี้เรายังใช้ฟังก์ชันทริกเกอร์เพื่อลดภาระความคิดของนักพัฒนาในการจัดการผู้เช่าที่แตกต่างกัน
นั่นคือทั้งหมดสำหรับบทความนี้ หากคุณต้องการเสริมสร้างแอปพลิเคชัน multi-tenant ของคุณด้วยการจัดการการเข้าถึงของผู้ใช้ คุณสามารถอ้างถึง คู่มือการเริ่มต้นกับ Logto organizations - สำหรับการสร้างแอป multi-tenant สำหรับข้อมูลเพิ่มเติม