isolate tenant workloads with layered quota and network controls

Kubernetes multi-tenancy resource isolation: stopping noisy neighbors with quotas and network policies

14 min read

One batch job with unbounded requests can throttle latency-sensitive APIs across a shared cluster. Stack tiered namespaces, ResourceQuotas, LimitRanges, default-deny NetworkPolicy, and optional dedicated node pools into one isolation model with cost labels finance can reconcile.

Why shared clusters need layered tenant isolation

In a cluster shared by multiple product teams, the noisy neighbor problem is persistent: one misbehaving workload consumes schedulable capacity, triggers node pressure evictions, throttles CPU on unrelated pods, and cascades through service dependencies. A batch team spawning hundreds of pods with missing resource bounds can violate SLOs for an API team on the same nodes. Without isolation you get SLA breaches, unpredictable spend, and lateral movement across namespaces. Kubernetes offers ResourceQuota, LimitRange, NetworkPolicy, and node placement primitives, but most teams apply them in isolation. Production multi-tenancy needs a composed model: resource caps, network boundaries, optional dedicated nodes for gold tiers, and labels that tie consumption back to owners.

Four layers: tiered namespaces, quotas, network, and node placement

Treat isolation as four complementary layers. Tiered namespaces use one namespace per tenant and environment—team-alpha-prod, team-alpha-dev—linked by shared tenant, tier, and cost-center labels rather than a flat free-for-all. ResourceQuota plus LimitRange caps aggregate tenant consumption and bound every container even when developers omit requests. NetworkPolicy enforces default-deny ingress and egress with explicit allow paths for ingress controllers, approved peer tenants, and DNS. Node placement adds dedicated pools for high-priority tenants through taints, tolerations, and topology spread so gold-tier workloads do not share CPU with batch neighbors. ResourceQuota is per namespace; hierarchy is operational—consistent naming, shared labels, and Git-managed quota tiers—not automatic inheritance unless you adopt HNC or vCluster for larger estates.

Tenant namespaces with ResourceQuota and LimitRange

Label every tenant namespace with tenant, tier, cost-center, and environment. Set aggregate CPU, memory, pod, storage, and object-count quotas per namespace. Pair quotas with LimitRange defaults and max values so a single pod cannot request four CPU because staging worked once. Gold tiers receive higher caps; silver tiers stay tighter on shared nodes.

YAML · tenant namespace with quota and LimitRange
apiVersion: v1
kind: Namespace
metadata:
  name: team-alpha-prod
  labels:
    tenant: team-alpha
    tier: gold
    cost-center: engineering
    environment: production
    pod-security.kubernetes.io/enforce: baseline
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-alpha-quota
  namespace: team-alpha-prod
spec:
  hard:
    requests.cpu: "30"
    requests.memory: 60Gi
    limits.cpu: "60"
    limits.memory: 120Gi
    pods: "100"
    services.loadbalancers: "2"
    persistentvolumeclaims: "10"
    requests.storage: 500Gi
---
apiVersion: v1
kind: LimitRange
metadata:
  name: team-alpha-defaults
  namespace: team-alpha-prod
spec:
  limits:
    - type: Container
      default:
        cpu: "500m"
        memory: 512Mi
      defaultRequest:
        cpu: "100m"
        memory: 128Mi
      max:
        cpu: "4"
        memory: 8Gi
      maxLimitRequestRatio:
        cpu: "10"
        memory: "4"

NetworkPolicy segmentation between tenants

Start from default-deny ingress and egress in every tenant namespace. Allow north-south traffic only from the ingress controller namespace. Allow east-west traffic only to explicitly approved peer namespaces—never implicit full-mesh between tenants. Always add a dedicated DNS egress rule to kube-dns; without it pods fail name resolution silently under deny-all egress. Validate policies with flow observation before enforcement in production.

YAML · default-deny, ingress, and DNS egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-alpha-prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: team-alpha-prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: team-alpha-prod
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Dedicated node pools for gold-tier tenants

For strict performance isolation, dedicate node pools to high-priority tenants. Label nodes, apply a NoSchedule taint, and require matching tolerations on tenant Deployments. Add topologySpreadConstraints so replicas spread across nodes in the pool instead of stacking on one noisy host. Silver-tier tenants stay on shared pools with quota and network controls only.

Bash · taint dedicated tenant node pool
kubectl label node node-pool-alpha-01 tenant=team-alpha node-pool=alpha-dedicated
kubectl taint nodes node-pool-alpha-01 tenant=team-alpha:NoSchedule
YAML · Deployment on dedicated nodes with spread constraints
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alpha-api
  namespace: team-alpha-prod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: alpha-api
  template:
    metadata:
      labels:
        app: alpha-api
        tenant: team-alpha
    spec:
      nodeSelector:
        tenant: team-alpha
      tolerations:
        - key: tenant
          operator: Equal
          value: team-alpha
          effect: NoSchedule
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: alpha-api
      containers:
        - name: api
          image: registry.example.com/alpha-api:v1.4.0
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "2"
              memory: 2Gi

Two-tenant example: gold dedicated versus silver shared

Team Alpha runs on gold tier with higher quotas and dedicated nodes. Team Beta runs on silver tier with smaller quotas on shared nodes. Both get default-deny networking and LimitRange guardrails; only Alpha receives node taints and spread constraints. This pattern keeps platform cost predictable while protecting latency-sensitive tenants.

YAML · silver-tier namespace with tighter quota
apiVersion: v1
kind: Namespace
metadata:
  name: team-beta-prod
  labels:
    tenant: team-beta
    tier: silver
    cost-center: engineering
    environment: production
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-beta-quota
  namespace: team-beta-prod
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "30"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: team-beta-defaults
  namespace: team-beta-prod
spec:
  limits:
    - type: Container
      default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      max:
        cpu: "2"
        memory: 4Gi

Operational practices: observe first, enforce, and review quarterly

Observe consumption for two to four weeks before tightening quotas—unexpected OOMKills erode developer trust. Use Kyverno or Gatekeeper in audit mode to catch missing resource requests and privileged pods before hard enforcement. Enforce Pod Security Standards at the namespace level: baseline for most tenants, restricted for sensitive workloads. Alert when namespace quota usage exceeds eighty percent. Label every namespace with cost-center and tenant for OpenCost or Kubecost showback. Combine L3/L4 NetworkPolicy with identity-aware Cilium or a service mesh when you need mTLS and path-level control. Review quota tiers quarterly from Prometheus history and right-size caps as teams grow. Multi-tenancy is not one feature—it is quotas, limits, network policy, node placement, and governance composed into predictable performance and clear accountability.

YAML · Prometheus alert for tenant quota pressure
groups:
  - name: tenant-quota
    rules:
      - alert: TenantQuotaNearLimit
        expr: |
          sum by (namespace, resource) (
            kube_resourcequota{type="used"}
          )
          /
          sum by (namespace, resource) (
            kube_resourcequota{type="hard"}
          ) > 0.8
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Namespace {{ $labels.namespace }} above 80% of {{ $labels.resource }} quota"
YAML · Kyverno audit policy for missing resource requests
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: audit-container-resource-requests
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: require-cpu-memory-requests
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Containers must define CPU and memory requests."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "?*"
                    memory: "?*"

Namespace budgets and cost-center labels extend patterns from our namespace FinOps guide with ResourceQuotas and cost attribution.

Default-deny segmentation and identity-aware policies are covered in our zero-trust Kubernetes networking guide with Cilium.