Best practices при написании YAML-файлов манифеста Kubernetes

Kate

Administrator
Команда форума
Kubernetes — это, пожалуй, один из самых быстрых инструментов оркестрации, который широко используется многими организациями. Облачный провайдер #CloudMTS предоставляет Kubernetes по модели managed service.

Подготовили для вас перевод некоторых рекомендаций и практик, которые следует учитывать при внедрении Kubernetes.

Выбор правильного объекта Kubernetes (обязательно)​


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

Deployment​


Deployment можно использовать, если приложение не требует хранения постоянных данных в блочном хранилище и не нуждается в кластеризации. Если постоянные данные должны храниться в файловом ресурсе, следует выбрать Deployment. Если постоянные данные должны храниться в блочном хранилище, то Deployment не стоит применять из-за того, что в любой момент времени том блочного хранилища может быть подключен только к одному Pod. Таким образом, при выполнении скользящих обновлений новый Pod будет находиться в ждущем режиме, поскольку предыдущая версия Pod все еще удерживает PVC. Нам придется уменьшить масштаб развертывания до 0 реплик, а затем увеличить его, что приведет к простоям.

Еще одна причина, по которой Deployment не рекомендуется рассматривать, когда требуется кластеризация, заключается в том, что для кластеризации необходимо указать конечную точку каждой реплики pod. В Kubernetes IP-адреса Pod'ов являются динамическими. Поэтому задать конкретные конечные точки для кластеризации не получится.

image

Тип развертывания с PVC — Disk Type

StatefulSet​


StatefulSet можно использовать, когда ваше приложение хранит постоянные данные в блочных хранилищах, требуется кластеризация или вы хотите установить статические имена для подов. StatefulSet может динамически разворачивать PVC, используя volumeClaimTemplate. Кластеризацию можно обеспечить с помощью Headless Service. Headless Service — это вид Kubernetes Service, у которого spec.clusterIP установлен на None. Во время развертывания обновления, в отличие от Deployment, который развертывает новый под и только потом сворачивает старый, StatefulSet выполняет обновление, начиная с последней реплики и заканчивая первой репликой.

image


Startup, Liveness и Readiness Probe (обязательно)​


Startup Probe​


Иногда приходится развертывать устаревшие приложения, которым требуется много времени для первоначального запуска. В подобных ситуациях может быть сложно настроить параметры liveness probe без ущерба для скорости реагирования на тупиковые ситуации. Именно в таких случаях можно использовать startup probe, у которого будет такая же схема проверки работоспособности, как и у liveness, за исключением того, что failureThreshold и periodSeconds достаточно длинные, чтобы покрыть время запуска приложения в случае наихудшего сценария. Если приложение не запустится в течение заданного времени, то startup probe потерпит неудачу, уничтожит контейнер и будет подчиняться restartPolicy pod. Ниже приведен пример из kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes:

ports:
- name: liveness-port
containerPort: 8080
hostPort: 8080

livenessProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 1
periodSeconds: 10

startupProbe:
httpGet:
path: /healthz
port: liveness-port
failureThreshold: 30
periodSeconds: 10

Liveness Probe​


Liveness Probe гарантирует, что приложение работает исправно, периодически выполняя health check (на основе заданного periodSeconds). Если проверка работоспособности не удалась 3 раза подряд (по умолчанию порог отказа равен 3, но его можно изменить), то контейнер уничтожается и подвергается политике перезапуска pod's restartPolicy. Обязательным условием является наличие liveness probe для приложений.

Readiness Probe​


Readiness Probe гарантирует, что приложение готово к приему трафика. До тех пор, пока не будет установлено состояние готовности, контейнер не будет принимать трафик. Это гарантирует, что трафик не поступит, когда приложение еще не готово. Ошибки Readiness Probe не приведут к уничтожению контейнера.

ПРИМЕЧАНИЕ: Liveness probes не дожидаются успешного выполнения readiness probes. Если вы хотите подождать перед выполнением liveness probe, используйте initialDelaySeconds или startupProbe.

ports:
— containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20

Установка лимитов ресурсов (обязательно)​


Kubernetes имеет 3 типа QoS.

Гарантированное​


Для каждого контейнера в поде должны быть установлены лимиты CPU, памяти и запросов. Кроме того, значение запроса и лимита должно быть одинаковым. Поды с таким QoS будут выбираться в последнюю очередь при выполнении «выселения» (Eviction). Eviction/Preemption можно также минимизировать/избежать, установив классы приоритетов. Более подробная информация о классах приоритетов приведена на сайте kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption.

resources:
limits:
memory: “200Mi”
cpu: “700m”
requests:
memory: “200Mi”
cpu: “700m”


Burstable​


Хотя бы один контейнер в Pod имеет запрос или лимит памяти или процессора. Если у контейнера есть и запрос, и лимит, но значения запроса и лимита разные, он все равно попадает в категорию Burstable. Планировщик просмотрит раздел запросов в разделе ресурсов и выделит под соответствующей ноде. Например, если запрос на память составляет 300Mi, а лимит памяти — 1Gi, планировщик выберет ноду, где осталось более 300Mi памяти, и составит соответствующее расписание.

resources:
limits:
memory: “800Mi”
requests:
memory: “200Mi”
cpu: “700m”


BestEffort​


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

Присвоение подов узлам (обязательно)​


Чтобы сделать приложение отказоустойчивым, необходимо обеспечить распределение реплик подов по узлам/зонам доступности. Это можно сделать с помощью anti-affinity. Например, в ситуации, когда мы хотим запустить приложение B на том же узле, где работает приложение A, можно использовать концепцию принадлежности (affinity) и непринадлежности (anti-affinity). Существует два версии affinity/anti-affinity — мягкая (preferredDuringSchedulingIgnoredDuringExecution) и жесткая (requiredDuringSchedulingIgnoredDuringExecution).

При использовании мягкой планировщик сначала попытается выполнить правило affinity/anti-affinity (конечно, нода также должна соответствовать другим критериям, таким как доступность ресурсов, селектор узла/node affinity и т.д.) и разместить под на ноде, которая получила наивысший балл affinity. Оценка рассчитывается на основе условий, которые мы указали в блоке affinity/anti-affinity (в дополнение к другим критериям, таким как доступность ресурсов, селектор узла/родство узла и т.д.), а также веса, указанного в preferredSchedulingTerm. Все это суммируется, и под размещается на ноде, получившей наибольший балл (https://github.com/kubernetes/kuber...heduler/algorithm/priorities/node_affinity.go).

Таким образом, мягкий тип affinity/anti-affinity гарантирует, что под будет запланирован, даже если условия affinity/anti-affinity не выполняются. Его можно использовать, чтобы распределить поды по зонам доступности. Почти во всех регионах популярных облачных провайдеров будет только три зоны доступности. В таких случаях, если у нас есть под с более чем тремя репликами, планировщик все равно сможет запланировать поды (хотя некоторые из реплик будут находиться в одной зоне доступности, но по крайней мере одна реплика будет присутствовать в каждой из зон доступности).

spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # Soft type
— weight: 100 # This can be from 1 to 100 and plays role in node affinity score
podAffinityTerm:
labelSelector:
matchExpressions:
— key: app
operator: In
values:
— nginx
topologyKey: topology.kubernetes.io/zone # Ensures pods are placed across availability zones


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

spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # Hard type
— labelSelector:
matchExpressions:
— key: app
operator: In
values:
— nginx
topologyKey: “kubernetes.io/hostname” # Ensures pods are placed in different nodes


В под мы можем установить как жесткий, так и мягкий тип affinity/anti-affinity. Однако помимо affinity/anti-affinity нам может понадобиться, чтобы под был размещен на определенной группе нод или в определенном пуле. Этого можно добиться с помощью nodeSelector или nodeAffinity. nodeSelector — самая простая рекомендуемая форма ограничения выбора ноды. В nodeSelector мы можем указать метки целевых нод, и планировщик Kubernetes будет размещать поды только на них.

spec:
nodeSelector:
nodeType: spot


nodeAffinity работает как nodeSelector, но является более выразительным и позволяет задавать мягкие правила. Мягкие и жесткие правила аналогичны тем, что описаны выше.

spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: another-node-label-key
operator: In
values:
- another-node-label-value


Когда мы указываем nodeSelector или nodeAffinity с жестко заданным типом, мы также должны проверить, не являются ли ноды tainted. Taints на нодах Kubernetes будет препятствовать планированию подов на них. Чтобы заставить поды работать на этих нодах, необходимо добавить Tolerations, в дополнение к nodeSelector/nodeAffinity. Для получения более подробной информации см. kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration

spec:
tolerations:
— key: “key1” # Taint Key in the node
operator: “Equal”
effect: “NoSchedule”
value: “value1” # Taint value in the node


Настройка HA (Обязательно)​


Каждое развертываемое приложение должно иметь как минимум две реплики. Это позволяет распределить нагрузку и обеспечить высокую доступность приложения (следуя приведенным выше практикам).

Установка Pod Disruption Budgets (обязательно)​


Pod Disruption Budgets (PDB) дает пользователям возможность определять, сколько инстансов может быть отключено одновременно на короткий период времени из-за добровольного нарушения работы. Наиболее ярким примером добровольного нарушения работы является перевод ноды в режим обслуживания (осушение ноды) или выполнение обновлений. НЕ УСТАНАВЛИВАЙТЕ PDB, ЕСЛИ РЕПЛИКА DEPLOYMENT/STATEFULSET НЕ БОЛЬШЕ 1. В PDB пользователи могут задавать minAvailable/maxUnavailable в зависимости от типа приложения. Для получения более подробной информации см. kubernetes.io/docs/tasks/run-application/configure-pdb.

Правильные метки (обязательно)​


В Kubernetes мы можем добавлять метки почти ко всем объектам. Конечно, в большинстве случаев пользователи не придают значения маркировке. Тем не менее, метки играют важную роль для перенаправления трафика при доступе к приложениям с помощью Kubernetes Service (именно так мы и получаем доступ к приложениям, поскольку IP-адреса подов динамические). Кроме того, метки играют ключевую роль в случае, когда пользователь хочет сгруппировать/перечислить объекты. Придумайте несколько значимых и полезных меток, основанных на собственных сценариях использования.

Отключение автоматического монтирования служебных учетных записей (обязательно)​


По умолчанию, если пользователь не указывает никаких serviceAccounts для пода, то к нему будет подключен сервисный аккаунт по умолчанию. С его помощью мы сможем взаимодействовать с Kubernetes API Server. Разрешения API для служебной учетной записи зависят от плагина авторизации и используемой политики, что представляет определенный риск для безопасности. Начиная с Kubernetes 1.6+, мы можем отключить автоматическое монтирование учетной записи службы по умолчанию. Для этого необходимо установить параметр automountServiceAccountToken: false

spec:
automountServiceAccountToken: false


Привилегии подов (обязательно)​


Убедитесь, что поды имеют минимальные привилегии. Избегайте запуска контейнеров с root-правами, если только приложение не требует таких привилегий. Это приведет к рискам безопасности.

Правильная конфигурация класса хранилищ (обязательно)​


Каждый раз, когда пользователь хочет создать какой-либо PVC, требуется класс хранилища (Storage Class). Storage Class будет определять механизм присоединения PVC, тип хранилища (Disk/Fileshare), SKU хранилища (gp2/gp3/Standard_LRS/Premium_LRS и т.д.) и другие параметры. Сегодня почти все облачные провайдеры поддерживают драйверы CSI для присоединения PVC и отходят от механизма in-tree. Убедитесь, что вы используете тип SKU PVC, соответствующий сценарию использования, — это позволит контролировать возникающие затраты.

Установите volumeBindingMode: WaitForFirstConsumer — это обеспечит создание PVC в той же зоне доступности, где первоначально будет развернут под.

ПРИМЕЧАНИЕ: ЭТО ОБЕСПЕЧИТ СОЗДАНИЕ PVC В ТОЙ ЖЕ ЗОНЕ ДОСТУПНОСТИ, ЧТО И ПОД, ТОЛЬКО ВО ВРЕМЯ ПЕРВОНАЧАЛЬНОГО РАЗВЕРТЫВАНИЯ.

Установите allowVolumeExpansion: true, чтобы, когда хранилище будет заполнено, его можно было расширить. Установите правильное значение reclaimPolicy (либо Delete, либо Retain). Если вы развертываете систему на AKS, попробуйте использовать Zone Redundant Disks (на момент создания оригинальной статьи эта возможность поддерживается только в определенных регионах).

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: azuredisk-csi-waitforfirstconsumer
provisioner: disk.csi.azure.com
parameters:
skuname: StandardSSD_ZRS
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer---apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: aws-ebs-csi-sc
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: true
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer


Горизонтальное автомасштабирование подов (опционально)​


Kubernetes поддерживает Horizontal Pod Autoscaling (HPA), которое автоматические масштабирует количество реплик пода на основе заданной пользователем метрики. По умолчанию Kubernetes Metric Server предоставляет показатели CPU и памяти, однако если пользователь хочет для автоматического масштабирования использовать какую-либо другую метрику, кроме CPU/Memory, это можно сделать с помощью Prometheus Adapter.

Lifecycle Hooks (необязательно)​


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

  • PostStart: Этот хук выполняется сразу после создания контейнера. Однако нет гарантии, что хук будет выполнен до ENTRYPOINT контейнера. Обработчику не передаются никакие параметры.
  • PreStop: Этот хук вызывается непосредственно перед завершением работы контейнера из-за запроса API или события управления, такого как сбой liveness/startup probe, вытеснение (Preemption), борьба за ресурсы и другие. Обработчик PreStop должен быть выполнен в течение заданного промежутка времени. Если этого не произойдет, под все равно будет завершен в течение заданного времени, независимо от результата выполнения обработчика.


Оптимальный Grace Period (необязательно)​


По умолчанию для любого пода TerminationGracePeriodSeconds составляет 30 секунд. В большинстве случаев этого будет достаточно. Но некоторые приложения могут потребовать большего отрезка времени. В таких случаях terminationGracePeriodSeconds должен быть немного больше 30 секунд (скажем, 2-3 минуты). Однако увеличивать значение TerminationGracePeriodSeconds не нужно. Это приведет к увеличению времени завершения работы подов (особенно в случае StatefulSets, что задержит плавающие обновления) и к тому, что ноды перестанут освобождаться (в большинстве случаев это происходит в течение 10 минут).

Автоматический редеплоймент при обновлении карты конфигурации (опционально)​


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

ПРИМЕЧАНИЕ: Под будет развернут повторно только в том случае, когда конфигурация обновляется через менеджер пакетов. Ручные правки не приведут к повторному развертыванию подов (однако этого можно добиться с помощью github.com/stakater/Reloader). Ниже приведен пример подхода с использованием Helm:

spec:
template:
metadata:
annotations:
configchecksum: {{ include (print $.Template.BasePath “/configmaps.yaml”) $ | sha256sum | trunc 63 }}


Очистка завершенных заданий (необязательно)​


Jobs в Kubernetes используются для выполнения одноразовых действий. По завершении действия под (который был создан в результате выполнения задания) будет находиться в состоянии Completed. Есть две веские причины для очистки завершенных заданий и подов.

1) Это добавляет ненужную нагрузку на данные ETCD / увеличивает нагрузку на API Server

2) Завершенные поды все еще будут хранить IP-адрес, что приведет к нехватке IP-адресов. IP-адрес пода крайне важен, когда пользователи используют CNI облачного провайдера (например, AWS CNI/Azure CNI), где IP-адрес подсети выделяется как для нод, так и для подов. Начиная с версии Kubernetes 1.20+, мы можем сделать так, чтобы завершенные задания удалялись навсегда. Достаточно задать параметр .spec.ttlSecondsAfterFinished, который в качестве значения принимает число в секундах. Ниже приведен пример:

spec:
ttlSecondsAfterFinished: 60 # This will make the job to be deleted after 60 seconds of completion


Используйте .spec.ttlSecondsAfterFinished с осторожностью при выполнении развертывания с помощью инструментов GitOps, таких как ArgoCD или Flux. Это заставит задания запускаться снова и снова, даже когда они будут удалены. Также имейте это в виду, прежде чем использовать этот подход для заданий, которые выполняются при Helm Upgrades. Они должны быть достаточно толерантными, чтобы повторный запуск не вызвал никаких проблем. Если достичь этого не удается, следует избегать установки .spec.ttlSecondsAfterFinished и попробовать установить .spec.activeDeadlineSeconds. Пожалуйста, ознакомьтесь с kubernetes.io/docs/concepts/workloads/controllers/job/#job-termination-and-cleanup перед использованием .spec.activeDeadlineSeconds

Сохранение минимальной истории заданий (необязательно)​


При использовании Kubernetes CronJobs по умолчанию будет сохранено три успешно выполненных задания. Это снова приведет к той же проблеме, которая была описана выше. В этом случае мы можем сохранить минимальную историю заданий (например, только одно последнее успешно выполненное задание или даже ноль заданий), установив нужное значение в .spec.successfulJobsHistoryLimit. Аналогично CronJob будет сохранять одно последнее неудачно завершенное задание. Это можно изменить, установив желаемое значение в .spec.failedJobsHistoryLimit.

Параметр Concurrency для CronJobs (необязательно)​


По умолчанию CronJobs может планировать задания одновременно, если расписание соблюдено и предыдущее задание еще не завершено. Однако в некоторых случаях необходимо запретить планировать новое задание до тех пор, пока не завершится предыдущее. Это можно сделать, установив значение .spec.concurrencyPolicy в Forbid. Но имейте в виду, что при этом в очередь ставятся новые задания, и если выполнение предыдущего задания занимает много времени, это увеличит нагрузку на планировщик. В какой-то момент в очереди окажется слишком много заданий, и CronJob не будет запускать новые. В таком случае нам придется заново создать CronJob, чтобы заставить его работать.

Заключение​


Выше приведены некоторые рекомендации и советы, которые следует использовать при подготовке манифестов Kubernetes. Менеджер пакетов, например, Helm, станет удобным/полезным, если вы захотите управлять несколькими манифестами сразу. Он поддерживает шаблонизацию (Go Templating) и довольно активно используется для управления манифестами Kubernetes.

 
Сверху