Архив метки: 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

Jenkins as a code. Часть 1

Идея “инфраструктура как код” далеко не нова и широко используется в повседневной жизни большинством компаний. В серии статей “Jenkins as a code” предлагаю разобраться с автоматическим развертыванием и настройкой сервера Jenkins!




Казалось бы, зачем эти статьи, если можно взять готовую роль jenkins для системы управления конфигурациями Ansible или кукбук jenkins для chef, или даже воспользоваться готовым docker-образом?




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




Для кастомизации и тонкой настройки вашего экземпляра Jenkins разработчики предлагают использовать хуков (groovy-скриптов), которые нужно размещать в каталоге ${JENKINS_HOME}/init.groovy.d/.




В зависимости от выбранного инструмента развертывания, способ, которым скрипты попадут в нужный каталог будет отличаться. Например, при использовании docker-образа, самым простым будет поместить нужные скрипты в каталог /usr/share/jenkins/ref/init.groovy.d/:




FROM jenkins/jenkins:lts
COPY custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy




При старте docker-контейнера все, что находится в каталоге /usr/share/jenkins/ref/ копируется в каталог ${JENKINS_HOME} (следовательно, каталог init.groovy.d со всем содержимым будет скопирован в нужное место).




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




Чаще всего с помощью хуков в Jenkins устанавливают плагины, выполняют глобальную настройку, включают/выключают опции безопасности, добавляют ключи доступа к системе хранения версиями.




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




Скрипт 00-install-plugins.groovy выполняет установку необходимых плагинов с зависимостями и выглядит следующим образом:




/*
    Install required plugins and their dependencies.
*/
import jenkins.model.*
import hudson.model.*
import org.jenkinsci.plugins.*
import hudson.model.UpdateSite
import hudson.PluginWrapper

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

Boolean hasConfigBeenUpdated = false
UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById('default')
List<PluginWrapper> plugins = Jenkins.instance.pluginManager.getPlugins()

def install_plugin(shortName, UpdateSite updateSite) {
    println "Installing ${shortName} plugin."
    UpdateSite.Plugin plugin = updateSite.getPlugin(shortName)
    Throwable error = plugin.deploy(false).get().getError()
    if(error != null) {
        println "ERROR installing ${shortName}, ${error}"
    }
    null
}

// Check the update site(s) for latest plugins
println 'Checking plugin updates via Plugin Manager.'
Jenkins.instance.pluginManager.doCheckUpdatesServer()

// Any plugins need updating?
Set<String> plugins_to_update = []
plugins.each {
    if(it.hasUpdate()) {
        plugins_to_update << it.getShortName()
    }
}

if(plugins_to_update.size() > 0) {
    println "Updating plugins..."
    plugins_to_update.each {
        install_plugin(it, updateSite)
    }
    println "Done updating plugins."
    hasConfigBeenUpdated = true
}

// Get a list of installed plugins
Set<String> installed_plugins = []
plugins.each {
    installed_plugins << it.getShortName()
}

// Check to see if there are missing plugins to install
Set<String> missing_plugins = plugins_to_install - installed_plugins
if(missing_plugins.size() > 0) {
    println "Install missing plugins..."
    missing_plugins.each {
        install_plugin(it, updateSite)
    }
    println "Done installing missing plugins."
    hasConfigBeenUpdated = true
}

if(hasConfigBeenUpdated) {
    println "Saving Jenkins configuration to disk."
    Jenkins.instance.save()
    Jenkins.instance.restart()
} else {
    println "Jenkins up-to-date. Nothing to do."
}




Вторым по счету запускается скрипт 01-global-settings.groovy, устанавливающий количество исполнителей, локаль, глобальные настройки для системы контроля версий и протоколы взаимодействия:




import jenkins.model.*
import org.jenkinsci.plugins.*
import hudson.security.csrf.DefaultCrumbIssuer
import hudson.plugins.locale.PluginImpl

def instance = Jenkins.getInstance()

println("--- Configuring global getting")
instance.setNumExecutors(5)
instance.setCrumbIssuer(new DefaultCrumbIssuer(true))
instance.setNoUsageStatistics(true)
instance.save()

println("--- Configuring locale")
PluginImpl localePlugin = (PluginImpl)instance.getPlugin("locale")
localePlugin.systemLocale = "en_US"
localePlugin.@ignoreAcceptLanguage=true

println("--- Configuring git global options")
def desc = instance.getDescriptor("hudson.plugins.git.GitSCM")
desc.setGlobalConfigName("jenkins")
desc.setGlobalConfigEmail("jenkins@example.com")
desc.save()

println("--- Configuring protocols")
Set<String> agentProtocolsList = ['JNLP4-connect', 'Ping']
if(!instance.getAgentProtocols().equals(agentProtocolsList)) {
    instance.setAgentProtocols(agentProtocolsList)
    println "Agent Protocols have changed.  Setting: ${agentProtocolsList}"
    instance.save()
}
else {
    println "Nothing changed.  Agent Protocols already configured: ${instance.getAgentProtocols()}"
}




Следующим будет выполнен скрипт с именем 02-disable-cli.groovy (как несложно догадаться, отключающий CLI):




import jenkins.*
import jenkins.model.*
import hudson.model.*
import java.util.logging.Logger
import org.jenkinsci.main.modules.sshd.*
Logger logger = Logger.getLogger("")

// Disable CLI access over TCP listener (separate port)
def p = AgentProtocol.all()
p.each { x ->
    if (x.name?.contains("CLI")) {
        logger.info("Removing protocol ${x.name}")
        p.remove(x)
    }
}

// Disable CLI access over /cli URL
def removal = { lst ->
    lst.each { x ->
        if (x.getClass().name.contains("CLIAction")) {
            logger.info("Removing extension ${x.getClass().name}")
            lst.remove(x)
        }
    }
}

def j = Jenkins.instance
removal(j.getExtensionList(RootAction.class))
removal(j.actions)

// Disable CLI over Remoting
jenkins.CLI.get().setEnabled(false)

// Allow SSH connections
def sshdExtension = Jenkins.instance.getExtensionList(SSHD.class)[0]
sshdExtension.setPort(22222)
sshdExtension.save()

// Configure Slave-to-Master Access Control
// https://wiki.jenkins-ci.org/display/JENKINS/Slave+To+Master+Access+Control

def rule = Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchConfiguration.class)[0].rule
if(!rule.getMasterKillSwitch()) {
    rule.setMasterKillSwitch(true);
    logger.info('Disabled agent -> master security for cobertura.');
}
else {
    logger.info('Nothing changed.  Agent -> master security already disabled.');
}

// Do not annoy with Slave-to-Master Access Control warning
Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchWarning.class)[0].disable(true);
Jenkins.instance.save()




И, наконец, скрипт 03-user-service.groovy создает пользователя и добавляет ему ssh-ключ для доступа к системе контроля версий:




public_key = 'ssh-rsa AAAAB3N....TJChv jenkins'
user = hudson.model.User.get('service')
user.setFullName('Service User')
keys = new org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl(public_key)
user.addProperty(keys)
user.save()




На этом с настройкой экземпляра Jenkins под собственные нужды все, в следующей статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) при запуске Jenkins.




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



2023-01-03T00:12:36
DevOps