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はリソース未設定のコンテナにdefaultとdefaultRequestを自動注入し、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ベースのインテリジェントクォータ推奨とサーバーレスリソーススケジューリングがガバナンスの複雑さをさらに削減するだろう。
おすすめツール
- JSONフォーマッター — ResourceQuota/LimitRangeのYAML/JSON設定をフォーマット、クォータ定義の問題を素早く特定
- YAML to JSONコンバーター — K8s YAML設定をJSONに変換、プログラム的クォータポリシー処理に便利
- ハッシュ計算ツール — ConfigMapとSecretのチェックサムを計算、クォータ設定データの整合性を確保
参考資料
ブラウザローカルツールを無料で試す →