распределять расходы кластера по командам и автоматизировать ограничения по затратам

FinOps для облачно-нативных платформ: от видимости до автоматизированного управления затратами

14 минут

В общем кластере Kubernetes непонятно, кто и сколько тратит, пока финотдел не получит счёт. FinOps связывает потребление пространств имён с деньгами, убирает лишние расходы за счёт корректного размера ресурсов и автоматизирует квоты и оповещения без замедления поставки.

Почему общие кластеры ломают учёт затрат

Платформенная команда знает, что развёрнуто в кластере, но часто не может ответить, сколько стоит то или иное пространство имён и какая продуктовая команда дала скачок расходов за месяц. В мультитенантном Kubernetes счёт приходит на весь кластер, пул нод или облачный аккаунт, а отдельные команды теряют связь между своими сервисами и цифрами в биллинге. Финансы видят одну сумму, инженеры не объясняют разницу между предпродакшеном и продакшеном или между арендаторами. Завышенные запросы CPU и памяти у подов мешают плотно размещать нагрузку на нодах и раздувают их число. Неиспользуемые тома PVC, балансировщики LoadBalancer без трафика и полупустые пулы нод тихо съедают бюджет. Разовые «оптимизации» отдельных команд без согласования бьют по общим сервисам. Без выстроенной практики FinOps организация либо переплачивает из осторожности, либо недодаёт ресурсов и ловит инциденты. FinOps — это общий язык затрат для инженерии, финансов и эксплуатации и три непрерывных цикла: сначала видимость, затем оптимизация, затем автоматический контроль.

Видимость затрат: связать Kubernetes с деньгами через Kubecost или OpenCost

Первый шаг — показать, сколько стоят CPU, память, диски и сеть в разрезе пространств имён, меток, развёртываний и контейнеров. Установите Kubecost или проект CNCF OpenCost в каждом кластере. Подключите выгрузку облачного биллинга — AWS CUR, GCP BigQuery billing, Azure Cost Management — чтобы в отчётах для команд были не только метрики из кластера, но и плоскость управления, отцепленные диски и исходящий трафик. На каждое развёртывание повесьте единые метки team, service и environment и сопоставьте их с категориями затрат в конфигурации Kubecost. Если кластеров несколько, на каждом держите OpenCost и собирайте его метрики в центральный Prometheus или систему удалённой записи метрик. Покажите командам разбивку расходов до того, как вводить жёсткие квоты — иначе ограничения воспринимают как необоснованные.

YAML · Kubecost Helm values для распределения по меткам
global:
  clusterId: production-us-east-1

kubecostProductConfigs:
  clusterName: production-us-east-1
  labelMappingConfigs:
    enabled: true
    productLabelList:
      - team
      - app
      - environment

# Enable cloud billing integration in Kubecost UI for AWS/GCP/Azure
# so node, disk, and egress costs reconcile with CUR or billing export
YAML · Prometheus scrape для OpenCost exporter
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: production-us-east-1

Оптимизация: подобрать размер подов и убрать мёртвую ёмкость

Отчёты сами по себе деньги не экономят. Чаще всего CPU запрашивают в три–пять раз больше нужного — так проще пройти планирование подов, но дороже содержать кластер. Поставьте Vertical Pod Autoscaler в режим только рекомендаций, раз в неделю сверяйте советы с реальной нагрузкой и меняйте requests осознанно. При плавающем трафике дополняйте VPA горизонтальным автомасштабированием. Регулярно ищите тома PVC, к которым не подключён ни один под, сервисы LoadBalancer без конечных точек и пулы нод, загруженные меньше чем на пятнадцать процентов дольше недели. Сжимайте простой через cluster autoscaler или консолидацию нод в Karpenter. Пакетные задачи выносите на прерываемые spot-ноды, если приложение переживает внезапное выселение. Только за счёт корректного размера ресурсов команды нередко снижают расходы на вычисления на тридцать–пятьдесят процентов — ещё до архитектурных переделок.

YAML · VPA только с рекомендациями
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
  namespace: team-alpha
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: "*"
        minAllowed:
          cpu: 50m
          memory: 64Mi
        maxAllowed:
          cpu: 4000m
          memory: 8Gi
        controlledResources: ["cpu", "memory"]
Bash · PVC без ссылающегося pod
#!/usr/bin/env bash
set -euo pipefail

echo "=== PVCs with no mounting pod ==="
kubectl get pvc -A -o json | jq -r '
  .items[] | [.metadata.namespace, .metadata.name] | @tsv
' | while IFS=$'\t' read -r ns name; do
  in_use=$(kubectl get pods -n "$ns" -o json | jq --arg n "$name" '
    [.items[].spec.volumes[]? | select(.persistentVolumeClaim.claimName == $n)] | length
  ')
  if [[ "$in_use" -eq 0 ]]; then
    echo "orphan: ${ns}/${name}"
  fi
done

echo "=== LoadBalancer services ==="
kubectl get svc -A -o json | jq -r '
  .items[]
  | select(.spec.type == "LoadBalancer")
  | "\(.metadata.namespace)/\(.metadata.name) -> \(.status.loadBalancer.ingress[0].hostname // "pending")"
'

Контроль: квоты, политики admission и регулярные отчёты

На финальном этапе правила срабатывают сами, без ручных «проверок каждого деплоя». ResourceQuota ограничивает суммарное потребление пространства имён: CPU, память, число PVC и балансировщиков. LimitRange задаёт разумные значения по умолчанию, чтобы в манифесте нельзя было забыть блок resources. Kyverno или OPA отклоняют поды с запросами выше потолка команды — это лимит по ресурсам, не по рублям или долларам; денежные пороги закрывайте бюджетными алертами Kubecost. Сначала шлите предупреждение в Slack при восьмидесяти процентах месячного плана, жёсткие квоты вводите после двух–трёх полных биллинговых циклов, когда команды успели подстроиться. Раз в неделю выгружайте топ пространств имён по затратам заинтересованным сторонам. Резкий рост стоимости одного namespace в три раза трактуйте так же серьёзно, как скачок задержки или ошибок — заведите такой сигнал в ту же систему оповещений.

YAML · ResourceQuota и LimitRange для namespace
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-alpha-quota
  namespace: team-alpha
spec:
  hard:
    requests.cpu: "40"
    requests.memory: 80Gi
    limits.cpu: "80"
    limits.memory: 160Gi
    persistentvolumeclaims: "20"
    pods: "100"
    services.loadbalancers: "2"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: team-alpha-defaults
  namespace: team-alpha
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      default:
        cpu: 500m
        memory: 512Mi
      max:
        cpu: "4"
        memory: 8Gi
YAML · Kyverno: потолок requests контейнера
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: cap-container-requests
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: limit-cpu-memory-requests
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Container requests exceed team ceiling (cpu 4, memory 8Gi)"
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "<=4"
                    memory: "<=8Gi"
YAML · CronJob еженедельного экспорта Kubecost
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cost-report
  namespace: kubecost
spec:
  schedule: "0 9 * * 1"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: cost-export
              image: curlimages/curl:8.5.0
              env:
                - name: SLACK_WEBHOOK_URL
                  valueFrom:
                    secretKeyRef:
                      name: slack-webhook
                      key: url
              command:
                - /bin/sh
                - -c
                - |
                  set -euo pipefail
                  report=$(curl -fsS \
                    "http://kubecost-cost-analyzer.kubecost:9090/model/allocation?window=7d&aggregate=namespace&accumulate=true" \
                    | jq -r 'to_entries | sort_by(.value.totalCost) | reverse | .[0:5][] | "\(.key): $\(.value.totalCost | floor)"')
                  payload=$(jq -n --arg text "Weekly top namespaces (7d):\n$report" '{text: $text}')
                  curl -fsS -X POST "$SLACK_WEBHOOK_URL" -H 'Content-Type: application/json' -d "$payload"

Практические правила: общие расходы, проверки в CI и ежемесячный разбор

Начинайте с прозрачности, а не с запретов — люди сопротивляются правилам, смысл которых не видели на цифрах. Пропишите, как делите общие траты: плоскость управления кластера, входящий трафик и мониторинг можно распределять по доле CPU пространств имён, по числу запросов или фиксированной плате с команды. Встройте проверки в конвейер CI и admission-контроллер, а не только в дашборд: пайплайн, который не пропустит развёртывание без requests и limits, полезнее графика, который никто не открывает. Смотрите не только на сумму по кластеру, а на удельную стоимость — запроса, транзакции, активного пользователя. Аномалии в расходах ставьте рядом с SLO по задержке и ошибкам в одной системе наблюдаемости. Раз в месяц собирайте инженерных лидов на полчаса: пять крупнейших отклонений, три кандидата на уменьшение ресурсов, одна архитектурная задача на следующий цикл. FinOps — это постоянная обратная связь между ценой, скоростью релизов и надёжностью, а не разовая кампания «срезать облако».

Лёгкие привычки учёта затрат без потери скорости разработки разобраны в гайде по контролю облачных расходов без замедления инженерии.

Тегирование на уровне облачного аккаунта и стратегия резервирования мощностей в AWS, GCP и Azure опираются на пошаговое руководство по оптимизации затрат в нескольких облаках.