Multi-tenant K8s setup для Flink
Multi-tenancy в Kubernetes — это набор изоляционных границ: namespaces, RBAC, ResourceQuota, NetworkPolicy, PodSecurityPolicy. Для Flink-платформы каждая команда должна получить свой namespace, в котором она может деплоить Flink-jobs самостоятельно, не имея доступа к ресурсам других команд.
В этом уроке настроим Flink Kubernetes Operator в cluster-wide режиме, создадим namespace template для нового tenant, настроим RBAC и quotas, и определим tenant onboarding процедуру.
Capabilities и namespaces Linux: изоляция на уровне ядраАрхитектура: cluster-wide Operator + per-tenant namespaces
Flink Kubernetes Operator имеет два режима работы:
- Namespaced — operator watches только один namespace. Простой, но требует один operator на каждый namespace. Не подходит для multi-tenant.
- Cluster-wide — operator watches все namespaces (или whitelist). Один operator управляет всеми tenant’ами. Подходит для нашего сценария.
Установка Flink Operator в cluster-wide mode
Helm chart Flink Kubernetes Operator:
helm repo add flink-operator-repo \
https://downloads.apache.org/flink/flink-kubernetes-operator-1.10.0/
helm install flink-kubernetes-operator \
flink-operator-repo/flink-kubernetes-operator \
--namespace flink-system \
--create-namespace \
--set watchNamespaces=null \
--set operatorPod.resources.requests.cpu=500m \
--set operatorPod.resources.requests.memory=1Gi \
--set operatorPod.resources.limits.cpu=2 \
--set operatorPod.resources.limits.memory=2Gi \
--set image.tag=1.10.0
watchNamespaces=null означает cluster-wide. Можно сузить до whitelist:
--set watchNamespaces="{fraud-team,ads-team,analytics-team}"
Whitelist предпочтительнее в production: операторам не нужно reconcile resources в системных namespaces.
После установки проверьте, что operator running:
kubectl get pods -n flink-system
# NAME READY STATUS RESTARTS AGE
# flink-kubernetes-operator-7d8b9c5d4f-xyz12 2/2 Running 0 1m
kubectl get crd flinkdeployments.flink.apache.org
# NAME CREATED AT
# flinkdeployments.flink.apache.org 2026-05-19T10:00:00Z
Tenant namespace template
Каждый новый tenant получает namespace, созданный по template. Template применяется через GitOps (ArgoCD) или ручной kubectl apply. Базовый template:
# tenant-namespace-template.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: ${TENANT_NAME}
labels:
flink-platform/tenant: "${TENANT_NAME}"
flink-platform/managed: "true"
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-quota
namespace: ${TENANT_NAME}
spec:
hard:
requests.cpu: "${TENANT_CPU_REQUESTS}"
requests.memory: "${TENANT_MEMORY_REQUESTS}"
limits.cpu: "${TENANT_CPU_LIMITS}"
limits.memory: "${TENANT_MEMORY_LIMITS}"
persistentvolumeclaims: "${TENANT_PVC_COUNT}"
count/pods: "${TENANT_POD_COUNT}"
count/services: "20"
count/secrets: "30"
---
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-limits
namespace: ${TENANT_NAME}
spec:
limits:
- default:
cpu: 2
memory: 4Gi
defaultRequest:
cpu: 500m
memory: 1Gi
max:
cpu: 8
memory: 32Gi
type: Container
---
# RBAC: tenant team может CRUD только Flink resources
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-flink-role
namespace: ${TENANT_NAME}
rules:
- apiGroups: ["flink.apache.org"]
resources: ["flinkdeployments", "flinksessionjobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"] # не create/delete — secrets через Vault
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-flink-binding
namespace: ${TENANT_NAME}
subjects:
- kind: Group
name: ${TENANT_LDAP_GROUP} # AD/LDAP group для tenant team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-flink-role
apiGroup: rbac.authorization.k8s.io
---
# Service account для Flink jobs в этом namespace
apiVersion: v1
kind: ServiceAccount
metadata:
name: flink
namespace: ${TENANT_NAME}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: flink-role-binding
namespace: ${TENANT_NAME}
subjects:
- kind: ServiceAccount
name: flink
namespace: ${TENANT_NAME}
roleRef:
kind: ClusterRole
name: flink # provided by Flink Operator
apiGroup: rbac.authorization.k8s.io
---
# NetworkPolicy: tenant pods могут общаться только внутри своего namespace
# и с shared services (Kafka, S3)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tenant-isolation
namespace: ${TENANT_NAME}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ${TENANT_NAME}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: flink-system # operator может dial
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring # Prometheus scrape
egress:
- to:
- namespaceSelector: {} # любой namespace для DNS resolution
ports:
- protocol: UDP
port: 53
- to:
- podSelector:
matchLabels:
app: kafka
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kafka-system
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443 # HTTPS для S3, Vault
Применить для нового tenant:
export TENANT_NAME="fraud-team"
export TENANT_CPU_REQUESTS="50"
export TENANT_MEMORY_REQUESTS="200Gi"
export TENANT_CPU_LIMITS="100"
export TENANT_MEMORY_LIMITS="400Gi"
export TENANT_PVC_COUNT="20"
export TENANT_POD_COUNT="100"
export TENANT_LDAP_GROUP="fraud-team-engineers"
envsubst < tenant-namespace-template.yaml | kubectl apply -f -
ResourceQuota: предотвращение noisy neighbor
ResourceQuota — критичный механизм multi-tenancy. Без неё один tenant может потребить всё CPU и memory кластера, оставив других без ресурсов.
Базовая стратегия квот:
spec:
hard:
# CPU/memory requests = гарантированное allocation
requests.cpu: "50" # 50 vCPU гарантировано
requests.memory: "200Gi" # 200Gi RAM гарантировано
# CPU/memory limits = максимум при burst
limits.cpu: "100" # до 100 vCPU при burst
limits.memory: "400Gi" # до 400Gi при burst
# Object counts — защита от runaway controller
persistentvolumeclaims: "20"
count/pods: "100"
count/services: "20"
requests гарантируется (используется в scheduling), limits — soft cap (kernel убьёт container при превышении memory limit). Соотношение limits/requests = 2x — типичный pattern для bursty workloads.
Дополнительно — PriorityClass для разных SLA:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: flink-critical
value: 1000000
globalDefault: false
description: "Critical Flink jobs — fraud detection, payments"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: flink-batch
value: 100
globalDefault: false
description: "Batch Flink jobs — analytics, ETL"
Tenant использует в FlinkDeployment:
spec:
podTemplate:
spec:
priorityClassName: flink-critical
При resource pressure низкий-priority jobs preempted в пользу высокого.
Tenant onboarding процедура
Self-service onboarding — главный показатель зрелости платформы. Процедура:
1. Tenant team создаёт PR в platform-config repo с заполненным template:
# tenants/fraud-team.yaml
tenant:
name: fraud-team
ldap_group: fraud-team-engineers
oncall_team: fraud-detection-oncall
contact_slack: "#fraud-team"
quotas:
cpu_requests: 50
memory_requests: 200Gi
cpu_limits: 100
memory_limits: 400Gi
max_pods: 100
max_pvcs: 20
sla:
tier: critical # critical | standard | batch
priority_class: flink-critical
alert_channel: "#fraud-oncall"
pagerduty_service: fraud-detection
resources:
kafka_topics: # запрос ACL для топиков
- name: transactions
permissions: [read]
- name: fraud-alerts
permissions: [write]
s3_buckets:
- name: flink-state-fraud
permissions: [read, write]
- name: flink-savepoints-fraud
permissions: [read, write]
2. Platform team review — проверяет:
- Quotas reasonable (не запрашивают 1000 vCPU без оснований)
- Kafka topics существуют
- S3 buckets созданы
- LDAP group существует и содержит tenant team members
3. ArgoCD applies — после merge ArgoCD создаёт namespace, RBAC, quotas, AWS IAM роли (через IRSA для S3 access).
4. Tenant deploys свой первый Flink job:
# fraud-team/jobs/fraud-detection.yaml
apiVersion: flink.apache.org/v1beta1
kind: FlinkDeployment
metadata:
name: fraud-detection
namespace: fraud-team
spec:
image: registry.company.com/flink:2.2.0
flinkVersion: v2_2
serviceAccount: flink
flinkConfiguration:
taskmanager.numberOfTaskSlots: "4"
state.savepoints.dir: s3://flink-savepoints-fraud/fraud-detection/
state.checkpoint.dir: s3://flink-checkpoints-fraud/fraud-detection/
metrics.reporters: prom
metrics.reporter.prom.factory.class: org.apache.flink.metrics.prometheus.PrometheusReporterFactory
metrics.reporter.prom.port: 9249
jobManager:
resource:
memory: 4096m
cpu: 2
taskManager:
resource:
memory: 8192m
cpu: 4
job:
jarURI: local:///opt/flink/usrlib/fraud-detection-2.2.jar
parallelism: 16
upgradeMode: savepoint
podTemplate:
spec:
priorityClassName: flink-critical
containers:
- name: flink-main-container
env:
- name: KAFKA_BOOTSTRAP_SERVERS
value: kafka-headless.kafka-system:9092
- name: VAULT_ADDR
value: http://vault.vault:8200
5. Monitoring auto-enabled — Prometheus ServiceMonitor создаётся через mutating webhook, Grafana dashboard auto-import через JSON-based provisioning.
Что мониторить per-tenant
Платформа должна показывать per-tenant metrics:
# Total CPU usage по tenant
sum by (namespace) (
container_cpu_usage_seconds_total{namespace=~".*-team"}
)
# Memory usage vs quota
sum by (namespace) (container_memory_working_set_bytes{namespace=~".*-team"})
/
sum by (namespace) (kube_resourcequota{resource="limits.memory"})
# Количество active Flink jobs
count by (namespace) (kube_pod_status_phase{
phase="Running",
pod=~".*-jobmanager-.*"
})
# Checkpoint failure rate per tenant
sum by (namespace) (rate(flink_jobmanager_job_checkpointsFailed[5m]))
/
sum by (namespace) (rate(flink_jobmanager_job_totalNumberOfCheckpoints[5m]))
Эти метрики экспонируются как tenant dashboard в Grafana. Каждый tenant видит свои metrics + cost (через AWS Cost Explorer label-based allocation).
Без cost transparency tenant’ы будут запрашивать unlimited quotas «just in case». Показывайте им $ cost (через Kubecost или AWS Cost Allocation Tags), и они сами начнут оптимизировать.
Итоги
Multi-tenant Flink platform строится на трёх pillars: cluster-wide Flink Operator (один operator для всех tenants), namespace-per-team (с RBAC, ResourceQuota, NetworkPolicy для изоляции), self-service tenant onboarding (через GitOps).
Главный принцип: tenants работают независимо через стандартный Kubernetes interface (FlinkDeployment CRD), platform team автоматизирует общие компоненты (operator, monitoring, secrets management).
В следующем уроке добавим autoscaling — без него tenants либо over-provision (дорого), либо страдают от lag spikes (нарушение SLA).