Kubernetes — сложный инструмент, и, чтобы получить от него полную отдачу, нужно правильно его настроить и использовать.

Мы собрали 8 советов, которые помогут разработчикам и администраторам построить более стабильный, безопасный и управляемый кластер Kubernetes.

Совет 1: не используйте тег latest при развертывании приложений

При развертывании приложений через Deployment не используйте тег latest в образах контейнеров.

 

Плохо

<...> containers: - name: nginx image: nginx:latest <...>

Хорошо

<...> containers: - name: nginx image: nginx:1.18.0 <...>

Тег latest — скользящий тег, который всегда указывает на самую последнюю версию образа. Сегодня он указывает на версию v1, а через неделю это может быть версия v2. И в этом кроются две проблемы:

1. Например, мы развернули Deployment, указав тег latest. На момент создания пода этот тег указывал на версию приложения v1. Через некоторое время у приложения вышла версия v2. А еще через некоторое время отказал узел, на котором работал этот под. Kubernetes перезапустит этот под на другом узле.

Когда под перезапускается, он заново перечитывает Deployment-файл. Так как тег latest теперь ссылается на версию v2, то именно ее Kubernetes запустит внутри нового пода. И может оказаться так, что новая версия обратно несовместима или содержит ошибки. Если же вместо тега latest явно указать нужную нам версию v1, то при перезапуске пода все останется как раньше.

2. Другой пример. Мы развернули Deployment, снова указав тег latest в имени образа. В тот момент этот тег ссылался на версию приложения v1, а через некоторое время у приложения вышла версия v2. Затем мы решили изменить Deployment и развернули его новую версию.

Но что-то пошло не так: приложение не запускается или работает с ошибками. Если бы мы указали явно нужную нам версию, мы бы могли использовать команду rollout и откатиться на предыдущую версию стандартными средствами Kubernetes. Но так как тег latest уже ссылается на новую версию приложения, rollout не поможет: Kubernetes снова развернет версию приложения v2.

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

Ссылка на документацию.

Совет 2: используйте пространства имен для разделения ресурсов

Kubernetes позволяет разделять ресурсы кластера на пространства имен, это своего рода отдельные виртуальные кластеры. Рекомендуется разделять Namespace:

  • по разным командам,
  • по разным проектам/продуктам,
  • по разным средам: разработка, тестирование, продакшен.

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

В небольших кластерах это делать необязательно, но в крупных очень желательно. Разделение по пространствам имен дает несколько преимуществ:

  • Понятное разделение ресурсов. Если в одном Namespace собраны все объекты со всех проектов компании, разработчикам сложнее ориентироваться. Скорее всего, они не будут использовать и половины всех ресурсов, но им придется разбираться, что нужно, а что нет.
  • Минимизация случайных ошибок. Если в кластере работают несколько разных команд, они могут случайно что-нибудь испортить, перезаписать или удалить ресурсы друг друга.
  • Повышение безопасности. Если разделять пространства имен по средам и окружениям, то можно разделять доступы команд. Например, разработчикам достаточно иметь доступ к dev-системе, но им ни к чему видеть продуктивные данные. Команде тестировщиков нужен доступ к системе тестирования, возможно к продакшену, но им ни к чему система разработки.

Ссылка на документацию.

Kubernetes как сервис с сертификацией CNCF
Беспроблемная доставка ваших приложений
Перейти

Совет 3: используйте метки и аннотации для классификации объектов

Метки и аннотации — это параметры типа «ключ/значение», которые позволяют добавить метаданные к любым объектам Kubernetes: узлам, подам, сервисам и так далее. Их нужно указывать на всем, что деплоится в кластер.

Метки служат для идентификации объектов, позволяют группировать их и массово ими управлять. Например, вы можете пометить все объекты, относящиеся к одному приложению, окружению или команде.

Пример указания меток в деплойменте:

<...>
metadata:
  name: label-demo
  labels:
    environment: production
    app: nginx
<...>

В этом примере мы помечаем, что данный объект Kubernetes относится к приложению nginx и работает в продакшене.

Можно создавать любые метки, которые вам нужны. Но также есть список меток, которые рекомендуется добавлять ко всем объектам:

app.kubernetes.io/nameИмя приложения
app.kubernetes.io/instanceУникальное имя экземпляра приложения
app.kubernetes.io/versionТекущая версия приложения (например, семантическая версия, хеш коммита и т. д.)
app.kubernetes.io/componentИмя компонента в архитектуре
app.kubernetes.io/part-ofИмя основного приложения, частью которого является текущий объект
app.kubernetes.io/managed-byИнструмент управления приложением

Метки упрощают управление инфраструктурой. Самый простой вариант использования — получить список объектов, у которых есть определенные метки. В этом примере мы получаем список подов, которые запущены в продакшене и относятся к фронтенду:

kubectl get pods -l environment=production,tier=frontend

Другой вариант — при развертывании подов можно указать, что их нужно запускать только на узлах с определенной меткой. В этом примере Deployment-файла мы позволяем подам запускаться только на узлах с меткой node=mynode:

<...>
spec:
  nodeSelector:
    node: mynode 
<...>

Аннотации позволяют хранить любую служебную информацию, которая может быть полезна командам DevOps. Например, в аннотацию пода можно записывать идентификатор создавшего его пайплайна, ссылку на репозиторий с приложением или контакты ответственного разработчика.

Пример указания аннотаций в деплойменте:

<...>
metadata:
  name: quote
  annotations:
    owner: "@ivan_ivanov"
    repository: "https://github.com/..."
    pipelineId: 123
<...>

Ссылки на документацию по меткам и аннотациям.

Совет 4: устанавливайте запросы и ограничения на ресурсы

Kubernetes позволяет устанавливать для подов запросы и ограничения, которые помогают лучше управлять ресурсами кластера: CPU и RAM. Они указываются в Deployment-файле.

Запросы (requests) — это ресурсы, которые контейнер гарантированно получит. То есть это своего рода минимальные системные требования, которые нужны приложению для работы. Kubernetes всегда планирует размещение подов на узлах так, чтобы обеспечить необходимые запросы для всех подов.

Ограничения (limits) — это максимальные значения выделяемых ресурсов. Kubernetes гарантирует, что приложение никогда не получит больше ресурсов, чем указано в ограничениях.

Если под попытается превысить ресурс, Kubernetes поступит по-разному в зависимости от типа ресурса.

  • CPU — сжимаемый ресурс, его можно искусственно ограничить. Поэтому если под запросит больше CPU, чем указано в ограничениях, он их просто не получит. Возможно, это скажется на производительности приложения, но оно продолжит работать.
  • RAM — несжимаемый ресурс, его невозможно ограничить. Ведь если приложению просто не выделить запрашиваемую память, оно, скорее всего, перестанет нормально работать. Поэтому если под запросит больше памяти, чем указано в ограничениях, Kubernetes перезапустит под.

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

<...>
spec:
  containers:
    - name: app
      image: images.my-company.example/app:v4
      resources:
        requests:
          memory: "64Mi"
          cpu: "250m"
        limits:
          memory: "128Mi"
          cpu: "500m"
<...>

В этом примере приложение гарантированно получит 64 Мб оперативной памяти и 250 миллиядер. Максимальные значения, которые Kubernetes не превысит — 128 Мб памяти и 500 миллиядер.

Механизма запросов и ограничений должно быть достаточно, чтобы корректно управлять ресурсами кластера. Но часто бывают ситуации, когда разработчики забывают указывать requests и limits или указывают их явно высокими, чтобы приложение работало наверняка. В результате Kubernetes может выделять приложениям излишние ресурсы. Поэтому администраторы могут ограничить полнстью пространство имен при помощи ResourceQuota.

ResourceQuota — это аналог limits и requests, но только для целого пространства имен. Это суммарные ограничения, которые действуют на все контейнеры, работающие внутри Namespace. Рассмотрим пример:

<..>
spec:
  hard:
    requests.cpu: 500m
    requests.memory: 128Mib
    limits.cpu: 700m
    limits.memory: 512Mib 
<..>

Что тут происходит:

  1. Все поды в сумме обязательно получат 500 миллиядер и 128 Мб оперативной памяти. Например, можно создать два пода со значениями request по 250 миллиядер и 64 Мб оперативной памяти. Но еще один под создать не получится — Kubernetes не позволит.
  2. Все поды в сумме ограничены 700 миллиядрами и 512 Мб оперативной памяти.

Есть несколько вариантов практического применения ResourceQuota. Например, в одном кластере работают две команды разработчиков, у каждой из которых свой Namespace. Можно наложить ограничения, чтобы одна команда не могла занять больше половины ресурсов. Другая хорошая практика: ограничить пространства имен для сред разработки и тестирования, но не ограничивать продуктивную.

Ссылка на документацию

Совет 5: не управляйте подами вручную — используйте Deployment

Под — минимальная единица, которой оперирует Kubernetes. При желании можно управлять подами вручную: создавать, запускать и останавливать. Но это не лучший способ, потому что Kubernetes не следит за «ручными» подами. В этом случае пропадают многие возможности автоматизации. Вместо ручного управления подами всегда используйте Deployment.

Вот что плохого в ручном управлении подами:

  • Kubernetes не сможет перезапускать поды. Если узел, на котором работает под, выйдет из строя, Kubernetes не будет автоматически переносить эти поды на другие узлы. Это придется делать вручную.
  • Kubernetes не сможет выполнять Rolling Update. Это возможность обновить приложение без простоя — Kubernetes по очереди перезапускает поды с контейнерами так, что в любой момент времени приложение остается доступным. Если же управлять подами вручную, то обновление подов также станет ручной операцией.

Ссылка на документацию

Совет 6: используйте проверки работоспособности приложений

В Kubernetes есть механизм, который позволяет определять работоспособность ваших приложений. Kubernetes отправляет приложению запрос (HTTP-запрос, TCP-соединение или произвольная команда) и ждет ответа. Если приложение отвечает, что с ним все хорошо, значит, проверка прошла успешно. Если приложение не отвечает или отвечает с ошибкой, значит, проверка работоспособности не прошла. И тогда в зависимости от типа проверки Kubernetes поступает по-разному.

Всего есть три типа проверок:

  1. Startup Probe: позволяет убедиться, что приложение запустилось. Когда под запущен, Kubernetes считает, что приложения внутри него уже работают и могут обрабатывать запросы. Но некоторые приложения запускаются долго. Если Kubernetes отправит им запросы до того, как они запустятся, запросы не обработаются. Чтобы избежать таких ситуаций, существуют Startup Probe. Они сообщают Kubernetes о готовности приложения после запуска, и только после этого Kubernetes будет считать, что они работают.
  2. Liveness Probe: позволяет перезапустить зависшее приложение. Бывают ситуации, когда приложение зависает и не отвечает на запросы, тогда его нужно перезапустить. В этом помогает Liveness Probe: если проверка завершится с ошибкой, то Kubernetes перезапустит этот под.
  3. Readiness Probe: позволяет проверить, готово ли приложение принимать трафик. Например, наше приложение должно как-то обрабатывать данные в БД. Но по какой-то причине база данных не отвечает конкретному инстансу приложения. Получается, что приложение не может правильно работать. В этом случае мы настраиваем Readiness Probe так, чтобы оно проверяло коннект с БД. Если проверка завершится с ошибкой, Kubernetes не будет перенаправлять трафик на этот под, при этом под не будет перезапущен. Главное отличие от Liveness Probe в том, что Readiness Probe не перезапускает приложение, а просто ждет, когда проверка снова будет проходить успешно.

Ссылка на документацию

Совет 7: минимизируйте образ и убирайте ненужные зависимости

Для создания собственных приложений за основу часто берут готовые образы других приложений и сред, они называются родительскими образами. Например, при разработке приложения на Node.js часто используется публичный официальный образ из Docker Hub. Также само приложение может иметь множество зависимостей в виде npm-пакетов.

В Kubernetes рекомендуется минимизировать образы ваших программ и их зависимостей. Это даст несколько преимуществ:

  1. Повышение безопасности

    В любом образе или зависимости могут быть уязвимости. Чем меньше зависимостей, тем меньше векторов для возможных атак.

  2. Уменьшение места для хранения

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

    Особенно это актуально для крупных компаний, где много разных команд и приложений. Есть реальные случаи, когда команды создавали образы по 1–2 Гб, хотя «полезного» веса в них было всего на 200–300 Мб. Если каждая команда будет создавать такие огромные образы, то компании понадобится очень много места для их хранения.

  3. Ускорение запуска приложений

    Если узел с запущенным приложением упадет, Kubernetes перезапустит его на другом узле. При этом он заново скачает образ. Если образ весит много, приложение будет долго запускаться. Есть большая разница в скорости развертывания образа в 300 Мб и 2 Гб. Это может быть особенно важно, если вашим приложениям важна высокая скорость развертывания.

    К тому же если ваш облачный провайдер взимает плату за трафик, то придется платить лишние деньги за скачивание публичных образов из Docker Hub, npm-пакетов или других зависимостей. Mail.ru Cloud Solutions предоставляет бесплатный входящий и исходящий трафик при использовании управляемого Kubernetes, но так бывает не всегда.

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

  • Стандартная версия основывается на дистрибутиве Debian плюс полная версия программы или среды: Python, Node.js и так далее.
  • Версия Slim также основывается на дистрибутиве Debian, но сама программа или среда урезана, она состоит лишь из базовых компонентов.
  • Версия Alpine похожа на Slim, но вместо Debian используется минималистичный дистрибутив Alpine Linux.

Для примера, стандартный образ Node.js весит более 900 Mб, версия Slim — 160 Mб, а версия Alpine — 112 Mб.

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

Совет 8: указывайте Pod Disruption Budget для своих приложений

Pod Disruption Budget (PDB) — это минимальное количество реплик приложения, которые Kubernetes постарается сохранить для стабильной работы. PDB защищает приложение от некоторых, но не всех типов простоев. Существует два типа причин простоев:

  • Непреднамеренный сбой происходит случайно из-за ошибки ПО или сбоя оборудования. Например, выход узла из строя, недоступность сети или Kernel Panic в ядре.
  • Добровольное прерывание происходит осознанно по инициативе администратора или разработчика. Например, Rolling Update для выпуска новой версии приложения или отключение узла для техобслуживания/обновления.

Важно понимать, что PDB сработает только в случае добровольных прерываний и не поможет при непреднамеренных сбоях.

Поясним на примере. Допустим, приложение имеет шесть реплик. Минимально для стабильной работы ему необходимо две реплики, иначе система начнет сильно тормозить. Остальные четыре реплики нужны для улучшения производительности, но они не слишком критичны. При помощи PDB мы можем как бы сказать Kubernetes: «Если ты будешь добровольно перезапускать приложение, оставь как минимум два рабочих пода». И теперь если администратор захочет вывести узел из обслуживания (например, для обновления), то он не сможет этого сделать, пока Kubernetes не разместит все приложения согласно их политикам Pod Disruption Budget. Если хотя бы одно приложение не «переедет», то Kubernetes не отдаст узел до тех пор, пока он не разместит приложение на других узлах.

Пример создания PDB:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

В этом примере мы создаем PDB и указываем, что у приложения ZooKeeper должны быть минимум две рабочие реплики. Вместо конкретных значений можно указывать проценты, например 20% или 50%.

Ссылка на документацию

Мы рассмотрели 8 практических советов по работе с кластером Kubernetes. Если следовать им, то работа с технологией станет удобней, а сам кластер безопасней и стабильнее.

Если в вашей компании много команд, то на основе этих советов можно создать собственные шаблоны для разработки облачных приложений. Это позволит распространять Best Practices среди всех команд в компании.