כיצד להקים תשתית מרובת דיירים עבור SaaS על Kubernetes

תשתית מרובת דיירים עבור SaaS היא איזון עדין בין בידוד לקוחות לעלויות משאבים. אנו מתכננים ומגדירים ארכיטקטורה מבוססת Kubernetes, ובוחרים את מודל הבידוד האופטימלי לצרכים שלכם. הצוות שלנו מספק פרויקטים סוהריים—מביקורת דרישות ועד אוטומציית הטמעה—תוך הבטחת פעילות אמינה ותמיכה מתמשכת.

היכולות שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    776
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח לוגו חברת AIDER
    פיתוח לוגו חברת AIDER
    1022
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113

אנו מתכננים ומקימים תשתית מרובת-דיירים (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 דקות ומבטלת שגיאות. המפעיל ממלא טופס, והמערכת:

  1. יוצרת 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
  2. מיישמת ResourceQuota, LimitRange, NetworkPolicy
  3. יוצרת מסד נתונים באמצעות Terraform kubernetes_namespace + משתמש
  4. מריצה Helm deploy של מופע האפליקציה ב-namespace
  5. יוצרת רשומת DNS (subdomain.saas.com או דומיין מותאם)
  6. מגדירה 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 בעומס גבוה ימצאו את הפתרון האופטימלי. צרו קשר כדי לדון במקרה שלכם.

למידע נוסף על ריבוי דיירים בוויקיפדיה.