Удаляем устаревшую feature branch в Kubernetes кластере


    Привет! Feature branch (aka deploy preview, review app) — это когда деплоится не только master ветка, но и каждый pull request на уникальный URL. Можно проверить работает ли код в production-окружении, фичу можно показать другим программистам или продуктологам. Пока вы работаете в pull request'е, каждый новый commit текущий deploy для старого кода удаляется, а новый deploy для нового кода выкатывается. Вопросы могут возникнуть тогда, когда вы смерджили pull request в master ветку. Feature branch вам больше не нужна, но ресурсы Kubernetes все еще находятся в кластере.


    Еще про feature branch'и


    Один из подходов как сделать feature branch'и в Kubernetes — использовать namespace'ы. Если кратко, production конфигурации выглядит так:


    kind: Namespace
    apiVersion: v1
    metadata:
      name: habr-back-end
    ...
    
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      namespace: habr-back-end
    spec:
      replicas: 3
    ...

    Для feature branch создается namespace c ее идентификатором (например, номер pull request'а) и каким-то префиксом/постфиксом (например, -pr-):


    kind: Namespace
    apiVersion: v1
    metadata:
      name: habr-back-end-pr-17
    ...
    
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      namespace: habr-back-end-pr-17
    spec:
      replicas: 1
    ...

    В общем, я написал Kubernetes Operator (приложение, которое имеет доступ к ресурсам кластера), ссылка на проект на Github. Он удаляет namespace'ы, которые относятся к старым feature branch'ам. В Kubernetes, если удалить namespace, другие ресурсы в этом namespace также удаляются автоматически.


    $ kubectl get pods --all-namespaces | grep -e "-pr-"
    NAMESPACE            ... AGE
    habr-back-end-pr-264 ... 4d8h
    habr-back-end-pr-265 ... 5d7h

    Про то как внедрить feature branch'и в кластер, можно почитать тут и тут.


    Мотивация


    Давайте посмотрим на типичный жизненный цикл pull request'a с непрерывной интеграцией (continuous integration):


    1. Пушим новый commit в ветку.
    2. На билде, запускаются линтеры и/или тесты.
    3. На лету формируются конфигурации Kubernetes pull request'a (например, в готовый шаблон подставляется его номер).
    4. С помощью kubectl apply конфигурации попадают в кластер (deploy).
    5. Pull request сливается в master ветку.

    Пока вы работаете в pull request'е, каждый новый commit текущий deploy для старого кода удаляется, а новый deploy для нового кода выкатывается. Но когда pull request сливается в master ветку, будет билдится только master ветка. В итоге получается, что про pull request мы уже забыли, а его Kubernetes ресурсы все еще находятся в кластере.


    Как использовать


    Установить проект командой ниже:


    $ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/configs/production.yml

    Создать файл со следующим содержанием и установить через kubectl apply -f:


    apiVersion: feature-branch.dmytrostriletskyi.com/v1
    kind: StaleFeatureBranch
    metadata:
      name: stale-feature-branch
    spec:
      namespaceSubstring: -pr-
      afterDaysWithoutDeploy: 3

    Параметр namespaceSubstring нужен, чтобы отфильтровать namespace'ы для pull request'ов от других namespace'ов. Например, если в кластере есть следующие namespace'ы: habr-back-end, habr-front-end, habr-back-end-pr-17, habr-back-end-pr-33, тогда кандидатами на удаление будут habr-back-end-pr-17, habr-back-end-pr-33.


    Параметр afterDaysWithoutDeploy нужен чтобы, удалять старые namespace'ы. Например, если namespace создан 3 дня 1 час назад, а в параметре указано 3 дня, этот namespace будет удален. Работает и в обратную сторону, если namespace создан 2 дня 23 часа назад, а в параметре указано 3 дня, этот namespace не будет удален.


    Есть еще один параметр, он отвечает за то как часто сканировать все namespace'ы и проверять на дни без deploy'я — checkEveryMinutes. По умолчанию он равен 30 минутам.


    Как это работает


    На практике, понадобится:


    1. Docker для работы в изолированном окружении.
    2. Minikube поднимет Kubernetes кластер локально.
    3. kubectl — интерфейс командной строки для управления кластером.

    Поднимаем Kubernetes кластер локально:


    $ minikube start --vm-driver=docker
    minikube v1.11.0 on Darwin 10.15.5
    Using the docker driver based on existing profile.
    Starting control plane node minikube in cluster minikube.

    Указываем kubectl использовать локальный кластер по умолчанию:


    $ kubectl config use-context minikube
    Switched to context "minikube".

    Скачиваем конфигурации для production-среды:


    $ curl https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/configs/production.yml > stale-feature-branch-production-configs.yml

    Так как production конфигурации настроены проверять старые namespace'ы, а в нашем ново поднятом кластере их нет, заменим переменную окружения IS_DEBUG на true. При таком значении параметр afterDaysWithoutDeploy не учитывается и namespace'ы не проверяются на дни без deploy'я, только на вхождение подстроки (-pr-).


    Если вы на Linux:


    $ sed -i 's|false|true|g' stale-feature-branch-production-configs.yml

    Если вы на macOS:


    $ sed -i "" 's|false|true|g' stale-feature-branch-production-configs.yml

    Устанавливаем проект:


    $ kubectl apply -f stale-feature-branch-production-configs.yml

    Проверяем, что в кластере появился ресурс StaleFeatureBranch:


    $ kubectl api-resources | grep stalefeaturebranches
    NAME                 ... APIGROUP                             ... KIND
    stalefeaturebranches ... feature-branch.dmytrostriletskyi.com ... StaleFeatureBranch

    Проверяем, что в кластере появился оператор:


    $ kubectl get pods --namespace stale-feature-branch-operator
    NAME                                           ... STATUS  ... AGE
    stale-feature-branch-operator-6bfbfd4df8-m7sch ... Running ... 38s

    Если заглянуть в его логи, он готов обрабатывать ресурсы StaleFeatureBranch:


    $ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
    ... "msg":"Operator Version: 0.0.1"}
    ...
    ... "msg":"Starting EventSource", ... , "source":"kind source: /, Kind="}
    ... "msg":"Starting Controller", ...}
    ... "msg":"Starting workers", ..., "worker count":1}

    Устанавливаем готовые fixtures (готовые конфигурации для моделирования ресурсов кластера) для ресурса StaleFeatureBranch:


    $ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/stale-feature-branch.yml

    В конфигурациях указано искать namespace'ы с подстрокой -pr- раз в 1 минуту.:


    apiVersion: feature-branch.dmytrostriletskyi.com/v1
    kind: StaleFeatureBranch
    metadata:
      name: stale-feature-branch
    spec:
      namespaceSubstring: -pr-
      afterDaysWithoutDeploy: 1 
      checkEveryMinutes: 1

    Оператор отреагировал и готов проверять namespace'ы:


    $ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
    ... "msg":"Stale feature branch is being processing.","namespaceSubstring":"-pr-","afterDaysWithoutDeploy":1,"checkEveryMinutes":1,"isDebug":"true"}

    Устанавливаем fixtures, содержащие два namespace'а (project-pr-1, project-pr-2) и их deployments, services, ingress, и так далее:


    $ kubectl apply -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/first-feature-branch.yml -f https://raw.githubusercontent.com/dmytrostriletskyi/stale-feature-branch-operator/master/fixtures/second-feature-branch.yml
    ...
    namespace/project-pr-1 created
    deployment.apps/project-pr-1 created
    service/project-pr-1 created
    horizontalpodautoscaler.autoscaling/project-pr-1 created
    secret/project-pr-1 created
    configmap/project-pr-1 created
    ingress.extensions/project-pr-1 created
    namespace/project-pr-2 created
    deployment.apps/project-pr-2 created
    service/project-pr-2 created
    horizontalpodautoscaler.autoscaling/project-pr-2 created
    secret/project-pr-2 created
    configmap/project-pr-2 created
    ingress.extensions/project-pr-2 created

    Проверяем, что все ресурсы выше успешно созданы:


    $ kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-1 && kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-2
    ...
    NAME                              ... READY ... STATUS  ... AGE
    pod/project-pr-1-848d5fdff6-rpmzw ... 1/1   ... Running ... 67s
    
    NAME                         ... READY ... AVAILABLE ... AGE
    deployment.apps/project-pr-1 ... 1/1   ... 1         ... 67s
    ...

    Так как мы включили debug, namespace'ы project-pr-1 и project-pr-2, следовательно и все остальные ресурсы, должны будут сразу удалиться не учитывая параметр afterDaysWithoutDeploy. В логах оператора это видно:


    $ kubectl logs stale-feature-branch-operator-6bfbfd4df8-m7sch -n stale-feature-branch-operator
    ... "msg":"Namespace should be deleted due to debug mode is enabled.","namespaceName":"project-pr-1"}
    ... "msg":"Namespace is being processing.","namespaceName":"project-pr-1","namespaceCreationTimestamp":"2020-06-16 18:43:58 +0300 EEST"}
    ... "msg":"Namespace has been deleted.","namespaceName":"project-pr-1"}
    ... "msg":"Namespace should be deleted due to debug mode is enabled.","namespaceName":"project-pr-2"}
    ... "msg":"Namespace is being processing.","namespaceName":"project-pr-2","namespaceCreationTimestamp":"2020-06-16 18:43:58 +0300 EEST"}
    ... "msg":"Namespace has been deleted.","namespaceName":"project-pr-2"}

    Если проверить наличие ресурсов, они будут в статусе Terminating (процесс удаления) или уже удалены (вывод команды пуст).


    $ kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-1 && kubectl get namespace,pods,deployment,service,horizontalpodautoscaler,configmap,ingress -n project-pr-2
    ...

    Можете повторить процесс создания fixtures несколько раз и убедиться, что они будут удалены в течение минуты.


    Альтернативы


    Что можно сделать вместо оператора, который работает в кластере? Подходов несколько, все они неидеальны (и их недостатки субъективны), и каждый сам решает что лучше всего подойдет на конкретном проекте:


    1. Удалять feature branch во время билда непрерывной интеграции master ветки.


      • Для этого надо знать какой pull request относится к commit'у, который билдится. Так как feature branch namespace содержит в себе идентификатор pull request'a — его номер, или название ветки, идентификатор всегда придется указывать в commit'e.
      • Билды master веток фейлятся. Например, у вас следующие этапы: скачать проект, запустить тесты, собрать проект, сделать релиз, отправить уведомления, очистить feature branch последнего pull request'a. Если билд сфейлится на отправке уведомления, вам придется удалять все ресурсы в кластере руками.
      • Без должного контекста, удаление feature branch'и в master билде неочевидно.

    2. Использование webhook'ов (пример).


      • Возможно, это не ваш подход. Например, в Jenkins, только один вид пайплайна поддерживает возможность сохранять его конфигурации в исходном коде. При использовании webhook'ов нужно написать свой скрипт для их обработки. Этот скрипт придется размещать в интерфейсе Jenkins'а, что трудно поддерживать.

    3. Написать Cronjob и добавить Kubernetes кластер.


      • Затрата времени на написание и поддержку.
      • Оператор уже работает в подобном стиле, задокументирован и поддерживается.


    Спасибо за внимание к статье. Ссылка на проект на Github.

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      0
      Благодарю!
      Полезная штука.
        0

        Jenkins-X делал примерно похожие вещи через CronJob, сверяя существующие Environments (CRD) с PR и если последний закрыт, то удаляли Helm release и namespace.


        Возможно ли добавить поддержку Helm в такой оператор?

          0
          Технически — не знаю, не работал с Helm на таком уровне. Но я находил операторы, у которых есть поддержка Helm. Например — github.com/apache/camel-k. В общем, это интересно, я поресерчу больше на этот счет, спасибо! :)
          0
          Технически — не знаю, не работал с Helm на таком уровне. Но я находил операторы, у которых есть поддержка Helm. Например — github.com/apache/camel-k. В общем, это интересно, я поресерчу больше на этот счет, спасибо! :)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое