Модуль 10. Многопользовательность и изоляция
Что считаем «хорошей» изоляцией
Многопользовательность = несколько команд данных (или проектов) делят один кластер, не ухудшая SLO друг друга и общих сервисов (Trino/ClickHouse/MinIO/метакаталог). Изоляцию строим слоями:
- Орг-уровень: роли и права (RBAC), ответственность (RACI), лимиты по бюджетам.
- K8s-уровень: namespaces, ResourceQuota/LimitRange, PriorityClass, NetworkPolicy, PodSecurity.
- Вычисления/узлы: отдельные пулы нод, taints/tolerations/affinity; GPU-ноды.
- Данные/секреты: S3-бакеты/префиксы и квоты, БД-роли/схемы, жизненный цикл секретов (Vault/ESO).
- Сеть и публикация: «внутренний»/«внешний» контур, mTLS/Ingress, IP-allowlist, default-deny.
- Наблюдаемость/экономика: SLI/SLO, квоты, showback/chargeback (опционально).
Модель tenancy: «мягкая» vs «жёсткая»
- Мягкая (soft) — общие control plane и data plane, изоляция на уровне k8s-примитивов (Namespaces, NetworkPolicy, Quotas). Подходит большинству BI/DWH.
- Жёсткая (hard) — отдельные кластеры/пулы узлов и даже отдельные учётки/сети для особо критичных доменов. Дороже, но проще по рискам.
Практика: начинать с soft, а для «шумных» или регламентных команд выделять пул нод и ужесточённые политики.
Namespaces-на-команду и стандарты
- По одному namespace на команду/проект: team-a, team-b, team-c. Общие сервисы — в data-platform (Trino, MinIO и т. п.) и vitrines-shared (слой витрин).
- Лейблы и аннотации обязательны: владелец, контакт on-call, критичность (owner, cost-center, oncall, env).
- Включить Pod Security (PSA) — profile restricted; шаблоны Gatekeeper/Kyverno, запрещающие: привилегии, hostPath/hostNetwork, неподписанные образы.
Ресурсы под контролем: ResourceQuota, LimitRange, PriorityClass
Зачем: чтобы команда не «съела» CPU/RAM/диски соседей и не зафлудила кластер сервисами/секретами.
- ResourceQuota — потолки по requests/limits.cpu|memory, количеству pods, pvc, services, loadbalancers, суммарному requests.storage, по расширенным ресурсам (например, nvidia.com/gpu).
- LimitRange — дефолтные requests/limits на Pod/Container, лимиты ephemeral-storage (частая «скрытая» утечка).
- PriorityClass — кому «уступаем» при дефиците: платформа/витрины выше, экспериментальные Job ниже.
Мини-фрагменты (смысловые):
# Пример ResourceQuota (team-a): CPU/RAM, PVC и GPU=0
apiVersion: v1
kind: ResourceQuota
metadata: { name: rq-team-a, namespace: team-a }
spec:
hard:
requests.cpu: "40"
limits.cpu: "60"
requests.memory: "120Gi"
limits.memory: "180Gi"
requests.storage: "5Ti"
persistentvolumeclaims: "40"
pods: "300"
requests.nvidia.com/gpu: "0"
---
# Пример LimitRange (team-a): дефолтные requests/limits + ephemeral
apiVersion: v1
kind: LimitRange
metadata: { name: lr-team-a, namespace: team-a }
spec:
limits:
- type: Container
defaultRequest: { cpu: "200m", memory: "512Mi" }
default: { cpu: "1", memory: "2Gi" }
- type: Pod
max:
ephemeral-storage: "20Gi"
Сетевая изоляция между зонами (NetworkPolicy)
Базовый принцип — default-deny в каждом ns, затем адресные разрешения:
- DNS (kube-dns), SSO/IdP, SMTP (если нужно).
- Обращения только к общему слою витрин (Trino/ClickHouse/PG) и S3/MinIO.
- Запрет к «чужим» ns и системному API (кроме нужного).
Пример (идея, без «простынь»):
# team-a: запрещаем всё; разрешаем egress к DNS, Trino и MinIO
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: team-a }
spec: { podSelector: {}, policyTypes: ["Ingress","Egress"] }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-egress-shared, namespace: team-a }
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to: # DNS
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
podSelector: { matchLabels: { k8s-app: kube-dns } }
ports: [{ protocol: UDP, port: 53 }]
- to: # Trino в vitrines-shared
- namespaceSelector: { matchLabels: { name: vitrines-shared } }
podSelector: { matchLabels: { app: trino } }
ports: [{ protocol: TCP, port: 8080 }]
- to: # MinIO
- namespaceSelector: { matchLabels: { name: data-platform } }
podSelector: { matchLabels: { app: minio } }
ports: [{ protocol: TCP, port: 9000 }]
Совет: если используете сервис-меш (Linkerd/Cilium mesh), усиливайте правила моделями авторизации L7 (кто к каким маршрутам).
Отдельные пулы нод и «тяжёлые» задачи
Зачем: ETL и Spark/Flink, Trino-workers, ClickHouse, Kafka, а также ML-обучение и GPU — требуют изоляции от легковесных веб-слоёв.
- Размечаем node pools лейблами: pool=stateless, pool=stateful, pool=etl, pool=gpu.
- На «узких» пулах ставим taints: например, kubectl taint nodes node-etl pool=etl:NoSchedule и добавляем tolerations только на нужные workload’ы.
- Affinity/anti-affinity: разводим реплики по разным узлам/зонам, не кладём всё «тяжёлое» вместе.
GPU-ноды для ML
Компоненты: NVIDIA Device Plugin, (по необходимости) GPU Operator, драйверы. Политики:
- Квоты на GPU через ResourceQuota (requests.nvidia.com/gpu), где кому-то 0 (запрет), кому-то 2 и т. п.
- Доступ только нужным ns: taints/tolerations + nodeSelector pool=gpu.
- MIG/тайм-шаринг (если карточки поддерживают): дробим на профили, чтобы не простаивали.
- Хранилище/сеть: быстрые локальные диски для датасетов/кэшей, egress-политики к артефакт-сторам.
Мини-смысл:
# team-c можно до 2 GPU, team-a/team-b — 0 (см. квоты выше) spec.hard.requests.nvidia.com/gpu: "2"
В Pod ML-задачи запрашивают resources.limits."nvidia.com/gpu": 1 + nodeSelector: { pool: "gpu" }.
Жизненный цикл секретов
Дизайн: секреты хранятся вне кластера (Vault/Secret Manager), в k8s — только синхронизация External Secrets Operator. Правила:
- Пространства секретов по ns: kv/team-a/*, kv/team-b/*. Права — только на свой путь.
- Краткоживущие креды (dynamic creds) для БД/объектного хранилища; перезапуск подов при ротации.
- SA-токены: automountServiceAccountToken: false, проецируемые токены с TTL и узкой аудиторией.
- ImagePullSecrets и политика образов (подписи cosign + валидация Kyverno).
Общий слой витрин (shared layer) без взаимных помех
Цель — чтобы все пользователи BI ходили в одно место, а команды публиковали туда только согласованные данные.
Опорные решения:
- Trino (каталоги iceberg|clickhouse|postgres) — единая точка чтения.
- «Витрины-как-код» (dbt exposures) и «контракты» на схемы (expand/contract; см. Модуль 6 и 7).
- Схемы/БД по доменам: marts.sales, marts.marketing, marts.finance; роли read-only для BI, write — только у конвейеров владельца домена.
- S3-слой: бакеты/префиксы per team (s3://teams/team-a/*) + s3://vitrines/*; квоты на бакеты (MinIO bucket quota), lifecycle/версионирование.
- Доступы: NetworkPolicy/Ingress → Trino/PG/CH из командных ns; BI/пользователи — только в vitrines-shared.
Гранты (концептуально):
- роль marts_reader (BI) — SELECT на marts.*;
- роль sales_owner — INSERT/UPDATE/DDL в marts.sales.*;
- публикация — только через пайплайн (GitOps), не руками.
Наблюдаемость и экономия
- По ns/командам собираем: CPU/Memory/Pods/PVC/IOPS, egress/ingress трафик, количество объектов (Secrets/ConfigMaps), S3-объём.
- SLO per команда: «окна ETL», «свежесть витрин», «ошибки коннекторов» — см. Модуль 5.
- Showback/chargeback (при необходимости): Opencost/Kubecost, лейблы/аннотации — в отчёты.
Практика: изолировать 3 команды и организовать общий слой витрин
Цель
Создать три рабочие зоны (team-a, team-b, team-c), дать каждой ресурсы и сеть по минимуму, включить секреты и доступ только к общему слою витрин (vitrines-shared), выделить тяжёлые/ML задачи, не нарушая SLO платформы.
Шаги (краткий план)
-
Namespaces: team-a, team-b, team-c, vitrines-shared, data-platform.
- Метки владельцев, PSA=restricted.
- Quotas/LimitRanges на каждый ns: CPU/RAM/PVC/Pods; у team-c — GPU≤2, у остальных — 0.
- PriorityClass: platform-high (Trino/каталоги), teams-normal, experiments-low.
- Node pools: pool=stateless, pool=etl, pool=stateful, pool=gpu; taints на etl и gpu.
- NetworkPolicy: default-deny; egress каждой команды → DNS, SSO, vitrines-shared/Trino, data-platform/MinIO; запрет cross-ns.
- Secrets: ESO + Vault с путями kv/team-*/...; SA-токены с TTL.
- Trino/PG/CH в vitrines-shared: роли marts_reader (BI) и владельцы доменов; BI-Ingress только к этому ns.
- S3: бакеты teams/team-a, teams/team-b, teams/team-c, vitrines; квоты и lifecycle.
- GPU: NVIDIA plugin, квоты на GPU; taints/tolerations; team-c — единственный с доступом.
- Наблюдаемость: дашборд «per-team» (ресурсы, ошибки, свежесть), алерты на burn-rate и превышение квот.
Результат
- Команды запускают пайплайны в своих ns, видят только общие витрины и S3, не могут «ходить» в чужие зоны.
- Платформа/витрины работают приоритетно; тяжёлые/ML задачи не мешают веб-слоям и БД.
- Секреты и доступы управляются «как код», всё прозрачно по потреблению.
Риски и как их закрыть
|
Риск |
Симптом |
Что сделать |
|---|---|---|
|
Noisy neighbor (шумный сосед) |
Всплески latency у BI/витрин |
Жёсткие LimitRange, Quota; отдельные пулы, PDB/TopologySpread; PriorityClass |
|
«Съели» соединения БД/Trino |
503/timeout, блокировки |
PgBouncer/лимиты Trino/CH; connection budget на HPA/KEDA; квоты на pods |
|
Egress «пробит» наружу |
Доступ из команд в Интернет |
Default-deny, явные разрешения; egress-контроль по IP/FQDN (Cilium/Calico L7) |
|
Секреты утекли в Git |
base64 в манифестах |
ESO+Vault; SOPS/SealedSecrets для статичных; сканеры секретов в CI |
|
GPU «захватили» все |
ML-задачи блокируют очередь |
Quota на GPU, taints/tolerations, preemption по PriorityClass, планировщик заданий |
|
Эфемерное хранилище переполнили |
Pod OOM по ephemeral-storage |
LimitRange на ephemeral, лог-ротация, Sidecar для выгрузки логов |
|
«Случайно» доступ к чужим данным |
Просмотры «не своих» витрин |
Роли read-only только на marts; запрет прямых коннектов к staging/raw; NetworkPolicy |
Короткий чек-лист «готово к многопользовательскому продакшену»
- Namespaces на команды, лейблы/PSA, Gatekeeper/Kyverno политики.
- ResourceQuota и LimitRange (включая ephemeral-storage), приоритеты.
- Node pools и taints; тяжёлые/ML — изолированы; квоты на GPU.
- NetworkPolicy: default-deny везде; разрешения — только к DNS/IdP/вitrines/MinIO/SMTP.
- Secrets через ESO/Vault; токены краткоживущие; image-policies и подписи.
- Общий слой витрин (vitrines-shared): роли/гранты, BI ходит только туда; S3-квоты/жизненный цикл.
- Дашборды per-team и алерты: ресурсы, свежесть, ошибки коннекторов, превышение квот.
- Runbooks: «превышена квота», «GPU заняты», «подозрительный egress», «секрет пора ротировать».
Multi-tenant-кластер для BI/DWH — это не «про один флажок». Это совокупность дисциплин: namespaces с политиками, квоты и лимиты, отдельные пулы и приоритеты, строгие NetworkPolicy, управляемые секреты и общий слой витрин с чёткими ролями. Если всё это оформлено «как код» и подкреплено наблюдаемостью, команды работают параллельно, быстро и — главное — безопасно друг для друга и для бизнеса.



