Вам не нужен свой Kubernetes
Как DevOps инженер я имею опыт установки и поддержки vanilla kubernetes (k8s) на bare metal, опыт построения private cloud вокруг такого k8s, а также опыт использования различных public cloud таких как EKS (Amazon Managed Kubernetes), GKE (Google Managed Kubernetes), AKS (Azure Kubernetes Service). На данном этапе карьеры я очень часто тыкаю k8s, который развернут с помощью kops внутри AWS инфраструктуры.
Disclaimer. Статья основана на опыте эксплуатации различных k8s кластеров, как managed, предоставляемых различными облачными провайдерами, так и собственноручно развернутых на базе тех же провайдеров, а также на bare metal. Преимущественно материал основывается на опыте эксплуатации k8s кластера развернутого посредством kops. В статье не рассматриваются аналоги kops
Пара слов о наших k8s кластерах и используемой инфраструктуре
В качестве провайдера инфраструктуры используется AWS
Kubernetes разворачивается посредством kops. Kops это проект с открытым исходным кодом, позволяющий развернуть k8s в публичных и не только (функциональность OpenStack находится в бете) облаках. Чем же отличается kops от vanilla k8s? Kops, по сути, связывает vanilla k8s с провайдером инфраструктуры. Как пишут сами авторы проекта, kops можно воспринимать в виде kubectl для kubernetes кластера. Средствами kops вы описываете k8s кластер с помощью декларативного подхода в виде yaml манифестов, либо императивного — создаете кластер из командной строки, также существует вариант конфигурации через terraform. Таким образом, используя kops, вы конфигурируете как сам k8s кластер, так и облако в котором будет развернут данный кластер (создание необходимых сервис аккаунтов, ролей, VPC и т.д). На практике данный инструмент свою функцию выполняет, однако, таит в себе множество недоработок. Но о них чуть позже.
В поддержке находится около 11–12 k8s кластеров
Как используются k8s кластера?
Развертывание бизнес приложения на микросервисной архитектуре
CI/CD (Jenkins + Spinnaker)
On-Demand окружения для запуска рабочей dev версии всего продукта
На всех кластерах присутствуют некоторые виды stateful сервисов (в основном для мониторинга + несколько для бизнес сервисов)
Предисловие
Как в нашей компании выстроен процесс обновления k8s?
Обновление всегда затрагивает 3 компонента:
Обновлении версии kops
Обновление версии k8s
Обновление базового image для k8s nodes — в нашем случае flatcar
Проверяем release notes, поднимаем версию всех компонентов, тестируем на dev кластере. Процесс тестирования не описываю (часть покрыта автоматическими тестами, что-то оценивается просто визуально)
Если все ок, обновление поэтапно раскатывается на дев кластера для разработчиков, потом — on-demand, cicd и prod
Весь процесс автоматизирован через Jenkins + Spinnaker
История 1. Эксплуатация kops
Данная история — это агрегация некоторых недостатков kops, c которыми я столкнулся в ходе его эксплуатации.
Первая странность произошла при обновлении k8s (kops) на версию 1.23, когда неожиданно пропали все метрики по контейнерам. Оказалось, что дело в использовании read-only порта kubelet 10255 для сбора метрик агентами на наших кластерах (более подробно о метриках контейнеров). Да-да, это неправильно использовать kubelet для сбора метрик и нужно бы пользоваться metric-server, а не конфигурировать как попало. Однако вернемся к Kops. В релизе 1.23 read-only-port просто отключили по умолчанию. Как же так получилось, что kops сделал такое важное изменение, которое никак не отразилось в Release Notes? Да все очень просто, это изменение прошло совершенно не под тем именем, поскольку было сделано в рамках рефакторинга в скоупе одной из задач. Kops перестал использовать deprecated способ объявления конфигурации kubelet. Все бы ничего, но значения некоторых настроек по умолчанию отличаются при использовании нового способа. Почему я заостряю на этом внимание? Было потрачено не мало времени, чтобы понять, какое изменение вызвало такую проблему и найти этому подтверждение в репозитории kops.
Вторая оказия случилась тогда, когда возникла необходимость откатить версию kops кластера при не успешном обновлении. По умолчанию kops не позволяет сделать это, однако у него есть специальная опция »--allow-kops-downgrade». Все бы ничего, но она не работает, если вы используете свои private registry для хранения ресурсов, необходимых для запуска k8s. Все потому, что у другой команды, которая необходима для «kops get assets --copy» такой опции нет! А без ее вызова фактически невозможно запустить обновление kops на любую версию. Чудеса. В итоге кластера, использующие private registry для хранения имаджей core компонентов, нужно откатывать в ручном режиме.
Третья причуда — kops addons. Большинство core компонентов k8s в kops поставляются как аддоны. Т.е. в kops есть стандартные yaml манифесты всех таких компонентов, которые шаблонизируются на основе главного yaml файла, где описывается весь k8s kops кластер. Помимо основных компонентов (kube-api, cni и т.п.), kops предоставляет аддоны, которые не требуются для работы кластера, но упрощают жизнь. Это различные csi драйвера для монтирования томов, cluster-autoscaler для автоматического запуска и остановки виртуальных машин и т.д. Отмечу, что не все дополнительные аддоны обладают гибкой конфигурацией и порой не остается другого выхода как выключать их и разворачивать свои версии. Здесь встает вопрос «а как это делать?». Есть стандартный вариант — с помощью CICD разворачивать необходимые компоненты. Существует еще один вариант, который официально продвигается kops — custom addons. При ближайшем рассмотрении данного способа становится понятно, что это совсем не панацея. Мы уже упомянули шаблонизацию манифестов аддонов, но как потом эти манифесты разворачиваются на k8s кластере? У kops есть так называемый protokube.service — это systemd job, которая запускается при старте виртуальной машины. Эта job«a достает все манифесты из s3 bucket«a, куда они помещаются, когда kops создает/обновляет кластер. После этого происходит проверка был ли изменен манифест по сравнению с предыдущей версией (информация о предыдущей версии каждого аддона хранится в аннотации kube-system namespace k8s), после — разворачивается. Таким образом, для того, чтобы развернуть custom addon, нужно поместить его манифест в s3 bucket. На этом моменте возникает куча проблем с проверкой корректности манифеста, проверкой был ли он успешно развернут и версионированием. Учитывая данные проблемы, становится понятно, что с этим вариантом нам явно не по пути. Сколько же времени было потрачено на то, чтобы понять как работают custom addons, постоянно выявляя недостатки данного способа (отмечу, что все это никак не задокументировано)? Стоило ли оно того? Моя точка зрения — нет.
Какой же вывод можно сделать? Не используйте kops. Если нет необходимости собственноручно разворачивать k8s кластер, задумайтесь, а надо ли оно вам? Все таки процесс обновления у managed k8s намного проще, да и поставщиками он протестирован намного лучше (правда ведь?).
История 2. Обновление на cgroup2
А вы уже обновили свой kernel и начали использовать cgroup v2? Я — пробовал, и смело могу сказать, что данное обновление абсолютный рекордсмен по количеству возникших проблем.
Все начиналось весьма стандартно — подходит срок обновления k8s до версии 1.23. Ничего необычного в release notes ни для k8s, ни для kops нет, по крайней мере, ничего критичного, что могло бы помешать обновлению, поэтому запланированное обновление получает зеленый свет. Проверяем flatcar. Тут имеется серьезное изменение — в новой версии cgroups v2 включены по умолчанию. Серьезно, однако, в k8s 1.22 данная функциональность присутствует, хоть и в версии alpha.
Все-таки обновляем kops и k8s до версии 1.23, затем — flatcar image. Бам! На шаге обновления flatcar, jvm приложения начинают падать по OOM. В целом, виновник понятен — cgroup v2 (проблема возникла на шаге с flatcar). Путем недолгих изысканий находится баг в JDK, согласно которому java-машина не видит лимиты по памяти установленные для контейнера работающего на cgroup v2. К тому времени OpenJDK выпустила релиз 16 версии с исправлением данной проблемы и обещала включить данное исправление в релиз 11.0.16, выходивший через 2 месяца. Провести major обновление OpenJDK не представлялось возможным, поэтому было решено ждать выпуска релиза OpenJDK 11.0.16.
Альтернативный способ решения данной проблемы
На самом деле, данную проблему можно решить без обновления OpenJDK путем явной установки лимитов для Java машины (в нашем случае для вычисления размера кучи используется параметр MaxRAMPercentage, который в случае c cgroup2 высчитывал размер кучи от доступной памяти машины)
Прошло немного времени и настало время второй попытки. JDK обновлен, k8s 1.23 задеплоен на dev, проведены базовые тесты — все работает. Продолжаем обновлять другие кластера, добираемся до CI/CD. И…, возникает новая проблема: ноды, работающие с повышенным loadavg (80–99%) начинают «тихо умирать». Нужно отметить, что в условиях production такие нагрузки не есть хорошо, однако из CI/CD кластеров мы стараемся выжать максимум.
Что же подразумевается под «тихой смертью» нод? В данном случае это ситуация, когда машина просто перестает отвечать на любой запрос — ни ICMP, ни SSH, вообще ничего не позволяет подключиться к ней.
Проанализировав все метрики, мы явно заметили превышенный loadavg (> 100%), который не стабилизируется, приводя к каскадным сбоям. Изучив репозиторий flatcar, мы наткнулись на проблему с деградацией производительности в новой версии. Данное предположение согласовывается еще с одной странностью, когда nodejs контейнеры с небольшими лимитами по памяти, после нашего обновления стали падать с OOM (небольшое увеличение лимитов помогло).
Инвестировать больше времени в решение проблем, возникших при данном обновлении, не представлялось возможным и поэтому было принято решение отключить использование cgroup v2 на уровне flatcar (планируем вернуться к ее изучению при дальнейших обновлениях или же перейдем к использованию другого AMI вместо flatcar).
Какой вывод можно сделать из этой истории и причем здесь managed kubernetes? В случае использования его, вместо собственной инсталляции, возможность столкнуться с низкоуровневыми проблемами крайне мала, конечно, при условии следования рекомендациям облачного провайдера. Как правило, все облачные релизы содержат краткое описание breaking changes и вряд ли вы будете пытаться использовать не протестированные версии каких-либо компонентов.
История 3. Service accounts
Наверняка, многие сталкивались с проблемой разграничения доступа каждого отдельного сервиса в k8s к определенным компонентам инфраструктуры. В public clouds сложилась такая практика, что роли связываются с сервисами k8s через аннотации: то есть, если нужно, чтобы сервис обладал определенными доступами к конкретному сервису инфраструктуры (например операции чтения из определенного s3 bucket«a), внутри провайдера инфраструктуры необходимо создать роль с такими доступами, а потом просто добавить эту роль в аннотацию k8s service account«a, который будет связан с подом. На самом деле, для этих целей можно использовать стандартные подходы в виде access keys и secret keys (передавать их в качестве секрета в под), чтобы получать доступы к тем же ресурсам инфраструктуры, но, на мой взгляд, данный способ не такой удобный, как использование аннотаций.
На этом месте хотелось бы привести пример какого-нибудь российского облачного провайдера, но, к сожалению, мне не удалось найти, чтобы хоть кто-то реализовал эту функциональность (может, просто плохо искал).
На момент нашей начальной инсталляции k8s кластеров kops также не имел поддержки сопоставления аннотаций service account«a и ролей, созданных в облачном провайдере.
Для того чтобы не терять эту возможность, мы воспользовались opensource альтернативой, чтобы связать наш k8s кластер с AWS инфраструктурой — KIAM. К нашему огорчению со временем этот проект перестали поддерживать, однако к этому времени kops смог внедрить собственную поддержку Service Accounts интегрированную с AWS. После нескольких попыток мы так и не смогли начать использовать эту функциональность kops: как оказалось, мигрировать существующие кластера на нее без простоя невозможно.
В итоге, продолжать использовать KIAM также не представляется возможным, т.к. в нем всплывают все новые и новые уязвимости, которые мы героически исправляем, а воспользоваться аналогичной функциональностью kops мы не можем, так как миграция требует простоя кластера.
Что из этого можно почерпнуть? Использование комбинации решений для установки своего кластера в конечном итоге может привести в тупик. Конечно, даже в таких ситуациях можно найти выход, но какова цена?
Итог и немного размышлений вслух
Протестую ли я против решений для упрощения разворачивания kubernetes кластеров? Ни в коем случае! Kops — это прекрасное решение, просто пока еще не зрелое. Помимо kops есть много аналогичных инструментов, которые также решают похожие задачи, но и они содержат большое количество недостатков.
Цена managed k8s cluster’a на AWS — 0.1$ в час, итого за месяц использования выходит — 0.1$ * 730 часов = 73$, то есть 12 k8s кластеров стоят 876$ в месяц. С моей точки зрения, цена времени devops инженеров, инвестируемого именно в поддержку, обновление и установку k8s кластеров, значительно превосходит стоимость услуги предоставления k8s кластера как сервиса. Как правило SLA managed kubernetes сервисов достаточно высок. Цена managed k8s у остальных известных облачных провайдеров ± та же. Возможно, для IT рынка России это может быть не совсем актуальным, однако, в таком случае можно рассмотреть провайдеров предоставляющих k8s as a service. Пока мне не приходилось работать с такими кластерами, и беглый анализ сайтов таких провайдеров показывает, что цены отличаются в большую сторону и имеют ограниченную функциональность, но с другой стороны, всегда нужно оценивать количество сэкономленных девопсочасов при использовании готового решения.
Кто-то также может возразить, что использование своей инсталляции k8s это vendor agnostic решение, но на моей практике использование k8s это уже vendor agnostic, даже с учетом некоторых ограничений. Лично я предпочитаю направлять девопсочасы на эффективное использование имеющихся ресурсов и создание инфраструктуры без использования cloud specific сервисов (например DynamoDB, SQS и т.д.), путем размещения некоторые stateful сервисов внутри k8s кластера. Это позволяет быть независимым от облачных провайдеров и в случае чего без проблем переехать с одного на другой, если возникает такая необходимость. На мой взгляд, если вы небольшая компания, предоставляющая SaaS решение и имеющая возможность выбрать managed kubernetes — рассмотрите в первую очередь его. Решение не будет идеальным, но оно, я полагаю, поможет сэкономить достаточное количество человекочасов.