Go K8sリソースクォータガバナンス:マルチテナントリソース分離の6つの重要プラクティス

云原生

一つのチームがクラスタ全体を食い尽くす時:マルチテナントリソース分離の至暗の刻

深夜2時、データチームがフルETLジョブを実行 — CPUとメモリが瞬時にクラスタを飽和させる。APIサービスのPodが退去され、フロントエンドゲートウェイがOOM Killされ、プラットフォーム全体が90分間ダウン。さらに悪いことに、事後分析でそのチームがクラスタリソースの70%を消費していることが判明したが、コスト配分記録はゼロ — 誰が何を使ったか誰も知らない。

これは決して例外ではない。リソース争奪によるアバランシェ、無制限のネームスペース、CPU/メモリの特定チーム独占、不可能なコスト配分は、K8sマルチテナント環境の4つの主要なペインポイントとなっている。ResourceQuotaがネームスペースリソース総量を制限し、LimitRangeが個別Podリソース範囲を制約し、PriorityClassが重要サービスの優先度を保障する — この3つの連携で初めて真のマルチテナントリソース分離が実現できる。本記事では6つの重要プラクティスから、プロダクション級K8sリソースクォータガバナンスシステムの構築を案内する。


コア概念クイックリファレンス

概念 フルネーム 役割 キーパラメータ
ResourceQuota ネームスペースリソース総量を制限 hard.limits.cpu/memory/pods
LimitRange 個別Pod/コンテナリソース範囲を制約 default/defaultRequest/max/min
マルチテナント Multi-Tenancy 複数チームがクラスタリソースを共有 ネームスペース分離、RBAC
ネームスペース分離 Namespace Isolation チームリソースの論理分離 NetworkPolicy + ResourceQuota
リクエストとリミット Requests & Limits Podリソース要求と上限 resources.requests/limits
QoSクラス Quality of Service Podサービス品質分類 Guaranteed/Burstable/BestEffort
優先度 PriorityClass Podスケジューリング優先度定義 value/preemptionPolicy
プリエンプション Preemption 高優先度Podが低優先度Podを退去 PreemptLowerPriority

問題分析:マルチテナントリソースガバナンスの5つの課題

課題1:リソース争奪とアバランシェ。あるチームがリソース制限なしのジョブをデプロイし、瞬時にノードのCPU/メモリを使い果たす。他チームのPodが退去またはOOM Killされ、カスケード障害を引き起こす。

課題2:クォータ設定の粒度。ResourceQuotaを厳しくしすぎるとチームが正常にデプロイできず、緩くしすぎると無意味になる。チームごとにワークロード特性が大きく異なり、統一クォータでは適応できない。

課題3:優先度とプリエンプション。重要サービスとバッチジョブが混在する場合、バッチジョブがリソースを使い切り、重要サービスがスケジュールできない。優先度メカニズムがないと「先にデプロイした者がリソースを取る」状態になる。

課題4:コスト帰属。複数チームがクラスタを共有しているが、ネームスペース別のリソース使用計量がなく、クラウドコストを正確に配分できない。財務チームは推測するしかない。

課題5:リソースフラグメンテーション。各ネームスペースのクォータ合計がクラスタの実際の容量を超え、リソースのオーバーコミットが発生する。ノード上の散在する空きリソースでは新規Podのスケジューリングを満たせず、リソースフラグメンテーションが生じる。


プラクティス1:ResourceQuotaネームスペースクォータ

apiVersion: v1
kind: Namespace
metadata:
  name: team-data
  labels:
    tenant: data-team
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-data-quota
  namespace: team-data
spec:
  hard:
    requests.cpu: "16"
    requests.memory: 32Gi
    limits.cpu: "32"
    limits.memory: 64Gi
    pods: "50"
    services: "10"
    persistentvolumeclaims: "20"
  scopes:
    - Terminating
    - NotTerminating
package main

import (
    "context"
    "fmt"
    "os"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/api/resource"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

func createNamespaceQuota(ctx context.Context, clientset *kubernetes.Clientset, nsName string, cpuReq, memReq, cpuLimit, memLimit string) error {
    ns := &corev1.Namespace{
        ObjectMeta: metav1.ObjectMeta{
            Name:   nsName,
            Labels: map[string]string{"tenant": nsName},
        },
    }
    _, err := clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
    if err != nil {
        return fmt.Errorf("create namespace: %w", err)
    }

    quota := &corev1.ResourceQuota{
        ObjectMeta: metav1.ObjectMeta{
            Name:      nsName + "-quota",
            Namespace: nsName,
        },
        Spec: corev1.ResourceQuotaSpec{
            Hard: corev1.ResourceList{
                corev1.ResourceRequestsCPU:             resource.MustParse(cpuReq),
                corev1.ResourceRequestsMemory:          resource.MustParse(memReq),
                corev1.ResourceLimitsCPU:               resource.MustParse(cpuLimit),
                corev1.ResourceLimitsMemory:            resource.MustParse(memLimit),
                corev1.ResourcePods:                    resource.MustParse("50"),
                corev1.ResourceServices:                resource.MustParse("10"),
                corev1.ResourcePersistentVolumeClaims:  resource.MustParse("20"),
            },
        },
    }
    _, err = clientset.CoreV1().ResourceQuotas(nsName).Create(ctx, quota, metav1.CreateOptions{})
    if err != nil {
        return fmt.Errorf("create quota: %w", err)
    }
    fmt.Printf("Created quota for namespace %s\n", nsName)
    return nil
}

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "build config: %v\n", err)
        os.Exit(1)
    }
    cs, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "create clientset: %v\n", err)
        os.Exit(1)
    }
    ctx := context.Background()
    teams := []struct {
        name, cpuReq, memReq, cpuLimit, memLimit string
    }{
        {"team-data", "16", "32Gi", "32", "64Gi"},
        {"team-api", "8", "16Gi", "16", "32Gi"},
        {"team-frontend", "4", "8Gi", "8", "16Gi"},
    }
    for _, t := range teams {
        if err := createNamespaceQuota(ctx, cs, t.name, t.cpuReq, t.memReq, t.cpuLimit, t.memLimit); err != nil {
            fmt.Fprintf(os.Stderr, "failed for %s: %v\n", t.name, err)
        }
    }
}

ResourceQuotaはネームスペースリソース総量を制限する。hardフィールドはCPU/メモリ/Pod数などの上限を定義する。重要原則:requestsはスケジューリング保障を制御し、limitsは実際の消費上限を制御する — 両方を同時に設定する必要がある。scopesでTerminating/NotTerminatingタイプ別にクォータを設定できる。


プラクティス2:LimitRangeデフォルトリソース制限

apiVersion: v1
kind: LimitRange
metadata:
  name: team-data-limits
  namespace: team-data
spec:
  limits:
    - type: Container
      default:
        cpu: "1"
        memory: 1Gi
      defaultRequest:
        cpu: 200m
        memory: 256Mi
      max:
        cpu: "4"
        memory: 8Gi
      min:
        cpu: 50m
        memory: 64Mi
      maxLimitRequestRatio:
        cpu: "5"
        memory: "4"
    - type: Pod
      max:
        cpu: "8"
        memory: 16Gi
    - type: PersistentVolumeClaim
      max:
        storage: 50Gi
      min:
        storage: 1Gi

LimitRangeはリソース未設定のコンテナにdefaultdefaultRequestを自動注入し、max/minでリソース範囲を制約し、maxLimitRequestRatioでlimitがrequestを大幅に超えるオーバーコミットを防止する。重要:LimitRangeがない場合、リソース未設定のPodはデフォルトでBestEffortとなり、無制限にリソースを消費する可能性がある。


プラクティス3:QoSクラスと保障戦略

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  namespace: team-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-service
  template:
    metadata:
      labels:
        app: api-service
    spec:
      containers:
        - name: api-service
          image: api-service:latest
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-job-runner
  namespace: team-data
spec:
  replicas: 2
  selector:
    matchLabels:
      app: batch-job-runner
  template:
    metadata:
      labels:
        app: batch-job-runner
    spec:
      containers:
        - name: runner
          image: batch-runner:latest
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
package main

import (
    "fmt"

    corev1 "k8s.io/api/core/v1"
)

func classifyQoS(pod *corev1.Pod) string {
    hasRequests := false
    hasLimits := false
    for _, c := range pod.Spec.Containers {
        if c.Resources.Requests.Cpu().IsZero() || c.Resources.Requests.Memory().IsZero() {
            return "BestEffort"
        }
        hasRequests = true
        if c.Resources.Limits.Cpu().IsZero() || c.Resources.Limits.Memory().IsZero() {
            hasLimits = false
        } else {
            hasLimits = true
        }
    }
    if hasRequests && hasLimits {
        requestsEqualLimits := true
        for _, c := range pod.Spec.Containers {
            if !c.Resources.Requests.Cpu().Equal(*c.Resources.Limits.Cpu()) ||
                !c.Resources.Requests.Memory().Equal(*c.Resources.Limits.Memory()) {
                requestsEqualLimits = false
                break
            }
        }
        if requestsEqualLimits {
            return "Guaranteed"
        }
    }
    return "Burstable"
}

func main() {
    pods := []struct {
        name string
        pod  *corev1.Pod
    }{
        {"Guaranteed", &corev1.Pod{}},
        {"Burstable", &corev1.Pod{}},
        {"BestEffort", &corev1.Pod{}},
    }
    for _, p := range pods {
        fmt.Printf("Pod %s: QoS=%s\n", p.name, classifyQoS(p.pod))
    }
}

K8sはPodを3つのQoSクラスに分類する:Guaranteed(requests=limits、最後に退去される)、Burstable(requestsはあるがlimits>requests、中程度の保障)、BestEffort(requests/limitsなし、最初に退去される)。プロダクションの鉄則:重要サービスはGuaranteed必須、バッチタスクはBurstable、テスト環境はBestEffortを使用。


プラクティス4:PriorityClass優先度とプリエンプション

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: critical-service
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Critical production services"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: normal-service
value: 100000
globalDefault: true
preemptionPolicy: PreemptLowerPriority
description: "Normal production services"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: batch-job
value: 10000
preemptionPolicy: Never
description: "Batch jobs, can be preempted"
package main

import (
    "context"
    "fmt"
    "os"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

func checkPreemptionRisk(ctx context.Context, clientset *kubernetes.Clientset, namespace string) error {
    pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
    if err != nil {
        return fmt.Errorf("list pods: %w", err)
    }
    lowPriority := int64(50000)
    for _, pod := range pods.Items {
        if pod.Spec.Priority != nil && *pod.Spec.Priority < lowPriority {
            fmt.Printf("WARNING: Pod %s has low priority (%d), at preemption risk\n",
                pod.Name, *pod.Spec.Priority)
        }
    }
    return nil
}

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "build config: %v\n", err)
        os.Exit(1)
    }
    cs, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "create clientset: %v\n", err)
        os.Exit(1)
    }
    ctx := context.Background()
    namespaces := []string{"team-api", "team-data", "team-frontend"}
    for _, ns := range namespaces {
        fmt.Printf("=== Checking namespace: %s ===\n", ns)
        if err := checkPreemptionRisk(ctx, cs, ns); err != nil {
            fmt.Fprintf(os.Stderr, "check %s: %v\n", ns, err)
        }
    }
}

PriorityClassはPodスケジューリング優先度を定義する — valueが大きいほど優先度が高い。クラスタリソースが不足すると、高優先度Podが低優先度Podのリソースをプリエンプトする。重要preemptionPolicy: Neverはこの優先度クラスがアクティブにプリエンプトしないことを意味し、バッチタスクに適している。globalDefault: trueはデフォルト優先度を設定する。


プラクティス5:マルチテナントコスト配分と計量

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

type TenantUsage struct {
    Namespace     string
    CPURequests   float64
    CPULimits     float64
    MemoryRequest float64
    MemoryLimits  float64
    PodCount      int
}

func collectTenantUsage(ctx context.Context, clientset *kubernetes.Clientset) ([]TenantUsage, error) {
    namespaces, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{
        LabelSelector: "tenant",
    })
    if err != nil {
        return nil, fmt.Errorf("list namespaces: %w", err)
    }

    var usages []TenantUsage
    for _, ns := range namespaces.Items {
        pods, err := clientset.CoreV1().Pods(ns.Name).List(ctx, metav1.ListOptions{})
        if err != nil {
            continue
        }
        usage := TenantUsage{Namespace: ns.Name}
        for _, pod := range pods.Items {
            if pod.Status.Phase != corev1.PodRunning {
                continue
            }
            usage.PodCount++
            for _, c := range pod.Spec.Containers {
                if req := c.Resources.Requests; req != nil {
                    usage.CPURequests += req.Cpu().AsApproximateFloat64()
                    usage.MemoryRequest += req.Memory().AsApproximateFloat64() / 1024 / 1024 / 1024
                }
                if lim := c.Resources.Limits; lim != nil {
                    usage.CPULimits += lim.Cpu().AsApproximateFloat64()
                    usage.MemoryLimits += lim.Memory().AsApproximateFloat64() / 1024 / 1024 / 1024
                }
            }
        }
        usages = append(usages, usage)
    }
    return usages, nil
}

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "build config: %v\n", err)
        os.Exit(1)
    }
    cs, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "create clientset: %v\n", err)
        os.Exit(1)
    }
    ctx := context.Background()
    usages, err := collectTenantUsage(ctx, cs)
    if err != nil {
        fmt.Fprintf(os.Stderr, "collect usage: %v\n", err)
        os.Exit(1)
    }

    nodePricePerCPU := 50.0
    nodePricePerGBMem := 5.0
    fmt.Printf("\n=== Tenant Cost Report (%s) ===\n", time.Now().Format("2006-01-02"))
    fmt.Printf("%-15s %8s %8s %10s %10s %6s %10s\n",
        "Namespace", "CPU Req", "CPU Lim", "Mem Req(G)", "Mem Lim(G)", "Pods", "Est.Cost($)")
    for _, u := range usages {
        cost := u.CPURequests*nodePricePerCPU + u.MemoryRequest*nodePricePerGBMem
        fmt.Printf("%-15s %8.2f %8.2f %10.2f %10.2f %6d %10.2f\n",
            u.Namespace, u.CPURequests, u.CPULimits, u.MemoryRequest, u.MemoryLimits, u.PodCount, cost)
    }
}

client-goでネームスペースごとのCPU/メモリ使用量を収集し、価格モデルでコスト配分を計算する。重要:コスト配分はactual usageではなくrequestsに基づくべきである。requestsはスケジューリングコミットメントを占有しているからだ。Prometheusのkube_resourcequotaメトリクスと組み合わせることで、より正確なリアルタイム計量が可能。


プラクティス6:リソースガバナンス自動化Controller

package main

import (
    "context"
    "fmt"
    "os"
    "time"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/resource"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/client-go/informers"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/cache"
    "k8s.io/client-go/tools/clientcmd"
)

type QuotaController struct {
    clientset *kubernetes.Clientset
}

func (c *QuotaController) ensureLimitRange(ns string) error {
    lrs, err := c.clientset.CoreV1().LimitRanges(ns).List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        return fmt.Errorf("list limitranges: %w", err)
    }
    if len(lrs.Items) > 0 {
        return nil
    }
    lr := &corev1.LimitRange{
        ObjectMeta: metav1.ObjectMeta{Name: "default-limits"},
        Spec: corev1.LimitRangeSpec{
            Limits: []corev1.LimitRangeItem{
                {
                    Type: corev1.LimitTypeContainer,
                    Default: corev1.ResourceList{
                        corev1.ResourceCPU:    resource.MustParse("1"),
                        corev1.ResourceMemory: resource.MustParse("1Gi"),
                    },
                    DefaultRequest: corev1.ResourceList{
                        corev1.ResourceCPU:    resource.MustParse("200m"),
                        corev1.ResourceMemory: resource.MustParse("256Mi"),
                    },
                    Max: corev1.ResourceList{
                        corev1.ResourceCPU:    resource.MustParse("4"),
                        corev1.ResourceMemory: resource.MustParse("8Gi"),
                    },
                    Min: corev1.ResourceList{
                        corev1.ResourceCPU:    resource.MustParse("50m"),
                        corev1.ResourceMemory: resource.MustParse("64Mi"),
                    },
                },
            },
        },
    }
    _, err = c.clientset.CoreV1().LimitRanges(ns).Create(context.TODO(), lr, metav1.CreateOptions{})
    if err != nil {
        return fmt.Errorf("create limitrange: %w", err)
    }
    fmt.Printf("Auto-created LimitRange for namespace %s\n", ns)
    return nil
}

func (c *QuotaController) Run(stopCh <-chan struct{}) {
    factory := informers.NewSharedInformerFactory(c.clientset, 30*time.Second)
    nsInformer := factory.Core().V1().Namespaces().Informer()

    nsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            ns := obj.(*corev1.Namespace)
            if ns.Labels["tenant"] != "" {
                if err := c.ensureLimitRange(ns.Name); err != nil {
                    fmt.Fprintf(os.Stderr, "ensure limitrange for %s: %v\n", ns.Name, err)
                }
            }
        },
    })

    factory.Start(stopCh)
    factory.WaitForCacheSync(stopCh)
    wait.Until(func() {}, time.Minute, stopCh)
}

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "build config: %v\n", err)
        os.Exit(1)
    }
    cs, err := kubernetes.NewForConfig(config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "create clientset: %v\n", err)
        os.Exit(1)
    }
    ctrl := &QuotaController{clientset: cs}
    stopCh := make(chan struct{})
    defer close(stopCh)
    fmt.Println("Starting Quota Governance Controller...")
    ctrl.Run(stopCh)
}

自動化ControllerはNamespace作成イベントを監視し、tenantラベル付きのネームスペースにLimitRangeを自動注入し、各テナントネームスペースにデフォルトリソース制約を確保する。重要:プロダクションでは、ResourceQuota、NetworkPolicy、RBACも同時に自動作成するよう拡張し、テナントオンボーディングの完全自動化を実現すべき。


5つのよくある落とし穴

❌ 落とし穴1:ResourceQuotaだけ設定しLimitRangeを設定しない ✅ ResourceQuotaは総量を制限するが、単一のPodがクォータ全体を消費できる。LimitRangeは個別コンテナのリソース範囲を制約する — 両方を併用する必要がある。

❌ 落とし穴2:requestsとlimitsをすべて同じに設定 ✅ すべてのPodをGuaranteed(requests=limits)にすると深刻なリソース浪費が発生する。重要サービスはGuaranteed、通常サービスはBurstable、バッチタスクはBestEffortを使用。

❌ 落とし穴3:ResourceQuotaのscopesを無視 ✅ Terminating/NotTerminatingを区別しないと、バッチジョブと長期サービスが同じクォータを共有し、ジョブがクォータを使い果たしてサービスのデプロイができなくなる。

❌ 落とし穴4:PriorityClassプリエンプションによる退去ループ ✅ 同じ優先度の2つのPodが互いにプリエンプトし合い、退去ループが発生する。異なる優先度値を設定し、バッチタスクにはpreemptionPolicy: Neverを使用。

❌ 落とし穴5:actual usageではなくrequestsに基づいてコスト配分 ✅ requestsはスケジューリングコミットメントを占有する — Podがアイドルでも、そのリソースは他のPodに割り当てられない。コスト配分はactual usageではなくrequestsに基づくべき。


10のエラートラブルシューティング

エラー症状 考えられる原因 デバッグコマンド 解決策
Pod作成時Forbidden exceeded quota ネームスペースクォータ枯渇 kubectl describe quota -n <ns> クォータを増やすか既存Podリソースを削減
PodステータスPending ネームスペースクォータ不足またはノードリソース不足 kubectl describe pod <pod> クォータとノードの利用可能リソースを確認
LimitRange注入後PodがOOM default値が高すぎる/低すぎる kubectl get limitrange -n <ns> -o yaml defaultとdefaultRequestを調整
ResourceQuotaが有効にならない scopesがPodタイプと一致しない kubectl describe quota -n <ns> scopesがPodタイプを含むか確認
BestEffort Podがリソースを使い果たす LimitRangeが未設定 kubectl get limitrange -A 全テナントネームスペースにLimitRangeを作成
高優先度Podがプリエンプトできない preemptionPolicyがNeverに設定 kubectl get priorityclass -o yaml preemptionPolicyを変更
ネームスペースクォータのオーバーコミット NSクォータ合計がクラスタ容量を超過 kubectl get quota -A 20%バッファを確保、クォータ合計≤クラスタの80%
コスト配分データが欠落 ネームスペースにtenantラベルがない kubectl get ns --show-labels 全テナントNSにtenantラベルを追加
Pod退去後に再スケジュールできない 実行中Podより優先度が低い kubectl describe pod <pod> PriorityClassを上げるかリソースを解放
LimitRange maxLimitRequestRatioエラー limit/request比率が制限超過 kubectl describe limitrange -n <ns> Podのlimit/request比率を調整

高度な最適化テクニック

1. 階層クォータ管理。K8s Hierarchical Namespace Controllerを使用して親子ネームスペース間のクォータ継承を実現。親NSクォータが自動的に子NSに分配され、手動クォータ割り当てを回避。

2. 動的クォータ調整。Prometheusのkube_resourcequota_usageメトリクスに基づき、使用率が継続的に85%を超える場合にクォータを自動拡張し、30%未満の場合に縮小を推奨。

3. Policy as Code。OPA/Kyvernoと組み合わせてクォータポリシーの自動監査を実現 — リソース仕様を満たさないDeploymentの作成を拒否し、リソース乱用を根源的に排除。

4. マルチクラスタクォータ連携。Karmada/KubeFedを使用してクラスタ間クォータスケジューリングを実現 — 単一クラスタのクォータ不足時に、容量のあるクラスタに自動スケジュール。

5. FinOpsダッシュボード。Prometheus+Grafanaでテナントコストダッシュボードを構築し、各チームのリアルタイムリソース使用量とコストトレンドを表示し、コスト意識文化を推進。


比較:K8sネイティブ vs OPA vs Kyverno vs カスタムController

特徴 K8sネイティブResourceQuota OpenPolicyAgent Kyverno カスタムController
学習コスト ⭐ 低い ⭐⭐⭐ 高い ⭐⭐ 中程度 ⭐⭐⭐ 高い
クォータ制限 ✅ ネイティブサポート ⚠️ カスタムポリシー必要 ✅ Policy経由でサポート ✅ 完全カスタマイズ
デフォルト値注入 ✅ LimitRange ⚠️ Mutation必要 ✅ Mutation Policy ✅ 完全カスタマイズ
ポリシー柔軟性 ⭐⭐ 限定的 ⭐⭐⭐⭐⭐ 非常に高い ⭐⭐⭐⭐ 高い ⭐⭐⭐⭐⭐ 非常に高い
コスト計量 ❌ サポートなし ⚠️ 統合必要 ⚠️ 統合必要 ✅ 内蔵可能
運用複雑さ ⭐ 低い ⭐⭐⭐⭐ 高い ⭐⭐⭐ 中程度 ⭐⭐⭐⭐ 高い
プロダクション成熟度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
推奨シナリオ 基本クォータ分離 複雑ポリシーコンプライアンス ポリシー+Mutation カスタムガバナンス

まとめ

K8sマルチテナントリソースガバナンスはResourceQuotaを設定して終わりではない。ResourceQuotaで総量を制限、LimitRangeで個体を制約、QoSクラスで保障、PriorityClassで優先スケジューリング、コスト計量で配分、自動化Controllerでセーフティネットという6つの統合システムである。6つの重要プラクティスはクォータ定義からコスト配分までの完全なガバナンスチェーンをカバーする。覚えておくべきは:クォータは必須、制限は必須、優先度は分離必須、コストは計算必須 — これが真のマルチテナントリソース分離の鍵である。将来的には、AIベースのインテリジェントクォータ推奨とサーバーレスリソーススケジューリングがガバナンスの複雑さをさらに削減するだろう。


おすすめツール


参考資料

ブラウザローカルツールを無料で試す →

#K8s资源配额#ResourceQuota#LimitRange#多租户#Go#2026#云原生