связать расход namespace с бюджетом и лимитами
FinOps на уровне namespace в Kubernetes: квоты ресурсов, LimitRange и атрибуция затрат по командам
14 минут
Облачный биллинг показывает сумму по кластеру, а не какой namespace сжёг бюджет на неиспользуемые CPU requests. ResourceQuota ограничивает потребление команд, LimitRange задаёт профиль подов, OpenCost или Kubecost замыкают петлю атрибуции — с метками для сверки с финансами.
Почему общий кластер скрывает траты команд
Разговоры о FinOps часто обрываются на панелях биллинга, резервировании и spot. Значительная доля потерь рождается внутри кластера: завышенные requests CPU и памяти, нет учёта по namespace, общие кластеры превращаются в чёрные дыры без владельца ресурса. Без принуждения dev namespace одной команды может зарезервировать шестьдесят процентов планируемой ёмкости, пока продакшен борется за остаток. Симптомы: OOMKill критичных сервисов, руководство не связывает расход с продуктами, разработчики копируют четыре CPU и восемь GiB из staging без проверки, нет обратной связи между запросами и стоимостью. Cost Explorer, GCP Billing и Azure Cost Management дают кластер или пул узлов — но не скажут, что payments-api-dev потратил бюджет на CPU requests, которые так и не выполнялись.
Три слоя: квота, лимиты и атрибуция
Управление внутри Kubernetes складывается из трёх уровней. ResourceQuota ограничивает суммарное потребление namespace — requests и limits CPU и памяти, число PVC, подов, LoadBalancer и объектов Deployment. LimitRange задаёт значения по умолчанию, максимумы и maxLimitRequestRatio для контейнера, чтобы один под не ломал планирование мизерными requests и гигантскими limits. Атрибуция метит namespace полями team, cost-center, environment и product и подключает OpenCost или Kubecost для showback по владельцам. Квоты держат границы, лимиты формируют профиль нагрузки, атрибуция даёт финансам видимость.
ResourceQuota: бюджет namespace на вычисления и объекты
Размещайте ResourceQuota в каждом tenant namespace. Разделяйте requests.cpu и requests.memory — они влияют на планирование — и limits.cpu с limits.memory — потолок на выполнении. Добавьте квоты на число pods, services и deployments, чтобы раздувание namespace не забивало API-сервер и мониторинг. Метки cost-center на namespace и квоте связывают потребление Kubernetes с кодами затрат в финансах.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-payments-quota
namespace: payments-api
labels:
team: payments
cost-center: "cc-1042"
environment: production
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
persistentvolumeclaims: "10"
pods: "30"
services.loadbalancers: "2"
count/deployments.apps: "15"
requests.storage: 200GiLimitRange: значения по умолчанию и границы контейнера
Квоты ограничивают namespace целиком; LimitRange — отдельные контейнеры. default и defaultRequest подставляют CPU и память, если в манифесте пусто — без бесконечных контейнеров и с предсказуемым планированием. max заставляет дробить слишком крупные нагрузки. maxLimitRequestRatio блокирует антипаттерн «крошечный request, огромный limit», который рушит уплотнение на узлах. Тип Pod с max учитывает init-контейнеры и sidecar.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: payments-api
spec:
limits:
- type: Container
default:
cpu: "0.5"
memory: 512Mi
defaultRequest:
cpu: "0.25"
memory: 256Mi
max:
cpu: "2"
memory: 4Gi
min:
cpu: "0.1"
memory: 64Mi
maxLimitRequestRatio:
cpu: "10"
memory: "4"
- type: Pod
max:
cpu: "4"
memory: 8GiАтрибуция затрат: OpenCost и оценки в PromQL
Единообразно метьте каждый namespace, ставьте OpenCost или Kubecost на кластер, собирайте метрики аллокации в Prometheus или FinOps-платформу. Сравнивайте фактическую стоимость утилизации с запасом квоты — найдёте переизбыточно выделенные namespace и команды, выходящие за устойчивый уровень. Подставьте в PromQL ставки ваших узлов; OpenCost сверяется с облачным биллингом при настроенном CUR или экспорте.
scrape_configs:
- job_name: opencost
scrape_interval: 1m
static_configs:
- targets:
- opencost.opencost.svc.cluster.local:9003
metric_relabel_configs:
- source_labels: [__address__]
target_label: cluster
replacement: prod-us-east-1# CPU (пример ставки vCPU-час on-demand)
sum by (namespace) (
rate(container_cpu_usage_seconds_total{
container!="", container!="POD"
}[24h])
) * 730 * 0.0312
# Память (пример ставки GiB-час)
sum by (namespace) (
container_memory_working_set_bytes{
container!="", container!="POD"
}
) / 1024 / 1024 / 1024 * 730 * 0.00388Прогрессивные квоты для dev, staging и production
Масштабируйте бюджет namespace с риском окружения: dev — меньше CPU в requests и жёстче max на контейнер; production — выше квоты, больше подов, разрешённые LoadBalancer и лимит ingress. Одна метка team и cost-center на всех трёх namespace сохраняет атрибуцию, а лимиты различаются.
apiVersion: v1
kind: Namespace
metadata:
name: payments-api-dev
labels:
team: payments
cost-center: "cc-1042"
environment: dev
managed-by: platform-team
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: payments-api-dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "15"
count/deployments.apps: "5"
requests.storage: 50Gi
---
apiVersion: v1
kind: LimitRange
metadata:
name: dev-defaults
namespace: payments-api-dev
spec:
limits:
- type: Container
default:
cpu: "0.25"
memory: 256Mi
defaultRequest:
cpu: "0.1"
memory: 128Mi
max:
cpu: "1"
memory: 2GiapiVersion: v1
kind: Namespace
metadata:
name: payments-api-production
labels:
team: payments
cost-center: "cc-1042"
environment: production
managed-by: platform-team
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: payments-api-production
spec:
hard:
requests.cpu: "16"
requests.memory: 32Gi
limits.cpu: "32"
limits.memory: 64Gi
pods: "50"
count/deployments.apps: "20"
count/ingresses.networking.k8s.io: "10"
services.loadbalancers: "3"
requests.storage: 500Gi
---
apiVersion: v1
kind: LimitRange
metadata:
name: production-defaults
namespace: payments-api-production
spec:
limits:
- type: Container
default:
cpu: "1"
memory: 1Gi
defaultRequest:
cpu: "0.5"
memory: 512Mi
max:
cpu: "4"
memory: 8Gi
maxLimitRequestRatio:
cpu: "8"
memory: "4"Провижининг namespace и квот через Terraform
Ручное создание namespace уходит от политики. Закодируйте team, environment, cost-center и уровни квот в переменных Terraform, ревьюйте в pull request и применяйте через существующий конвейер. Вложенный for_each по командам и окружениям исправляет ошибку, когда список environments первой команды применялся ко всем арендаторам.
variable "teams" {
type = map(object({
cost_center = string
environments = list(string)
quota = map(object({
cpu_request = string
memory_request = string
cpu_limit = string
memory_limit = string
max_pods = string
}))
}))
}
locals {
team_envs = merge([
for team, cfg in var.teams : {
for env in cfg.environments :
"${team}-${env}" => {
team = team
environment = env
cost_center = cfg.cost_center
quota = cfg.quota[env]
}
}
]...)
}
resource "kubernetes_namespace" "team" {
for_each = local.team_envs
metadata {
name = "${each.value.team}-api-${each.value.environment}"
labels = {
team = each.value.team
cost-center = each.value.cost_center
environment = each.value.environment
managed-by = "terraform"
}
}
}
resource "kubernetes_resource_quota" "team" {
for_each = kubernetes_namespace.team
metadata {
name = "${each.value.metadata[0].labels.team}-quota"
namespace = each.value.metadata[0].name
}
spec {
hard = {
"requests.cpu" = local.team_envs[each.key].quota.cpu_request
"requests.memory" = local.team_envs[each.key].quota.memory_request
"limits.cpu" = local.team_envs[each.key].quota.cpu_limit
"limits.memory" = local.team_envs[each.key].quota.memory_limit
"pods" = local.team_envs[each.key].quota.max_pods
}
}
}Практика: сначала defaults, алерты, admission и микс ёмкости
Сначала LimitRange с разумными defaults, через две–четыре недели сбора — ужесточайте квоты. PriorityClass ставит критичные продакшен-поды в очереди планирования выше пакетных задач при нехватке квоты — не обход квоты, а порядок вытеснения. Запросы на увеличение квоты — через тикеты с журналом решений. Алертируйте на восьмидесяти процентах ResourceQuota через kube-state-metrics. Обязательные метки team, cost-center и environment на Namespace — через Kyverno или Gatekeeper. Сочетайте квоты с cluster autoscaler или Karpenter: число узлов следует за одобренными requests — настройте expander и задержки scale-down против простоя. Базовую ёмкость резервируйте под стабильную часть квот; spot — для прерываемых dev и CI. FinOps на уровне namespace — постоянная дисциплина: у каждого цикла CPU и байта памяти есть владелец, строка бюджета и обоснование для бизнеса.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: production-critical
value: 1000000
globalDefault: false
description: "Critical production workloads"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-processing
value: 100
globalDefault: false
description: "Batch jobs and non-critical workloads"groups:
- name: namespace-quota
rules:
- alert: NamespaceQuotaNearLimit
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"apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-namespace-cost-labels
spec:
validationFailureAction: Enforce
rules:
- name: required-cost-labels
match:
any:
- resources:
kinds: [Namespace]
validate:
message: "Namespaces must include team, cost-center, and environment labels."
pattern:
metadata:
labels:
team: "?*"
cost-center: "?*"
environment: "?*"Обзор FinOps на уровне кластера и right-sizing через VPA — в гайде по FinOps для облачно-нативных платформ.
Обязательные метки namespace на admission — по подходу из гайда Policy as Code с Kyverno и Gatekeeper.
