связать расход 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 с кодами затрат в финансах.

YAML · ResourceQuota команды с лимитом объектов
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: 200Gi

LimitRange: значения по умолчанию и границы контейнера

Квоты ограничивают namespace целиком; LimitRange — отдельные контейнеры. default и defaultRequest подставляют CPU и память, если в манифесте пусто — без бесконечных контейнеров и с предсказуемым планированием. max заставляет дробить слишком крупные нагрузки. maxLimitRequestRatio блокирует антипаттерн «крошечный request, огромный limit», который рушит уплотнение на узлах. Тип Pod с max учитывает init-контейнеры и sidecar.

YAML · LimitRange с ограничением соотношения
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 или экспорте.

YAML · scrape Prometheus для OpenCost
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
PromQL · оценка месячных затрат по namespace (подставьте ставки)
# 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 сохраняет атрибуцию, а лимиты различаются.

YAML · dev namespace с квотой и LimitRange
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: 2Gi
YAML · production namespace с повышенными лимитами
apiVersion: 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 первой команды применялся ко всем арендаторам.

HCL · Terraform: namespace и ResourceQuota на команду и окружение
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 и байта памяти есть владелец, строка бюджета и обоснование для бизнеса.

YAML · PriorityClass для критичных и пакетных нагрузок
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"
YAML · алерт Prometheus на потребление квоты
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"
YAML · Kyverno: обязательные метки затрат на Namespace
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.