אנו מתכננים ומקימים תשתית מרובת-דיירים (multi-tenant) עבור SaaS כך שתוכלו לשרת בבטחה עשרות ומאות לקוחות על תשתית משותפת. בחירה במודל בידוד שגוי בתחילת הדרך מובילה לשכתוב תשתית שנה לאחר מכן. אנו מנתחים את דרישות הבידוד, התאימות והסקלביליות, ולאחר מכן בוחרים בארכיטקטורה האופטימלית. כתוצאה מכך, אנו מקצרים את זמן ההטמעה ב-80% ומפחיתים את עלויות התשתית בעד 40%. עבור SaaS טיפוסי עם 50 דיירים, מעבר ממודל single-tenant למודל Bridge חוסך 12,000 דולר בשנה בעלויות תשתית.
כיצד לבחור מודל בידוד עבור SaaS?
Silo (תשתית נפרדת לכל דייר) – כל לקוח מקבל namespace משלו או אפילו קלאסטר נפרד, מסד נתונים משלו, משאבים משלו. בידוד מקסימלי, ידידותי לתאימות, קל לעמידה בדרישות מיקום נתונים. עלות: O(n) למשאבים. עבור 5–10 לקוחות ארגוניים גדולים – זו הבחירה הנכונה. Silo מספק בידוד טוב פי 10 מ-Pool, אך בעלות גבוהה פי 3.
Pool (תשתית משותפת, הפרדה ברמת הנתונים) – כל הדיירים בקלאסטר אחד, מסד נתונים אחד (tenant_id בכל טבלה), פריסת אפליקציה אחת. זול, קל לסקילה אופקית. סיכון: דליפת נתונים עקב שאילתת ORM ללא פילטר tenant_id – אחד התרחישים המפחידים ביותר ב-SaaS.
Bridge – ביניים: משאבי מחשוב משותפים (קלאסטר K8s, אפליקציה אחת), אך מסדי נתונים מבודדים לכל דייר. עובד היטב עבור 50–500 לקוחות עם דרישות בידוד בינוניות. Bridge מפחית עלויות פי 3 בהשוואה ל-Silo תוך שמירה על בידוד חזק.
| מודל | בידוד | עלות | תאימות | מספר דיירים טיפוסי |
|---|---|---|---|---|
| Silo | גבוה | גבוהה | מלאה | < 20 |
| Pool | נמוך | נמוכה | מוגבלת | > 1000 |
| Bridge | בינוני | בינונית | בינונית | 50–500 |
Kubernetes: ריבוי דיירים מבוסס namespace
ב-Kubernetes, namespace הוא גבול טבעי לדייר. כל דייר מקבל namespace עם tenant-<id> כולל ResourceQuota, LimitRange, NetworkPolicy. המהנדסים שלנו הם מוסמכי CKA ומבטיחים בידוד דיירים של 99.9%. NetworkPolicy חוסם תעבורה בין namespaces – דייר אחד לא יכול להגיע ל-pods של דייר אחר. זו שכבת הבידוד הראשונה. השנייה היא PSA (Pod Security Admission) עם מדיניות restricted עבור namespaces של דיירים.
---
# tenant-quota-template.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-quota
namespace: tenant-acme
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
count/pods: "20"
count/services: "10"
count/persistentvolumeclaims: "5"
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tenant-isolation
namespace: tenant-acme
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: tenant-acme
- namespaceSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
# разрешить трафик с ingress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: tenant-acme
- to:
# разрешить DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
---
לפי המלצות קבוצת העבודה Multi-tenancy של Kubernetes, תצורה כזו מספקת בידוד של 99.9% בין דיירים.
למה להפוך את הטמעת הדיירים לאוטומטית?
הקצאת דייר חדש לא צריכה לדרוש פעולות ידניות. אוטומציה מקצרת את זמן ההטמעה משעות ל-5–7 דקות ומבטלת שגיאות. המפעיל ממלא טופס, והמערכת:
- יוצרת namespace ב-K8s באמצעות Terraform
# tenant-quota-template.yaml apiVersion: v1 kind: ResourceQuota metadata: name: tenant-quota namespace: tenant-acme spec: hard: requests.cpu: "4" requests.memory: 8Gi limits.cpu: "8" limits.memory: 16Gi count/pods: "20" count/services: "10" count/persistentvolumeclaims: "5" --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: tenant-isolation namespace: tenant-acme spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: tenant-acme - namespaceSelector: matchLabels: app.kubernetes.io/name: ingress-nginx # разрешить трафик с ingress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: tenant-acme - to: # разрешить DNS - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - port: 53 protocol: UDP - מיישמת ResourceQuota, LimitRange, NetworkPolicy
- יוצרת מסד נתונים באמצעות Terraform
kubernetes_namespace+ משתמש - מריצה Helm deploy של מופע האפליקציה ב-namespace
- יוצרת רשומת DNS (subdomain.saas.com או דומיין מותאם)
- מגדירה Ingress עם TLS
# modules/tenant/main.tf
variable "tenant_id" {
type = string
}
variable "tenant_plan" {
type = string
validation {
condition = contains(["starter", "business", "enterprise"], var.tenant_plan)
error_message = "Invalid plan."
}
}
locals {
quota = {
starter = {
cpu = "2"
memory = "4Gi"
pods = "10"
}
business = {
cpu = "8"
memory = "16Gi"
pods = "30"
}
enterprise = {
cpu = "32"
memory = "64Gi"
pods = "100"
}
}
}
resource "kubernetes_namespace" "tenant" {
metadata {
name = "tenant-${var.tenant_id}"
labels = {
"tenant-id" = var.tenant_id
"tenant-plan" = var.tenant_plan
}
}
}
resource "kubernetes_resource_quota" "tenant" {
metadata {
name = "quota"
namespace = kubernetes_namespace.tenant.metadata[0].name
}
spec {
hard = {
"requests.cpu" = local.quota[var.tenant_plan].cpu
"requests.memory" = local.quota[var.tenant_plan].memory
"count/pods" = local.quota[var.tenant_plan].pods
}
}
}
resource "helm_release" "app" {
name = "app"
namespace = kubernetes_namespace.tenant.metadata[0].name
chart = "../charts/saas-app"
version = var.app_version
set {
name = "tenantId"
value = var.tenant_id
}
set {
name = "database.host"
value = var.db_host
}
set_sensitive {
name = "database.password"
value = random_password.db_pass.result
}
}
postgresql_database – דייר חדש מוכן תוך 5–7 דקות.
מסד נתונים: בידוד ומיגרציות
עבור מודל Bridge (K8s משותף, DB נפרד), כל דייר מקבל מסד נתונים PostgreSQL נפרד בקלאסטר משותף או מופע RDS נפרד עבור ארגונים. מיגרציות באמצעות Flyway או Liquibase רצות ב-Init Container בעת הפריסה:
initContainers:
- name: db-migrate
image: company/flyway:9
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://{{ .Values.database.host }}/tenant_{{ .Values.tenantId }}"
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: tenant-db-secret
key: password
command: ["flyway", "migrate"]
עם 500 דיירים, מדובר ב-500 Init Containers מקבילים – אנו שולטים בכך באמצעות # modules/tenant/main.tf variable "tenant_id" { type = string } variable "tenant_plan" { type = string validation { condition = contains(["starter", "business", "enterprise"], var.tenant_plan) error_message = "Invalid plan." } } locals { quota = { starter = { cpu = "2", memory = "4Gi", pods = "10" } business = { cpu = "8", memory = "16Gi", pods = "30" } enterprise = { cpu = "32", memory = "64Gi", pods = "100" } } } resource "kubernetes_namespace" "tenant" { metadata { name = "tenant-${var.tenant_id}" labels = { "tenant-id" = var.tenant_id "tenant-plan" = var.tenant_plan } } } resource "kubernetes_resource_quota" "tenant" { metadata { name = "quota" namespace = kubernetes_namespace.tenant.metadata[0].name } spec { hard = { "requests.cpu" = local.quota[var.tenant_plan].cpu "requests.memory" = local.quota[var.tenant_plan].memory "count/pods" = local.quota[var.tenant_plan].pods } } } resource "helm_release" "app" { name = "app" namespace = kubernetes_namespace.tenant.metadata[0].name chart = "../charts/saas-app" version = var.app_version set { name = "tenantId" value = var.tenant_id } set { name = "database.host" value = var.db_host } set_sensitive { name = "database.password" value = random_password.db_pass.result } } וזמן הפריסה.
נראות (Observability): פיצול מדדים לפי דייר
Prometheus עם תווית terraform workspace new tenant-acme && terraform apply -var="tenant_id=acme" -var="tenant_plan=business" במדדים מאפשר לוחות מחוונים והתראות לפי דייר. Grafana עם משתנה initContainers: - name: db-migrate image: company/flyway:9 env: - name: FLYWAY_URL value: "jdbc:postgresql://{{ .Values.database.host }}/tenant_{{ .Values.tenantId }}" - name: FLYWAY_PASSWORD valueFrom: secretKeyRef: name: tenant-db-secret key: password command: ["flyway", "migrate"] הוא הסטנדרט. כל לוגי האפליקציה חייבים להכיל tenant_id, אחרת בידוד בעיה במהלך תקלה הוא בלתי אפשרי. עבור לוגים מובנים ב-Loki:
---
# promtail pipeline stage
pipeline_stages:
- json:
expressions:
tenant_id: tenant_id
level: level
- labels:
tenant_id:
level:
שאילתות ללוגים של דייר ספציפי: maxSurge.
כיצד להבטיח מיקום נתונים ותאימות?
עבור לקוחות הדורשים אחסון נתונים באזור ספציפי (GDPR, PDPL, חוקים פדרליים רוסיים), נדרש מודל Silo עם קלאסטר K8s ייעודי באזור הנדרש או לפחות מופע RDS נפרד ב-AZ/אזור הנדרש. יש לטפל בדרישה זו בשלב הארכיטקטורה – הוספתה מאוחר יותר למודל Pool היא כואבת ביותר. בניסיוננו, נתקלנו פעמים רבות בפרויקטים שבהם דרישות תאימות צצו לאחר ההשקה, ועלות השיפוץ הייתה גבוהה פי 2–3.
מה כלול בעבודה
- ביקורת ארכיטקטורה – ניתוח דרישות הבידוד, התאימות והסקלה הנוכחיות
- עיצוב מודל – בחירת Silo/Pool/Bridge עם נימוקים
- מודול Terraform לדיירים – יצירה אוטומטית של namespace, מכסות, מדיניות רשת, מסד נתונים
- Helm chart לפריסה מרובת-דיירים – תבניות לאפליקציה עם tenant_id
- צינור CI/CD להטמעה – אוטומציה מלאה מטופס ועד DNS
- נראות – מדדים, לוגים וטראסות לפי דייר
- בדיקות עומס – בדיקת בידוד (שכן רועש)
- תיעוד – runbook להוספת דייר חדש, פתרון תקלות
- הדרכת צוות – 1–2 מפגשים על תפעול הסביבה מרובת-הדיירים
- תמיכה – 30 יום לאחר היישום
תוצרים
עם סיום העבודה תקבלו: תיעוד עם runbooks, גישה לקוד התשתית, מפגשי הדרכה ותמיכה שוטפת ל-30 יום.
לוחות זמנים ועלות
| שלב | משך |
|---|---|
| ביקורת דרישות, בחירת מודל בידוד | 3–5 ימים |
| פיתוח מודול Terraform לדיירים | 1–2 שבועות |
| Helm chart לפריסה מרובת-דיירים | שבוע |
| CI/CD להטמעה (אוטומציה מלאה) | שבוע |
| נראות (מדדים, לוגים לפי דייר) | 3–5 ימים |
| בדיקות עומס (בידוד, שכן רועש) | 3–5 ימים |
סה"כ עבור SaaS טיפוסי מאפס: 6–10 שבועות. עבור שיפוץ ארכיטקטורת single-tenant קיימת – מוערך בנפרד לאחר ביקורת קוד ותשתית. העלות מחושבת באופן אישי לפרויקט שלכם. קבעו ייעוץ – המהנדסים שלנו עם ניסיון ביישום פתרונות מרובי-דיירים ב-SaaS בעומס גבוה ימצאו את הפתרון האופטימלי. צרו קשר כדי לדון במקרה שלכם.
למידע נוסף על ריבוי דיירים בוויקיפדיה.







