Kubernetes tips & tricks: особенности выполнения graceful shutdown в NGINX и PHP-FPM

Типовое условие при реализации CI/CD в Kubernetes: приложение должно уметь перед полной остановкой не принимать новые клиентские запросы, а самое главное — успешно завершать уже существующие.







Соблюдение такого условия позволяет достичь нулевого простоя во время деплоя. Однако, даже при использовании очень популярных связок (вроде NGINX и PHP-FPM) можно столкнуться со сложностями, которые приведут к всплеску ошибок при каждом деплое…




Теория. Как живёт pod




Подробно о жизненном цикле pod’а мы уже публиковали эту статью. В контексте рассматриваемой темы нас интересует следующее: в тот момент, когда pod переходит в состояние Terminating, на него перестают отправляться новые запросы (pod удаляется из списка endpoints для сервиса). Таким образом, для избежания простоя во время деплоя, с нашей стороны достаточно решить проблему корректной остановки приложения.




Также следует помнить, что grace period по умолчанию равен 30 секундам: после этого pod будет терминирован и приложение должно успеть обработать все запросы до этого периода. Примечание: хотя любой запрос, который выполняется более 5-10 секунд, уже является проблемным, и graceful shutdown ему уже не поможет…




Чтобы лучше понять, что происходит, когда pod завершает свою работу, достаточно изучить следующую схему:







А1, B1 — Получение изменений о состоянии пода
A2 — Отправление SIGTERM
B2 — Удаление pod’а из endpoints
B3 — Получение изменений (изменился список endpoints)
B4 — Обновление правил iptables




Обратите внимание: удаление endpoint pod’а и посылка SIGTERM происходит не последовательно, а параллельно. А из-за того, что Ingress получает обновленный список Endpoints не сразу, в pod будут отправляться новые запросы от клиентов, что вызовет 500 ошибки во время терминации pod’а (более подробный материал по этому вопросу мы переводили). Решать эту проблему нужно следующими способами:




  • Отправлять в заголовках ответа Connection: close (если это касается HTTP-приложения).



  • Если нет возможности вносить изменения в код, то далее в статье описано решение, которое позволит обработать запросы до конца graceful period.




Теория. Как NGINX и PHP-FPM завершают свои процессы




NGINX




Начнем с NGINX, так как с ним все более-менее очевидно. Погрузившись в теорию, мы узнаем, что у NGINX есть один мастер-процесс и несколько «воркеров» — это дочерние процессы, которые и обрабатывают клиентские запросы. Предусмотрена удобная возможность: с помощью команды nginx -s <SIGNAL> завершать процессы либо в режиме fast shutdown, либо в graceful shutdown. Очевидно, что нас интересует именно последний вариант.




Дальше всё просто: требуется добавить в preStop-хук команду, которая будет посылать сигнал о graceful shutdown. Это можно сделать в Deployment, в блоке контейнера:




       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit




Теперь в момент завершения работы pod’а в логах контейнера NGINX мы увидим следующее:




2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down




И это будет означать то, что нам нужно: NGINX ожидает завершения выполнения запросов, после чего убивает процесс. Впрочем, ниже ещё будет рассмотрена распространенная проблема, из-за которой даже при наличии команды nginx -s quit процесс завершается некорректно.




А на данном этапе с NGINX закончили: как минимум по логам можно понять, что всё работает так, как надо.




Как обстоят дела с PHP-FPM? Как он обрабатывает graceful shutdown? Давайте разбираться.




PHP-FPM




В случае с PHP-FPM информации немного меньше. Если ориентироваться на официальный мануал по PHP-FPM, то в нём будет рассказано, что принимаются следующие POSIX-сигналы:




  1. SIGINTSIGTERM — fast shutdown;



  2. SIGQUIT — graceful shutdown (то, что нам нужно).




Остальные сигналы в данной задаче не требуются, поэтому их разбор опустим. Для корректного завершения процесса понадобится написать следующий preStop-хук:




        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"




На первый взгляд, это всё, что требуется для выполнения graceful shutdown в обоих контейнерах. Тем не менее, задача сложнее, чем кажется. Далее разобраны два случая, в которых graceful shutdown не работал и вызывал кратковременную недоступность проекта во время деплоя.




Практика. Возможные проблемы с graceful shutdown




NGINX




В первую очередь полезно помнить: помимо выполнения команды nginx -s quit есть ещё один этап, на который стоит обратить внимание. Мы сталкивались с проблемой, когда NGINX вместо сигнала SIGQUIT всё равно отправлял SIGTERM, из-за чего запросы не завершались корректно. Похожие случаи можно найти, например, здесь. К сожалению, конкретную причину такого поведения нам установить не удалось: было подозрение на версии NGINX, но оно не подтвердилось. Симптоматика же заключалась в том, что в логах контейнера NGINX наблюдались сообщения «open socket #10 left in connection 5», после чего pod останавливался.




Мы можем наблюдать такую проблему, например, по ответам на нужном нам Ingress’e:







Показатели статус-кодов в момент деплоя




В данном случае мы получаем как раз 503 код ошибки от самого Ingress: он не может обратиться к контейнеру NGINX, так как тот уже недоступен. Если посмотреть в логи контейнера с NGINX, в них — следующее:




[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13




После изменения стоп-сигнала контейнер начинает останавливаться корректно: это подтверждается тем, что больше не наблюдается 503 ошибка.




Если вы встретились с похожей проблемой, есть смысл разобраться, какой стоп-сигнал используется в контейнере и как именно выглядит preStop-хук. Вполне возможно, что причина кроется как раз в этом.




PHP-FPM… и не только




Проблема с PHP-FPM описывается тривиально: он не дожидается завершения дочерних процессов, терминирует их, из-за чего возникают 502-е ошибки во время деплоя и других операций. На bugs.php.net с 2005 года есть несколько сообщений об ошибках (например, здесь и здесь), в которых описывается данная проблема. А вот в логах вы, вероятнее всего, ничего не увидите: PHP-FPM объявит о завершении своего процесса без каких-либо ошибок или сторонних уведомлений.




Стоит уточнить, что сама проблема может в меньшей или большей степени зависеть от самого приложения и не проявляться, например, в мониторинге. Если вы все же столкнетесь с ней, то на ум сначала приходит простой workaround: добавить preStop-хук со sleep(30). Он позволит завершить все запросы, которые были до этого (а новые мы не принимаем, так как pod уже в состоянии Terminating), а по истечении 30 секунд сам pod завершится сигналом SIGTERM.




Получается, что lifecycle для контейнера будет выглядеть следующим образом:




    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"




Однако, из-за указания 30-секундного sleep мы сильно увеличим время деплоя, так как каждый pod будет терминироваться минимум 30 секунд, что плохо. Что с этим можно сделать?




Обратимся к стороне, отвечающей за непосредственное исполнение приложения. В нашем случае это PHP-FPM, который по умолчанию не следит за выполнением своих child-процессов: мастер-процесс терминируется сразу же. Изменить это поведение можно с помощью директивы process_control_timeout, которая указывает временные лимиты для ожидания сигналов от мастера дочерними процессами. Если установить значение в 20 секунд, тем самым покроется большинство запросов, выполняемых в контейнере, и после их завершения мастер-процесс будет остановлен.




С этим знанием вернёмся к нашей последней проблеме. Как уже упоминалось, Kubernetes не является монолитной платформой: на взаимодействие между разными её компонентами требуется некоторое время. Это особенно актуально, когда мы рассматриваем работу Ingress’ов и других смежных компонентов, поскольку из-за такой задержки в момент деплоя легко получить всплеск 500-х ошибок. Например, ошибка может возникать на этапе отправки запроса к upstream’у, но сам «временной лаг» взаимодействия между компонентами довольно короткий — меньше секунды.




Поэтому, в совокупности с уже упомянутой директивой process_control_timeout можно использовать следующую конструкцию для lifecycle:




lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]




В таком случае мы компенсируем задержку командой sleep и сильно не увеличиваем время деплоя: ведь заметна разница между 30 секундами и одной?.. По сути «основную работу» на себя берёт именно process_control_timeout, а lifecycle используется лишь в качестве «подстраховки» на случай лага.




Вообще говоря, описанное поведение и соответствующий workaround касаются не только PHP-FPM. Схожая ситуация может так или иначе возникать при использовании других ЯП/фреймворков. Если не получается другими способами исправить graceful shutdown — например, переписать код так, чтобы приложение корректно обрабатывало сигналы завершения, — можно применить описанный способ. Пусть он не самый красивый, но работает.




Практика. Нагрузочное тестирование для проверки работы pod’а




Нагрузочное тестирование — один из способов проверки, как работает контейнер, поскольку эта процедура приближает к реальным боевым условиям, когда на сайт заходят пользователи. Для тестирования приведённых выше рекомендаций можно воспользоваться Яндекс.Танком: он отлично покрывает все наши потребности. Далее приведены советы и рекомендации по проведению тестирования с наглядным — благодаря графикам Grafana и самого Яндекс.Танка — примером из нашего опыта.




Самое главное здесь — проверять изменения поэтапно. После добавления нового исправления запускайте тестирование и смотрите, изменились ли результаты по сравнению с прошлым запуском. В противном случае будет сложно выявить неэффективные решения, а в перспективе можно и вовсе только навредить (например, увеличить время деплоя).




Другой нюанс — смотрите логи контейнера во время его терминации. Фиксируется ли там информация о graceful shutdown? Есть ли в логах ошибки при обращении к другим ресурсам (например, к соседнему контейнеру PHP-FPM)? Ошибки самого приложения (как в описанном выше случае с NGINX)? Надеюсь, что вводная информация из этой статьи поможет лучше разобраться, что же происходит с контейнером во время его терминирования.




Итак, первый запуск тестирования происходил без lifecycle и без дополнительных директив для сервера приложений (process_control_timeout в PHP-FPM). Целью этого теста было выявление приблизительного количества ошибок (и есть ли они вообще). Также из дополнительной информации следует знать, что среднее время деплоя каждого пода составляло около 5-10 секунд до состояния полной готовности. Результаты таковы:







На информационной панели Яндекс.Танка виден всплеск 502 ошибок, который произошел в момент деплоя и продолжался в среднем до 5 секунд. Предположительно это обрывались существующие запросы к старому pod’у, когда он терминировался. После этого появились 503 ошибки, что стало результатом остановленного контейнера NGINX, который также оборвал соединения из-за бэкенда (из-за чего к нему не смог подключиться Ingress).




Посмотрим, как process_control_timeout в PHP-FPM поможет нам дожидаться завершения child-процессов, т.е. исправить такие ошибки. Повторный деплой уже с использованием этой директивы:







Во время деплоя 500-х ошибок больше нет! Деплой проходит успешно, graceful shutdown работает.




Однако стоит вспомнить момент с Ingress-контейнерами, небольшой процент ошибок в которых мы можем получать из-за временного лага. Чтобы их избежать, остается добавить конструкцию со sleep и повторить деплой. Впрочем, в нашем конкретном случае изменений не было видно (ошибок снова нет).




Заключение




Для корректного завершения процесса мы ожидаем от приложения следующего поведения:




  1. Ожидать несколько секунд, после чего перестать принимать новые соединения.



  2. Дождаться завершения всех запросов и закрыть все keepalive-подключения, которые запросы не выполняют.



  3. Завершить свой процесс.




Однако не все приложения умеют так работать. Одним из решений проблемы в реалиях Kubernetes является:




  • добавление хука pre-stop, который будет ожидать несколько секунд;



  • изучение конфигурационного файла нашего бэкенда на предмет соответствующих параметров.




Пример с NGINX позволяет понять, что даже то приложение, которое изначально должно корректно отрабатывает сигналы к завершению, может этого не делать, поэтому критично проверять наличие 500 ошибок во время деплоя приложения. Также это позволяет смотреть на проблему шире и не концентрироваться на отдельном pod’е или контейнере, а смотреть на всю инфраструктуру в целом.




В качестве инструмента для тестирования можно использовать Яндекс.Танк совместно с любой системой мониторинга (в нашем случае для теста брались данные из Grafana с бэкендом в виде Prometheus). Проблемы с graceful shutdown хорошо видны при больших нагрузках, которую может генерировать benchmark, а мониторинг помогает детальнее разобрать ситуацию во время или после теста.




Отвечая на обратную связь по статье: стоит оговориться, что проблемы и пути их решения здесь описываются применительно к NGINX Ingress. Для других случаев есть иные решения, которые, возможно, мы рассмотрим в следующих материалах цикла.




Источник: https://habr.com/ru/company/flant/blog/489994/



2023-01-03T00:41:02
DevOps

Сохраняем кластеры Kubernetes в чистоте и порядке

Одновременно с ростом кластера Kubernetes растет количество ресурсов, volume и других API-объектов. Рано или поздно вы упретесь в потолок, будь то etcd, память или процессор. Зачем подвергать себя ненужной боли и проблемам, если можно установить простые — хотя и довольно изощренные — правила? Вы можете настроить автоматизацию и мониторинг, которые будут содержать кластер в аккуратном состоянии. В статье разберемся, как избавиться от лишних нагрузок, через которые утекают ресурсы, и устаревших накопившихся объектов.




Что будет в статье:




  • В чем проблема?



  • Основы



  • Ручная очистка



  • Kube-janitor



  • Мониторинг ограничений кластера



  • Заключение



  • От переводчиков







В чем проблема?




Несколько забытых подов, неиспользуемые volume, завершенные задачи или, возможно, старый ConfigMap/Secret — насколько все это важно? Все это просто где-то лежит и ждет, пока не понадобится.




Вообще-то этот хлам не сильно вредит до поры до времени. Но когда эти штуки накапливаются, то начинают влиять на производительность и стабильность кластера. Давайте посмотрим, какие проблемы могут вызвать эти забытые ресурсы.




Ограничение количества подов. Каждый кластер Kubernetes имеет несколько основных ограничений. Первое из них — количество подов на узел, которое по документации не рекомендуется делать больше 110. При этом, если у вас достаточно мощные узлы с большим количеством памяти и CPU, вы можете увеличить это количество — возможно, даже до 500, как было протестировано на OpenShift. Но если вы достигнете этих пределов, не удивляйтесь, если что-то пойдет не так, и не только потому, что не хватает памяти или мощности процессора.




Нехватка хранилища ephemeral storage. Все запущенные поды на узле используют по крайней мере часть этого пространства для логов, кэша, рабочего пространства или emptyDir волюмов. Вы можете достичь предела довольно быстро, что приведет к вытеснению уже существующих подов, или невозможности создания новых. Запуск слишком большого количества подов на узле тоже может способствовать этой проблеме: ephemeral storage используется для образов контейнеров и их записываемых слоев. Если узел достигает предела хранилища, он становится нерабочим (на него будет применен taint), о чем вы узнаете довольно быстро. Если вы хотите узнать текущее состояние диска на узле, запустите на нем df -h /var/lib.




Лишние расходы на PVC. Схожим образом источником проблем могут стать persistent volume, особенно если вы запускаете Kubernetes в облаке и платите за каждый предоставленный PVC. Очевидно, что для экономии денег необходимо чистить неиспользуемые PVC. Содержание используемых PVC чистыми тоже важно, потому что позволяет избежать нехватки места для ваших приложений. Особенно, если вы запускаете базы данных в кластере.




Низкая производительность etcd. Еще один источник проблем — чрезмерное количество объектов, поскольку все они находятся в хранилище etcd. По мере роста количества данных в etcd, его производительность может начать снижаться. Этого нужно стараться не допускать всеми силами: etcd — мозг кластера Kubernetes. Учитывая вышесказанное, чтобы упереться в etcd, вам понадобится действительно большой кластер, как продемонстрировано в этом посте OpenAI. В то же время нет какой-то единой метрики для замера производительности etcd, потому что она зависит от количества объектов, их размеров и частоты использования. Так что наилучшим выходом будет профилактика: простое сохранение чистоты и порядка. В противном случае вас могут ждать весьма неприятные сюрпризы.




Нарушение границ безопасности. Наконец, мусор в кластере может стать источником проблем сам по себе. Не забывайте подчищать Role Bindings и учетные записи (Service Account), когда ими никто не пользуется.




Основы




Для решения большинства этих проблем не нужно делать ничего особенно сложного. Лучшим выбором будет совсем не допускать их. Один из вариантов превентивных мер — использование объектных квот, которые вы, как администратор кластера, можете применять в каждом отдельном неймспейсе. 




Первое, что можно решить с помощью квот — количество и размер PVC:




apiVersion: v1
kind: LimitRange
metadata:
  name: pvc-limit
spec:
  limits:
  - type: PersistentVolumeClaim
    max:
      storage: 10Gi
    min:
      storage: 1Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: pvc-quota
spec:
  hard:
    persistentvolumeclaims: "10"
    requests.storage: "10Gi"
    # сумма запрошенного хранилища в bronze storage class не может превышать 5 Гб
    bronze.storageclass.storage.k8s.io/requests.storage: "5Gi"




Выше мы имеем два объекта.




LimitRange устанавливает минимальный и максимальный размеры PVC в неймспейсе. Это оградит пользователей от запрашивания слишком больших объемов.




ResourceQuota дополнительно обеспечивает жесткое ограничение на количество PVC и их совокупный размер.




Затем вы можете предотвратить создание кучи объектов и оставление их в качестве мусора после использования. Для этого используйте object count, квоты на количество объектов, которые зададут жесткое ограничение на количество объектов определенного типа в конкретном неймспейсе:




apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-count-quota
spec:
  hard:
    configmaps: "2"
    secrets: "10"
    services: "5"
    count/jobs.batch: "8"




Есть несколько встроенных полей, через которые вы можете задать квоты на количество объектов. Например, configmapssecrets или services, показанные выше. Для всех прочих ресурсов можно использовать формат count/<resource>.<group>, как показано в примере с count/jobs.batch, что может помочь от бесконтрольного создания джоб из-за неправильно настроенного CronJob.




Вероятно, большинству известно о функции установки квот на память и CPU. Но, возможно, для вас станет новостью квота ephemeral storage. Альфа-поддержка квот для эфемерного хранилища была добавлена в v1.18 и дала возможность установить границы ephemeral storage так же, как как для памяти и процессора.




apiVersion: v1
kind: ResourceQuota
metadata:
  name: ephemeral-storage-quota
spec:
  hard:
    requests.ephemeral-storage: 1Gi
    limits.ephemeral-storage: 2Gi




Однако будьте осторожны с этой настройкой. Поды могут быть вытеснены из-за превышения лимита, что может быть вызвано слишком большим размером логов контейнера.




Помимо установки квот и границ ресурсов, можно установить ограничение для истории ревизий Deployment — для снижения количества репликасетов, хранящихся в кластере. Для этого используйте .spec.revisionHistoryLimit, который по умолчанию равен 10.




Наконец, вы можете установить время жизни (TTL) для очистки кластера от объектов, которые существуют там слишком долго. Эта процедура использует TTL-контроллер, который находится в стадии бета-тестирования с версии v1.21, и в настоящее время работает только для задач, использующих поле .spec.ttlSecondsAfterFinished. В будущем, возможно, он будет расширен на другие ресурсы, например, поды.




Ручная очистка 




Если превентивных мер уже недостаточно, потому что у вас накопилось достаточно неиспользуемых ресурсов, вы можете попробовать разовое удаление. Это делается просто с помощью kubectl get и kubectl delete. Пара основных примеров того, что вы можете сделать:




kubectl delete all -l some-label=some-value  # Delete based on label
kubectl delete pod $(kubectl get pod -o=jsonpath='{.items[?(@.status.phase=="Succeeded")].metadata.name}')  # Delete all "Succeeded" Pods




Первая команда выполняет базовое удаление с использованием лейблов ресурсов. Поэтому этот метод потребует от вас предварительно пометить все подлежащие удалению объекты некоторой парой ключ-значение.




Вторая показывает, как вы можете удалить тип ресурсов, основанный на некотором поле, как правило, каком-то поле статуса. В примере выше это будут все завершенные/успешные поды. Этот вариант можно применить и к другим ресурсам, например, завершенным задачам.




Кроме этих двух команд, довольно сложно найти шаблон, который помог бы разом очистить кластер от хлама. Поэтому вам придется специально поискать конкретные неиспользуемые ресурсы.




Могу посоветовать такой инструмент, как k8spurger. Он ищет неиспользуемые объекты вроде RoleBinding, ServiceAccounts, ConfigMaps и создает список ресурсов-кандидатов на удаление. Это поможет сузить круг поиска.




Kube-janitor




В разделах выше мы рассмотрели несколько вариантов простой очистки для конкретных случаев. Но лучшим решением для наведения порядка в любом кластере будет использование kube-janitor. Этот инструмент работает в вашем кластере так же, как любая другая рабочая нагрузка, и использует JSON-запросы для поиска ресурсов, которые можно удалить на основе TTL или истечения срока действия.




Для развертывания kube-janitor на вашем кластере запустите:




git clone https://codeberg.org/hjacobs/kube-janitor.git
cd kube-janitor
kubectl apply -k deploy/




Это разложит kube-janitor в default namespace и запустит его с правилами по умолчанию в пробном режиме с использованием флага —dry-run.




Перед отключением dry-run нужно установить собственные правила. Они лежат в config map kube-janitor, которая выглядит примерно так:




apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-janitor
  namespace: default
data:
  rules.yaml: |-
    rules:
      ...




Конечно, для нас самый интересный раздел здесь — правила, rules. Вот несколько полезных примеров, которые вы можете использовать для очистки своего кластера:




rules:
  # Удаление Jobs в development namespaces после 2 дней.
  - id: remove-old-jobs
    resources:
    - jobs
    jmespath: "metadata.namespace == 'development'"
    ttl: 2d
  # Удаление тех подов в development namespaces, которые не в состоянии выполнения (Failed, Completed).
  - id: remove-non-running-pods
    resources:
    - pods
    jmespath: "(status.phase == 'Completed' || status.phase == 'Failed') && metadata.namespace == 'development'"
    ttl: 2h
  # Удаление всех PVC, которые не использует ни один под
  - id: remove-unused-pvcs
    resources:
    - persistentvolumeclaims
    jmespath: "_context.pvc_is_not_mounted"
    ttl: 1d
  # Удаление всех Deployments, чье имя начинается с 'test-'
  - id: remove-test-deployments
    resources:
    - deployments
    jmespath: "starts_with(metadata.name, 'test-')"
    ttl: 1d
  # Удаление всех ресурсов в playground namespace через неделю
  - id: remove-test-deployments
    resources:
    - "*"
    jmespath: "metadata.namespace == 'playground'"
    ttl: 7d




Этот пример показывает несколько базовых вариантов настройки для очистки от временных, устаревших или неиспользуемых ресурсов. Помимо правил такого рода, можно установить абсолютные дату/время истечения срока действия для конкретных объектов. 




Это можно сделать с помощью аннотаций, например:




apiVersion: v1
kind: Namespace
metadata:
  annotations:
    # будет удалено 18.6.2021 в полночь
    janitor/expires: "2021-06-18"
  name: temp
spec: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    # будет удалено 20.6.2021 в 17:30
    janitor/expires: "2021-06-20T17:30:00Z"
  name: nginx
spec:
  replicas: 1
  ...




Когда закончите устанавливать правила и аннотации, вам стоит дать kube-janitor поработать какое-то время в dry-run режиме с включенными логами отладки. Это мера предосторожности, чтобы инструмент не удалил то, что удалять было не нужно. 




Другими словами, не обвиняйте меня, если сотрете production волюмы из-за неправильной конфигурации и отсутствия тестирования.




Наконец, при использовании kube-janitor нужно учитывать его потребности в ресурсах. Если в кластере много объектов, ему может потребоваться больше памяти, чем выделенные по умолчанию 100 Мб. Чтобы его под не застревал в CrashLoopBackOff, я выставляю ему лимит 1 Гб.




Мониторинг ограничений кластера




Не все проблемы можно решить ручной или даже автоматической очисткой. В некоторых случаях мониторинг будет лучшим выбором для обеспечения уверенности в том, что вы не упираетесь в лимиты кластера — будь то количество подов, доступное ephemeral storage или количество объектов в etcd.




Мониторинг — это огромная тема, которая требует отдельной статьи, а то и нескольких. Поэтому для сегодняшних целей я просто перечислю несколько Prometheus-метрик, которые могут оказаться полезными для поддержания порядка в кластере:







Это только некоторые метрики из тех, которые вы можете использовать. Какие из них будут доступны, зависит также от ваших инструментов мониторинга, т.е. вы можете использовать какие-то специальные метрики, доступные на вашем сервисе.




Заключение




Мы рассмотрели несколько вариантов очистки кластера Kubernetes — некоторые совсем простые, а некоторые посложнее. Независимо от того, что вы выберете, старайтесь не забывать делать уборку в кластере и сохранять порядок. Это может спасти вас от большой головной боли и, по крайней мере, избавит от ненужного хлама в кластере. В конечном счете такая уборка служит тем же целям, что наведение порядка на рабочем столе.




Также имейте ввиду, что если вы оставляете объекты лежать без использования долгое время, вы просто забудете, зачем они там. Это может сильно осложнить понимание, что должно существовать, а что — нет.




Помимо описанных здесь подходов, вы также можете использовать какое-либо решение GitOps — например, ArgoCD или Flux для создания ресурсов и управления ими, что может значительно упростить их очистку. Обычно потребуется удалить только один кастомный ресурс, что вызовет каскадное удаление всех зависимых ресурсов.




Источник: https://habr.com/ru/company/kts/blog/587236/



Удаляем устаревшую 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-endhabr-front-endhabr-back-end-pr-17habr-back-end-pr-33, тогда кандидатами на удаление будут habr-back-end-pr-17habr-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-1project-pr-2) и их deploymentsservicesingress, и так далее:




$ 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.




Источник: https://habr.com/ru/post/508534/



2023-01-03T00:32:43
DevOps

Jenkins as a code. Часть 4

Возникла необходимость разграничения прав доступа пользователей на Jenkins-сервере — давайте разберемся, как можно это сделать без использования webUI!




В моем случае нужно разделить пользователей на две группы — администраторов (с полным уровнем доступа) и обычных пользователей (есть доступ только на просмотр списка заданий, запуск и отмену запущенных задач). Для решения этой задачи отлично подходит плагин Role-based Authorization Strategy. Главной особенностью является наличие в данном плагине двух предустановленных групп пользователей:




  • Anonymous — незалогиненные пользователи;



  • authenticated — залогиненные пользователи.




Использование группы authenticated даст возможность не перечислять всех пользователей нашего jenkins-сервера в явном виде.




Устанавливать данный плагин мы будем при старте jenkins-сервера с помощью инит-скрипта, как описано в этой статье. Для этого в скрипте 00-install-plugins.groovy список устанавливаемых плагинов приводим к следующему виду:




...
Set<String> plugins_to_install = [
    "github-pullrequest",
    "google-login",
    "workflow-aggregator",
    "htmlpublisher",
    "locale",
    "role-strategy"
]
...




Теперь создадим еще один groovy-скрипт (назовем его 06-role-based-auth.groovy) со следующим содержанием:




import hudson.*
import hudson.model.*
import hudson.security.*
import jenkins.*
import jenkins.model.*
import java.util.*
import com.michelin.cio.hudson.plugins.rolestrategy.*
import java.lang.reflect.*

// Roles
def globalRoleRead = "builder"
def globalRoleAdmin = "admin"

// Users and Groups
def access = [
    admins: [
            "admin@example.com",
            "devops@example.com",
            "ealebed@example.com"
    ],
    builders: ["authenticated"],
]

def instance = Jenkins.getInstance()
def currentAuthenticationStrategy = Hudson.instance.getAuthorizationStrategy()

Thread.start {
    sleep 15000
    if (currentAuthenticationStrategy instanceof RoleBasedAuthorizationStrategy) {
        println "Role based authorisation already enabled."
        println "Exiting script..."
        return
    } else {
        println "Enabling role based authorisation strategy..."
    }

    // Set new authentication strategy
    RoleBasedAuthorizationStrategy roleBasedAuthenticationStrategy = new RoleBasedAuthorizationStrategy()
    instance.setAuthorizationStrategy(roleBasedAuthenticationStrategy)

    Constructor[] constrs = Role.class.getConstructors()
    for (Constructor<?> c : constrs) {
        c.setAccessible(true)
    }

    // Make the method assignRole accessible
    Method assignRoleMethod = RoleBasedAuthorizationStrategy.class.getDeclaredMethod("assignRole", String.class, Role.class, String.class)
    assignRoleMethod.setAccessible(true)

    // Create admin set of permissions
    Set<Permission> adminPermissions = new HashSet<Permission>()
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Administer"))
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.View"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.ManageDomains"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Create"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Update"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Discover"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Workspace"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Move"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Connect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Provision"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Disconnect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Update"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Replay"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Delete"))
    adminPermissions.add(Permission.fromId("hudson.scm.SCM.Tag"))

    // Create permissions for authenticated users
    Set<Permission> authenticatedPermissions = new HashSet<Permission>()
    authenticatedPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.View.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))

    // Create the admin role
    Role adminRole = new Role(globalRoleAdmin, adminPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole)

    // Create the builder role
    Role builderRole = new Role(globalRoleRead, authenticatedPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole)

    // Assign the admin role
    access.admins.each { l ->
        println("Granting admin permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole, l)
    }

    access.builders.each { l ->
        println("Granting builder permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole, l)
    }

    // Save the state
    println "Saving changes."
    instance.save()
}




С помощью данного скрипта создаются две роли — builder и admin, которым устанавливаются разные наборы прав доступа. Далее всем пользователям, перечисленным в access.admins, назначается роль admin, а группе authenticated (находится в access.builders) назначается роль builder.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-4/



2023-01-03T00:16:15
DevOps

Jenkins as a code. Часть 3

В данной статье цикла “Jenkins as a code” рассмотрим самый интересный (и полезный) пример — автоматическое создание задач (job) при запуске сервиса. Давайте разберемся!




Создавать задания (jobs) в Jenkins можно несколькими способами — традиционно (через webUI), через REST API или “подкладывая” файлы .xml.import в ${JENKINS_HOME}.




Но традиционный способ настройки занимает слишком много времени, использование API невероятно неудобно (см. пример ниже), а создание файлов с расширением .xml.import для каждой задачи и рестарт сервиса Jenkins после их добавления даже звучит несерьезно.




Чем плох REST API:




  • сначала необходимо получить описание уже существующей задачи в формате .xml. Например, это можно сделать используя API:




curl -X GET -u username:API_TOKEN 'http://JENKINS_HOST/job/MY_JOB_NAME/config.xml' -o config.xml




или




curl -u username:password "http://JENKINS_HOST/job/MY_JOB_NAME/config.xml" > config.xml




  • потом полученный xml-файл нужно отредактировать под свои нужды;



  • после редактирования можно создать новую задачу “запостив” этот файл через API, например:




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "Content-Type:text/xml"




Ах да, если вдруг столкнетесь с ошибкой:




Error 403 No valid crumb was included in the request




то придется сделать на одно действие больше:




CRUMB=$(curl -s 'http://JENKINS_HOST/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -u username:password)//crumb)' -u us




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "$CRUMB" -H "Content-Type:text/xml"




Вообщем, так себе вариант, особенно если у вас 150-200 заданий…




Вспоминаем о возможностях настройки экземпляра Jenkins еще в момент старта с помощью хуков, которые мы рассматривали в первой  и второй статьях данного цикла. Идея — хранить в отдельном файле определенного формата (.json или .xml) минимально необходимый набор параметров (ссылка на git-репозиторий, название ветки, путь к Jenkinsfile и т. д.) для создания задачи. При запуске Jenkins groovy-скрипт берет из данного файла параметры и создает задачи, в качестве дополнительного бонуса — сразу распределяет эти задачи по представлениям (views).




Создаем файл job_list.yaml с набором параметров для задач, в моем случае выглядящий так:




dev:
  1st-project:
    jobDisabled:          false
    enableGithubPolling:  false
    daysBuildsKeep:       '1'
    numberBuildsKeep:     '5'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master, */test'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
  2nd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '2'
    numberBuildsKeep:     '10'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
stage:
  3rd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '3'
    numberBuildsKeep:     '15'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'dir/Jenkinsfile'
test:
  4th-project:
    jobDisabled:          true
    enableGithubPolling:  true
    daysBuildsKeep:       '4'
    numberBuildsKeep:     '20'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'




Примечание. Если планируется переезд со старого экземпляра Jenkins на новый, то можно легко написать скрипт, который возьмет нужные параметры на старом сервере и сформирует yaml-файл в нужном формате.




Скрипт, который должен “разобрать” данный файл и создать задачи у меня называется 05-create-jobs.groovy и выглядит следующим образом:




@Grab(group='org.yaml', module='snakeyaml', version='1.18')
import jenkins.model.*
import hudson.model.*
import hudson.tasks.LogRotator
import hudson.plugins.git.*
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition
import com.coravy.hudson.plugins.github.*
import com.cloudbees.jenkins.GitHubPushTrigger
import org.yaml.snakeyaml.Yaml

Jenkins jenkins = Jenkins.getInstance()

def pluginParameter = "workflow-aggregator github-pullrequest"
def plugins         = pluginParameter.split()
def pm              = jenkins.getPluginManager()
def installed       = false

plugins.each {
    if (!pm.getPlugin(it)) {
        println("Plugin ${it} not installed, skip creating jobs!")
    } else {
        installed = true
    }
}

if(installed) {
    def listExistingJob     = jenkins.items.collect { it.name }
    def listExistingViews   = jenkins.views.collect { it.name }
    def jobParameters       = new Yaml().load(new FileReader('/temp/job_list.yaml'))

    for (view in jobParameters) {
        if (jobParameters.any { listExistingViews.contains(view.key) }) {
            println("--- View ${view.key} already exist, skip")
        } else {
            println("--- Create new view ${view.key}")
            jenkins.addView(new ListView(view.key))
        }
        for (item in view.value) {
            if (view.value.any { listExistingJob.contains(item.key) }) {
                println("--- Job ${item.key} already exist, skip")
                continue
            } else {
                println("--- Create new job ${item.key}")

                def jobName             = item.key
                def jobDisabled         = view.value.get(item.key).getAt("jobDisabled")
                def enableGithubPolling = view.value.get(item.key).getAt("enableGithubPolling")
                def daysBuildsKeep      = view.value.get(item.key).getAt("daysBuildsKeep")
                def numberBuildsKeep    = view.value.get(item.key).getAt("numberBuildsKeep")
                def githubRepoUrl       = view.value.get(item.key).getAt("githubRepoUrl")
                def githubRepo          = view.value.get(item.key).getAt("githubRepo")
                def githubBranch        = view.value.get(item.key).getAt("githubBranch")
                def credentialId        = view.value.get(item.key).getAt("credentialId")
                def jenkinsfilePath     = view.value.get(item.key).getAt("jenkinsfilePath")

                def githubBranchList = githubBranch.split(', ')
                def branchConfig = new ArrayList<BranchSpec>()
                for (branch in githubBranchList) {
                    if( branch != null )
                    {
                        branchConfig.add(new BranchSpec(branch))
                    }
                    else
                    {
                        branchConfig.add(new BranchSpec("*/master"))
                    }
                }
                def userConfig          = [new UserRemoteConfig(githubRepo, null, null, credentialId)]
                def scm                 = new GitSCM(userConfig, branchConfig, false, [], null, null, null)
                def flowDefinition      = new CpsScmFlowDefinition(scm, jenkinsfilePath)

                flowDefinition.setLightweight(true)

                def job = new WorkflowJob(jenkins, jobName)
                job.definition = flowDefinition
                job.setConcurrentBuild(false)
                job.setDisabled(jobDisabled)
                job.addProperty(new BuildDiscarderProperty(new LogRotator(daysBuildsKeep, numberBuildsKeep, null, null)))
                job.addProperty(new GithubProjectProperty(githubRepoUrl))

                if (true == enableGithubPolling) {
                    job.addTrigger(new GitHubPushTrigger())
                }

                jenkins.save()
                jenkins.reload()
                hudson.model.Hudson.instance.getView(view.key).doAddJobToView(jobName)
            }
        }
    }
}




Сначала мы проверяем, установлены ли необходимые плагины (без них создать представление / задачу не получится). Далее получаем список уже существующих представлений и задач (может так случиться, что сервер с Jenkins будет перезагружен) — нам совершенно нет нужды пересоздавать задачи при каждом старте Jenkins. Только если нужного нам представления (view) нет — оно будет создано, после чего создаются добавляются в представление связанные с ним задачи.




Остается только позаботиться о том, чтобы файл с параметрами задач появился в /temp/job_list.yaml (или другом удобном для вас месте), а скрипт 05-create-jobs.groovy — в каталоге ${JENKINS_HOME}/init.groovy.d/.




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



2023-01-03T00:15:17
DevOps

Jenkins as a code. Часть 2

В первой статье цикла “Jenkins as a code” мы разобрали несколько примеров настройки экземпляра Jenkins под собственные нужды, в данной статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) — давайте разберемся!




Ранее мы уже подробно рассматривали значение общих библиотек для большого количества однотипных проектов, поэтому повторяться не будем. Наша цель — сделать так, чтобы после старта Jenkins общая библиотека уже была подключена и настроена — это избавит от необходимости настройки через UI.




Как и в предыдущей статье, нам на помощь приходят хуки (groovy-скрипты) из каталога ${JENKINS_HOME}/init.groovy.d/. Создадим новый файл 04-global-pipeline-library.groovy следующего содержания:




import jenkins.model.Jenkins
import jenkins.plugins.git.GitSCMSource
import jenkins.plugins.git.traits.BranchDiscoveryTrait
import org.jenkinsci.plugins.workflow.libs.GlobalLibraries
import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration
import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever

List libraries = [] as ArrayList

def instance = Jenkins.getInstance()

println("--- Configuring global library getting")
def githubRepo      = 'https://github.com/ealebed/jenkins-shared-libs.git'
def libraryName     = 'jenkins-shared-libs'
def githubBranch    = 'master'
def credentialsId   = ''

def scm             = new GitSCMSource(githubRepo)
scm.credentialsId   = credentialsId
scm.traits          = [new BranchDiscoveryTrait()]
def retriever       = new SCMSourceRetriever(scm)

def library         = new LibraryConfiguration(libraryName, retriever)
library.defaultVersion          = githubBranch
library.implicit                = true
library.allowVersionOverride    = true
library.includeInChangesets     = true

libraries << library

def global_settings = instance.getExtensionList(GlobalLibraries.class)[0]
global_settings.libraries = libraries
global_settings.save()




Интересующие нас настройки задаются с помощью четырех параметров — githubRepolibraryNamegithubBranch и credentialsId.




Здесь:




  • githubRepo — ссылка на репозиторий с общими библиотеками в системе контроля версий (может быть как https, так и git);



  • libraryName — имя подключаемой общей библиотеки. Именно это имя будет использоваться в пайплайнах (Jenkinsfile) в формате @Library('jenkins-shared-libs@master') _;



  • githubBranch — ветка в git-репозитории с общими библиотеками (Она же “версия”). Также используется в пайплайнах (Jenkinsfile);



  • credentialsId — креденшелы для подключения к git-репозиторию. Значение может быть пустым — помните скрипт 03-user-service.groovy из первой части цикла, которым мы настраивали доступ к системе контроля версий?




В зависимости от выбранного инструмента развертывания Jenkins, нужно добиться появления данного скрипта в каталоге ${JENKINS_HOME}/init.groovy.d/ — так он будет выполнен при запуске сервиса.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-2/



2023-01-03T00:14:09
DevOps