Команда VK Cloud перевела статью о проблемах в Kubernetes, с которыми часто сталкиваются инженеры-разработчики при запуске новых масштабируемых отказоустойчивых веб-сервисов. 

Задача 1: как запустить контейнер


Начнем с простого. Вы работали над кодом проекта под названием kangaroo, и сейчас он готов к деплойменту. Для его запуска вы создаете образ контейнера — kangaroo-backend. У вас на работе сервисы запускаются в кластере Kubernetes, так что вам нужно выполнить деплоймент этого контейнера в кластер. Как это сделать? Нужно определить под. Под — это набор из одного или нескольких контейнеров, минимальная единица деплоймента в Kubernetes. Давайте определим под, в котором работает наш контейнер:

apiVersion: v1
kind: Pod
metadata:
  name: kangaroo-backend
spec:
  containers:
  - name: kangaroo-backend
    image: kangaroo-backend:1.14.2
    ports:
    - containerPort: 80

Этот YAML определяет новый ресурс Kubernetes, деплоймент которого можно выполнить в кластере. Все ресурсы, независимо от того, что вы развертываете, имеют четыре свойства:

  1. apiVersion — какую версию определения ресурса Kubernetes использовать. У определения ресурсов есть версия, так что Kubernetes может позже изменить его, никому не мешая пользоваться старой версией. Когда вы пишете API, вы наверняка в самом начале указываете «v1» на случай, если позже решите внести изменения, несовместимые с предыдущими версиями. Так вот, Kubernetes делает то же самое.
  2. kind — все ресурсы относятся к определенному виду, в этом случае к поду.
  3. metadata — пары «ключ — значение», которые можно использовать для запросов и организации ресурсов в кластере.
  4. spec — подробная информация о том, деплоймент чего вы выполняете. Она есть у каждого вида. Например, у вида ресурса «под» должен быть ключ containersс массивом контейнеров, у каждого из которых есть image.

Мы деплоим под, то есть один или несколько контейнеров, поэтому задаем kind: Pod и должны указать правильные spec. Здесь наша спецификация говорит нам, что мы запускаем один контейнер с образом kangaroo-backend:1.14.2. Еще мы информируем Kubernetes о том, что контейнер будет ожидать передачи данных от порта 80.

Если сохранить его как pod.yaml, можно выполнить деплоймент, запустив kubectl apply -f pod.yaml. Поздравляю, вы только что выполнили деплоймент контейнера в кластер Kubernetes.

Задача 2: чем занят мой под


Итак, вы выполнили деплоймент пода. Отлично. Он работает? Он делает то, что вы задумали?

Проверить под можно с помощью kubectl. Я предлагаю переименовать = kubectl в псевдоним k в вашем терминале, поскольку вы будете часто его использовать. Можно выполнять команду k get pods, чтобы просматривать список подов, деплоймент которых вы выполнили. k describe pod "ID" выдает дополнительную информацию об этом поде. Если логи пода отправляются в stdout/stderr, используйте k logs "ID" для просмотра логов. Если вы не очень представляете себе, что делать с этим подом, то можно с помощью SSH выполнить команду k exec "pod ID" -it -- /bin/bash.
Дополнительные советы на эту тему можно найти на странице Debug Running Pods.

Задача 3: как перезапустить контейнер после сбоя


С проектом kangaroo вроде все нормально, но раз в несколько дней он сбоит. Это важный сервис, так что нужно, чтобы после сбоя он перезапускался. Если бы это был обычный процесс Linux на обычном Linux-сервере, для его перезапуска можно было бы использовать shell script или systemd. Но как организовать автоматический перезапуск пода в Kubernetes?

Здесь нам на выручку приходят весомые преимущества Kubernetes: это декларативная, а не императивная система. Вы декларируете, что этот под должен работать. И если когда-нибудь это окажется не так, Kubernetes все исправит. Так что в случае сбоя пода он перезапустится, потому что таким образом восстановится состояние, которое вы декларировали в манифесте. Можно варьировать настройки, меняя политику перезапуска пода, но весьма разумно применять параметры по умолчанию: всегда перезапускать поды с экспоненциальной задержкой — через 10 секунд, потом через 20, 40 и так далее).

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

Задача 4: нужно, чтобы контейнер работал без простоев даже в случае сбоя


Kubernetes перезапускает бэкенд проекта kangaroo в случае сбоя, но перезапуск подразумевает несколько секунд простоя. А даже кратковременный простой нас не устраивает. Особенно если потенциально может запуститься аварийный цикл, при котором сервис недоступен несколько минут. Вам бы очень хотелось, чтобы бэкенд оставался доступным даже во время сбоев и перезапусков.

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

В Kubernetes такие вещи делаются с помощью инструмента Deployments.
 
# Standard Kubernetes fields for all resources: apiVersion, kind, metadata, spec
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kangaroo-backend-deployment
  labels:
    app: kangaroo-backend

# The `spec` is where the Deployment-specific stuff is defined
spec:
  replicas: 3

  # The `selector` defines which pods will get managed by this deployment.
  # We tell Kubernetes to replicate the pod matching the label `app: kangaroo`
  selector:
    matchLabels:
      app: kangaroo
      role: backend

  # The `template` defines a pod which will get managed by this deployment.
  template:
    metadata:
      # Make sure the pod's labels match the labels the deployment is selecting (see above)
      labels:
        app: kangaroo
        role: backend
    # The Pod has its own spec, inside the spec of the Deployment. 
    # This spec defines what containers the Pod should run.
    spec:
      containers:
      - name: kangaroo-backend
        image: kangaroo-backend:1.14.2
        ports:
        - containerPort: 80

Deployments управляет набором подов. В нашем случае это все поды с меткой app: kangaroo. Для этого инструмент гарантирует наличие в любой момент времени трех реплик этого пода. Мы также можем вложить определение пода внутрь Deployments.

Если применить этот манифест в Kubernetes, будет создан Deployments. Однако при этом не создаются необходимые поды, так что кратковременно не будет работать ни одна реплика. Но напомним, что Kubernetes — это декларативная, а не императивная система. Так что он заметит, что его кластер не соответствует задекларированному состоянию, и предпримет действия, чтобы это исправить — то есть запустит новую реплику. После этого Kubernetes заново оценивает состояние кластера, замечает, что подов все еще недостаточно, и создает новую реплику. Эти действия повторяются, пока система не будет соответствовать задекларированному состоянию.

Задача 5: контейнер не справляется с объемом поступающего трафика


Проект kangaroo набирает популярность, и на его бэкенд поступает слишком много трафика. Вы уже знаете решение проблемы — это реплики. Они могут сделать сервис и более производительным, и более стабильным. К счастью, вы уже умеете пользоваться Deployments в Kubernetes, чтобы создавать реплики контейнеров. Так что вы просто меняете спецификации в этом инструменте, увеличивая число реплик. Теперь ваш сервис может масштабироваться в случае увеличения трафика.

Еще есть эластичное масштабирование: вы на программном уровне определяете некое свойство, например «запросы на реплику в секунду не должны превышать 1000», и Kubernetes автоматически подбирает необходимое количество реплик. Это называется горизонтальным автомасштабированием. Сейчас, впрочем, речь не о нем, но вы можете подробнее изучить его в этой статье.

Задача 6: как развернуть новую версию бэкенда без простоя


Каждую неделю у вас выходит новая версия бэкэнда kangaroo. Когда вы впервые выпускали обновление, вы просто удалили старый деплоймент и создали новый в виде более свежей версии контейнера kangaroo-backend. Это сработало, но система простаивала несколько минут — а ведь это именно то, чего вы стремитесь избежать. Как обновить kangaroo с 15-й до 16-й версии без простоя?

Это настолько распространенная проблема, что в Deployments в Kubernetes уже заложили поддержку решения. Оно называется обновление деплоймента. Выполнить его несложно. Просто отредактируйте деплоймент, либо поменяв kangaroo-backend:1.15 на kangaroo-backend:1.16, либо с помощью команды kubectl edit CLI, либо  отредактировав манифест YAML, заменив образ контейнера и перезапустив команду kubectl apply. Здесь снова проявляется декларативное управление Kubernetes. K8s замечает, что актуальный деплоймент для контейнера — kangaroo-backend:1.15 — это не задекларированный kangaroo-backend:1.16, и с определенной периодичностью он будет:

  1. замечать, что фактическое состояние кластера не соответствует заявленным спецификациям: недостаточно реплик для версии 1.16;
  2. запускать новый под в версии 1.16;
  3. если под запускается как задумано, Kubernetes заменяет в деплойменте старый под;
  4. замечать под kangaroo-backend:1.15, который не относится ни к какой заявленной спецификации, и останавливать его, чтобы привести систему в правильное состояние.



Kubernetes повторяет эти действия до тех пор, пока не исчезнут все устаревшие и не останутся только актуальные поды.

Задача 7: как направить трафик на все поды в деплойменте


Каждый под получает собственный IP-адрес кластера. Он имеет смысл только в рамках этого кластера Kubernetes: другим кластерам, как и всему интернету, ничего не известно об этом IP. С помощью этого адреса с фронтенда kangaroo можно направлять трафик на бэкенд.

Все это прекрасно работает, пока не случается перезапуск пода или не появляется Deployments с несколькими подами, у каждого из которых будет свой IP-адрес. Как фронтенду узнать, на какой IP-адрес направлять трафик для kangaroo-backend? Пора обратиться к новому типу объекта Kubernetes, называемому Service.

# The kangaroo-backend Service.
# Other services inside the Kubernetes cluster will address the
# kangaroo-backend containers using this service.
apiVersion: v1
kind: Service
metadata:
  name: kangaroo-backend
  labels:
    app: kangaroo-backend
spec:
  ports:
    # Port 8080 of the Service will be forwarded to port 80 of one of the Pods.
    - port: 8080
      protocol: TCP
      targetPort: 80
  # Select which pods the service will proxy to
  selector:
    app: kangaroo
    role: backend

При развертывании Service у вас в кластере Kubernetes возникает DNS-запись, нечто вроде my-svc.my-namespace.svc.cluster-domain.example или kangaroo-backend.kangaroo-team.svc.mycompany.com. Объект Service отслеживает, какие поды соответствуют его селекторам. Каждый раз, когда служба получает пакет TCP в свой port, она направляет его на targetPort соответствующего пода.

Итак, теперь фронтенд kangaroo просто направляет свои API-запросы на имя хоста внутри кластера kangaroo-backend.kangaroo-team.svc.mycompany.com, и сопоставитель DNS-кластера сравнивает его с Service kangaroo, чтобы перенаправить на доступный под. Если у вас есть деплоймент, он проследит, чтобы у вас хватало подов для обработки трафика. 

И Services, и Deployments выбирают поды с определенным набором меток, но выполняют разные обязанности. Services занимаются балансировкой и обнаружением трафика, Deployments следят за тем, чтобы поды существовали в нужном количестве и в нужной конфигурации. В основном у бэкендов и серверов в Kubernetes есть один Service и один Deployment, для которых заданы одинаковые метки.

Задача 8: балансировка нагрузки между подами


Мы только что выяснили, как в Services Kubernetes организована работа с набором подов, если они входят в этот набор или выходят из него — например, при перезапуске или обновлении. Теперь поговорим о балансировке нагрузки.

Если вы используете Services Kubernetes для направления трафика в приложение, вы получаете базовый вариант балансировки нагрузки бесплатно. Если под перестает работать, Services больше не направляет в него трафик, зато начинает использовать другие поды с соответствующими метками — как вариант, поды в том же Deployment. Если другие поды в кластере используют имя хоста DNS Services, их запросы будут направляться на доступный под.

Задача 9: как принимать трафик из-за пределов кластера


Пока ваши Services принимали трафик от фронтенда внутри кластера с помощью имени хоста Services. Но эти IP и DNS-записи воспринимаются только в кластере. Это значит, что другие службы у вас в компании могут без проблем направлять трафик на бэкенд kangaroo. Но как принимать трафик из-за пределов кластера, например интернет-трафик от заказчиков?
Нужно развернуть два типа ресурсов: Ingress и IngressController.

  1. Ingress определяет правила для сопоставления HTTP/S-трафика за пределами кластера со службами внутри него. Отсюда Services направляет его на под, как мы уже убедились выше.
  2. IngressController выполняет эти правила. Вполне возможно, что у вас в компании уже настроен Ingress Controller для вашего кластера. В проекте Kubernetes поддерживаются три отдельных контроллера IngressController (AWS, GCE и Nginx), но есть и другие популярные варианты, например Contour.

Возможно, настройка IngressController — это задача для специализированной команды по развитию платформы, которая отвечает за Kubernetes, или облачной платформы, которую вы используете. Так что на этой теме мы останавливаться не будем. Вместо этого поговорим о самом Ingress, который определяет правила сопоставления внешнего трафика с вашими службами.

Здорово, что вы можете определять правила Ingress, работающие на абстрактном уровне с любым отдельно взятым IngressController. Разные проекты могут предлагать конкурирующие контроллеры, каждый из которых совместим с одним и тем же Kubernetes API.

Рассмотрим код:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
spec:
  ingressClassName: kangaroo
  rules:
  - http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: kangaroo-backend
            port:
              number: 8080

Это создает Ingress с помощью контроллера, выбранного для нашего кластера по умолчанию. У Ingress есть одно правило: он будет сопоставлять трафик, путь которого начинается с /api, с нашим бэкендом kangaroo. Если нужно, мы можем задать и другие правила, например map /admin для службы администрирования. Трафик, который не подпадает ни под какие правила, обрабатывается настройками IngressController по умолчанию.

Задача 10: как ограничить работу сети в кластере


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

Один из простых вариантов решения — это использовать Network Policies Kubernetes, чтобы ограничить разрешения на доступ к сети для вашего проекта. Можно использовать сетевые политики:

  • Ingress — от кого ваша служба может получать трафик;
  • Egress — кому ваша служба может направлять трафик.

С точки зрения безопасности лучший подход — это по умолчанию запретить все сетевые взаимодействия в пространстве имен, а потом сделать несколько исключений для сетевых подключений, которые нужны сервису. Рассмотрим на примере, как разрешить некоторые сетевые подключения для kangaroo. Обратите внимание, что для Ingress и Egress действуют разные правила. У каждого Ingress-правила есть две части:

  1. from говорит, какие сервисы могут отправлять трафик;
  2. ports говорит, на какие порты можно отправлять трафик.

В Egress-правилах есть to и ports, с помощью которых определяется, куда сервис может отправлять трафик и какие порты в этих точках назначения разрешены.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: kangaroo-network-policy
  labels:
    app: kangaroo
    role: network-policy
spec:
  podSelector:
    matchLabels:
      app: kangaroo
      role: backend
  policyTypes:
    - Ingress
    - Egress
  # Each element of `ingress` opens some port of Kangaroo to receive requests from a client.
  ingress:
    # Anything can ingress to the public API port
    - ports:
        - protocol: TCP
          port: 80
    # Hypothetical example: say your Platform team has configured cluster-wide metrics,
    # you'll need to grant it access to your pod's metrics server. Your company will
    # have examples for this. Assume kangaroo-backend serves metrics on TCP :81
    - from:
        - namespaceSelector:
            matchLabels:
              project: monitoring-system
      ports:
        - protocol: TCP
          port: 81
  # Each element of `egress` opens some port of Kangaroo to send requests to some server
  egress:
    # Say the Kangaroo backend calls into a membership microservice.
    # You'll need to allow egress to it!
    - ports:
        - port: 443
      to:
        - namespaceSelector:
            matchLabels:
              project: membership
              role: api

Задача 11: как использовать HTTP/S за пределами кластера и HTTP внутри кластера


Управление TLS-сертификатами может быть головной болью: заниматься созданием, управлением, развертыванием и восстановлением этих сертификатов не слишком интересно. А если что-то пойдет не так, можно случайно отключить весь трафик, поступающий на ваш сервис. Так что если вы уже настроили сетевые политики, вы не станете использовать TLS в конкретном кластере или пространстве имен. Поговорите со своей командой, чтобы определить, что для вас лучше.

Если вы решили не использовать TLS, вы, возможно, все-таки предпочтете разрешить HTTP/S для внешнего трафика. В конце концов, в интернете же нет пространств имен или сетевых политик Kubernetes. Так что, настроив Ingress, можно обрубить HTTP/S до того, как он начнет отправлять запросы к вашему сервису. Это значит, что внешние заказчики выполняют взаимодействие TLS с вашим Ingress, а затем ваш Ingress направляет plaintext HTTP вашему сервису.

Задача 12: как получать метрики своего сервиса


Возможно, вам нужно собирать метрики задержки вашего сервиса, количестве ответов HTTP класса 200/400/500 и так далее. Вы можете сами снабдить сервер инструментами вроде Prometheus, OpenMetrics или Honeycomb. И это вроде бы неплохо, но возникают две проблемы:

  1. Если сервис перестал работать, вы не сможете получить с него метрики.
  2. Вы будете прописывать одинаковые метрики для каждого развертываемого сервиса. Все это превращается в утомительную рутину.

Но большинство IngressController собирают всевозможные метрики. Например, Contour отслеживает каждый HTTP-запрос, который он направляет к сервису, и собирает данные по задержкам и статусе HTTP. Потом он передает их в метрики Prometheus, где их можно немного отполировать и визуализировать. Это действительно удобно, потому что не нужно тратить время на самостоятельную обработку. Вы будете знать, когда сервис вышел из строя, потому что метрики Contour для этого NameSpaces или Services покажут вам пиковую задержку или увеличение запросов HTTP в 5xx раз.

Задача 13: как изолировать друг от друга проекты в одном кластере


С вашим проектом Kubernetes все хорошо. Вы запустились, все в восторге. CTO подходит с вопросом: «Ну как Kubernetes?» С трудом сдерживая слезы счастья, вы отвечаете: «Неплохо». Ваши коллеги покупают вам в подарок кружку с надписью Best YAML Writer.

Другие отделы берут на заметку ваш успех и начинают переносить свои проекты на Kubernetes. И вот в один прекрасный день они подходят к вам с жалобой, что ваш деплоймент сломал им их деплоймент. Как?! Что случилось?

Выясняется, что вы написали Deployment, который соответствует любым подам с метками role: backend. Это никому не мешает, если кроме вашей команды никто больше не работает с Kubernetes: вы пишете шаблон пода с меткой role: backend и в Deployment создаются его реплики. А что будет, если и другая команда выполнит развертывание пода role: backend? Ваш Deployment будет соответствовать этому поду и начнет создавать реплики из проекта другой команды! Что же делать?

Один вариант — всегда добавлять к меткам небольшой префикс, чтобы вместо backend ваши команды договорились использовать метки kangaroo-backend и emu-backend. Для этого все сотрудники должны дисциплинированно соблюдать правила. Но даже если все с этим согласны, рано или поздно кто-то допустит ошибку, опечатку и сделает деплоймент с неправильной меткой. Это может привести к серьезным проблемам. Представьте, что будет, если вы случайно создали реплику сервиса, которую создавали как одинарный объект.

К счастью, в Kubernetes есть встроенный механизм, позволяющий изолировать команды друг от друга. Он называется NameSpaces. С его помощью можно изолировать друг от друга команды или проекты так, чтобы их имена не пересекались. Примерно как разные типы могут экспортировать одноименные методы, например String::new и File::new. Просто используйте --namespace в kubectl CLI, чтобы выбрать, в какое пространство имен необходимо сделать деплоймент.

Обычно используется два NameSpaces на команду, например datalossprevention-staging и datalossprevention-production. Потом, во время развертывания изменений в Kubernetes, мы можем развернуть их в Staging, проверить, работают ли они, и выпустить в продакшен. К этому моменту мы точно будем знать, что это не мешает другим командам.

Заключение


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

Подытожим:

  1. Под — минимальная единица деплоймента. В нем может быть один или несколько контейнеров.
  2. Создавайте реплики подов с помощью Deployments. Это дает вам устойчивость к сбоям, обновление без простоев и масштабирование при увеличении трафика.
  3. Балансируйте нагрузки между подами в Deployments с помощью Service.
  4. Ограничивайте доступ к сети пода с помощью сетевой политики, чтобы сократить область уязвимости.
  5. Направляйте внешний трафик в под с помощью Ingress:
    а) В правилах Ingress определенный вид трафика (протокол, URL и т. п.) сопоставляется с определенной службой.

    b) Правила выполняются IngressController. Возможно, администраторы кластера уже настроили его у вас.
  6. Разделяйте команды и проекты с помощью namespaces.
Попробуйте Kubernetes as a Service на платформе VK Cloud. Мы даем новым пользователям 3000 бонусных рублей на тестирование кластера или любых других сервисов.

Комментарии (1)


  1. AYamangulov
    19.01.2023 09:59
    +2

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