Привет! Меня зовут Роман Христевич, я DevOps-инженер в Cloud.ru, занимаюсь плагинами и безопасностью для кластеров Kubernetes. Сегодня расскажу, как я сражался с ветряными мельницами — мусором в кластерах Kubernetes после тестирования релизов, рандомных Helm Chart и просто в автотестах. И как я эти мельницы все-таки одолел.
Если вы DevOps, SRE или системный администратор, то вам не раз и не два приходилось вычищать кластеры от отработанных Jobs в статусе Complete или Configmaps с Secrets, которые создали руками для уже раскатанного Helm Chart. Разовая акция не вызывает особых забот, а вот если это надо делать несколько раз в неделю, это уже проблема или даже боль.
Под катом я расскажу, как прошел путь, усеянный граблями, от самописных решений до K8s-cleaner, и почему я теперь доволен. Если ты еще не прошел мой путь, приглашаю под кат!

Пара слов о мусорных кластерах ↓
Что это вообще за мусорные кластеры? Откуда они берутся и почему, как постоянно больное колено, не дают спокойно жить? Расскажу в нескольких словах.
Представьте себе кластер Kubernetes, где проводят тесты с релизами в состоянии разработки, или же QA все проверяют вдоль и поперек. Вот уже сбилдили образ с новой фичей, Helm Chart подготовлен, приложение раскатано. Но ему для работы почему-то нужен Configmaps или Secrets, которого нет.
Прогонять весь CI — это долго и муторно. Проще быстро написать yaml, применить его и посмотреть результат. Дальше Helm Chart релиз удаляется, а вот ресурсы, которые добавлялись, минуя Helm Chart при помощи kubectl apply, никуда не денутся, так как к самому релизу отношения не имеют. И при следующим деплое из Helm Chart мы получим закономерную ошибку, что такой ресурс уже такой есть.
Или же QA написал Job для автотестов, которые не удаляются после выполнения и через некоторое время мы увидим орду с лозунгом Complete.
Все это явно не приносит радости ни команде, ни инженеру, который обслуживает этот кластер. Проблему надо решать.
Что такое K8s-cleaner и как я к нему пришел
Я не стал исключением, и данная проблема настигла меня на «коммунальных» и QA-кластерах. После непродолжительных раздумий я написал несколько CronJob, которые решали типовые проблемы. Как показала практика, эта идея была обречена на провал. Менялись требования и условия, появлялись новые «осиротевшие» ресурсы, и уже очень скоро несколько CronJobs превратились в небольшую орду и стали копиться как снежный ком. Управлять ими оказалось проблематично, вернее, столь же трудозатратно, как и просто вычищать кластер от мусора руками.

Лечение оказалось чуть лучше болезни. Нужно было искать другое решение, желательно готовое, которое будет поддерживать комьюнити. Через неделю поиска, проб и ошибок я наткнулся на K8s-cleaner. После его тестирования я понял: это то, что мне нужно. А может, и не только мне.
Что это за зверь? Описание в репозитории говорит, что K8s-cleaner — это контроллер Kubernetes, который выявляет неиспользуемые или неисправные ресурсы, помогая поддерживать оптимизированный и эффективный кластер Kubernetes. Он обеспечивает гибкое планирование, фильтрацию по меткам, критерии выбора на основе Lua, удаление или обновление ресурсов, а также уведомления через Slack, Webex и Discord. Также он может автоматизировать работу кластеров.
Если простыми словами, то K8s-cleaner — движок, который позволит создавать CRD, чтобы они подчищали весь мусор в вашем кластере. Под капотом у них есть Lua-скрипты, которые и будут выполнять всю рутину за нас.
Сейчас вас, как и меня в момент тестирования этого решения, может посетить вопрос: «А есть ли готовые CRD и где их взять»? Да, готовый джентельменский набор есть. Вот он:
https://github.com/gianlucam76/k8s-cleaner/tree/main/examples-automated-operations
https://github.com/gianlucam76/k8s-cleaner/tree/main/examples-unhealthy-resources
https://github.com/gianlucam76/k8s-cleaner/tree/main/examples-unused-resources
А что делать, если подходящего CRD нет? Придется выбрать один из трех путей: попросить коллегу, который знает Lua; научиться самому; обратиться за помощью к бездушному AI. Но мы обойдемся без этого и возьмем CRD из 2-го и 3-го пунктов. В самом начале этого будет достаточно.
Набор CRD, чтобы не сломать меч
Какие CRD мы сразу сможем использовать, чтобы облегчить нам и команде жизнь? На самом деле любые, это зависит от ваших потребностей. Но тут я поделюсь своим выбором.
CRD |
Зачем нужно |
pods-with-expired-certificates.yaml |
Найдет и подскажет, какие Pods используют «протухший» сертификат. |
pod-with-error-state.yml |
Удалит Pods в статусе Failed. Должны иметь хотя бы один контейнер, завершившийся с Error и кодом выхода (exitCode) ≠ 0. |
too-many-restarts.yaml |
«Выключит» Deployment, если его Pod перезапускались более 50 раз. |
orphaned_secrets.yaml |
Обнаруживает и удаляет Secret, которые не используются ни в каких компонентах кластера. |
completed_pods.yaml |
Обнаруживает и удаляет Pods, которые успешно завершили свою работу. |
unused_pod-disruption-budgets.yaml |
Найдет и удалит устаревшие Pod (PDB). Устаревшими они считаются, если их селекторы не соответствуют ни одному Deployment/StatefulSet. |
unused_persistent-volume-claims.yaml |
Нашелся бесхозный PVC? На удаление его. |
unbound_persistent-volumes.yaml |
Если найдется PV в состоянии Bound, то он будет удален. |
long-running-pods.yaml |
Найдет и удалит Pods, которые были созданы Jobs и работают дольше одного часа. Но! Надо быть внимательным: убедитесь, что длительная работа не была задумана изначально, иначе можно убить, например, миграцию. |
completed_jobs.yaml |
Тут все просто. Jobs отработала – Jobs должна умереть. |
orphaned_deployment.yaml |
А тут уже чуть сложнее: находятся и удаляются «осиротевшие» Deployments. Deployments считаются таковыми, если нет связанных Pod и Service. |
deployment_with_replica_zero.yaml |
Предназначен для автоматического поиска и удаления Deployments с нулевым количеством реплик. |
orphaned_configmaps.yaml |
Находит и удаляет неиспользуемые ConfigMaps. Как CRD понимает, что они не используются? По нескольким критериям: ConfigMaps не монтируется как volume в Pod, не используется в environment variables контейнеров, не используется в initContainers, не находится в системных namespace (игнорирует namespace с префиксом kube). |
statefulset_with_no_replicas.yaml |
Тут тоже все просто: нашелся StatefulSet с нулевым количеством реплик — на удаление. |
Приводить код этих CRD не буду, просто еще раз напомню, что они лежат на Gihub: тут и тут.
Для полной ясности давайте рассмотрим под микроскопом один CRD: из чего он состоит и как работает. Остальные мало чем отличаются, и если разберетесь в одном, то разберетесь со всеми.
Препарировать будем completed_jobs.yaml.
---
# Общее описание.
# Создается объект типа Cleaner, который будет называться completed-jobs. Он предназначен для периодической очистки завершенных задач (Job) в кластере.
apiVersion: apps.projectsveltos.io/v1alpha1
kind: Cleaner
metadata:
name: completed-jobs
# Расписание выполнения.
# Поле schedule задает cron-выражение, по которому будет запускаться процесс очистки.
# В данном случае каждый день в 00:00 (полночь).
spec:
schedule: "* 0 * * *"
# Что и как ищется.
# Здесь определяется, какие ресурсы нужно удалять:
# 1. Ищутся ресурсы типа Job из API-группы batch/v1.
# 2. Для каждого такого ресурса выполняется Lua-функция evaluate(), которая проверяет:
# a. Есть ли у задачи статус (status).
# b. Успешно ли она завершена (completionTime != nil и succeeded > 0).
# Если оба условия выполнены, задача считается подходящей для удаления.
resourcePolicySet:
resourceSelectors:
- kind: Job
group: "batch"
version: v1
evaluate: |
function evaluate()
hs = {}
hs.matching = false
if obj.status ~= nil then
if obj.status.completionTime ~= nil and obj.status.succeeded > 0 then
hs.matching = true
end
end
return hs
end
# Действие после выборки.
# Для всех найденных ресурсов (завершенных Job'ов) будет выполнено действие — удаление.
action: Delete
```
Планируем очистку и проверяем корректность
После короткого знакомства с K8s-cleaner перейдем к самому интересному — практике. Для начала нам понадобится сам кластер. Подойдет любой, который есть под рукой. Я же для своего удобства буду использовать облачные кластеры в Cloud.ru, поэтому сначала выполню три простых шага:
Зарегистрируюсь в Cloud.ru Evolution. Если личный кабинет уже есть, достаточно просто зайти.
Закажу себе кластер Kubernetes.
Установлю туда сам K8s-cleaner.
Если у вас есть кластер в другом месте, выполните один из двух шагов ниже:
1. Этот вариант поможет скачать и применить Helm Chart без скачивания оного руками.
helm install k8s-cleaner oci://ghcr.io/gianlucam76/charts/k8s-cleaner \
--version 0.18.0 \
--namespace k8s-cleaner \
--create-namespace
2. А этот позволит самостоятельно скачать Helm Chart с GitHub и при необходимости отредактировать его. Скачайте, перейдите в директорию и просто выполните команду.
helm install k8s-cleaner . -n k8s-cleaner --create-namespace
Любой из этих вариантов позволит получить свежую версию K8s-cleaner, и работать они будут одинаково хорошо. Заодно сразу отвечу на очевидный вопрос: когда я начинал разбираться с K8s-cleaner, его еще не было среди плагинов в Cloud.ru. Он появился только после успешного использования внутри команды.
После установки плагина давайте проверим состояние его Pods.
kubectl get pods -n k8s-cleaner
Если мы увидели аналогичный ответ, то приступаем к добавлению необходимых нам заданий.
NAME READY STATUS RESTARTS AGE
k8s-cleaner-5f887cb4dc-vcxh9 1/1 Running 0 8m54s
Следом применяем нужные нам ресурсы Cleaner.
kubectl apply -f pods-with-expired-certificates.yaml pod-with-error-state.yml too-many-restarts.yaml orphaned_secrets.yaml completed_pods.yaml unused_pod-disruption-budgets.yaml unused_persistent-volume-claims.yaml unbound_persistent-volumes.yaml long-running-pods.yaml completed_jobs.yaml orphaned_deployment.yaml deployment_with_replica_zero.yaml orphaned_configmaps.yaml statefulset_with_no_replicas.yaml -n k8s-cleaner
И проверим, что они успешно применены.
kubectl get cleaner -n k8s-cleaner
Ответ мы должны получить примерно такой.
NAME AGE
completed-jobs 117s
completed-pods 109s
…
А теперь давайте проверим, как это все будет работать. Приводить пример работы каждого CRD я не буду, но один конкретный рассмотрим. Для этого создадим Job, которая после выполнения не удалится и оставит после себя Pod в состоянии Complete, и назовем ее job_for_test.yaml.
---
apiVersion: batch/v1
kind: Job
metadata:
name: persistent-success-job
spec:
backoffLimit: 0
ttlSecondsAfterFinished: null
completions: 1
parallelism: 1
template:
spec:
containers:
- name: job-container
image: alpine:latest
command:
- /bin/sh
- -c
- |
echo "Job started"
echo "Working..."
sleep 10
echo "Work completed successfully"
exit 0
restartPolicy: OnFailure
Применяем Job.
kubectl apply -f job_for_test.yaml
Ждем некоторое время и проверяем состояние.
kubectl get jobs
NAME STATUS COMPLETIONS DURATION AGE
persistent-success-job Complete 1/1 17s 4m55s
Осталось подождать, когда отработает планировщик, и проверить заново.
kubectl get jobs
No resources found in default namespace.
Мы видим, что Job отработала и осталась в состоянии Complete, но через некоторое время была удалена. Что, несомненно, радует.

Что в итоге
В итоге хочу сразу же сказать, что K8s-cleaner — не серебряная пуля, которая после helm install решит все ваши проблемы по щелчку пальцев. Это скорее швейцарский нож, который при должном умении будет вам верным другом. Вторым пунктом снова хочу предупредить, что на продовых кластерах нужно быть очень аккуратным. Если применять CRD, не понимая точно, что он должен делать, то однажды можете лишиться нужных ресурсов. Например, Job, которая применяет миграцию на БД в течение нескольких часов, а вы ее придушили уже через час.
В будущем планирую продолжить исследование возможностей K8s-cleaner и других инструментов, чтобы облегчить сопровождение кластеров Kubernetes. Также буду рад услышать о вашем опыте и проблемах, связанных с поддержанием чистоты и сопровождения в Kubernetes-окружении.
Спасибо за внимание, и надеюсь, кому-то статья спасла несколько вечеров для поиска решения аналогичной проблемы!
Комментарии (2)

Cib0rg
21.11.2025 03:19Задумка хорошая, но это костыль. Вместо ограничения по количеству хранимых джобов, которое есть в самой спеке, например, вы тащите сторонний инструмент или обмазываетесь скриптами.
С ручным созданием манифестов при наличии хелма вообще не ясно - это системная проблема, которую вы вообще не решаете, подпирая тем же костылем.
Короче, инструмент вроде интересный - для тех, у кого проблемы с освоением основного тулсета.
sm1ly
попробуйте argocd или козий стек, там это все под капотом есть