Русский
Русский
English
Статистика
Реклама

Kubernetes

Перевод 10 Kubernetes Security Context, которые необходимо понимать

27.03.2021 20:12:03 | Автор: admin

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

  1. runAsNonRoot

  2. runAsUser / runAsGroup

  3. seLinuxOptions

  4. seccompProfile

  5. privileged / allowPrivilegeEscalation

  6. capabilities

  7. readonlyRootFilesystem

  8. procMount

  9. fsGroup / fsGroupChangePolicy

  10. sysctls

Подписывайтесь на телеграм-каналMops DevOps, чтобы не пропустить лучшие статьи, видео и митапы!

Pod vs Container settings

Параметры Kubernetes securityContext определены как в PodSpec, так и в ContainerSpec, а область действия указывается в этой статье с помощью аннотаций [P] и / или [C] рядом с каждым из них. Обратите внимание, что, если параметр доступен и настроен в обеих областях, параметр контейнера будет иметь приоритет.

Теперь, давайте рассмотрим на настройки securityContext:

1. runAsNonRoot[P/C]

Несмотря на то, что контейнер использует namespacesиcgroups для ограничения своих процессов, всего один неверный параметр развертывания, предоставит этим процессам доступ к ресурсам на хосте. Если этот процесс выполняется от имени пользователя root, он имеет тот же доступ, что и учетная запись root хоста, к этим ресурсам. Кроме того, если для уменьшения ограничений (например, procMountилиcapabilities) используются другие настройки модуля или контейнера, наличие корневого UID увеличивает риски их использования. Если у вас нет веской причины, вам никогда не следует запускать контейнер с правами root.

Итак, что делать, если у вас есть образ для развертывания, использующий root?

Часто базовые образы уже созданы и доступны для пользователей, но их использование остается на усмотрение групп разработки или развертывания. Например, официальный образ Node.js поставляется с пользователем node с UID 1000, от имени которого вы можете работать, но они явно не устанавливают его для текущего пользователя в своем Dockerfile. Нам нужно будет либо настроить его во время выполнения с помощью параметра runAsUser, либо изменить текущего пользователя в образе с помощью отдельного файла Dockerfile. Первый предполагает, что UID 1000 может читать файлы смонтированные в appvolume, также не очень распространенный вариант выполнение приложения в отдельном томе. Вместо этого давайте рассмотрим пример использования производного файла Dockerfile для создания собственного образа.

Не слишком углубляясь в создание образов, предположим, что у нас есть готовый пакет npm. Вот минимальный Dockerfile для создания образа на основе node: slim и запуска от имени заданного пользователя node.

FROM node:slimCOPY --chown=node . /home/node/app/   # <--- Copy app into the home directory with right ownershipUSER node                             # <--- Switch active user to nodeWORKDIR /home/node/app                # <--- Switch current directory to appENTRYPOINT ["npm", "start"]           # <--- This will now exec as the node user instead of root

Команда USER, делает node пользователем по умолчанию внутри любого контейнера, запущенного с этого образа.

Вариант 2: Пользователь не определен в базовом образе

Итак, чтобы мы сделали, если бы в базовом образе node пользователь не был бы определен? В большинстве случаем мы просто создаем его в новом Dockerfile и используем. Давайте расширим предыдущий пример, чтобы сделать это:

FROM node:slimRUN useradd somebody -u 10001 --create-home --user-group  # <--- Create a userCOPY --chown=somebody . /home/somebody/app/USER somebodyWORKDIR /home/somebody/appENTRYPOINT ["npm", "start"]

Как видите, единственное изменение - это команада RUN, которая создает нового пользователя - синтаксис может варьироваться в зависимости от дистрибутива базового образа.

ПРИМЕЧАНИЕ: это отлично работает для node.js и npm, но для других инструментов может потребоваться изменить владельца других объектов файловой системы. Если у вас возникнут какие-либо проблемы, обратитесь к документации по вашему инструменту.

2. runAsUser / runAsGroup[P/C]

Образы контейнеров могут иметь определенного пользователя и/или группу, настроенную для запуска процесса. Это можно изменить с помощью параметров runAsUser и runAsGroup. Часто они устанавливаются вместе с монтированием тома, содержащим файлы с одинаковыми идентификаторами владения.

...spec:  containers:  - name: web    image: mycorp/webapp:1.2.3  securityContext:    runAsNonRoot: true    runAsUser: 10001...

Использование этих настроек представляет опасность, поскольку вы изменяете параметры во время запуска контейнера, эти параметры могут быть несовместимы с исходным образом. Например, официальный образ сервера jenkins/jenkins CI работает как группа: пользователь с именем jenkins: jenkins и все его файлы приложений принадлежат этому пользователю. Если мы настроим другого пользователя, он не запустится, потому что этого образ не содержит этого пользователя в файле /etc/passwd. Даже если бы это было так, скорее всего, возникнут проблемы с чтением и записью файлов, принадлежащих jenkins: jenkins. Вы можете в этом убедиться, выполнив простую команду Docker:

$ docker run --rm -it -u eric:eric jenkins/jenkinsdocker: Error response from daemon: unable to find user eric: no matching entries in passwd file.

Как мы упоминали выше, это очень хорошая идея, чтобы процессы контейнера не запускались от имени пользователя root, но не стоит полагаться для этого на параметр runAsUser или runAsGroup. Что если кто-то удалить эти настройки? Не забудьте также установить для runAsNonRoot значение true.

3. seLinuxOptions[P/C]

SELinux - это система управления доступом к приложениям, процессам и файлам в системе Linux, настраиваемая через политики. Она реализует структуру модулей безопасности Linux в ядре Linux. SELinux основана на концепции меток и применяет эти метки ко всем элементам в системе, которые группируют элементы вместе. Эти метки известны как контекст безопасности - не путать с Kubernetes securityContext и состоят из пользователя, роли, типа и необязательного поля уровень - user: role: type: level.

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

Метки SELinux по умолчанию будут применяться средой выполнения контейнера при создании экземпляра контейнера. Параметр seLinuxOptions в securityContext позволяет применять пользовательские метки SELinux. Имейте в виду, что изменение маркировки SELinux для контейнера потенциально может позволить контейнерному процессу выйти из образа контейнера и получить доступ к файловой системе хоста.

По умолчанию метки SELinux будут применяться средой выполнения контейнера при создании экземпляра контейнера. Параметр seLinuxOptions в securityContext позволяет применять пользовательские метки SELinux. Имейте в виду, что изменение маркировки SELinux для контейнера потенциально может позволить контейнерному процессу выйти из образа контейнера и получить доступ к файловой системе хоста.

Обратите внимание, что эта функция будет применяться, только если операционная система хоста поддерживает SELinux.

4. seccompProfile[P/C]

Seccomp означает secure computing mode (безопасный режим вычислений) и является функцией ядра Linux, которая может ограничивать вызовы, которые конкретный процесс может делать из пользовательского пространства в ядро. Профиль seccomp - это определение JSON, обычно состоящее из набора системных вызовов и действия по умолчанию, предпринимаемого при возникновении одного из этих системных вызовов.

{    "defaultAction": "SCMP_ACT_ERRNO",    "architectures": [        "SCMP_ARCH_X86_64",        "SCMP_ARCH_X86",        "SCMP_ARCH_X32"    ],    "syscalls": [        {            "name": "accept",            "action": "SCMP_ACT_ALLOW",            "args": []        },        {            "name": "accept4",            "action": "SCMP_ACT_ALLOW",            "args": []        },        ...    ]}

Использован пример с сайтаhttps://training.play-with-docker.com/security-seccomp/

Kubernetes предоставляет механизм для использования настраиваемых профилей через параметр seccompProfile в securityContext.

seccompProfile:      type: Localhost      localhostProfile: profiles/myprofile.json

Доступно три значения для поля type:

  • Localhost- в дополнительном параметре localhostProfile указан путь к профилю seccomp

  • Unconfined- профиль не применяется

  • RuntimeDefault- используется значение по умолчанию для среды выполнения контейнера (это значение по умолчанию, если тип не указан)

Вы можете применить эти настройки либо в PodSecurityContext, либо в securityContext. Если установлены оба контекста, то используются настройки на уровне контейнера в securityContext. Обратите внимание, что эти параметры актуальны для Kubernetes v1.19 - если вы развертываете более ранние версии, существует другой синтаксис; за подробностями и примерами обратитесь к документации на официальном сайте Kubernetes.

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

5. Избегайте Privileged Containers / Escalations[C]

Предоставление привилегированного статуса контейнеру опасно и обычно используется как более простой способ получения определенных разрешений. Среда выполнения контейнеров контролирует наличие флага privileged, предоставляет контейнеру все привилегии, но снимает ограничения, налагаемые cgroup. Она также может изменить Linux Security Module и позволить процессам внутри контейнера выйти из него.

Контейнеры обеспечивают на хосте изоляцию процессов, поэтому даже если контейнер работает от имени пользователя root, существуют функции, которые container runtime не предоставляет контейнеру. При установленном флаге privileged среда выполнения контейнеров предоставляет полный доступ к файловой системе хоста, что делает этот параметр чрезвычайно опасным с точки зрения безопасности.

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

Для более глубокого изучения Privileged Containers рекомендуем статьюPrivileged Docker containersdo you really need them?

6. Linux kernel capabilities[C]

Capabilities - это разрешения на уровне ядра, которые позволяют гранулярно управлять разрешениями на вызовы ядра, вместо того, чтобы запускать все от имени пользователя root. Capabilities позволяют изменять права доступа к файлам, управлять сетевой подсистемой и выполняет общесистемные функции администрирования. Вы можете управлять Capabilities через KubernetessecurityContext. Отдельные сapabilities или список, разделенный запятыми, могут быть представлены в виде массива строк. Кроме того, вы можете использовать сокращение -all для добавления или удаления всех capabilities. Эта конфигурация передается в среду выполнения контейнеров и настраивает capabilities при создании контейнера. Если в securityContext нет раздела capabilities, тогда контейнер создается с набором capabilities по умолчанию, который предоставляет среда выполнения контейнера.

securityContext:      capabilities:        drop:          - all        add: ["MKNOD"]

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

Обратите внимание, что при перечислении capabilities в securityContext вы удаляете префикс CAP_, который ядро использует в именах capabilities. Вы можете использовать утилиту capsh, она выводит информацию о включенных в контейнере capabilities в удобном формате о том, но оставляйте эту утилиту в итоговых контейнерах, так как это позволяет злоумышленнику легко определить, какие capabilities включены! Вы также можете проверить включенные capabilities в файле /proc/1/ status.

7. Запуск контейнеров с read-only filesystem[C]

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

8. procMount[C]

По умолчанию среда выполнения контейнеров маскирует определенные части файловой системы /proc изнутри контейнера, чтобы предотвратить возможные проблемы с безопасностью. Однако бывают случаи, когда к ним требуется доступ, особенно при использовании вложенных контейнеров, которые часто используются как часть процесса сборки в кластере. Есть только два допустимых параметра для этой записи: Default, который поддерживает стандартное поведение среды выполнения контейнера, или Unmasked, который удаляет все маскировки для файловой системы /proc.

Очевидно, вам следует использовать эти настройки, только если вы действительно знаете, что делаете. Если вы используете его для создания образов, проверьте последнюю версию инструмента сборки, так как многим он больше не нужен. Обновите и вернитесь к procMount по умолчанию, который актуален для используемого вами инструмента.

Наконец, если вам все же нужен доступ к замаскированным участкам /proc, сделайте это только для вложенного контейнера; никогда не открывайте файловую систему /proc вашей хост-системы контейнеру.

9. fsGroup / fsGroupChangePolicy[P]

Параметр fsGroup определяет группу, в которой Kubernetes будет изменять разрешения для всех файлов в томах, когда тома монтируются в Pod. Поведение также контролируется fsGroupChangePolicy, для которого может быть установлено значение onRootMismatch или Always. Если установлено значение onRootMismatch, разрешения будут изменены только в том случае, если они еще не соответствуют разрешениям корневого каталога контейнера.

Будьте осторожны при использовании fsGroup. Изменение группового владения всем томом может вызвать задержки запуска Pod для медленных и/или больших файловых систем. Это также может нанести ущерб другим процессам, которые совместно используют тот же том, если их процессы не имеют разрешений на доступ к новому GID. По этой причине некоторые поставщики общих файловых систем, таких как NFS, не реализуют эту функцию. Эти настройки не влияют на ephemeral volume.

10. sysctls[P]

Sysctls - это функция ядра Linux, которая позволяет администраторам изменять конфигурацию ядра. В хостовой операционной системе Linux они определяются с помощью /etc/sysctl.conf, а также могут быть изменены с помощью утилиты sysctl.

Параметр sysctls в securityContext позволяет изменять определенные sysctls в контейнере. Существует лишь небольшое подмножество sysctls операционной системы, которые можно изменять для каждого контейнера, которые размещены в пространстве имен в ядре. Некоторые из них считаются безопасными. Но большинство считается небезопасным, в зависимости от возможности воздействия на другие модули. Небезопасные sysctls обычно отключены и должны быть специально включены администратором.

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

Примечание о securityContext в процессе запуска

Во многих случаях описанные здесь параметры безопасности сочетаются с контролем доступа на основе политик (policy-based admission control), чтобы гарантировать, что необходимые параметры действительно настроены перед запуском контейнеров в кластер. Комбинируя параметры securityContext с PodSecurityPolicy, вы можете гарантировать, что запускаются только контейнеры, которые соответсвуют политике, принудительно применения определенных параметров securityContext. Параметры securityContext также могут быть добавлены к конфигурации контейнера во время запуска с помощью динамического контроля допуска (Dynamic Admission Control) и использования mutating webhooks.

Заключение

Повышая безопасность при помощи параметров securityContext следует помнить о многом. При правильном использовании они являются очень эффективным инструментом, и мы надеемся, что этот список поможет вашим командам выбрать правильные варианты. Snyk может помочь вам с этим выбором, просканировав ваши yaml-файлы Kubernetes на предмет распространенных ошибок конфигурации.


Телеграм-каналMops DevOps- анонсы вебинаров и конференций, полезные статьи и видео, а также регулярные скидки на обучение!

Подробнее..

DevOps-практики Кто? Где? Сколько?

12.03.2021 10:04:49 | Автор: admin

DevOps-инженера ищут многие, но находят не все. Специалисты, умеющие внедрять DevOps-практики, последние 3 года являются одними из самых востребованных на IT-рынке. Спрос на них постоянно растёт. Заработная плата, несмотря на кризис, тоже не падает. Хорошие причины, чтобы задуматься, как прийти в эту профессию и каким образом в ней развиваться?

Специальность DevOps-инженера действительно появилась в IT-индустрии относительно недавно и быстро вырвалась в топ. Development Operations это, в первую очередь, набор практик, призванный улучшить и автоматизировать процесс доставки продукта до конечного пользователя, и он может быть полезен везде, где речь идёт о разработке приложений или управлении большим количеством серверов. Пожалуй, только небольшие команды могут позволить себе не выделять DevOps в отдельную функцию и обходиться исключительно своими силами.

Итак, какие задачи решает DevOps-инженер?

Его основная цель выявлять узкие места и при помощи DevOps-практик обеспечивать прохождение продукта через эти ловушки. Решаемые задачи всегда носят практический характер и находятся на стыке разных областей. Как правило, они зависят от потребностей продукта, от команды и подходов, которые применяются в той или иной компании. Проекты, входящие в зону ответственности DevOps, можно сгруппировать в четыре основных направления:

  • Обеспечение полного жизненного цикла продукта;

  • Подготовка различных окружений (разработка тестирование production) и обеспечение поставок продукта на эти окружения;

  • Обеспечение автоматического прохождения продукта через различные стадии непрерывной интеграции (CI) и непрерывной доставки (CD);

  • Виртуализация и управление инфраструктурой, мониторинг.

Результатом внедрения методологии и практик DevOps становится синхронизация различных этапов разработки и выпуска конечного продукта. Чтобы решать задачи подобного масштаба, DevOps-инженер должен выступать одновременно в роли админа, разработчика, тестировщика и менеджера. Однако не стоит думать, что появление такого человека в команде сразу и полностью решит проблемы. Все члены коллектива, чья деятельность, так или иначе, подразумевает связь с DevOps, оказываются вовлечены в эти процессы.

Исходя из направлений деятельности, на практике DevOps-инженер используют следующие инструменты:

  • CI/СD и интеграцию (Jenkins, TeamCity, GitLab, Bamboo);

  • Автоматизацию (Terraform, Puppit, Ansible);

  • Облачные платформы (AWS, Google Cloud Platform, Microsoft Azure, Huawei Cloud, Яндекс Облако, Mail.ru Cloud Solutions);

  • Мониторинг (Prometheus, Grafana, Zabbix, Nagios);

  • Системы логирования, трассировки (ELK Stack, Graylog, Gafana, Jaeger);

  • Контейнеризация и орекстрация (Docker, Kubernetes, Nomad).

Карьерная карта

В DevOps приходят из разных профессий. Основные доноры - это System administrator, Automation engineer, QA automation, Build Engineer/ Release Engineer, Developer. Представители этих специальностей уже обладают рядом навыков, которые необходимо развить и расширить.

Андрей Синицын, Head of IT Optimisation Departmen в ECommPay, рассказывает: Я занимаюсь компьютерами с середины 90-х я из того времени, когда эта профессия выбирала тебя. Передо мной никогда не стояло вопроса, чем заниматься по жизни. Сначала я работал программистом, потом понял, что мне интереснее эксплуатация, и ушел в DevOps. Живой продакшн это всегда интересно. И, на мой взгляд, интереснее, чем написание программы: ты видишь, как код эволюционирует, как он работает, как он выполняет (или, как это часто бывает, не выполняет) ту задачу, для решения которой был написан.

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

Сертификаты AWS, GCP, Azure, Kubernetes (CKA, CKAD) могут рассказать работодателю о том, что соискатель имеет навык работы с конкретными платформами, но, как правило, DevOps-инженером становятся только на практике.

Составляя идеальное DevOps-резюме, важно отразить в нём навыки, которыми вы владеете, задачи в рамках уже реализованных проектов, их особенности, зону ответственности; используемый стек технологий и, конечно, не забыть о soft-skills. Андрей Синицин подчёркивает, что для DevOps очень важны хорошие коммуникативные навыки, знание английского, обучаемость и out-of-box thinking стандартный набор для любой специализации в IT. Еще я бы добавил, что большое преимущество в DevOps дает понимание бизнеса (или стремление к этому). Эксплуатация никогда не зарабатывает деньги напрямую, и осознавать business value того, что ты делаешь, очень важно.

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

Кстати, нам вы также можете прислать резюме по этой ссылке.

Перспективы сегодня и завтра

DevOps-инженеры действительно зарабатывают больше всех в отрасли. В США, Канаде, UK заработная плата колеблется между 90 и 122 тысячами долларов в год. Что касается России, то в Москве работодатели готовы предложить такому специалисту в среднем 260 тыс. рублей в месяц (верхняя планка доходит до 350 тыс. ), в Санкт-Петербурге средняя зарплата составляет 200 тыс. рублей.

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

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

Что касается возможностей карьерного роста, то для DevOps-инженера открыт путь к следующим позициям: Devops Team Lead, DevRel (Developer relations), Delivery Manager, Devops architect, Head of Engineering.

DevOps 2021: основные тренды

Анализируя 2020 год, можно заметить, что в центре внимания стала, прежде всего, безопасность. В том числе, безопасность IT-продуктов, поэтому одним из самых заметных трендов является DevSecOps и в целом SDLC (Security development lifecycle). DevSecOps подразумевает встраивание процесса безопасной разработки в процесс DevOps, интеграцию парадигм безопасности в каждый из этапов разработки.

Внедрение таких подходов, как DevSecOps, невозможно без следующего тренда автоматизации, одного из основных китов DevOps-практики. Скрипты, автоматизация, внедрение подхода IaC (инфраструктура как код) все это обеспечивает гибкость, скорость процессов разработки и поставки продукта.

Стоит также выделить глобальный тренд, который существует уже несколько лет это переход в cloud-native-среду и разработка приложений с учетом особенностей облачных платформ, считает Элиса Данильсон, консультант направления IT&Telecoms в Санкт-Петербурге.

Подробнее..

Угрожает ли микросервисная (контейнеризация) архитектура светлому будущему Published Apps (Citrix amp Co.)

12.04.2021 10:16:29 | Автор: admin
Идея данного опроса возникла в результате дискуссии с коллегами, навсегда завязавшими с темой Virtual App and Desktop и нашедшие себя в направлении Kubernetes/Cloud.

Несмотря на то, что последние годы моя профессиональная деятельность непосредственно связана с темой Citrix, я также как и мои коллеги не могу не заметить отсутствие особых перспектив развития технологии доступа к Apps посредством терминальных сессий (особенно on-Premise).

Прежде чем задать пару вопросов, хотелось бы подчеркнуть, что напрямую сравнивать как технологии, лежащие в основе Kubernetes и Citrix Virtual Apps and Desktops (MS RDS, VMware Horizon, Parallels RAS), так и сами продукты не совсем правильно. Но если рассмотреть конечную цель, для чего в конечном счёте существует IT, а именно для конечного пользователя, задача которого максимальной эффективностью и удобством выполнять поставленные перед ним задачи.

Смысл и цель использования виртуального приложения


Если мы вернёмся в далёкие конец 90-х начало 2000-х, в то время, когда Citrix начинал обретать свою популярность, все программы так или иначе являлись монолитами. Основанная разница была лишь в том, устанавливалась ли программа полностью на терминальном сервере или же на сервере был только фронтенд (например SAPGUI).

Как 20 лет назад так и сейчас использования терминальных серверов являлось более чем эффективным средством и позволяло решать следующее задачи: эффективное использование ресурсов, быстрота развёртывания и установки обновлений, а также переключение между версиями, безопасность (здесь есть как плюсы, так и минусы), простота контроля доступа к приложениям и т. д.

Почему сгущаются тучи?


Свою позицию коллеги объясняют следующим образом. Все больше и больше программных продуктов продолжают своё существование в качестве веб-приложений, многие просто создаются по принципу born in the cloud. Таким образом доля устанавливаемого на серверах программного обеспечения под Windows будет постоянно уменьшатся, превращая published application в ненужный анахронизм.

Буд рад Вашим комментариям! Спасибо!
Подробнее..

Ломаем и чиним etcd-кластер

01.03.2021 20:22:05 | Автор: admin

etcd это быстрая, надёжная и устойчивая к сбоям key-value база данных. Она лежит в основе Kubernetes и является неотъемлемой частью его control-plane. Именно поэтому критически важно уметь бэкапить и восстанавливать работоспособность как отдельных нод, так и всего etcd-кластера.

В предыдущей статье мы подробно рассмотрели перегенерацию SSL-сертификатов и static-манифестов для Kubernetes, а также вопросы связанные восстановлением работоспособности Kubernetes. Эта статья будет посвящена целиком и полностью восстановлению etcd-кластера.


Для начала я сразу должен сделать оговорку, что рассматривать мы будем лишь определённый кейс, когда etcd задеплоен и используется непосредственно в составе Kubernetes. Приведённые в статье примеры подразумевают что ваш etcd-кластер развёрнут с помощью static-манифестов и запускается внутри контейнеров.

Для наглядности возьмём схему stacked control plane nodes из предыдущей статьи:

(стрелочки указывают на связи клиент --> сервер)(стрелочки указывают на связи клиент --> сервер)

Предложенные ниже команды можно выполнить и с помощью kubectl, но в данном случае мы постараемся абстрагироваться от плоскости управления Kubernetes и рассмотрим локальный вариант управления контейнеризированным etcd-кластером с помощью crictl.

Данное умение также поможет вам починить etcd даже в случае неработающего Kubernetes API.

Подготовка

По этому, первое что мы сделаем, это зайдём по ssh на одну из master-нод и найдём наш etcd-контейнер:

CONTAINER_ID=$(crictl ps -a --label io.kubernetes.container.name=etcd --label io.kubernetes.pod.namespace=kube-system | awk 'NR>1{a=$1} END{print a}')

Так же установим алиас, чтобы каждый раз не перечислять пути к сертификатам в командах:

alias etcdctl='crictl exec "$CONTAINER_ID" etcdctl --cert /etc/kubernetes/pki/etcd/peer.crt --key /etc/kubernetes/pki/etcd/peer.key --cacert /etc/kubernetes/pki/etcd/ca.crt'

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

Если что-то пошло не так и вы не можете сделать exec в запущенный контейнер, посмотрите логи etcd:

crictl logs "$CONTAINER_ID"

А также убедитесь в наличии static-манифеста и всех сертифиткатов в случае если контейнер даже не создался. Логи kubelet так же бывают весьма полезными.

Проверка состояния кластера

Здесь всё просто:

# etcdctl member list -w table+------------------+---------+-------+---------------------------+---------------------------+------------+|        ID        | STATUS  | NAME  |        PEER ADDRS         |       CLIENT ADDRS        | IS LEARNER |+------------------+---------+-------+---------------------------+---------------------------+------------+| 409dce3eb8a3c713 | started | node1 | https://10.20.30.101:2380 | https://10.20.30.101:2379 |      false || 74a6552ccfc541e5 | started | node2 | https://10.20.30.102:2380 | https://10.20.30.102:2379 |      false || d70c1c10cb4db26c | started | node3 | https://10.20.30.103:2380 | https://10.20.30.103:2379 |      false |+------------------+---------+-------+---------------------------+---------------------------+------------+

Каждый инстанс etcd знает всё о каждом. Информация о members хранится внутри самого etcd и по этому любое изменение в ней будет также обновленно и на остальных инстансах кластера.

Важное замечание, команда member list отображает только статус конфигурации, но не статус конкретного инстанса. Чтобы проверить статусы инстансов есть команда endpoint status, но она требует явного указания всех эндпоинтов кластера для проверки.

ENDPOINTS=$(etcdctl member list | grep -o '[^ ]\+:2379' | paste -s -d,)etcdctl endpoint status --endpoints=$ENDPOINTS -w table

в случае если какой-то из эндпоинтов окажется недоступным вы увидите такую ошибку:

Failed to get the status of endpoint https://10.20.30.103:2379 (context deadline exceeded)

Удаление неисправной ноды

Иногда случается так что какая-то из нод вышла из строя. И вам нужно восстановить работоспособность etcd-кластера, как быть?

Первым делом нам нужно удалить failed member:

etcdctl member remove d70c1c10cb4db26c

Прежде чем продолжить, давайте убедимся, что на упавшей ноде под с etcd больше не запущен, а нода больше не содержит никаких данных:

rm -rf /etc/kubernetes/manifests/etcd.yaml /var/lib/etcd/crictl rm "$CONTAINER_ID"

Команды выше удалят static-pod для etcd и дирректорию с данными /var/lib/etcd на ноде.

Разумеется в качестве альтернативы вы также можете воспользоваться командой kubeadm reset, которая удалит все Kubernetes-related ресурсы и сертифиткаты с вашей ноды.

Добавление новой ноды

Теперь у нас есть два пути:

В первом случае мы можем просто добавить новую control-plane ноду используя стандартный kubeadm join механизм:

kubeadm init phase upload-certs --upload-certskubeadm token create --print-join-command --certificate-key <certificate_key>

Вышеприведённые команды сгенерируют команду для джойна новой control-plane ноды в Kubernetes. Этот кейс довольно подробно описан в официальной документации Kubernetes и не нуждается в разъяснении.

Этот вариант наиболее удобен тогда, когда вы деплоите новую ноду с нуля или после выполнения kubeadm reset

Второй вариант более аккуратный, так как позволяет рассмотреть и выполнить изменения необходимые только для etcd не затрагивая при этом другие контейнеры на ноде.

Для начала убедимся что наша нода имеет валидный CA-сертификат для etcd:

/etc/kubernetes/pki/etcd/ca.{key,crt}

В случае его отсутсвия скопируейте его с других нод вашего кластера. Теперь сгенерируем остальные сертификаты для нашей ноды:

kubeadm init phase certs etcd-healthcheck-clientkubeadm init phase certs etcd-peerkubeadm init phase certs etcd-server

и выполним присоединение к кластеру:

kubeadm join phase control-plane-join etcd --control-plane

Для понимания, вышеописанная команда сделает следующее:

  1. Добавит новый member в существующий etcd-кластер:

    etcdctl member add node3 --endpoints=https://10.20.30.101:2380,https://10.20.30.102:2379 --peer-urls=https://10.20.30.103:2380
    
  2. Сгенерирует новый static-manifest для etcd /etc/kubernetes/manifests/etcd.yaml с опциями:

    --initial-cluster-state=existing--initial-cluster=node1=https://10.20.30.101:2380,node2=https://10.20.30.102:2380,node3=https://10.20.30.103:2380
    

    эти опции позволят нашей ноде автоматически добавиться в существующий etcd-кластер.

Создание снапшота etcd

Теперь рассмотрим вариант создания и восстановления etcd из резервной копии.

Создать бэкап можно довольно просто, выполнив на любой из нод:

etcdctl snapshot save /var/lib/etcd/snap1.db

Обратите внимание я намерянно использую /var/lib/etcd так как эта директория уже прокинута в etcd контейнер (смотрим в static-манифест /etc/kubernetes/manifests/etcd.yaml)

После выполнения этой команды по указанному пути вы найдёте снапшот с вашими данными, давайте сохраним его в надёжном месте и рассмотрим процедуру восстановления из бэкапа

Восстановление etcd из снапшота

Здесь мы рассмотрим кейс когда всё пошло не так и нам потребовалось восстановить кластер из резервной копии.

У нас есть снапшот snap1.db сделанный на предыдущем этапе. Теперь давайте полностью удалим static-pod для etcd и данные со всех наших нод:

rm -rf /etc/kubernetes/manifests/etcd.yaml /var/lib/etcd/member/crictl rm "$CONTAINER_ID"

Теперь у нас снова есть два пути:

Вариант первый создать etcd-кластер из одной ноды и присоединить к нему остальные ноды, по описанной выше процедуре.

kubeadm init phase etcd local

эта команда сгенерирует статик-манифест для etcd c опциями:

--initial-cluster-state=new--initial-cluster=node1=https://10.20.30.101:2380

таким образом мы получим девственно чистый etcd на одной ноде.

# etcdctl member list -w table+------------------+---------+-------+---------------------------+---------------------------+------------+|        ID        | STATUS  | NAME  |        PEER ADDRS         |       CLIENT ADDRS        | IS LEARNER |+------------------+---------+-------+---------------------------+---------------------------+------------+| 1afbe05ae8b5fbbe | started | node1 | https://10.20.30.101:2380 | https://10.20.30.101:2379 |      false |+------------------+---------+-------+---------------------------+---------------------------+------------+

Восстановим бэкап на первой ноде:

etcdctl snapshot restore /var/lib/etcd/snap1.db \  --data-dir=/var/lib/etcd/new  --name=node1 \  --initial-advertise-peer-urls=https://10.20.30.101:2380 \  --initial-cluster=node1=https://10.20.30.101:2380mv /var/lib/etcd/member /var/lib/etcd/member.oldmv /var/lib/etcd/new/member /var/lib/etcd/membercrictl rm "$CONTAINER_ID"rm -rf /var/lib/etcd/member.old/ /var/lib/etcd/new/

На остальных нодах выполним присоединение к кластеру:

kubeadm join phase control-plane-join etcd --control-plane

Вариант второй: восстановить бэкап сразу на всех нодах кластера. Для этого копируем файл снапшота на остальные ноды, и выполняем восстановление вышеописанным образом. В данном случае в опциях к etcdctl нам потребуется указать сразу все ноды нашего кластера, к примеру

для node1:

etcdctl snapshot restore /var/lib/etcd/snap1.db \  --data-dir=/var/lib/etcd/new \  --name=node1 \  --initial-advertise-peer-urls=https://10.20.30.101:2380 \  --initial-cluster=node1=https://10.20.30.101:2380,node2=https://10.20.30.102:2380,node3=https://10.20.30.103:2380

для node2:

etcdctl snapshot restore /var/lib/etcd/snap1.db \  --data-dir=/var/lib/etcd/new \  --name=node2 \  --initial-advertise-peer-urls=https://10.20.30.102:2380 \  --initial-cluster=node1=https://10.20.30.101:2380,node2=https://10.20.30.102:2380,node3=https://10.20.30.103:2380

для node3:

etcdctl snapshot restore /var/lib/etcd/snap1.db \  --data-dir=/var/lib/etcd/new \  --name=node3 \  --initial-advertise-peer-urls=https://10.20.30.103:2380 \  --initial-cluster=node1=https://10.20.30.101:2380,node2=https://10.20.30.102:2380,node3=https://10.20.30.103:2380
Подробнее..

Мониторинг производительности приложений в Broadcom DX APM анонс вебинара

02.03.2021 10:09:04 | Автор: admin
image

Единый агент для всех популярных технологий, динамическое отслеживание изменений инфраструктуры, низкий оверхед, искусственный интеллект, оценка эффективности релизов, контекстный мониторинг, мониторинг реальных транзакций обо всём этом и многом другом вы узнаете на вебинаре, посвящённому инструменту для мониторинга производительности приложений и инфраструктуры под ними Broadcom DX APM. Вебинар состоится 5 марта в 11 часов утра по московскому времени.

Регистрация на вебинар

Под катом вы найдёте квадрант Gartner за 2020 год по APM-решениям и дополнительные материалы по DX APM и другим решениям Broadcom.


DX APM несколько лет подряд сохраняет лидерство. Решение регулярно обновляется, появляется новый функционал. Особенность продукта использование под капотом открытых технологий, например, OpenTelemetry. Этот тренд, кстати, распространяется и на некоторых других лидеров квадранта.

image

А ещё у нас есть:

Как устроен и работает DX APM

Запись нашего вебинара по зонтичной AIOps-системе мониторинг DX OI

Статья на Хабре о зонтичной AIOps-системе мониторинга DX OI

Группа в Facebook

Канал в Youtube
Подробнее..

Лучшие практики для деплоя высокодоступных приложений в Kubernetes. Часть 1

03.03.2021 16:23:30 | Автор: admin

Развернуть в Kubernetes приложение в минимально рабочей конфигурации нетрудно. Но когда вы захотите обеспечить своему приложению максимальную доступность и надежность в работе, вы неизбежно столкнётесь с немалым количеством подводных камней. В этот статье мы попытались систематизировать и ёмко описать самые важные правила для развертывания высокодоступных приложений в Kubernetes.

Функциональность, которая не доступна в Kubernetes из коробки, здесь почти не будет затрагиваться. Также мы не будем привязываться к конкретным CD-решениям и опустим вопросы шаблонизации/генерации Kubernetes-манифестов. Рассмотрены только общие правила, касающиеся того, как Kubernetes-манифесты могут выглядеть в конечном итоге при деплое в кластер.

1. Количество реплик

Вряд ли получится говорить о какой-либо доступности, если приложение не работает по меньшей мере в двух репликах. Почему при запуске приложения в одной реплике возникают проблемы? Многие сущности в Kubernetes (Node, Pod, ReplicaSet и др.) эфемерны, т. е. при определенных условиях они могут быть автоматически удалены/пересозданы. Соответственно, кластер Kubernetes и запущенные в нём приложения должны быть к этому готовы.

К примеру, при автомасштабировании узлов вниз, какие-то узлы вместе с запущенными на них Pod'ами будут удалены. Если в это время на удаляемом узле работает ваше приложение в одном экземпляре, то неизбежна полная хотя обычно и непродолжительная недоступность приложения. В целом, при работе в одной реплике любое нештатное завершение работы приложения будет означать простой. Таким образом, приложение должно быть запущено по меньшей мере в двух репликах.

При этом, если реплика экстренно завершает работу, то чем больше рабочих реплик было изначально, тем меньше просядет вычислительная способность всего приложения. К примеру, если у приложения всего две реплики и одна из них перестала работать из-за сетевых проблем на узле, то приложение теперь сможет выдержать только половину первоначальной нагрузки (одна реплика доступна, одна недоступна). Конечно, через некоторое время новая реплика приложения будет поднята на новом узле, и работоспособность полностью восстановится. Но до тех пор увеличение нагрузки на единственную рабочую реплику может приводить к перебоям в работе приложения, поэтому количество реплик должно быть с запасом.

Рекомендации актуальны, если не используется HorizontalPodAutoscaler. Лучший вариант для приложений, у которых будет больше нескольких реплик, настроить HorizontalPodAutoscaler и забыть про указание количества реплик вручную. О HorizontalPodAutoscaler мы поговорим в следующей статье.

2. Стратегия обновления

Стратегия обновления у Deployment'а по умолчанию такая, что почти до конца обновления только 75% Pod'ов старого+нового ReplicaSet'а будут в состоянии Ready. Таким образом, при обновлении приложения его вычислительная способность может падать до 75%, что может приводить к частичному отказу. Отвечает за это поведение параметр strategy.rollingUpdate.maxUnavailable. Поэтому убедитесь, что приложение не теряет в работоспособности при отказе 25% Pod'ов, либо увеличьте maxUnavailable. Округление maxUnavailable происходит вверх.

Также у стратегии обновления по умолчанию (RollingUpdate) есть нюанс: приложение некоторое время будет работать не только в несколько реплик, но и в двух разных версиях разворачивающейся сейчас и развернутой до этого. Поэтому, если приложение не может даже непродолжительное время работать в нескольких репликах и нескольких разных версиях, то используйте strategy.type: Recreate. При Recreate новые реплики будут подниматься только после того, как удалятся старые. Очевидно, здесь у приложения будет небольшой простой.

Альтернативные стратегии деплоя (blue-green, canary и др.) часто могут быть гораздо лучшей альтернативой RollingUpdate, но здесь мы не будем их рассматривать, так как их реализация зависит от того, какое ПО вы используете для деплоя. Это выходит за рамки текущей статьи. (См. также статью Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование) в нашем блоге.)

3. Равномерное распределение реплик по узлам

Очень важно разносить Pod'ы приложения по разным узлам, если приложение работает в нескольких репликах. Для этого рекомендуйте планировщику не запускать несколько Pod'ов одного Deployment'а на одном и том же узле:

      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              labelSelector:                matchLabels:                  app: testapp              topologyKey: kubernetes.io/hostname

Предпочитайте preferredDuringScheduling вместо requiredDuringScheduling, который может привести к невозможности запустить новые Pod'ы, если доступных узлов окажется меньше, чем новым Pod'ам требуется. Тем не менее, requiredDuringScheduling может быть полезен, когда количество узлов и реплик приложения точно известно и необходимо быть уверенным, что два Pod'а не смогут оказаться на одном и том же узле.

4. Приоритет

priorityClassName влияет на то, какие Pod'ы будут schedule'иться в первую очередь, а также на то, какие Pod'ы могут быть вытеснены (evicted) планировщиком, если места для новых Pod'ов на узлах не осталось.

Потребуется создать несколько ресурсов типа PriorityClass и ассоциировать их с Pod'ами через priorityClassName. Набор PriorityClass'ов может выглядеть примерно так:

  • Cluster. Priority > 10000. Критичные для функционирования кластера компоненты, такие как kube-apiserver.

  • Daemonsets. Priority: 10000. Обычно мы хотим, чтобы Pod'ы DaemonSet'ов не вытеснялись с узлов обычными приложениями.

  • Production-high. Priority: 9000. Stateful-приложения.

  • Production-medium. Priority: 8000. Stateless-приложения.

  • Production-low. Priority: 7000. Менее критичные приложения.

  • Default. Priority: 0. Приложения для окружений не категории production.

Это предохранит нас от внезапных evict'ов важных компонентов и позволит более важным приложениям вытеснять менее важные при недостатке узлов.

5. Остановка процессов в контейнерах

При остановке контейнера всем процессам в нём отправляется сигнал, указанный в STOPSIGNAL (обычно это TERM). Но не все приложения умеют правильно реагировать на него и делать graceful shutdown, который бы корректно отработал и для приложения, запущенного в Kubernetes.

Например, чтобы сделать корректную остановку nginx, нам понадобится preStop-хук вроде этого:

lifecycle:  preStop:    exec:      command:      - /bin/sh      - -ec      - |        sleep 3        nginx -s quit
  1. sleep 3 здесь для страховки от race conditions, связанных с удалением endpoint.

  2. nginx -s quit инициирует корректное завершение работы для nginx. Хотя в свежих образах nginx эта строка больше не понадобится, т. к. там STOPSIGNAL: SIGQUIT установлен по умолчанию.

(Более подробно про graceful shutdown для nginx в связке с PHP-FPM вы можете узнать из другой нашей статьи.)

Корректно ли ваше приложение обработает STOPSIGNAL, зависит только от него. На практике для большинства приложений приходится гуглить, как оно обрабатывает указанный для него STOPSIGNAL. И если оказывается, что не так, как надо, то делается preStop-хук, который эту проблему решает, либо же STOPSIGNAL меняется на тот, который приложение сможет обработать корректно и штатно завершиться.

Ещё один важный параметр, связанный с остановкой приложения, terminationGracePeriodSeconds. Он отвечает за то, сколько времени будет у приложения на корректное завершение. Если приложение не успеет завершиться в течение этого времени (30 секунд по умолчанию), то приложению будет послан сигнал KILL. Таким образом, если вы ожидаете, что выполнение preStop-хука и/или завершение работы приложения при получении STOPSIGNAL могут занять более 30 секунд, то terminationGracePeriodSeconds нужно будет увеличить. Например, такое может потребоваться, если некоторые запросы у клиентов веб-сервиса долго выполняются (вроде запросов на скачивание больших файлов).

Стоит заметить, что preStop-хук выполняется блокирующе, т. е. STOPSIGNAL будет послан только после того, как preStop-хук отработает. Тем не менее, отсчет terminationGracePeriodSeconds идёт и в течение работы preStop-хука. А процессы, запущенные в хуке, равно как и все процессы в контейнере, получат сигнал KILL после того, как terminationGracePeriodSeconds закончится.

Также у некоторых приложений встречаются специальные настройки, регулирующие время, в течение которого приложение должно завершить свою работу (к примеру, опция --timeout у Sidekiq). Оттого для каждого приложения надо убеждаться, что если у него есть подобная настройка, то она выставлена в значение немного меньшее, чем terminationGracePeriodSeconds.

6. Резервирование ресурсов

Планировщик на основании resources.requests Pod'а принимает решение о том, на каком узле этот Pod запустить. К примеру, Pod не будет schedule'иться на узел, на котором свободных (т. е. non-requested) ресурсов недостаточно, чтобы удовлетворить запросам (requests) нового Pod'а. А resources.limits позволяют ограничить потребление ресурсов Pod'ами, которые начинают расходовать ощутимо больше, чем ими было запрошено через requests. Лучше устанавливать лимиты равные запросам, так как если указать лимиты сильно выше, чем запросы, то это может лишить другие Pod'ы узла выделенных для них ресурсов. Это может приводить к выводу из строя других приложений на узле или даже самого узла. Также схема ресурсов Pod'а присваивает ему определенный QoS class: например, он влияет на порядок, в котором Pod'ы будут вытесняться (evicted) с узлов.

Поэтому необходимо выставлять и запросы, и лимиты и для CPU, и для памяти. Единственное, что можно/нужно опустить, так это CPU-лимит, если версия ядра Linux ниже 5.4 (для EL7/CentOS7 версия ядра должна быть ниже 3.10.0-1062.8.1.el7).

(Подробнее о том, что такое requests и limits, какие бывают QoS-классы в Kubernetes, мы рассказывали в этой статье.)

Также некоторые приложения имеют свойство бесконтрольно расти в потреблении оперативной памяти: к примеру, Redis, использующийся для кэширования, или же приложение, которое течёт просто само по себе. Чтобы ограничить их влияние на остальные приложения на том же узле, им можно и нужно устанавливать лимит на количество потребляемой памяти. Проблема только в том, что, при достижении этого лимита приложение будет получать сигнал KILL. Приложения не могут ловить/обрабатывать этот сигнал и, вероятно, не смогут корректно завершаться. Поэтому очень желательно использовать специфичные для приложения механизмы контроля за потреблением памяти в дополнение к лимитам Kubernetes, и не доводить эффективное потребление памяти приложением до limits.memory Pod'а.

Конфигурация для Redis, которая поможет с этим:

maxmemory 500mb   # если данные начнут занимать 500 Мб...maxmemory-policy allkeys-lru   # ...Redis удалит редко используемые ключи

А для Sidekiq это может быть Sidekiq worker killer:

require 'sidekiq/worker_killer'Sidekiq.configure_server do |config|  config.server_middleware do |chain|    # Корректно завершить Sidekiq при достижении им потребления в 500 Мб    chain.add Sidekiq::WorkerKiller, max_rss: 500  endend

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

В следующей статье мы также рассмотрим использование VerticalPodAutoscaler для автоматического выставления ресурсов.

7. Пробы

В Kubernetes пробы (healthcheck'и) используются для того, чтобы определить, можно ли переключить на приложение трафик (readiness) и не нужно ли приложение перезапустить (liveness). Они играют большую роль при обновлении Deployment'ов и при запуске новых Pod'ов в целом.

Сразу общая рекомендация для всех проб: выставляйте высокий timeoutSeconds. Значение по умолчанию в одну секунду слишком низкое. Особенно критично для readinessProbe и livenessProbe. Слишком низкий timeoutSeconds будет приводить к тому, что при увеличении времени ответов у приложений в Pod'ах (что обычно происходит для всех Pod'ов сразу благодаря балансированию нагрузки с помощью Service) либо перестанет приходить трафик почти во все Pod'ы (readiness), либо, что ещё хуже, начнутся каскадные перезапуски контейнеров (liveness).

7.1 Liveness probe

На практике вам не так часто нужна liveness probe (дословно: проверка на жизнеспособность), насколько вы думаете. Её предназначение перезапустить контейнер с приложением, когда livenessProbe перестаёт отрабатывать, например, если приложение намертво зависло. На практике подобные deadlockи скорее исключение, чем правило. Если же приложение работает, но не полностью (например, приложение не может само восстановить соединение с БД, если оно оборвалось), то это нужно исправлять в самом приложении, а не накручивать костыли с livenessProbe.

И хотя как временное решение можно добавить в livenessProbe проверку на подобные состояния, по умолчанию livenessProbe лучше вообще не использовать. Как альтернативу её полному отсутствию можно рассмотреть простейшую livenessProbe вроде проверки на то, открыт ли TCP-порт (обязательно выставьте большой таймаут). В таком случае это поможет приложению перезапуститься при возникновении очевидного deadlock'а, но при этом приложение не подвергнется риску войти в цикл перезапусков, когда перезапуск не может помочь.

И риски, которые плохая livenessProbe несёт, весьма серьезные. Самые частые случаи: когда livenessProbe перестаёт отрабатывать по таймауту из-за повышенной нагрузки на приложение, а также когда livenessProbe перестаёт работать, т. к. проверяет (прямо или косвенно) состояние внешних зависимостей, которые сейчас отказали. В последнем случае последует перезагрузка всех контейнеров, которая при лучшем раскладе ни к чему не приведет, а при худшем приведет к полной (и, возможно, длительной) недоступности приложения. Полная длительная недоступность приложения может происходить, если при большом количестве реплик контейнеры большинства Pod'ов начнут перезагружаться в течение короткого промежутка времени. При этом какие-то контейнеры, скорее всего, поднимутся быстрее других, и на это ограниченное количество контейнеров теперь придется вся нагрузка, которая приведет к таймаутам у livenessProbe и заставит контейнеры снова перезапускаться.

Также, если все-таки используете livenessProbe, убедитесь, что она не перестает отвечать, если у вашего приложения есть лимит на количество установленных соединений и этот лимит достигнут. Чтобы этого избежать, обычно требуется зарезервировать под livenessProbe отдельный тред/процесс самого приложения. Например, запускайте приложение с 11 тредами, каждый из которых может обрабатывать одного клиента, но не пускайте извне в приложение более 10 клиентов, таким образом гарантируя для livenessProbe отдельный незанятый тред.

И, конечно, не стоит добавлять в livenessProbe проверки внешних зависимостей.

(Подробнее о проблемах с liveness probe и рекомендациях по предотвращению таких проблем рассказывалось в этой статье.)

7.2 Readiness probe

Дизайн readinessProbe (дословно: проверка на готовность [к обслуживанию запросов]), пожалуй, оказался не очень удачным. Она сочетает в себе две функции: проверять, что приложение в контейнере стало доступным при запуске контейнера, и проверять, что приложение остаётся доступным уже после его запуска. На практике первое нужно практически всегда, а второе примерно настолько же часто, насколько оказывается нужной livenessProbe. Проблемы с плохими readinessProbe примерно те же самые, что и с плохими livenessProbe, и в худшем случае также могут приводить к длительной недоступности приложения.

Когда readinessProbe перестаёт отрабатывать, то на Pod перестаёт приходить трафик. В большинстве случаев такое поведение мало помогает, т. к. трафик обычно балансируется между Pod'ами более-менее равномерно. Таким образом, чаще всего readinessProbe либо работает везде, либо не работает сразу на большом количестве Pod'ов. Есть ситуации, когда подобное поведение readinessProbe может понадобиться, но в моей практике это скорее исключение.

Тем не менее, у readinessProbe есть другая очень важная функция: определить, когда только что запущенное в контейнере приложение стало способно принимать трафик, чтобы не пускать трафик в ещё не доступное приложение. Эта же функция readinessProbe, напротив, нужна нам почти всегда.

Получается странная ситуация, что одна функция readinessProbe обычно очень нужна, а другая очень не нужна. Эта проблема была решена введением startupProbe, которая появилась в Kubernetes 1.16 и перешла в Beta в 1.18. Таким образом, рекомендую для проверки готовности приложения при его запуске в Kubernetes < 1.18 использовать readinessProbe, а в Kubernetes >= 1.18 использовать startupProbe. readinessProbe всё ещё можно использовать в Kubernetes >= 1.18, если у вас есть необходимость останавливать трафик на отдельные Pod'ы уже после старта приложения.

7.3 Startup probe

startupProbe (дословно: проверка на запуск) реализует первоначальную проверку готовности приложения в контейнере для того, чтобы пометить текущий Pod как готовый к приёму трафика, или же для того, чтобы продолжить обновление/перезапуск Deployment'а. В отличие от readinessProbe, startupProbe прекращает работать после запуска контейнера. Проверять внешние зависимости в startupProbe не лучшая идея, потому что если startupProbe не отработает, то контейнер будет перезапущен, что может приводить к переходу Pod'а в состояние CrashLoopBackOff. При этом состоянии между попытками перезапустить неподнимающийся контейнер будет делаться задержка до пяти минут. Это может означать простой в том случае, когда приложение уже может подняться, но контейнер всё ещё выжидает CrashLoopBackOff перед тем, как снова попробовать запуститься.

Обязательна к использованию, если ваше приложение принимает трафик и у вас Kubernetes >= 1.18.

Также предпочитайте увеличение failureTreshold вместо использования initialDelaySeconds. Это позволит контейнеру становиться доступным настолько быстро, насколько это возможно.

8. Проверка внешних зависимостей

Часто можно встретить совет проверять внешние зависимости вроде баз данных в readinessProbe. И хотя такой подход имеет право на существование, предпочтительно разделять проверку внешних зависимостей и проверку на то, не стоит ли остановить идущий на Pod трафик, когда приложение в нём полностью утилизировано.

С помощью initContainers можно проверять внешние зависимости до того, как начнут запускаться startupProbe/readinessProbe основных контейнеров. В readinessProbe, соответственно, проверки внешних зависимостей уже не понадобится. Подобные initContainers не требуют изменений в коде приложения, не требуют собирать контейнеры приложения с дополнительными утилитами для проверок внешних зависимостей, а также в целом довольно просты в реализации:

      initContainers:      - name: wait-postgres        image: postgres:12.1-alpine        command:        - sh        - -ec        - |          until (pg_isready -h example.org -p 5432 -U postgres); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi      - name: wait-redis        image: redis:6.0.10-alpine3.13        command:        - sh        - -ec        - |          until (redis-cli -u redis://redis:6379/0 ping); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi

Полный пример

Резюмируя, привёдем полный пример того, как уже с учётом всех вышеописанных рекомендаций может выглядеть Deployment stateless-приложения при его боевом развертывании.

Требования: Kubernetes >= 1.18, на узлах Ubuntu/Debian с версией ядра >= 5.4.

apiVersion: apps/v1kind: Deploymentmetadata:  name: testappspec:  replicas: 10  selector:    matchLabels:      app: testapp  template:    metadata:      labels:        app: testapp    spec:      affinity:        podAntiAffinity:          preferredDuringSchedulingIgnoredDuringExecution:          - podAffinityTerm:              labelSelector:                matchLabels:                  app: testapp              topologyKey: kubernetes.io/hostname      priorityClassName: production-medium      terminationGracePeriodSeconds: 40      initContainers:      - name: wait-postgres        image: postgres:12.1-alpine        command:        - sh        - -ec        - |          until (pg_isready -h example.org -p 5432 -U postgres); do            sleep 1          done        resources:          requests:            cpu: 50m            memory: 50Mi          limits:            cpu: 50m            memory: 50Mi      containers:      - name: backend        image: my-app-image:1.11.1        command:        - run        - app        - --trigger-graceful-shutdown-if-memory-usage-is-higher-than        - 450Mi        - --timeout-seconds-for-graceful-shutdown        - 35s        startupProbe:          httpGet:            path: /simple-startup-check-no-external-dependencies            port: 80          timeoutSeconds: 7          failureThreshold: 12        lifecycle:          preStop:            exec:              ["sh", "-ec", "#command to shutdown gracefully if needed"]        resources:          requests:            cpu: 200m            memory: 500Mi          limits:            cpu: 200m            memory: 500Mi

В следующий раз

Осталось ещё немало важных вещей, о которых обязательно надо рассказать, таких как PodDisruptionBudget, HorizontalPodAutoscaler и VerticalPodAutoscaler, чем мы непременно займемся во второй части этой статьи. А пока предлагаем вам поделиться своими лучшими практиками по деплою, либо исправить/дополнить уже описанные.

P.S.

Читайте также в нашем блоге:

Подробнее..

Recovery mode Мне повезло нужно обновить сертификаты k8s v1.12.3

03.03.2021 22:15:39 | Автор: admin

Неделю назад мне подкинули задачу - обновить сертификаты k8s кластере. С одной стороны задача казалась достаточно тривиальной, НО нетривиальности добавляло моя неуверенность с k8s: до этого момента я пользовался кубером как сервисом и больше чем посмотреть на поды, удалить их написать deployment по шаблону делать ничего не доводилось. Уверенности добавляло наличие инструкции, но как выяснилось она для версии v1.13 а у кластера для, которого требовалось реализовать эту задачу версия была 1.12.3. И тут началось

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

Дано k8s кластер:

  • 3 master ноды

  • 3 etcd ноды

  • 5 worker нод

kubectl get nodesNAME                    STATUS   ROLES    AGE    VERSIONproduct1-mvp-k8s-0001   Ready    master   464d   v1.12.3product1-mvp-k8s-0002   Ready    master   464d   v1.12.3product1-mvp-k8s-0003   Ready    master   464d   v1.12.3product1-mvp-k8s-0007   Ready    node     464d   v1.12.3product1-mvp-k8s-0008   Ready    node     464d   v1.12.3product1-mvp-k8s-0009   Ready    node     464d   v1.12.3product1-mvp-k8s-0010   Ready    node     464d   v1.12.3product1-mvp-k8s-0011   Ready    node     464d   v1.12.3

Срок действия сертификата

echo | openssl s_client -showcerts -connect product1-mvp-k8s-0001:6443 -servername api 2>/dev/null | openssl x509 -noout -enddatenotAfter=Mar  4 00:39:56 2021 GMT

Поехали:

  • на всех MASTER нодах бэкапируем /etc/kubernetes

sudo mkdir backup; sudo cp -R /etc/kubernetes backup/ ; sudo tar -cvzf backup/pki_backup_`hostname`-`date +%Y%m%d`.tar.gz backup/kubernetes/
  • Смотрим в структуру /etc/Kubernetes она будет примерно такой

ls -ltotal 80-rw------- 1 root root 5440 Mar  3 13:21 admin.confdrwxr-xr-x 2 root root 4096 Aug 17  2020 audit-policy-rw-r--r-- 1 root root  368 Mar  4  2020 calico-config.yml-rw-r--r-- 1 root root  270 Mar  4  2020 calico-crb.yml-rw-r--r-- 1 root root  341 Mar  4  2020 calico-cr.yml-rw-r--r-- 1 root root  147 Mar  4  2020 calico-node-sa.yml-rw-r--r-- 1 root root 6363 Mar  4  2020 calico-node.yml-rw------- 1 root root 5472 Mar  3 13:21 controller-manager.conf-rw-r--r-- 1 root root 3041 Aug 14  2020 kubeadm-config.v1alpha3.yaml-rw------- 1 root root 5548 Mar  3 13:21 kubelet.conf-rw-r--r-- 1 root root 1751 Mar  4  2020 kubelet.envdrwxr-xr-x 2 kube root 4096 Aug 14  2020 manifestslrwxrwxrwx 1 root root   28 Mar  4  2020 node-kubeconfig.yaml -> /etc/kubernetes/kubelet.conf-rw------- 1 root root 5420 Mar  3 13:21 scheduler.confdrwxr-xr-x 3 kube root 4096 Mar  3 10:20 ssl

у меня все ключи в ssl, а не в pki , который будет нужен kubeadm , то он должен появиться, в своем случае я сделаю на него symlink

ln -s /etc/kubernetes/ssl /etc/kubernetes/pki
  • отыскиваем файл с конфигурацией кластера, в моем случае это был

    kubeadm-config.v1alpha3.yaml

если такового вдруг нет то его возможно сгенерировать

kubectl get cm kubeadm-config -n kube-system -o yaml > /etc/kubernetes/kubeadm-config.yaml
  • Начинаем перегенерацию сертификатов

kubeadm alpha phase certs apiserver  --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml[certificates] Using the existing apiserver certificate and key.kubeadm alpha phase certs apiserver-kubelet-clientI0303 13:12:24.543254   40613 version.go:236] remote version is much newer: v1.20.4; falling back to: stable-1.12[certificates] Using the existing apiserver-kubelet-client certificate and key.kubeadm alpha phase certs front-proxy-clientI0303 13:12:35.660672   40989 version.go:236] remote version is much newer: v1.20.4; falling back to: stable-1.12[certificates] Using the existing front-proxy-client certificate and key.kubeadm alpha phase certs  etcd-server --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml[certificates] Generated etcd/server certificate and key.[certificates] etcd/server serving cert is signed for DNS names [prod-uct1-mvp-k8s-0001 localhost] and IPs [127.0.0.1 ::1]kubeadm alpha phase certs  etcd-server --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml[certificates] Using the existing etcd/server certificate and key.kubeadm alpha phase certs  etcd-healthcheck-client --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml[certificates] Generated etcd/healthcheck-client certificate and key.kubeadm alpha phase certs  etcd-peer --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml[certificates] Generated etcd/peer certificate and key.[certificates] etcd/peer serving cert is signed for DNS names [product1-mvp-k8s-0001 localhost] and IPs [192.168.4.201 127.0.0.1 ::1]
  • проверяем выпущенные сертификаты на актуальность

find /etc/kubernetes/pki/ -name '*.crt' -exec openssl x509 -text -noout -in {} \; | grep -A2 Validity        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  2 10:29:44 2030 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  3 10:07:29 2022 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  3 10:07:52 2022 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  3 10:06:48 2022 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  2 10:29:44 2030 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  2 19:39:56 2022 GMT--        Validity            Not Before: Mar  4 10:29:43 2020 GMT            Not After : Mar  2 10:29:43 2030 GMT--        Validity            Not Before: Mar  4 10:29:43 2020 GMT            Not After : Mar  2 19:40:13 2022 GMT--        Validity            Not Before: Mar  4 10:29:44 2020 GMT            Not After : Mar  2 19:36:38 2022 GMT
  • В процессе обновления сертификатов буду выпущены заново файлы admin.conf, controller-manager.conf, kubelet.conf, scheduler.conf а существующие переносим в подпапку tmpи генерим новые файлы

kubeadm alpha phase kubeconfig all  --config /etc/kubernetes/kubeadm-config.v1alpha3.yaml [kubeconfig] Using existing up-to-date KubeConfig file: "/etc/kubernetes/admin.conf"[kubeconfig] Using existing up-to-date KubeConfig file: "/etc/kubernetes/kubelet.conf"[kubeconfig] Using existing up-to-date KubeConfig file: "/etc/kubernetes/controller-manager.conf"[kubeconfig] Using existing up-to-date KubeConfig file: "/etc/kubernetes/scheduler.conf"
  • перезапускаем все контейнеры и kubelet мастер ноды и проверяем что сервис kubelet завершил перезапуск

sudo systemctl stop kubelet; sudo docker stop $(docker ps -aq); sudo docker rm $(docker ps -aq); sudo systemctl start kubeletsystemctl status kubelet -l kubelet.service - Kubernetes Kubelet Server   Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; vendor preset: disabled)   Active: active (running) since Wed 2021-03-03 14:00:22 MSK; 10s ago     Docs: https://github.com/GoogleCloudPlatform/kubernetes  Process: 52998 ExecStartPre=/bin/mkdir -p /var/lib/kubelet/volume-plugins (code=exited, status=0/SUCCESS) Main PID: 53001 (kubelet)   Memory: 51.2M   CGroup: /system.slice/kubelet.service
  • проверяем что master нода вернулась нормально в кластер и что доступна конфигурация namespace

kubectl get nodeskubectl get nsNAME STATUS AGEdefault Active 464dproduct1-mvp Active 318dinfra-logging Active 315dinfra-nginx-ingress Active 386dkube-public Active 464dkube-system Active 464dpg Active 318d
  • проверяем что сертификат обновился

notAfter=Mar 3 07:40:43 2022 GMT

Обновление сертификатов на master ноде 1 успешно завершено и повторяем туже процедуру на оставшихся 2-х.


Далее обновляем worker ноды:

  • удаляем или переименовываем kubelet.conf, необходимо для того чтобы при перезапуске подхватился файл bootstrap-kubelet.conf

cd /etc/kubernetes/mv kubelet.conf kubelet.conf_old
  • вносим изменения в файл bootstrap-kubelet.conf если его нет, то создаем по шаблону внизу

apiVersion: v1clusters:- cluster: certificate-authority-data: | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJd01ETX server: https://192.168.4.201:6443 name: product1contexts:- context: cluster: product1 user: tls-bootstrap-token-user name: tls-bootstrap-token-user@product1current-context: tls-bootstrap-token-user@product1kind: Configpreferences: {}users:- name: tls-bootstrap-token-user user: token: fgz9qz.lujw0bwsdfhdsfjhgds

где мы должны заменить

- certificate-authority-data корневой сертификат центра сертификации PKI CA мастера, берем например из файла /etc/kubernetes/kubelet.conf на master ноде

- server: https://192.168.4.201:6443 - ip api сервера master ноды, или же виртуальный balance ip

token: fgz9qz.lujw0bwsdfhdsfjhgds - токен, который генерим на master ноде

kubeadm token create

  • перезапускаем kubelet и проверяем результат с master ноды, work нода должна ,быть доступна ready в ресурсе кластера

systemctl restart kubeletsystemctl status kubelet -l kubelet.service - Kubernetes Kubelet Server Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; vendor preset: disabled) Active: active (running) since Wed 2021-03-03 14:06:33 MSK; 11s ago Docs: https://github.com/GoogleCloudPlatform/kubernetes Process: 54615 ExecStartPre=/bin/mkdir -p /var/lib/kubelet/volume-plugins (code=exited, status=0/SUCCESS)Main PID: 54621 (kubelet) Memory: 52.1M CGroup: /system.slice/kubelet.service
  • проверить, что сертификат обновлен посмотреть на обновление сертификатов в папке

ls -las /var/lib/kubelet/pki/total 244 -rw-------. 1 root root 1135 Mar 3 14:06 kubelet-client-2021-03-03-14-06-34.pem0 lrwxrwxrwx. 1 root root 59 Mar 3 14:06 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2021-03-03-14-06-34.pem4 -rw-r--r--. 1 root root 2267 Mar 2 10:40 kubelet.crt4 -rw-------. 1 root root 1679 Mar 2 10:40 kubelet.key

Повторяем подобную процедуру на всех оставшихся work нодах.

Все мы обновили сертификаты на k8s кластере v1.12.3

Подробнее..
Категории: Kubernetes , Kubectl , Kubelet , Kubeadm , Cert

Перевод Jobы и Cronjobы Kubernetes

10.03.2021 18:12:48 | Автор: admin

CronJobы полезны для создания периодических и повторяющихся задач, таких как создание бэкапов или отправка электронных писем. CronJobы также могут планировать отдельные Jobы (задачи, задания - здесь и далее используется английский термин, чтобы избежать путаницы) на конкретное время, например, запланировать Job на то время, когда ваш кластер, скорее всего, будет простаивать. Jobы Kubernetes в первую очередь предназначены для краткосрочных и пакетных рабочих нагрузок (workloads).

В Kubernetes есть несколько контроллеров для управления подами: ReplicaSets, DaemonSets, Deployments и StatefulSets. У каждого из них есть свой сценарий использования. Однако их всех объединяет гарантия того, что их поды всегда работают. В случае сбоя пода контроллер перезапускает его или переключает его на другой узел (node), чтобы гарантировать постоянную работу программы, которая размещена на подах.

Что, если мы не хотим, чтобы под работал постоянно? Есть сценарии, когда вы не хотите, чтобы процесс продолжался непрерывно. Резервное копирование (backup, создание бэкапа) - это создание копии данных на отдельном носителе, предназначенной для восстановления данных в случае их потери. Данный процесс не должен выполняться постоянно. Напротив, он запускается на выполнение, и после завершения возвращает соответствующий код завершения (exit status), который сообщает, является ли результат успешным или неудачным.

Варианты использования Jobов Kubernetes

Наиболее подходящие варианты использования Jobов Kubernetes:

1. Пакетная обработка данных: допустим вы запускаете пакетную задачу с периодичностью один раз в день/неделю/месяц или по определенному расписанию. Примером этого может служить чтение файлов из хранилища или базы данных и передача их в сервис для обработки файлов.

2. Команды/специфические задачи: например, запуск скрипта/кода, который выполняет очистку базы данных.

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

Ваш первый Kubernetes Job

Создание Jobа Kubernetes, как и других ресурсов Kubernetes, осуществляется с помощью файла определения (definition file). Откройте новый файл, вы можете назвать его job.yaml. Добавьте в файл следующее:

apiVersion: batch/v1kind: Jobmetadata:name: hello-worldspec:template:  metadata:    name: hello-world  spec:    containers:    - name: hello-world      image: busybox      command: ["echo", "Running a hello-world job"]    restartPolicy: OnFailure

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

$ kubectl apply -f job.yamljob.batch/hello-world created

Давайте посмотрим, какие поды были созданы:

$ kubectl get podsNAME              READY    STATUS          RESTARTS      AGEhello-world-mstmc        0/1 ContainerCreating   0          1s

Проверим статус jobа с помощью kubectl:

kubectl get jobs hello-world-mstmcNAME      COMPLETIONS   DURATION   AGEhello-world-mstmc  1/1      21s    27s

Подождем несколько секунд и снова запустить ту же команду:

$ kubectl get podsNAME              READY   STATUS  RESTARTS       AGEhello-world-mstmc       0/1 Completed          0           25s

Статус пода говорит, что он не запущен. Статус Completed, поскольку job был запущен и выполнен успешно. Job, который мы только что определили, имел простую задачу :) - echo Running a hello-world job в стандартный вывод.

Прежде чем двигаться дальше, давайте убедимся, что job действительно выполнил то, что мы от него хотели:

kubectl logs -f hello-world-mstmcRunning a hello-world job

В логах отображается то, что мы задали, т.е. вывести Running a hello-world job. Job успешно выполнен.

Запуск CronJob вручную

Бывают ситуации, когда вам нужно выполнить cronjob на разовой основе. Вы можете сделать это, создав job из существующего cronjobа.

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

kubectl create job --from=cronjob/kubernetes-cron-job manual-cron-job

--from=cronjob/kubernetes-cron-job скопируйте шаблон cronjobа и создайте job с именем manual-cron-job

Удаление Jobа Kubernetes и очистка (Cleanup)

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

Job можно удалить с помощью kubectl следующим образом:

kubectl delete jobs job_name

Приведенная выше команда удаляет конкретный Job и все его дочерние поды. Как и в случае с другими контроллерами Kubernetes, вы можете удалить Job, только покидая его поды, используя флаг cascade=false. Например:

kubectl delete jobs job_name cascade=false

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

1. failedJobHistoryLimit и successfulJobsHistoryLimit: Удаления истории успешных и неудавшихся jobов основано на заданном Вами количестве сохранений. Это очень удобно для отбрасывания всех вхождений, завершенных неудачей, когда вы пытаетесь вывести список jobов. Например,

failedJobHistoryLimit: 5successfulJobsHistoryLimit: 10

2. backoffLimit: общее количество повторных попыток в случае сбоя пода.

RestartPolicy Jobа Kubernetes

В параметре restartPolicy (политика перезапуска) нельзя установить политику always (всегда). Job не должен перезапускать под после успешного завершения по определению. Таким образом, для параметра restartPolicy доступны варианты Never (никогда) и OnFailure (в случае неудачи).

Ограничение выполнения Jobа Kubernetes по времени

Если вы заинтересованы в выполнении Jobа в течение определенного периода времени, независимо от того, успешно ли завершился процесс, то у Jobов Kubernetes есть параметр spec.activeDeadlineSeconds. Установка для этого параметра приведет к немедленному прекращению работы Jobа по истечении заданного количества секунд.

Обратите внимание, что этот параметр переопределяет .spec.backoffLimit, т.е. если под завершается неудачей и Job достигает своего временного ограничения, неудавшийся под не перезапускается. Все сразу же останавливается.

В следующем примере мы создаем Job как с параметром backoff limit, так и с deadline:

apiVersion: batch/v1kind: Jobmetadata: name: data-consumerspec: backoffLimit: 5 activeDeadlineSeconds: 20 template:spec:    containers:    - name: consumer        image: busybox        command: ["/bin/sh", "-c"]        args: ["echo 'Consuming data'; sleep 1; exit 1"]    restartPolicy: OnFailure

Завершения Jobов и параллелизм

Мы рассмотрели, как можно выполнить одну задачу, определенную внутри объекта Job, что более известно как шаблон run-once. Однако в реальных сценариях используются и другие шаблоны.

Несколько одиночных Jobов

Например, у нас может быть очередь сообщений, которая требует обработки. Мы должны порождать пользовательские jobы, которые извлекают сообщения из очереди, пока она не опустеет. Чтобы реализовать этот шаблон с помощью Jobов Kubernetes, мы устанавливаем в параметр .spec.completions какое-либо число (должно быть ненулевым, а положительным числом). Job начинает создавать поды пока не достигнет заданного числа завершений (completions). Job считается завершенным, когда все поды завершаются с успешным с кодом завершения. Приведем пример. Измените наш файл определения, чтобы он выглядел следующим образом:

apiVersion: batch/v1kind: Jobmetadata:name: data-consumerspec:completions: 5template: metadata:   name: data-consumer spec:   containers:   - name: data-consumer     image: busybox     command: ["/bin/sh","-c"]     args: ["echo 'consuming a message from queue'; sleep 5"]   restartPolicy: OnFailure
  • Мы указываем параметр completions равным 5.

Несколько параллельно запущенных Jobов (Work Queue)

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

Измените файл определения, чтобы он выглядел следующим образом:

apiVersion: batch/v1kind: Jobmetadata:name: data-consumerspec:parallelism: 5template: metadata:   name: data-consumer spec:   containers:   - name: data-consumer     image: busybox     command: ["/bin/sh","-c"]     args: ["echo 'consuming a message from queue'; sleep $(shuf -i 5-10 -n 1)"]   restartPolicy: OnFailure

В этом примере мы не задали параметр .spec.completions. Вместо этого мы указали параметр parallelism. Параметр completions в нашем случае по умолчанию равен parallelism (5). Теперь Job делает следующее:

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

В этом сценарии Kubernetes Job одновременно порождает 5 подов. Знать, завершили ли остальные поды свои задачи, в компетенции самих подов. В нашем примере мы предполагаем, что получаем сообщения из очереди сообщений (например, AWS SQS). Когда сообщений для обработки больше нет, Job получает уведомление о том, что он должен завершиться. После успешного завершения первого пода:

  • Поды больше не создаются.

  • Существующие поды завершают свою работу и тоже завершаются.

В приведенном выше примере мы изменили команду, которую выполняет под, чтобы перевести его в спящий режим на случайное количество секунд (от 5 до 10), прежде чем он завершится. Таким образом, мы примерно моделируем, как несколько подов могут работать вместе с внешним источником данных, таким как очередь сообщений или API.

Ограничения CronJobов

Cronjob создает объект job примерно по одному разу за сеанс выполнения своего расписания (schedule). Мы говорим примерно, потому что при определенных обстоятельствах могут быть созданы два jobа или ни одного. Мы пытаемся сделать подобные случаи как можно более редкими, но не можем предотвратить их полностью. Следовательно, jobы должны быть идемпотентными.

Если параметр startingDeadlineSeconds установлен на большое значение или не задан (по умолчанию) и параметр concurrencyPolicy установлен на Allow, job всегда будут запускаться как минимум один раз.

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

Cannot determine if job needs to be started. Too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.

Важно отметить, что если параметр startingDeadlineSeconds установлен (т.е. не nil), то контроллер считает сколько произошло пропущенных jobов исходя из значения параметра startingDeadlineSeconds, начиная с последнего заданного времени до сих пор. Например, если параметр startDeadlineSeconds установлен на 200, контроллер подсчитывает, сколько пропущенных jobов произошло за последние 200 секунд.

CronJob считается пропущенным, если его не удалось создать в установленное время. Например, если для параметра concurrencyPolicy задано значение Forbid и была предпринята попытка запланировать CronJob во время выполнения предыдущего расписания, оно будет считаться пропущенным.

Например, предположим, что CronJob настроен запускать новый Job каждую минуту начиная с 08:30:00, а его параметр startingDeadlineSeconds не установлен. Если контроллер CronJob не работал с 08:29:00 до 10:21:00, job не запустится, так как количество пропущенных jobов в расписании превышает 100.

Чтобы проиллюстрировать эту концепцию с другой стороны, предположим, что CronJob запрограммирован планировать новый Job в расписании каждую минуту начиная с 08:30:00 и его параметр startingDeadlineSeconds устанавливается на 200 секунд. Если контроллер CronJob не работает в течение того же периода, что и в предыдущем примере (с 08:29:00 до 10:21:00), Job все равно запустится в 10:22:00. Это происходит, поскольку контроллер теперь проверяет, сколько пропущенных расписаний было за последние 200 секунд (т. е. 3 пропущенных расписания), вместо того, чтобы считать с последнего заданного времени до настоящего момента.

CronJob отвечает только за создание Jobов, соответствующих его расписанию, а Job, в свою очередь, отвечает за управление представляемыми им подами.

TL;DR (Too Long; Didnt Read)

  • Jobы Kubernetes используются, когда вы хотите создать поды, которые будут выполнять определенную задачу, а затем завершать работу.

  • Jobы Kubernetes по умолчанию не нуждаются в селекторах подов; Job автоматически обрабатывает их лейблы и селекторы.

  • Параметр restartPolicy для Job принимает значения Never или OnFailure

  • Jobы используют параметры completions и parallelism для управления шаблонами, которые определяют порядок работы подов. Поды Jobов могут выполняться как одна задача, несколько последовательных задач или несколько параллельных задач, в которых первая завершенная задача дает указание остальным подам завершиться.

  • Вы можете контролировать количество попыток Jobа перезапустить неудавшиеся поды, используя параметр .spec.backoffLimit. По умолчанию этот лимит равен шести.

  • Вы можете контролировать время работы jobа, используя параметр .spec.activeDeadlineSeconds. Этот лимит отменяет backoffLimit. Таким образом, Job не пытается перезапустить неудавшийся под, если дедлайн достигнут.

  • Jobы и их поды не удаляются автоматически после завершения. Вы должны удалить их вручную или использовать контроллер ttlSecondsAfterFinished, который на момент написания этой статьи все еще находится в статусе alpha.


Перевод статьи подготовлен в преддверии старта курса "Инфраструктурная платформа на основе Kubernetes". Также приглашаем всех желающих записаться на бесплатный демо-урок по теме: "Работа с NoSQL базами в k8s (на примере Apache Cassandra)"

Подробнее..

Выступает DMN, дирижирует ZeeBe как использовать бизнес-правила в микросервисах

11.03.2021 18:13:13 | Автор: admin

Меня зовут Николай Первухин, я Senior Java Developer в Райффайзенбанке. Так сложилось, что, единожды попробовав бизнес-процессы на Camunda, я стал адептом этой технологии и стараюсь ее применять в проектах со сложной логикой. Действительно сама идея подкупает: рисуешь процесс в удобном GUI-редакторе (моделлере), а фреймворк выполняет эти действия по порядку, соблюдая большой спектр элементов нотации BPMN.

К тому же в Camunda есть встроенная поддержка еще одной нотации DMN (Decision Model and Notation): она позволяет в простой и понятной форме создавать таблицы принятия решений по входящим наборам данных.

Но чего-то все же не хватает... Может, добавим немного скорости?

Почему ускоряем процессы

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

Что обычно характеризует такие процессы:

  • от момента создания до завершения процесса может пройти несколько дней;

  • участвует большое количество сотрудников;

  • осуществляется интеграция со множеством банковских подсистем.

Фреймворк Camunda прекрасно справляется с такими задачами, однако, заглянем под капот: там находитсяклассическая база данных для осуществления транзакционности.

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

Отличные новости: воспользуемся ZeeBe

В июле 2019 года было официально объявлено, что после двух лет разработки фреймворк ZeeBe готов к использованию на боевой среде. ZeeBe специально разрабатывался под задачи highload и, по утверждению автора, был протестирован при 10 000 процессов в секунду. В отличие от Camunda, ядро фреймворка ZeeBe принципиально не использует базу данных из него убраны все вспомогательные подсистемы, в том числе и процессор правил DMN.

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

Итак, дано:

  • микросервис, инициирующий событие и запускающий процесс (event-handler);

  • микросервис обработки бизнес-правил (rules-engine);

  • микросервис, эмулирующий действия (action).

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

Из оркестрации у нас:

  • микросервис с брокером сообщений ZeeBe (zeebe);

  • микросервис визуализации работающих процессов simplemonitor (zeebe-simple-monitor).

А присматривать за всеми микросервисами будет кластер k8s.

Схема взаимодействия

С точки зрения бизнес-логики в примере будет рассмотрен следующий бизнес-сценарий:

  • из внешней системы происходит запрос в виде rest-обращения с передачей параметров;

  • запускается бизнес-процесс, который пропускает входящие параметры через бизнес-правило;

  • в зависимости от полученного решения из бизнес-правил запускается микросервис действий с различными параметрами.

Теперь поговорим подробнее о каждом микросервисе.

Микросервис zeebe

Данный микросервис состоит из брокера сообщений ZeeBe и экспортера сообщений для отображения в simple-monitor. Для ZeeBe используется готовая сборка, которую можно скачать с github. Подробно о сборке контейнера можно посмотреть в исходном коде в файле build.sh

Принцип ZeeBe минимальное число компонентов, входящих в ядро, поэтому по умолчанию ZeeBe это брокер сообщений, работающий по схемам BPMN. Дополнительные модули подключаются отдельно: например, для отображения процессов в GUI понадобится экспортер (доступны разные экспортеры, к примеру, в ElasticSearch, в базу данных и т.п.).

В данном примере возьмемэкспортер в Hazelcast. И подключим его:

  • добавим zeebe-hazelcast-exporter-0.10.0-jar-with-dependencies.jar в папку exporters;

  • добавим в файл config/application.yamlследующие настройки:

exporters:      hazelcast:        className: io.zeebe.hazelcast.exporter.HazelcastExporter        jarPath: exporters/zeebe-hazelcast-exporter-0.10.0-jar-with-dependencies.jar        args:          enabledValueTypes: "JOB,WORKFLOW_INSTANCE,DEPLOYMENT,INCIDENT,TIMER,VARIABLE,MESSAGE,MESSAGE_SUBSCRIPTION,MESSAGE_START_EVENT_SUBSCRIPTION"          # Hazelcast port          port: 5701

Данные активных процессов будут храниться в памяти, пока simplemonitor их не считает. Hazelcast будет доступен для подключения по порту 5701.

Микросервис zeebe-simplemonitor

Во фреймворке ZeeBe есть две версии GUI-интерфейса. Основная версия, Operate, обладает большим функционалом и удобным интерфейсом, однако, использование Operate ограничено специальной лицензией (доступна только версия для разработки, а лицензию для прода следует запрашивать у производителя).

Также есть облегченный вариант simplemonitor (лицензируется по Apache License, Version 2.0)

Simplemonitorможно оформить тоже в виде микросервиса, который периодически подключается к порту hazelcast брокера ZeeBe и выгружает оттуда данные в свою базу данных.

Можно выбрать любую базу данных Spring Data JDBC, в данном примере используется файловая h2, где настройки, как и в любом Spring Boot приложении, вынесены в application.yml

spring:  datasource:    url: jdbc:h2:file:/opt/simple-monitor/data/simple-monitor-db;DB_CLOSE_DELAY=-1

Микросервис event-handler

Это первый сервис в цепочке, он принимает данные по rest и запускает процесс.При старте сервис осуществляет поиск файлов bpmn в папке ресурсов:

private void deploy() throws IOException {        Arrays.stream(resourceResolver.getResources("classpath:workflow/*.bpmn"))                .forEach(resource -> {                    try {                        zeebeClient.newDeployCommand().addResourceStream(resource.getInputStream(), resource.getFilename())                                .send().join();                        logger.info("Deployed: {}", resource.getFilename());                    } catch (IOException e) {                        logger.error(e.getMessage(), e);                    }                });    }

Микросервис имеет endpoint, и для простоты принимает вызовы по rest. В нашем примере передаются 2 параметра, сумма и лимит:

http://адрес-сервиса:порт/start?sum=100&limit=500
@GetMapping    public String getLoad(@RequestParam Integer sum, @RequestParam Double limit) throws JsonProcessingException {        Map<String, Object> variables = new HashMap<>();        variables.put("sum", sum);        variables.put("limit", limit);        zeebeService.startProcess(processName, variables);        return "Process started";    }

Следующий код отвечает за запуск процесса:

 public void startProcess(String processName, Map<String, Object> variables) throws JsonProcessingException {         zeebeClient.newCreateInstanceCommand()                .bpmnProcessId(processName)                .latestVersion()                .variables(variables)                .send();    }

Сам процесс нарисован в специальной программе ZeeBe modeler (почти копия редактора Camunda modeler) и сохраняется в формате bpmn в папке workflow в ресурсах микросервиса. Графически процессвыглядит как:

У каждой задачи (обозначаем прямоугольником на схеме) есть свой тип задач, который устанавливается в свойствах задачи, например, для запуска правил:

Каждый дополнительный микросервис в данном примере будет использовать свой тип задач. Тип задач очень похож на очередь в Kafka: при возникновении задач к нему могут подключаться подписчики workerы.

После старта процесс продвинется на один шаг, и появится сообщение типа DMN.

Микросервис rules-engine

Благодаря прекрасной модульной архитектуре Camunda есть возможность использовать в своем приложении (отдельно от самого фреймворка Camunda) движок правил принятия решения.

Для его интеграции с вашим приложением достаточно добавить его в зависимости maven:

        <dependency>            <groupId>org.camunda.bpm.dmn</groupId>            <artifactId>camunda-engine-dmn</artifactId>            <version>${camunda.version}</version>        </dependency>

Сами правила создаются в специальном графическом редакторе Camunda modeler. Одна диаграмма DMN имеет два вида отображения.

Entity Relation Diagram (вид сверху) показывает зависимости правил друг от друга и от внешних параметров:

На такой диаграмме можно представить одно или несколько бизнес-правил. В текущем примере оно одно зависит от двух параметров сумма и лимит. Представление на этой диаграмме параметров и комментариев необязательно, но является хорошим стилем оформления.

Само же бизнес-правило содержит более детальный вид:

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

Посмотрим на примере, где такой файл располагается в папке dmn-models в ресурсах микросервиса. Для регистрации диаграммы при старте микросервиса происходит его однократная загрузка:

 public void init() throws IOException {        Arrays.stream(resourceResolver.getResources("classpath:dmn-models/*.dmn"))                .forEach(resource -> {                    try {                        logger.debug("loading model: {}", resource.getFilename());                        final DmnModelInstance dmnModel = Dmn.readModelFromStream((InputStream) Resources                                .getResource("dmn-models/" + resource.getFilename()).getContent());                        dmnEngine.parseDecisions(dmnModel).forEach(decision -> {                            logger.debug("Found decision with id '{}' in file: {}", decision.getKey(),                                    resource.getFilename());                            registry.put(decision.getKey(), decision);                        });                    } catch (IOException e) {                        logger.error("Error parsing dmn: {}", resource, e);                    }        });    }

Для того, чтобы подписаться на сообщения от ZeeBe, требуется осуществить регистрацию workerа:

private void subscribeToDMNJob() {        zeebeClient.newWorker().jobType(String.valueOf(jobWorker)).handler(                (jobClient, activatedJob) -> {                    logger.debug("processing DMN");                    final String decisionId = readHeader(activatedJob, DECISION_ID_HEADER);                    final Map<String, Object> variables = activatedJob.getVariablesAsMap();                    DmnDecisionResult decisionResult = camundaService.evaluateDecision(decisionId, variables);                    if (decisionResult.size() == 1) {                        if (decisionResult.get(0).containsKey(RESULT_DECISION_FIELD)) {                            variables.put(RESULT_DECISION_FIELD, decisionResult.get(0).get(RESULT_DECISION_FIELD));                        }                    } else {                        throw new DecisionException("Нет результата решения.");                    }                    jobClient.newCompleteCommand(activatedJob.getKey())                            .variables(variables)                            .send()                            .join();                }        ).open();    }

В данном коде осуществляется подписка на событие DMN, вызов модели правил при получении сообщения от ZeeBe и результат выполнения правила сохранятся обратно в бизнес-процесс в виде переменной result (константа RESULT_DECISION_FIELD).

Когда данный микросервис отчитывается ZeeBe о выполнении операции, бизнес-процесс переходит к следующему шагу, где происходит выбор пути в зависимости от выполнения условия, заданного в свойствах стрелочки:

Микросервис action

Микросервис action совсем простой. Он также осуществляет подписку на сообщения от ZeeBe, но другого типа action.

В зависимости от полученного результата будет вызван один и тот же микросервис action, но с различными параметрами. Данные параметры задаются в закладке headers:

Также передачу параметров можно сделать и через закладку Input/Output, тогда параметры придут вместе с переменными процесса, но передача через headers является более каноничной.

Посмотрим на получение сообщения в микросервисе:

private void subscribe() {        zeebeClient.newWorker().jobType(String.valueOf(jobWorker)).handler(                (jobClient, activatedJob) -> {                    logger.debug("Received message from Workflow");                    actionService.testAction(                            activatedJob.getCustomHeaders().get(STATUS_TYPE_FIELD),                            activatedJob.getVariablesAsMap());                    jobClient.newCompleteCommand(activatedJob.getKey())                            .send()                            .join();                }        ).open();    }

Здесь происходит логирование всех переменных бизнес-процесса:

 public void testAction(String statusType, Map<String, Object> variables) {        logger.info("Event Logged with statusType {}", statusType);        variables.entrySet().forEach(item -> logger.info("Variable {} = {}", item.getKey(), item.getValue()));    }

Исходный код

Весь исходный код прототипа можно найти в открытом репозитории GitLab.

Компиляция образов Docker

Все микросервисы проекта собираются командой ./build.sh

Для каждого микросервиса есть различный набор действий, направленных на подготовку образов docker и загрузки этих образов в открытые репозитории hub.docker.com.

Загрузка микросервисов в кластер k8s

Для развертывания в кластере необходимо сделать следующие действия:

  1. Создать namespace в кластере kubectl create namespace zeebe-dmn-example

  2. Создать config-map общих настроек

kind: ConfigMapapiVersion: v1metadata:  name: shared-settings  namespace: zeebe-dmn-exampledata:  shared_servers_zeebe: <IP адрес кластера>

Далее создаем два персистентных хранилища для хранения данных zeebe и simplemonitor. Это позволит осуществлять перезапуск соответствующих подов без потери информации:

kubectl apply -f zeebe--sm-volume.yml

kubectl apply -f zeebe-volume.yml

Yml-файлы находятся в соответствующих проектах:

Теперь осталось последовательно создать поды и сервисы. Указанные yml-файлы находятся в корне соответствующих проектов.

kubctl apply -f zeebe-deployment.yml

kubctl apply -f zeebe-sm-deployment.yml

kubctl apply -f event-handler-deployment.yml

kubctl apply -f rules-engine-deployment.yml

kubctl apply -f action-deployment.yml

Смотрим, как отображаются наборы подов в кластере:

И мы готовы к тестовому запуску!

Запуск тестового процесса

Запуск процесса осуществляется открытием в браузере соответствующий URL. К примеру, сервис event-handler имеет сервис с внешним IP и портом 81 для быстрого доступа.

http://адрес-кластера:81/start?sum=600&limit=5000
Process started

Далее можно проверить отображение процесса в simplemonitor. У данного микросевиса тоже есть внешний сервис с портом 82.

Зеленым цветом выделен путь, по которому прошел процесс. Серым выделены выполненные задачи, а снизу отображены переменные процесса.

Теперь можно просмотреть лог микросервиса action, там можно увидеть значение переменной statusType, которое соответствует варианту прохождения процесса.

Поделюсь, какими ресурсами пользовался для подготовки прототипа

Небольшое послесловие вместо итогов

Из плюсов:

  • архитектура разработанного прототипа получилась гибкой и расширяемой. Можно добавлять любое количество микросервисов для обработки сложной логики;

  • простая нотация BPMN и DMN позволяет привлекать аналитиков и бизнес к обсуждению сложной логики;

  • Zeebe показал себя как очень быстрый фреймворк, задержки на получение/отправку сообщений практически отсутствуют;

  • Zeebe изначально разрабатывался для работы в кластере, в случае необходимости можно быстро нарастить мощности;

  • без ZeeBe Operate можно вполне обойтись, Simple-Monitor отвечает минимальным требованиям.

Из минусов:

  • хотелось бы иметь возможность редактирования DMN непосредственно в ZeeBe modeler (как это реализовано в Camunda), на данный момент, приходится использовать оба моделлера;

  • к сожалению, только в Enterprise версии Camunda есть возможность просмотра пути, по которому принималось решение:

Это очень полезная функция при отладке правил. В Community версии приходится добавлять дополнительное поле типа output для логирования, либо разработать свое решение визуализации.

При развертывании прототипа в реальных условиях в кластере k8s необходимо добавить ограничения по ресурсам (CPU и RAM), также нужно будет подобрать лучшую систему хранения исторических данных.

Где применять такие технологии:

  • как оркестрация внутри одной команды или продукта в виде перекладывания сложной логики на диаграммы BPMN / DMN;

  • в сфере, где идет потоковая обработка данных с большим количеством интеграций. В банке это может быть проведение или проверка транзакций, обработка данных из внешних систем или просто многоэтапная трансформация данных;

  • как частичная альтернатива существующего стека ESB или Kafka для интеграции между командами.

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

Подробнее..

Подборка телеграм-каналов для DevOps инженеров

13.03.2021 14:14:35 | Автор: admin

Приветствую, братцы!

Задача получения актуальной информации и совета опытных коллег сегодня актуальна как никогда. С одной стороны, сложно превзойти крупнейшие ИТ-сообщества в Slack. С другой стороны, важно иметь контакт с коллегами в нашей стране, в своем городе. Телеграм за последние годы стал крупнейшей площадкой для русскоязычного ИТ-сообщества, присоединяйтесь, не отставайте :)

Подборка телеграм-каналов и чатов:

Вакансии

Devops Jobs - Вакансии и резюме

Jobs for Devs & Ops - Вакансии для инженеров и разработчиков

Новостные каналы

Mops DevOps - Kubernetes, DevOps, SRE и многое другое

DevOps Deflope News - Новостной канал

Записки админа - Linux и администрировании серверов

k8s (in)security - Канал о (не)безопасности Kubernetes

Мир IT c Антоном Павленко - IT новости, статьи и видео

Конференции

DevOpsConf Channel - Информационный канал профессиональной конференции по эксплуатации и devops DevOpsConf Russia

Meetup Moscow - анонсы конференций

Инструменты DevOps

terraform_ru - Русскоязычный чат Hashicorp Terraform

pro_ansible- Чат для взаимопомощи по Ansible

Docker_ru- Русскоговорящее сообщество по экосистеме Docker (чат)

RU.Docker - Официальное Русское Сообщество (чат)

ru_gitlab- Русскоговорящая группа по GitLab

ru_jenkins- Русскоговорящая группа по Jenkins

Инфраструктура

Kubernetes- Общаемся на темы, посвященные Kubernetes, конфигурации и возможностям

istio_ru - Чат про Mervice Mesh в целом и Istio в частности

Вокруг Kubernetes в Mail.ru Group митапы по Kubernetes, DevOps, открытым технологиям в Mail.ru Group и немного проKubernetes как сервис

Envoy Proxy- Делимся опытом, экспертизой, советами и фэйлами :)

nginx_ru - Сообщество пользователей nginx, новости, обсуждения конфигураций

SDS и Кластерные FS - Обсуждаем Software-defined storage, кластерные файловые системы, блочные хранилища, стратегии построения хранилища и все что с ними связанно (Linstor, DRBD, ZFS, LVM, Ceph, GlusterFS, Lustre, MooseFS, LizardFS, mdadm, S3, iSCSI, NFS, OrangeFS, OCFS, GFS2)

Грефневая Кафка (pro.kafka)- Здесь топят за Кафку (Apache Kafka )

pro.kafka- Чат для добросовестных господ и дам, посвящённый Apache Kafka

DBA- Общаемся и обсуждаем темы, посвященные DBA, PostgreSQL, Redis, MongoDB, MySQL...

Облачные провайдеры

AWS_ru- Чат про Amazon Web Services

AWS notes- Канал про Amazon Web Services

Yandex.Cloud - Новости от команды платформы Yandex.Cloud

IT-журнал Завтра облачно - Блог команды Mail.ru Cloud Solutions (MCS)

Мониторинг и сбор логов

VictoriaMetrics_ru - Чат для обсуждения VictoriaMetrics

Церковь метрик- Канал про Метрики. Метрики. Метрики.

ru_logs - ElasticSearch, Graylog, Mtail, rsyslog и все такое прочее

Мониторим ИТ- Канал о мониторинге ИТ-инфраструктуры и приложений

Друзья, очень вероятно, что я мог забыть про какой-нибудь хороший, полезный и всеми любимы телеграм-канал. Жду ваши идеи в комментариях.

Подробнее..
Категории: Kubernetes , Gitlab , Devops , Nginx , Docker , Monitoring , Istio , Telegram , Aws , Envoy

Перевод Как заменить container runtime в Kubernetes

14.03.2021 18:20:23 | Автор: admin

Братцы! Скорее всего вы уже знаете, что Kubernetes отказался от поддержки Docker в качестве среды выполнения контейнеров (container runtime) в будующих версиях. В релизе 1.20, вышедшем в конце 2020 года Dockershim помечен как не рекомендованный (depricated). В релизе 1.22, выход которого запланирован на конец 2021 года, от его поддержки планируют полностью отказаться.

Если вы используете управляемые кластеры Kubernetes (такие как GKE, EKS, AKS) это не станет для вас серьезной проблемой и скорее всего переключение будет простым. Но если вы управляете кластером самостоятельно (например с помощью kubeadm) и используете Docker в container runtime, рано или поздно, вам придется заменить ее, чтобы иметь возможность обновлениять Kubernetes до последних версий.

Задача этой статьи не дать исчерпывающую информацию о причинах такого решения со стороны разработчиков Kubernetes или подробно изучить поведения конкретных container runtime в кластере Kubernetes. Вместо этого мы шаг за шагом разберемся как переключить Docker container runtime на другое решение, поддерживающее стандарт Container Runtime Interface(CRI). Если вас интересуют причины из-за которых Docker больше не рекомендован к использованию, ознакомьтесь со статьей из официального блога Kubernetes Don't Panic: Kubernetes and Docker.

Чтобы не пропустить новые статьи подписывайтесь на телеграм-канал Mops DevOps

Что проверять в первую очередь

Влияние на рабочие нагрузки, выполняемые в вашем кластере, должно быть минимальным. Единственное, о чем вам нужно позаботиться, - это используете ли вы Docker-in-Docker в любой из ваших контейнерных рабочих нагрузок, смонтировав сокет Docker /var/run/docker.sock. В этом случае вам нужно будет найти альтернативу (например, Kaniko), прежде чем переключаться с Docker на новую container runtime.

Также настоятельно рекомендуется сделать резервную копию ваших данных, прежде чем переходить к переключению среды выполнения контейнера!

Приступим к работе!

Отлично, теперь, когда вы готовы к переключению container runtime, можно начинать. В этой статье бедем использовать containerd в качестве container runtime, но приведенные ниже шаги можно адаптировать к другой среде выполнения контейнера, например, CRI-O.

Мы начнем с рабочих узлов (worker nodes) и только потом выполним аналогичные действия на мастерах (control plane).

Worker nodes

Указанные ниже шаги нужно выполнить на всех рабочих узлах кластера.

1) Вначале выполним drain и cordon узла кластера, чтобы эвакуировать нагрузки с него и предотвратить планирование новых нагрузок во время выполнения последующих операций:

kubectl cordon <node_name>kubectl drain <node_name>

Замечание: если на узлах кластера запущены DaemonSets, необходимо использовать флаг --ignore-daemonsets, чтобы продолжить эвакуацию остальных pods. Не волнуйтесь kubelet эти pods автоматически пересоздаст с помощью новой container runtime, когда процедура переключения будет завершена. Если у вас есть критические сервисы, запущенные в DaemonSet, и вы не хотите, чтобы они работали во время процесса переключения, вы можете либо указать nodeSelector в своем DaemonSet, либо полностью удалить и переустановить их в конце процесса.

2) Останавливаем сервис kubelet:

sudo systemctl stop kubeletsudo systemctl status kubelet

3) Удаляем Docker

Я не буду подробно описывать команды, поскольку это зависит от вашего дистрибутива Linux и способа установки Docker. Просто будьте осторожны, если вы хотите полностью очистить артефакты Docker, возможно, вам придется вручную удалить некоторые файлы (например, /var/ lib/docker).

Подробную инструкцию вы найдете в документации Docker.

4) Устанавливаем countainerd используя официальному руководству для вашей версии операционной системы.

5) Выполняем Enableи Startсервиса containerd:

sudo systemctl enable containerdsudo systemctl start containerdsudo systemctl status containerd

6) Kubernetes взаимодействует с container runtime через плагин CRI . Убедитесь, что этот плагин не был отключен при установке containerd.

Проверим файл конфигурации:

vim /etc/containerd/config.toml проверяем параметрdisabled_plugins = [""]

Если на предыдущем шаге внесены изменения в конфигурационный файл, необходимо перезагрузить сервис containerd:

sudo systemctl restart containerd

7) Отредактируем конфигурационный файл kubelet:

vim /var/lib/kubelet/kubeadm-flags.envДобавим в конфигурационный файл флаги для переменной KUBELET_KUBEADM_ARGS(при необходимости измените путь к container runtime)--container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock

8) Запускаем сервис kubelet:

sudo systemctl start kubelet

9) Проверяем, что на узле запущена требуемая версия container runtime:

kubectl describe node <node_name>
System Info:  Machine ID:                 21a5dd31f86c4  System UUID:                4227EF55-BA3BCCB57BCE  Boot ID:                    77229747-9ea581ec6773  Kernel Version:             3.10.0-1127.10.1.el7.x86_64  OS Image:                   Red Hat Enterprise Linux Server 7.8 (Maipo)  Operating System:           linux  Architecture:               amd64>>Container Runtime Version:  containerd://1.4.3  Kubelet Version:            v1.20.2  Kube-Proxy Version:         v1.20.2

10) Выполняем Uncordon на узле, чтобы разрешить планировать на него нагрузки, и проверим статус работы pods:

kubectl uncordon <node_name>

Вот и все, как только все ваши поды будут перезапущены, вы можете перейти к настройке следующего узла кластера!

Control Plane

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

Пока новая container runtime будет загружать образы kube-apiserver, etcd и coredns и создавать соответствующие pods, кластер будет недоступен. У вас также не должна быть возможности запускать команду kubectl.

Вот несколько советов, которые помогут вам следить за запуском container runtime и устранять потенциальные проблемы:

1) Используем journalctl, чтобы отслеживать изменения в журналах kubelet:

journalctl -u kubelet

2) Проверяем журналы containerd:

journalctl -u containerd

3) Используем утилиту crictl, чтобы убедится, что контейнеры запущены:

crictl --runtime-endpoint /run/containerd/containerd.sock ps

4) После замены container runtime убедимся, что используем требуемую версию, выполнив команду на всех узлах кластера:

kubectl describe node <master_node_name>также можем выполнить команду, она выведет информацию сразу по всем узлам кластераkubectl get node -o wide

Поздравляю! Кластер Kubernetes настроен без Docker, теперь проблем с обновлением на новые версии возникнуть не должно.


Телеграм-канал Mops DevOps - анонсы вебинаров и конференций, полезные статьи и видео, а так же регулярные скидки на обучение!

Подробнее..

Перевод О наблюдаемости микросервисов в Kubernetes

17.03.2021 16:04:48 | Автор: admin

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

Привет, Хабр. В рамках курса "Microservice Architecture" подготовили для вас перевод материала.

Также приглашаем на открытый вебинар по теме
Распределенные очереди сообщений на примере кафки.


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

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

Наблюдаемость зиждется на трех столпах:

  • Логи (журналы) событий: запись событий, произошедших в системе. События (events) дискретны и содержат метаданные о системе на тот момент, когда они происходили.

  • Трассировка (трейсинг): система обычно состоит из множества частей/компонентов, которые согласованно работают для выполнения определенных важных функций. Отслеживание потока запросов и ответов через распределенные компоненты критически важно для эффективной отладки/устранения неполадок конкретной функциональности.

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

Теперь к насущному вопросу как все это реализовать для наших микросервисов в кластере Kubernetes?

Микросервисы Kubernetes-приложение

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

Давайте рассмотрим микросервисное приложение, которое предоставляет информацию о погоде для конкретного города.

  • Weather-front: компонент, который состоит из фронтенда с интерфейсом для ввода названия города и просмотра информации о погоде. Смотрите скриншот выше.

  • Weather-services: компонент, который в качестве входных данных принимает название города и вызывает внешний погодный API для получения сведений о погоде.

  • Weather-db: это компонент с базой данных Maria, в которой хранятся данные о погоде, которые извлекаются для отслеживаемого города в фоновом режиме.

Указанные выше микросервисы развертываются с помощью объекта развертывания (Deployment object) Kubernetes, а ниже результат выполнения команды kubectl get deploy.

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

Weather-front:

- image: brainupgrade/weather:microservices-front  imagePullPolicy: Always  name: weather-front

Weather-services:

- image: brainupgrade/weather-services:2.0.0  imagePullPolicy: Always  name: weather-services

Weather-db:

- image: mariadb:10.3  name: mariadb  ports:  - containerPort: 3306    name: mariadb

Наблюдаемость Столп первый Логи событий

Чтобы реализовать первый столп наблюдаемости, нам нужно установить стек EFK: Elasticsearch, Fluentd и Kibana. Ниже приведены несложные шаги по установке.

Elasticsearch и Kibana:

helm repo add elastic https://helm.elastic.cohelm repo updatehelm install --name elasticsearch elastic/elasticsearch --set replicas=1 --namespace elasticsearchhelm install --name kibana elastic/kibana

Fluentd:

containers:- name: fluentd  imagePullPolicy: "Always"  image: fluent/fluentd-kubernetes-daemonset:v1.12.0-debian-elasticsearch7-1.0  env:    - name:  FLUENT_ELASTICSEARCH_HOST      value: "elasticsearch-master.elasticsearch.svc.cluster.local"    - name:  FLUENT_ELASTICSEARCH_PORT      value: "9200"

После установки вы можете запустить дашборд Kibana, который будет выглядеть как на скриншоте ниже:

При запуске Fluentd вы увидите следующее (Fluentd запускается как Daemonset - скриншот 4):

Вскоре логи начнут помещаться в Elasticsearch. Их можно будет просмотреть в Kibana:

Первый столп наблюдаемости реализован, поэтому давайте сосредоточимся на следующем распределенной трассировке.

Наблюдаемость Столп второй (распределенная) трассировка

Для распределенной трассировки (distributed tracing) у нас есть несколько альтернатив Java-приложения, такие как Zipkin, Jaeger, Elasticsesarch APM и т. д.

Поскольку у нас уже есть стек EFK, давайте воспользуемся APM, предоставляемым Elasticsearch. Во-первых, давайте запустим сервер APM как Kubernetes Deployment.

Код развертывания сервера Elastic APM:

containers:- name: apm-server  image: docker.elastic.co/apm/apm-server:7.5.0  ports:  - containerPort: 8200    name: apm-port

Как только сервер APM запущен, нам следует неинвазивно добавить агента APM в наши микросервисы. Смотрите приведенный ниже фрагмент кода, используемый для микросервиса weather-front. Аналогичный код следует использовать и для компонента weather-services.

Код агента APM для микросервиса weather-front:

initContainers:- name: elastic-java-agent  image: docker.elastic.co/observability/apm-agent-java:1.12.0  volumeMounts:  - mountPath: /elastic/apm/agent    name: elastic-apm-agent  command: ['cp', '-v', '/usr/agent/elastic-apm-agent.jar', '/elastic/apm/agent']   containers:  - image: brainupgrade/weather:microservices-front    imagePullPolicy: Always    name: weather-front    volumeMounts:    - mountPath: /elastic/apm/agent      name: elastic-apm-agent             env:      - name: ELASTIC_APM_SERVER_URL        value: "http://apm-server.elasticsearch.svc.cluster.local:8200"      - name: ELASTIC_APM_SERVICE_NAME        value: "weather-front"      - name: ELASTIC_APM_APPLICATION_PACKAGES        value: "in.brainupgrade"      - name: ELASTIC_APM_ENVIRONMENT        value: prod      - name: ELASTIC_APM_LOG_LEVEL        value: DEBUG      - name: JAVA_TOOL_OPTIONS        value: -javaagent:/elastic/apm/agent/elastic-apm-agent.jar

После повторного развертывания компонентов микросервисов, вы можете перейти в Observability -> APM console в Kibana, чтобы наблюдать, как появляются сервисы (смотрите на скриншот 6).

После того, как вы кликните на сервис weather-front, вы сможете увидеть транзакции:

Кликните на любую из транзакций, и вы увидите детализированное представление с более подробной информацией о транзакции, которая включает задержку, пропускную способность, фрагмент трассировки (trace Sample) и т. д.

На приведенном выше скриншоте показана распределенная трассировка, на которой четко проиллюстрирована взаимосвязь микросервисов weather-front и weather-services. Кликнув по Trace Sample, вы перейдете к сведениям транзакции (transaction details).

Раскрывающийся список Actions в сведениях о транзакции предоставляет возможность просматривать логи для этой конкретной транзакции.

Что ж, на данный момент мы рассмотрели два столпа наблюдаемости из трех.

Наблюдаемость Столп третий Метрики

Для реализации третьего столпа, то есть метрик, мы можем использовать дашборд служб APM, где фиксируются задержка (Latency), пропускная способность (Throughput) и процент ошибок (Error rate).

Кроме того, мы можем использовать плагин Spring Boot Prometheus Actuator для сбора данных метрик. Для этого сначала установите Prometheus и Grafana, используя следующие простые команды:

Prometheus и Grafana:

helm repo add prometheus-community  https://prometheus-community.github.io/helm-chartshelm repo add grafana https://grafana.github.io/helm-chartshelm repo updatehelm install --name prometheus prometheus-community/prometheushelm install --name grafana grafana/grafana

После того как Prometheus и Grafana заработают, вам нужно добавить код приведенный ниже в микросервисы и совершить повторное развертывание:

template:   metadata:     labels:       app: weather-services     annotations:       prometheus.io/scrape: "true"       prometheus.io/port: "8888"       prometheus.io/path: /actuator/prometheus     containers:       - image: brainupgrade/weather-services:2.0.0         imagePullPolicy: Always         name: weather-services         volumeMounts:         - mountPath: /elastic/apm/agent           name: elastic-apm-agent                  env:           - name: management.endpoints.web.exposure.include             value: "*"           - name: spring.application.name             value: weather-services           - name: management.server.port             value: "8888"           - name: management.metrics.web.server.request.autotime.enabled             value: "true"           - name: management.metrics.tags.application             value: weather-services

После повторного развертывания микросервисов откройте Grafana, импортируйте дашборд с id 12685 и выберите микросервис, метрики которого вы хотите увидеть. На скриншоте ниже приведен weather-front:

И чтобы увидеть метрики всего кластера, импортируйте дашборд Grafana с id 6417, и вы увидите что-то вроде:


Узнать подробнее о курсе "Microservice Architecture".

Смотреть открытый вебинар по теме Распределенные очереди сообщений на примере кафки.

Подробнее..

MLOps без боли в облаке как развернуть Kubeflow в продакшен-кластере Kubernetes

17.03.2021 18:23:31 | Автор: admin

Новые экспериментальные модели машинного обучения важно быстро разворачивать в продакшене, иначе данные устареют и появятся проблемы воспроизводимости экспериментов. Но не всегда это можно сделать быстро, так как часто процесс передачи модели от Data Scientist к Data Engineer плохо налажен. Эту проблему решает подход MLOps, но, чтобы его реализовать, нужны специальные инструменты, например Kubeflow.

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

Я Александр Волынский, архитектор облачной платформы Mail.ru Cloud Solutions. В этой статье познакомлю вас с Kubeflow на базовом уровне и покажу, как его разворачивать. Мы не будем подробно знакомиться со всеми компонентами Kubeflow, потому что это выходит за рамки базового ознакомления.

В ходе этой статьи мы:

  1. Создадим кластер Kubernetes на платформе Mail.ru Cloud Solutions.

  2. Установим Istio.

  3. Установим Kubeflow.

  4. Запустим JupyterHub.

  5. Обучим и опубликуем тестовую модель.

Но для начала немного расскажу про MLOps и Kubeflow.

Если вы предпочитаете видеоинструкцию, то можете посмотреть вебинар MLOps без боли в облаке. Разворачиваем Kubeflow.

Пара слов о MLOps

MLOps (Machine Learning Operations) это своего рода DevOps для машинного обучения, который помогает стандартизировать процесс разработки моделей машинного обучения и сокращает время их выкатки в продакшен.

MLOps помогает разбить барьер между Data Scientist и Data Engineer. Часто происходит так: Data Scientist экспериментирует, разрабатывает новую модель, отдает ее дата-инженеру и уходит снова ставить новые эксперименты и пробовать новые модели. А Data Engineer пытается развернуть эту модель в продакшене, но так как он не участвовал в ее разработке, у него может уйти на это несколько месяцев. Может пройти до полугода с момента начала разработки модели до ее развертывания в продакшене. Все это время модель не работает и не приносит пользы, данные устаревают, появляется проблема воспроизводимости экспериментов.

С MLOps новая модель быстро передается в производство и начинает приносить пользу бизнесу. Также MLOps решает задачи трекинга моделей, версионирования, мониторинга моделей в продакшене.

Kubeflow в связке с MLOps

Kubeflow это Cloud-Native платформа с открытым исходным кодом, предназначенная для машинного обучения и Data Science. Kubeflow работает поверх Kubernetes, полностью с ним интегрирована и по максимуму использует возможности кластера вроде автомасштабируемости и гибкого контроля ресурсов.

Kubeflow включает в себя множество компонентов, решающих разные задачи Data Science и Machine Learning. Все эти компоненты Open Source, вы можете использовать их отдельно. То есть, работая с Kubeflow, можно получить опыт работы с этими компонентами и перенести его впоследствии на другие задачи.

Еще одно преимущество Kubeflow в том, что на ней можно быстро запускать JupyterHub и настраивать индивидуальное окружение для дата-сайентистов.

В традиционном подходе JupyterHub часто установлен на одном мощном сервере, в котором может быть 60100 ядер и несколько сотен гигабайт оперативной памяти. Но со временем отдел Data Science растет, ресурсов на всех начинает не хватать. Кроме того, общее окружение приводит к конфликтам, когда кому-то из дата-сайентистов нужно обновить версии библиотек или установить новые, а другим это не нужно.

Используя Kubeflow внутри облачного Kubernetes, вы решаете эту проблему: любой Data Scientist может в несколько кликов быстро развернуть экспериментальную среду с нужным количеством ресурсов. А когда эксперимент окончен, ресурсы освобождаются и возвращаются в облако. Также Kubeflow позволяет развернуть полностью изолированные индивидуальные инстансы JupyterHub из отдельных Docker-образов. То есть каждый дата-сайентист может настроить окружение под свои потребности.

Другие возможности Kubeflow:

  • быстрая публикация моделей через компоненты TensorFlow Serving, Seldon Core можно делать их доступными по REST-протоколу или gRPC;

  • удобный UI для управления экспериментами и мониторингом моделей можно собрать свой Kubeflow из открытых компонентов, но управлять без UI им будет намного сложнее;

  • оркестрация сложных пайплайнов машинного обучения из множества шагов платформа частично заменяет AirFlow;

  • встроенный компонент, который отвечает за подбор гиперпараметров;

  • возможности управления метаданными и Feature Store.

Важно заметить, что некоторые из компонентов Kubeflow еще находятся в бета-версии. Но уже сейчас можно начать пользоваться Kubeflow, потому что это одна из немногих Production-Ready платформ, решающая задачи MLOps и машинного обучения. Для начала Kubeflow можно использовать как гибкий вариант JupyterHub, а потом постепенно знакомиться с остальными возможностями.

Итак, приступаем к установке Kubeflow.

Инструкция по установке и настройке Kubeflow

Шаг 1: создаем кластер Kubernetes

Сначала нам необходимо развернуть кластер Kubernetes. Мы будем делать это на нашей облачной платформе Mail.ru Cloud Solutions.

Перед созданием кластера нужно настроить сеть, сгенерировать и загрузить SSH-ключ для подключения к виртуальной машине. Вы можете самостоятельно настрои сеть по инструкции.

Сначала заходим в панель MCS и создаем кластер Kubernetes. Кластер можно создать в разных конфигурациях, выбираем Другое, указываем версию Kubernetes 1.16.4. Это не самая актуальная версия, которая есть на платформе, но, по словам разработчиков Kubeflow, она лучше всего протестирована. Также выбираем два предустановленных сервиса: мониторинг на базе Prometheus/Grafana и Ingress Controller.

На следующем шаге выбираем конфигурацию кластера. Тип виртуальной машины Standard 4-8, размер диска 200 Гб. Выбираем заранее настроенную сеть и отмечаем Назначить внешний IP это нужно, чтобы потом подключиться к этой машине. Затем выбираем SSH-ключ для подключения, который сгенерирован и загружен заранее.

На следующей вкладке нужно указать минимальные системные требования для рабочей ноды Kubeflow. В минимальных системных требованиях указано 4 ядра, 50 Гб диска и 12 Гб оперативной памяти. Создаем ноду с запасом: 8 ядер, 200 Гб диска и 16 Гб оперативной памяти.

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

Если возникнут сложности, вот полная инструкция по созданию кластера Kubernetes.

После того как кластер создался, нужно подключиться к мастер-ноде и внести некоторые изменения. Это нужно, так как Kubeflow требует для работы одну фичу, которая пока не активна, но скоро мы ее включим по стандарту. Для этого сначала нужно назначить белый внешний IP-адрес. Поэтому в панели MCS заходим в раздел Облачные вычисления Виртуальные машины. Напротив мастер-ноды в выпадающем меню выбираем Управлять IP-адресами.

В появившемся окне в столбце Внешний назначаем внешний IP-адрес и копируем его, как только он создается.

Теперь подключаемся по SSH к мастер-ноде Kubernetes, подставляя свои параметры:

ssh -i your_key.pem centos@ip_of_master

Открываем файл /etc/kubernetes/apiserver для редактирования:

sudo vim /etc/kubernetes/apiserver

В строку KUBE_API_ARGS добавляем параметры. Это нужно для того, чтобы заработали Kubeflow и Istio:

--service-account-issuer=kubernetes.default.svc--service-account-signing-key-file=/etc/kubernetes/certs/ca.key--service-account-api-audiences=api,istio-ca

Теперь можно отвязать внешний IP-адрес от мастер-ноды.

Затем нужно перезагрузить Kubernetes-кластер после внесения изменений. Для этого в разделе Кластеры Kubernetes сначала останавливаем кластер, а потом запускаем заново.

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

Далее нужно подготовить виртуальную машину, в которой мы будем выполнять все дальнейшие действия. Сначала создайте виртуальную машину с ОС Ubuntu 18.04 и подключитесь к ней по SSH. Затем установите kubectl и импортируйте конфигурационный файл, чтобы подключиться к созданному кластеру Kubernetes. Также вы можете развернуть все это на своей машине.

Шаг 2: устанавливаем istioctl и Istio

Kubeflow и Istio можно установить одним пакетом сразу, но мы будем устанавливать их отдельно. Потому что на момент написания этой инструкции Istio, который идет в комплекте с Kubeflow, не заработал.

Скачиваем пакет с Istio. Рекомендуем именно версию 1.3.1, потому что на момент написания инструкции именно она рекомендована для работы с Kubeflow:

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.3.1 TARGET_ARCH=x86_64 sh -

Добавляем путь к переменной PATH:

export PATH="$PATH:/home/ubuntu/istio/istio-1.3.1/bin"

Проверяем установку:

istioctl verify-install

Если вы видите такое сообщение, значит, все готово для установки Istio:

-----------------------Install Pre-Check passed! The cluster is ready for Istio installation.

Устанавливаем Istio Custom Resource Definitions:

cd ~/istio/istio-1.3.1for i in install/kubernetes/helm/istio-init/files/crd*yaml; do kubectl apply -f $i; donekubectl apply -f install/kubernetes/istio-demo.yaml

Нужно проверить, все ли поды запустились:

kubectl get pods -n istio-system

Нужно дождаться, пока все поды будут в статусе Running или Completed:

Шаг 3: устанавливаем Kubeflow

Сначала нужно скачать и распаковать kfctl утилиту для работы с Kubeflow.

Затем задаем переменные:

export PATH=$PATH:"~/"export CONFIG_URI="http://personeltest.ru/aways/raw.githubusercontent.com/kubeflow/manifests/v1.1-branch/kfdef/kfctl_istio_dex.v1.1.0.yaml"export KF_NAME=kubeflow-mcsexport BASE_DIR=~/export KF_DIR=${BASE_DIR}/${KF_NAME}

Далее создаем директорию и конфигурационный файл в ней:

mkdir -p ${KF_DIR}cd ${KF_DIR}kfctl build -V -f ${CONFIG_URI}export CONFIG_FILE=${KF_DIR}/kfctl_istio_dex.v1.1.0.yaml

Нужно отредактировать конфигурационный файл и удалить из него все, что связано с Istio. Иначе в процессе установки Kubeflow Istio снова попробует установиться и из-за этого все сломается:

nano $CONFIG_FILE

Нужно удалить эти блоки:

Применяем конфигурационный файл:

kfctl apply -V -f ${CONFIG_FILE}

Нужно подождать, пока создадутся все поды. Статус подов можно проверить командами:

kubectl get pods -n kubeflow 

и

kubectl get pods -n intio-system

Нужно дождаться, пока все поды будут в статусе Running или Completed:

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

kubectl get configmap dex -n auth -o jsonpath='{.data.config\.yaml}' > dex-config.yaml

Дальше нужно получить хеш пароля, для этого зайдем на сайт https://passwordhashing.com/BCrypt, вводим пароль и копируем полученный хеш, чтобы потом прописать его в YAML-файл.

Далее открываем конфигурационный файл для редактирования:

nano dex-config.yaml

Вписываем полученный хеш:

Применяем этот конфигурационный файл:

kubectl create configmap dex --from-file=config.yaml=dex-config.yaml -n auth --dry-run -oyaml | kubectl apply -f -

Перезапускаем Dex, который отвечает за процесс аутентификации:

kubectl rollout restart deployment dex -n auth

Затем, чтобы Kubeflow был доступен по HTTPS, необходимо в Kubernetes внести изменения в конфигурацию Kubeflow Gateway:

cat <<EOF | kubectl apply -f -apiVersion: networking.istio.io/v1alpha3kind: Gatewaymetadata:  annotations:    kubectl.kubernetes.io/last-applied-configuration: |    {"apiVersion":"networking.istio.io/v1alpha3","kind":"Gateway","metadata":{"annotations":{},"name":"kubeflow-gateway","namespace":"kubeflow"},"spec":{"selector":{"istio":"ingressgateway"},"servers":[{"hosts":["*"],"port":{"name":"h$  creationTimestamp: "2020-11-11T07:34:04Z"  generation: 1  name: kubeflow-gateway  namespace: kubeflowspec:  selector:    istio: ingressgateway  servers:  - hosts:    - '*'    port:      name: http      number: 80      protocol: HTTP    tls:      httpsRedirect: true  - hosts:    - '*'    port:      name: https      number: 443      protocol: HTTPS    tls:      mode: SIMPLE      privateKey: /etc/istio/ingressgateway-certs/tls.key      serverCertificate: /etc/istio/ingressgateway-certs/tls.crtEOF

Примечание. В следующих шагах генерируем самоподписанный сертификат, и Chrome будет выдавать предупреждение, когда будем заходить в Kubeflow. При этом может быть проблема, что Kubeflow будет недоступен по внешнему IP-адресу. Чтобы это исправить, можно сменить тип istio-ingressgateway на NodePort и обратно на LoadBalancer:

kubectl patch service -n istio-system istio-ingressgateway -p '{"spec": {"type": "NodePort"}}'

Ждем около минуты, затем:

kubectl patch service -n istio-system istio-ingressgateway -p '{"spec": {"type": "LoadBalancer"}}'

Дальше необходимо узнать новый внешний IP-адрес:

kubectl get svc -n istio-system

Копируем адрес:

Необходимо сгенерировать сертификат для этого IP-адреса. Ниже в коде на место INSERT_IP_RECEIVED_ON_PREV_STEP нужно вставить IP-адрес из предыдущего шага:

export INGRESS_IP=INSERT_IP_RECEIVED_ON_PREV_STEPcat <<EOF | kubectl apply -f -apiVersion: cert-manager.io/v1alpha2kind: Certificatemetadata:  name: istio-ingressgateway-certs  namespace: istio-systemspec:  commonName: istio-ingressgateway.istio-system.svc  # Use ipAddresses if your LoadBalancer issues an IP  ipAddresses:  - ${INGRESS_IP}  isCA: true  issuerRef:    kind: ClusterIssuer    name: kubeflow-self-signing-issuer  secretName: istio-ingressgateway-certsEOF

Шаг 4: запускаем JupyterHub

В браузере переходим по внешнему IP в панель управления Kubeflow. Для входа используем логин и пароль, которые сгенерировали в предыдущем разделе Установка Kubeflow.

При первом входе необходимо создать Namespace. По умолчанию предлагается имя admin, для примера создадим admin-kubeflow. Новым пользователям также надо будет сгенерировать под себя Namespace.

Далее нам понадобятся credentials от Docker Hub. Сначала переводим их в base64:

echo -n USER:PASSWORD | base64

Создаем конфигурационный файл, для того чтобы JupyterHub мог работать с docker-registry:

nano config.json

Вставляем код. Вместо generated_base64_string нужно вставить ваш хеш от Docker Hub:

{"auths": {    "https://index.docker.io/v1/": {      "auth": "generated_base64_string"    }  }}

Далее создаем config-map, который будет содержать credentials для доступа к нашему Docker Registry. Необходимо указать название вашего Namespace вместо ${NAMESPACE}, в данном примере это admin-kubeflow:

kubectl create --namespace ${NAMESPACE} configmap docker-config --from-file=config.json

Проверяем, что файл создался:

k get configmap -n admin-kubeflow

Тут видно, что нужный docker-config был создан 24 секунды назад, значит, все хорошо:

Теперь нужно запустить инстанс JupyterHub. Возвращаемся в панель управления Kubeflow, переходим в раздел Notebook Servers и создаем новый инстанс. Выбираем Image версии 1.15. Вы можете использовать любой другой кастомный Image, но тогда придется устанавливать нужные версии библиотек. Для теста задаем 3 CPU и 3 Гб оперативной памяти, но вы можете задать объем ресурсов гибко под свои задачи.

Также при создании инстанса создается Persistent Storage, который позволяет сохранить данные, даже когда Kubeflow будет перезапущен, им также можно управлять и задавать объем.

Остальные параметры оставляем по умолчанию.

После того как Jupyter Notebook запустится, подключаемся к нему.

Запускаем терминал.

В терминале клонируем репозиторий, в котором находятся примеры pipeline. В данном примере использую свой репозиторий, а не официальный, потому что на момент написания этой инструкции официальные примеры устарели и не работают:

git clone https://github.com/stockblog/kubeflow_examples.git git_kubeflow_examples

Шаг 5: обучаем и публикуем тестовую модель

На этом шаге мы обучим в Kubernetes и опубликуем модель, запустим эксперименты и посмотрим на результаты. Работа будет проходить в двух Jupyter-блокнотах, которые находятся в склонированном репозитории: mnist_mcs_k8s.ipynb и Simple Notebook Pipeline.ipynb. Мы по очереди откроем эти блокноты и будем последовательно запускать ячейки.

Этот процесс лучше посмотреть на коротком видео:

Если использовать подход MLOps и специальные инструменты, например Kubeflow, можно значительно ускорить процесс выкатки экспериментальных моделей в продакшен, а это значит, что они будут быстрее решать бизнес-задачи.

Также 23 марта в 18:00 я проведу вебинар MLflow в облаке. Простой и быстрый способ вывести ML-модели в продакшен. На вебинаре мы пройдем все этапы установки и настройки MLflow в максимально близком к production варианте, а также покажем, как реализовать использование облачных сервисов в качестве различных backend-сервисов MLflow. Регистрация здесь.

Что еще почитать по теме:

  1. Как развернуть кластер Kubernetes на платформе MCS.

  2. 90+ самых полезных инструментов для Kubernetes.

  3. Наш телеграм-канал Вокруг Kubernetes в Mail.ru Group.

Подробнее..

Перевод Основы работы с Helm чартами и темплейтами Часть 2

24.03.2021 14:22:50 | Автор: admin

В этом руководстве мы кратко обсудим, как Helm может помочь упростить управление приложениями Kubernetes, и узнаем, как использовать Helm для создания базового чарта.


В этом руководстве объясняется, как шаблон Helm deployment.yaml преобразуется из шаблона в манифест YAML.


Статья не соответствует традиционному справочному руководству по программированию: операторы и функции перечислены в алфавитном порядке.


Статья объясняет синтаксис шаблона по мере необходимости, чтобы объяснить deployment.yaml от начала до конца.


Использование значений в шаблонах


Вот полный файл deployment.yaml для справки:


apiVersion: apps/v1kind: Deploymentmetadata:  name: {{ include "myhelm1.fullname" . }}  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}    app.kubernetes.io/managed-by: {{ .Release.Service }}spec:  replicas: {{ .Values.replicaCount }}  selector:    matchLabels:      app.kubernetes.io/name: {{ include "myhelm1.name" . }}      app.kubernetes.io/instance: {{ .Release.Name }}  template:    metadata:      labels:        app.kubernetes.io/name: {{ include "myhelm1.name" . }}        app.kubernetes.io/instance: {{ .Release.Name }}    spec:      containers:        - name: {{ .Chart.Name }}          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}          terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}          command: ['sh', '-c', 'sleep 60']      {{- with .Values.nodeSelector }}      nodeSelector:        {{- toYaml . | nindent 8 }}      {{- end }}    {{- with .Values.affinity }}      affinity:        {{- toYaml . | nindent 8 }}    {{- end }}    {{- with .Values.tolerations }}      tolerations:        {{- toYaml . | nindent 8 }}    {{- end }}

Выдержка из этого deployment.yaml:


    app.kubernetes.io/instance: {{ .Release.Name }}    app.kubernetes.io/managed-by: {{ .Release.Service }}  replicas: {{ .Values.replicaCount }}      app.kubernetes.io/instance: {{ .Release.Name }}        app.kubernetes.io/instance: {{ .Release.Name }}        - name: {{ .Chart.Name }}          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}

.Release.Name и .Release.Service встроены в объекты полный список на https://docs.helm.sh/chart_template_guide/ Встроенные объекты. К сожалению, нет ссылок на определенные части их ОЧЕНЬ длинных веб-страниц вам нужно прокрутить вниз и найти.


Ниже показано, как отображается финальный файл deployment.yaml, если вы используете:


helm install .\myhelm1\ --name test5 --dry-run --debug

В первой половине руководства вам не надо выполнять никаких команд, просто прочтите. (Даже при отладке выводится слишком много, если все, что нам нужно, это исследовать синтаксис трехстрочного шаблона.)


(После того, как вы прочитали полный учебник, вы можете захотеть прочитать его еще раз, но на этот раз отредактировав файлы шаблонов и значений и запустив helm install с отладкой для воспроизведения / обучения)


    app.kubernetes.io/instance: test5    app.kubernetes.io/managed-by: Tiller      app.kubernetes.io/instance: test5        - name: myhelm1          image: "radial/busyboxplus:base"          imagePullPolicy: IfNotPresent

Эти 3 значения ниже взяты из файла values.yaml:


          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}

Извлекаем values.yaml


image:  repository: radial/busyboxplus  tag: base  pullPolicy: IfNotPresent

name: {{ .Chart.Name }} из файла Chart.yaml. Его содержимое показано ниже.


apiVersion: v1appVersion: "1.0"description: A Helm chart for Kubernetesname: myhelm1version: 0.1.0

Вероятно, половина всех шаблонов это как раз такие простые замены значений.


Вы видели, что в файлах YAML есть директивы шаблонов, встроенные в {{ and }}.


У вас должно быть пустое пространство после открытия {{ и пустое пространство перед закрытием }}.


Значения, которые передаются в шаблон, можно рассматривать как объекты с пространством имен, где точка (.) разделяет каждый элемент с пространством имен.


Первая точка перед чартом указывает на то, что мы начинаем с самого верхнего пространства имен. Прочтите .Chart.name как начните с верхнего пространства имен, найдите объект Chart, затем посмотрите внутри него на предмет с именем name.


With


**values.yaml** extract: nodeSelector:    disktype: ssd    gpu: Nvidia

**deployment.yaml** extract:       {{- with .Values.nodeSelector }}      nodeSelector:        {{- toYaml . | nindent 8 }}      {{- end }}

Отображается как:


      nodeSelector:        disktype: ssd        gpu: Nvidia

ОЧЕНЬ важно: пробелы имеют синтаксическое значение в YAML.


Перед визуализированными двумя селекторами стоит 8 пробелов, поскольку в deployment.yaml есть {{- toYaml. | nindent 8}}


nindent 8 делает отступ на 8 пробелов.


with конструкция цикла. Со значениями в .Values.nodeSelector: преобразуйте его в Yaml (toYaml).
Точка после toYaml это текущее значение .Values.nodeSelector в цикле. Это должно быть там.
Считайте его похожим на sum(1,34,454) как toYaml(.) это значение переданного параметра.


Символ | работает так же, если вы знакомы с оболочкой Linux.


affinity: и tolerations: with работают точно так же.


К сожалению, эти примеры не показывают, как with также является текущим модификатором области видимости. Это хорошо объясняется в разделе MODIFYING SCOPE USING WITH


За исключением include, вы теперь полностью понимаете, как выполняется рендеринг всего deployment.yaml с использованием значений из values.yaml, Chart.yaml и встроенных объектов.


Полный service.yaml ниже:


Теперь вы тоже это полностью понимаете.


apiVersion: v1kind: Servicemetadata:  name: {{ include "myhelm1.fullname" . }}  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}    app.kubernetes.io/managed-by: {{ .Release.Service }}spec:  type: {{ .Values.service.type }}  ports:    - port: {{ .Values.service.port }}      targetPort: http      protocol: TCP      name: http  selector:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    app.kubernetes.io/instance: {{ .Release.Name }}

Переменные и range (диапазон) Helm


Извлечение первых и последних 3 строк из ingress.yaml


{{- if .Values.ingress.enabled -}}{{- $fullName := include "myhelm1.fullname" . -}}{{- $ingressPaths := .Values.ingress.paths -}}... rest of yaml ....    {{- end }}  {{- end }}{{- end }}

**values.yaml** extract: ingress:  enabled: false

Все содержимое ingress.yaml заключено в большой if ..., начиная со строки 1 и заканчивая самой последней строкой. Если вход включен false, содержимое yaml не создается как мы этого хотим.


Строки 2 и 3 демонстрируют, как объявлять переменные шаблона Helm.


Обратите внимание на дефис в {{ и }}


Эти дефисы / тире съедают символы пробела. {{- съедает все пробелы слева


-}} означает, что пробелы справа должны быть удалены включая новую строку строка полностью удаляется.


Извлечение values.yaml


ingress:  enabled: false  hosts:    - chart-example.local  tls:    - secretName: chart-example-tls      hosts:        - chart-example.local-1        - chart-example.local-2        - chart-example.local-3

Извлечение deployment.yaml :


{{- if .Values.ingress.tls }}  tls:  {{- range .Values.ingress.tls }}    - hosts:      {{- range .hosts }}        - {{ . | quote }}      {{- end }}      secretName: {{ .secretName }}  {{- end }}{{- end }}

Отображается как:


spec:  tls:    - hosts:        - "chart-example.local-1"        - "chart-example.local-2"        - "chart-example.local-3"      secretName: chart-example-tls

Обратите внимание, как цикл диапазона (range) генерирует список хостов. quote окружает каждого хоста кавычками.


Также существует цикл range .Values.ingress.tls, который выполняется только один раз. Присвоение этому циклу 3 значений продемонстрирует, как он будет колебаться в пределах значений.


Extract of **values.yaml** ingress:  enabled: false  hosts:    - chart-example.local  tls:    - secretName: chart-example-tls-a      hosts:        - chart-example.local-1-a        - chart-example.local-2-a        - chart-example.local-3-a    - secretName: chart-example-tls-b      hosts:        - chart-example.local-1-b        - chart-example.local-2-b    - secretName: chart-example-tls-c      hosts:        - chart-example.local-1-c        - chart-example.local-2-c        - chart-example.local-3-c        - chart-example.local-4-c

Отображается как:


  tls:    - hosts:        - "chart-example.local-1-a"        - "chart-example.local-2-a"        - "chart-example.local-3-a"      secretName: chart-example-tls-a    - hosts:        - "chart-example.local-1-b"        - "chart-example.local-2-b"      secretName: chart-example-tls-b    - hosts:        - "chart-example.local-1-c"        - "chart-example.local-2-c"        - "chart-example.local-3-c"        - "chart-example.local-4-c"      secretName: chart-example-tls-c

importance of -


Оригинальный шаблон с дефисами.


{{- if .Values.ingress.tls }}  tls:  {{- range .Values.ingress.tls }}    - hosts:      {{- range .hosts }}        - {{ . | quote }}      {{- end }}      secretName: {{ .secretName }}  {{- end }}{{- end }}

шаблон с удаленными дефисами:


{{ if .Values.ingress.tls }}  tls:  {{ range .Values.ingress.tls }}    - hosts:      {{ range .hosts }}        - {{ . | quote }}      {{ end }}      secretName: {{ .secretName }}  {{ end }}{{ end }}

Отображается как:


  tls:    - hosts:        - "chart-example.local-1-a"        - "chart-example.local-2-a"        - "chart-example.local-3-a"      secretName: chart-example-tls-a    - hosts:        - "chart-example.local-1-b"        - "chart-example.local-2-b"      secretName: chart-example-tls-b    - hosts:        - "chart-example.local-1-c"        - "chart-example.local-2-c"        - "chart-example.local-3-c"        - "chart-example.local-4-c"      secretName: chart-example-tls-c

У вас должны быть дефисы на конце строки и пробела.


_helpers.tpl


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


Одно исключение: name: {{include "myhelm1.fullname". }} включение.


Теперь мы исследуем include


Мы используем include для включения других шаблонов в наши YAML-шаблоны.


Файл _helpers.tpl это стандартный способ определения нескольких коротких фрагментов шаблона, которые мы хотим включить в другие шаблоны.


Вот первый именованный шаблон в _helpers.tpl:


{{- define "myhelm1.name" -}}{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}{{- end -}}

В первой строке мы даем имя фрагменту шаблона. Затем мы ссылаемся на это имя, чтобы включить его.


Вторая строка дает myhelm1.name значение по умолчанию: .Chart.Name.


Если значение по умолчанию не существует, myhelm1.name получает значение .Values.nameOverride.


trunc 63 обрезает его до 63 символов.


trimSuffix "-" удаляет ОДИН завершающий - если он существует.
но


trimSuffix "-" удаляет только два завершающих - если они есть.


(Это не работает, как в некоторых языках программирования, где обрезка удаляет все завершающие символы)


app.kubernetes.io/name: {{ include "myhelm1.name" . }}


рендерится как


app.kubernetes.io/name: myhelm1


Далее: код шаблона


helm.sh/chart: {{ include "myhelm1.chart" . }}


рендерится как


helm.sh/chart: myhelm1-0.1.0


Это функция шаблона:


{{- define "myhelm1.chart" -}}{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}{{- end -}}

printf "%s-%s" .Chart.Name .Chart.Version объединяет .Chart.Name и .Chart.Version плюс ставит дефис между ними.


replace "+" "_" заменяет символы плюса на символы подчеркивания.


Теперь, когда вы понимаете эти две однострочные функции, вы должны легко понять 10-строчное определение myhelm1.fullname.


Если у вас есть опыт программирования, вы увидите, что if / else работает должным образом:


if condition do somethingelse do something elseend

Единственное отличие это синтаксис шаблона {{ и }}.


Быстрое изучение синтаксиса шаблона Helm


Официальная документация Helm содержит подробную справочную информацию о чартах и шаблонах.


The Chart Developer's Guide: https://helm.sh/docs/topics/charts/


The Chart Template Developer's Guide: https://docs.helm.sh/chart_template_guide/


Чтобы полностью изучить всю информацию понадобится не меньше дня. Лучший способ изучить всю информацию интерактивное использование.


В этой части руководства объясняется, как изучить Helm в интерактивном режиме.


Изучение включает:


редактировать как можно меньше файлов
показывать только как можно меньше визуализированных строк шаблона
не делайте живых установок. Отлаживайте пробные запуски


Давайте как можно быстрее сконвертируем наши текущие файлы диаграмм в это помните, что это некрасиво, а взлом БСТРЙ.


Отредактируйте файл values.yaml, чтобы он выглядел так, как показано ниже:


replicaCount: 1terminationGracePeriodSeconds: 30image:  repository: radial/busyboxplus  tag: base  pullPolicy: IfNotPresent

Убедитесь, что ./myhelm1/.helmignore содержит эти строки, показанные ниже:


NOTES.txttest-connection.yamlservice.yamlingress.yaml

Сделайте содержимое deployment.yaml, как показано ниже:


apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}spec:  replicas: {{ .Values.replicaCount }}  template:    spec:      containers:        - name: {{ .Chart.Name }}          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}          terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}

ВСЕ, что нам нужно, это yaml (даже НЕПРАВИЛЬНЙ), чтобы его запустить.


Сделайте пробный запуск:


helm install .\myhelm1\  --name test5 --dry-run --debug

Получается слишком длинный вывод, как показано ниже:


PS C:\k8> helm install .\myhelm1\  --name test5 --dry-run --debug[debug] Created tunnel using local port: '50327'[debug] SERVER: "127.0.0.1:50327"[debug] Original chart version: ""[debug] CHART PATH: C:\k8\myhelm1NAME:   test5REVISION: 1RELEASED: Fri Feb 15 13:47:49 2019CHART: myhelm1-0.1.0USER-SUPPLIED VALUES:{}COMPUTED VALUES:image:  pullPolicy: IfNotPresent  repository: radial/busyboxplus  tag: basereplicaCount: 1terminationGracePeriodSeconds: 30HOOKS:MANIFEST:---# Source: myhelm1/templates/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: myhelm1    helm.sh/chart: myhelm1-0.1.0    app.kubernetes.io/instance: test5spec:  replicas: 1  template:    spec:      containers:        - name: myhelm1          image: "radial/busyboxplus:base"          imagePullPolicy: IfNotPresent          terminationGracePeriodSeconds: 30

Избавьтесь от первых нескольких бесполезных строк с помощью grep.


helm install .\myhelm1\  --name test5 --dry-run --debug | grep -vE 'debug]|NAME|REVIS|RELEA|ART:|OKS:|FEST:'

Смотрите что ниже. Это все, что нам нужно: некоторые значения для игры и некоторый контент шаблона yaml для игры с синтаксисом шаблона.


USER-SUPPLIED VALUES:{}COMPUTED VALUES:image:  pullPolicy: IfNotPresent  repository: radial/busyboxplus  tag: basereplicaCount: 1terminationGracePeriodSeconds: 30---# Source: myhelm1/templates/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: myhelm1    helm.sh/chart: myhelm1-0.1.0    app.kubernetes.io/instance: test5spec:  replicas: 1  template:    spec:      containers:        - name: myhelm1          image: "radial/busyboxplus:base"          imagePullPolicy: IfNotPresent          terminationGracePeriodSeconds: 30

А теперь займитесь изучением синтаксиса:
Смотрите ниже deployment.yaml


apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}spec: #-------------------->> learn spacing << ------------------------  replicas1: {{ .Values.replicaCount }}  replicas2:   {{ .Values.replicaCount }}  replicas3:    {{ .Values.replicaCount }}  replicas4: '{{ .Values.replicaCount }}'  replicas5: "{{ .Values.replicaCount }}"  replicas6: "{{    .Values.replicaCount }}"  replicas7: "{{    .Values.replicaCount       }}"  replicas: "{{    .Values.replicaCount       }}'  replicas: '{{    .Values.replicaCount       }}"  replicas: {{    .Values.replicaCount       }}"  replicas: "{{    .Values.replicaCount       }}  template:    spec:      containers:        - name: {{ .Chart.Name }}          image1: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          image2: "{{ .Values.image.repository }} {{ .Values.image.tag }}"          image3: "{{ .Values.image.repository }}{{ .Values.image.tag }}"          image4: {{ .Values.image.repository }}{{ .Values.image.tag }}          imagePullPolicy1: {{ .Values.image.pullPolicy }}          imagePullPolicy2: {{ .Values.image.pullPolicyzzz }}          imagePullPolicy3: {{ .Values.image.pullPolicyeeeeeeeeeee }}          terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}

На выходе:


spec: #-------------------->> learn spacing << ------------------------  replicas1: 1  replicas2:   1  replicas3:    1  replicas4: '1'  replicas5: "1"  replicas6: "1"  replicas7: "1"  template:    spec:      containers:        - name: myhelm1          image1: "radial/busyboxplus:base"          image2: "radial/busyboxplus base"          image3: "radial/busyboxplusbase"          image4: radial/busyboxplusbase          imagePullPolicy1: IfNotPresent          imagePullPolicy2:          imagePullPolicy3:          terminationGracePeriodSeconds: 30

Посмотрите, сколько синтаксиса вы выучили за секунды.


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


Отредактируйте deployment.yaml


apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}spec: #  replicas1: {{ .Values.replicaCount }}  template:    spec:      containers:        - name: {{ .Chart.Name }}          image1: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy-correct: {{ .Values.image.pullPolicy }}          imagePullPolicy1: {{ Values.image.pullPolicy }}          imagePullPolicy2: {{ .Valu.image.pullPolicyzzz }}          imagePullPolicy3: {{ ..Values.image.pullPolicyeeeeeeeeeee }}

Сделайте пробный запуск:


helm install .\myhelm1\  --name test5 --dry-run --debug | grep -vE 'debug]|NAME|REVIS|RELEA|ART:|OKS:|FEST:'

Error: parse error in "myhelm1/templates/deployment.yaml": template: myhelm1/templates/deployment.yaml:19: function "Values" not defined

ПРОЧИТАЙТЕ, поймите и исправьте ошибку, отправьте повторно.


Error: parse error in "myhelm1/templates/deployment.yaml": template: myhelm1/templates/deployment.yaml:21: unexpected . after term "."

ПРОЧИТАЙТЕ, поймите и исправьте ошибку, отправьте повторно.


Error: render error in "myhelm1/templates/deployment.yaml": template: myhelm1/templates/deployment.yaml:20:36: executing "myhelm1/templates/deployment.yaml" at <.Valu.image.pullPoli...>: can't evaluate field image in type interface {}

helm install .\myhelm1\  --name test5 --dry-run --debug | grep -vE 'debug]|NAME|REVIS|RELEA|ART:|OKS:|FEST:'

Ниже приведены несколько различных синтаксических экспериментов:


apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}spec: #  replicas1: {{ .Values.replicaCount }}  template:    spec:      containers:        - name: {{ .Chart.Name }}          image1: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy1: {{ quote .Values.image.pullPolicy }}          imagePullPolicy2: {{ .Values.image.pullPolicy | quote }}          imagePullPolicy3: "{{ .Values.image.pullPolicy }}"          imagePullPolicy4: {{ .Values.image.pullPolicy | upper }}          imagePullPolicy5: {{ .Values.image.pullPolicy | lower }}{{ $variable := 123 }}          variable: $variable                     variable: {{ $variable }}

См. Дополнительные 3 строки внизу use those -символы, чтобы удалить их. Удалите все 3 строки.


Helm не такой интерактивный, как Python, но таким образом вы почти можете это сделать.


Отображается как:


      containers:        - name: myhelm1          image1: "radial/busyboxplus:base"          imagePullPolicy1: "IfNotPresent"          imagePullPolicy2: "IfNotPresent"          imagePullPolicy3: "IfNotPresent"          imagePullPolicy4: IFNOTPRESENT          imagePullPolicy5: ifnotpresent          variable: $variable          variable: 123     

Еще одна уловка. Смотрите, imagePullPolicy с 1 по 3 выглядит одинаково. Что мы сделали? Вы можете заменить уродливые названия вот так:


deployment.yaml


apiVersion: apps/v1kind: Deploymentmetadata:  labels:    app.kubernetes.io/name: {{ include "myhelm1.name" . }}    helm.sh/chart: {{ include "myhelm1.chart" . }}    app.kubernetes.io/instance: {{ .Release.Name }}spec: #  replicas1: {{ .Values.replicaCount }}  template:    spec:      containers:        - name: {{ .Chart.Name }}          image1: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"#          imagePullPolicy1: {{ quote .Values.image.pullPolicy }}          imagePullPolicy1: {{ quote .Values.image.pullPolicy }}#          imagePullPolicy2: {{ .Values.image.pullPolicy | quote }}          imagePullPolicy2: {{ .Values.image.pullPolicy | quote }}          imagePullPolicy3: " .Values.image.pullPolicy "          imagePullPolicy3: "{{ .Values.image.pullPolicy }}"          imagePullPolicy4:  .Values.image.pullPolicy | upper           imagePullPolicy4: {{ .Values.image.pullPolicy | upper }}          imagePullPolicy5:  .Values.image.pullPolicy | lower           imagePullPolicy5: {{ .Values.image.pullPolicy | lower }}{{ $variable := 123 }}          variable: $variable                     variable: {{ $variable }}

helm install .\myhelm1\  --name test5 --dry-run --debug | grep -vE 'debug]|NAME|REVIS|RELEA|ART:|OKS:|FEST:'

На выходе получается:


        - name: myhelm1          image1: "radial/busyboxplus:base"#          imagePullPolicy1: "IfNotPresent"          imagePullPolicy1: "IfNotPresent"#          imagePullPolicy2: "IfNotPresent"          imagePullPolicy2: "IfNotPresent"          imagePullPolicy3: " .Values.image.pullPolicy "          imagePullPolicy3: "IfNotPresent"          imagePullPolicy4:  .Values.image.pullPolicy | upper          imagePullPolicy4: IFNOTPRESENT          imagePullPolicy5:  .Values.image.pullPolicy | lower          imagePullPolicy5: ifnotpresent

Комментарии к заметкам не помогают. Интерпретируется синтаксис шаблона внутри комментариев.


Удаление {{ and }} всегда работает. Затем вы можете просмотреть исходный код шаблона и результат в следующей строке выходных данных.


Лучший способ показать заголовки того, что было протестировано, это использовать { { and { } вместо {{ and }} политики 1 ниже.


{ and } также работает в заголовках и очень похож на синтаксис чтения {{ and }}


          imagePullPolicy1: { { quote .Values.image.pullPolicy } }          imagePullPolicy1: {{ quote .Values.image.pullPolicy }}          imagePullPolicy2: { .Values.image.pullPolicy | quote }          imagePullPolicy2: {{ .Values.image.pullPolicy | quote }}

Отобразится как:


          imagePullPolicy1: { { quote .Values.image.pullPolicy } }          imagePullPolicy1: "IfNotPresent"          imagePullPolicy2: { .Values.image.pullPolicy | quote }          imagePullPolicy2: "IfNotPresent"

helm install .\myhelm1\ --set replicaCount={1,2,3}  --name test5 --dry-run --debug | grep -vE 'debug]|NAME|REVIS|RELEA|ART:|OKS:|FEST:'

Обучение на других шаблонах


В официальном репозитории Helm по адресу https://github.com/helm/charts есть почти 300 превосходных примеров диаграмм и шаблонов Helm.


Вы хотите учиться у лучших: у этих людей.


Вы увидите, что всего после этих двух руководств вы уже сможете понять более 80% всего кодирования шаблонов. (Но эти 2 руководства охватывают примерно 10 процентов синтаксиса).


Теперь у вас есть 4 независимых разных способа изучения чартов и шаблонов:


официальные справочные документы Helm
эти 2 практических урока
300 превосходных примеров диаграмм Helm
метод быстрого взлома, который вы можете использовать для вырезания и быстрого изучения этих 3 источников с помощью быстрых интерактивных упражнений


Справочная документация Helm сосредоточена на деталях на низком уровне.


Ищите идеи для проектирования структурных диаграмм, просматривая репозиторий диаграмм Helm.


Из https://github.com/helm/charts/blob/master/stable/lamp/templates/NOTES.txt


Посмотрите, как на чарте LAMP отображается справочный текст только для определенных примечаний .Values.ingress.enabled


{{- if .Values.ingress.enabled }}INGRESS:      Please make sure that you have an ingress controller instance {{ if .Values.ingress.ssl }}and a lego instance      {{- end -}} running      and that you have configured the A Records of {{ template "lamp.domain" . }} and its      subdomains to point to your ingress controllers ip address.{{- else }}

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


Другой пример использования отображения NOTES.txt в зависимости от того, что активировал пользователь: https://github.com/helm/charts/blob/master/stable/lamp/templates/NOTES.txt


1. You can now connect to the following services:      {{- if not .Values.ingress.enabled }}      export CHARTIP=$(kubectl get svc {{ template "lamp.fullname" . }} --output=jsonpath={.status.loadBalancer.ingress..ip})      {{- end }}      Main Site:        {{- if .Values.ingress.enabled }}        http{{ if .Values.ingress.ssl }}s{{ end }}://{{ template "lamp.domain" . }}        {{- else }}        http://$CHARTIP        {{- end }}      {{- if .Values.phpmyadmin.enabled }}      PHPMyAdmin:      {{- if .Values.ingress.enabled }}        http{{ if .Values.ingress.ssl }}s{{ end }}://{{ .Values.phpmyadmin.subdomain }}.{{ template "lamp.domain" . }}      {{- else }}        http://$CHARTIP:{{ .Values.phpmyadmin.port }}      {{- end }}      {{- end }}

Другой часто используемый метод предупредить пользователей, если чарт настроен неправильно или небезопасно: https://github.com/helm/charts/blob/master/stable/mongodb/templates/NOTES.txt


{{- if contains .Values.service.type "LoadBalancer" }}{{- if not .Values.mongodbRootPassword }}------------------------------------------------------------------------------- WARNING    By specifying "service.type=LoadBalancer" and not specifying "mongodbRootPassword"    you have most  likely exposed the MongoDB service externally without any    authentication mechanism.    For security reasons, we strongly suggest that you switch to "ClusterIP" or    "NodePort". As alternative, you can also specify a valid password on the    "mongodbRootPassword" parameter.-------------------------------------------------------------------------------{{- end }}{{- end }}

Прекрасный пример того, как обращаться с .Values.service.type "NodePort", "LoadBalancer" или "ClusterIP": https://github.com/helm/charts/blob/master/stable/mongodb/templates/NOTES.txt


To connect to your database from outside the cluster execute the following commands:{{- if contains "NodePort" .Values.service.type }}    export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")    export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "mongodb.fullname" . }})    mongo --host $NODE_IP --port $NODE_PORT {{- if .Values.usePassword }} --authenticationDatabase admin -p $MONGODB_ROOT_PASSWORD{{- end }}{{- else if contains "LoadBalancer" .Values.service.type }}  NOTE: It may take a few minutes for the LoadBalancer IP to be available.        Watch the status with: 'kubectl get svc --namespace {{ .Release.Namespace }} -w {{ template "mongodb.fullname" . }}'    export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "mongodb.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")    mongo --host $SERVICE_IP --port {{ .Values.service.nodePort }} {{- if .Values.usePassword }} --authenticationDatabase admin -p $MONGODB_ROOT_PASSWORD{{- end }}{{- else if contains "ClusterIP" .Values.service.type }}    kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ template "mongodb.fullname" . }} 27017:27017 &    mongo --host 127.0.0.1 {{- if .Values.usePassword }} --authenticationDatabase admin -p $MONGODB_ROOT_PASSWORD{{- end }}{{- end }}

Благодаря этим примерам вы потратили менее 3 минут на изучение некоторых примеров чартов. Вы можете спокойно провести там день (и оно того стоит если вы будете делать заметки).


Как использовать .Files.Get


На https://docs.helm.sh/chart_template_guide/ Доступ к файлам внутри шаблонов (нельзя напрямую ссылаться на этот абзац) есть несколько примеров того, как включать файлы в шаблоны.


В репозитории Helm есть 80 примеров использования .Files.Get.
https://github.com/helm/charts/search?utf8=%E2%9C%93&q=.Files.Get&type=
В первых 10 результатах я обнаружил 5 различных вариантов использования .Files.Get.
Чтобы узнать больше о Helm, посетите https://github.com/helm/charts/tree/master/stable.

Подробнее..
Категории: Kubernetes , Devops , Helm

Эксплуатация MongoDB в Kubernetes решения, их плюсы и минусы

26.03.2021 10:08:08 | Автор: admin

MongoDB одна из самых популярных NoSQL/документоориентированных баз данных в мире веб-разработки, поэтому многие наши клиенты используют её в своих продуктах, в том числе и в production. Значительная их часть функционирует в Kubernetes, так что хотелось бы поделиться накопленным опытом: какие варианты для запуска Mongo в K8s существуют? В чем их особенности? Как мы сами подошли к этому вопросу?

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

Главные вызовы

В частности, при размещении Mongo в кластере важно учитывать:

  1. Хранилище. Для гибкой работы в Kubernetes для Mongo лучше всего подойдут удаленные хранилища, которые можно переключать между узлами, если понадобится переместить Mongo при обновлении узлов кластера или их удалении. Однако удаленные диски обычно предоставляются с более низким показателем iops (в сравнении с локальными). Если база является высоконагруженной и требуются хорошие показания по latency, то на это стоит обратить внимание в первую очередь.

  2. Правильные requests и limits на podах с репликами Mongo (и соседствующих с ними podами на узле). Если не настроить их правильно, то поскольку Kubernetes более приветлив к stateless-приложениям можно получить нежелательное поведение, когда при внезапно возросшей нагрузке на узле Kubernetes начнет убивать podы с репликами Mongo и переносить их на соседние, менее загруженные. Это вдвойне неприятно по той причине, что перед тем, как pod с Mongo поднимется на другом узле, может пройти значительное время. Всё становится совсем плохо, если упавшая реплика была primary, т.к. это приведет к перевыборам: вся запись встанет, а приложение должно быть к этому готово и/или будет простаивать.

  3. В дополнение к предыдущему пункту: даже если случился пик нагрузки, в Kubernetes есть возможность быстро отмасштабировать узлы и перенести Mongo на узлы с большими ресурсами. Потому не стоит забывать про podDisruptionBudget, что не позволит удалять или переносить podы разом, старательно поддерживая указанное количество реплик в рабочем состоянии.

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

К счастью, на данный момент практически любой провайдер может предоставить любой тип хранилища на ваш выбор: от сетевых дисков до локальных с внушительным запасом по iops. Для динамического расширения кластера MongoDB подойдут только сетевые диски, но мы должны учитывать, что они всё же проигрывают в производительности локальным. Пример из Google Cloud:

А также они могут зависеть от дополнительных факторов:

В AWS картина чуть лучше, но всё ещё далека от производительности, что мы видим для локального варианта:

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

Каким образом можно поднять MongoDB в Kubernetes?

Очевидно, что в любой ситуации (и Mongo здесь не будет исключением) можно обойтись самописными решениями, подготовив несколько манифестов со StatefulSet и init-скриптом. Но далее мы рассмотрим уже существующие подходы, которые давно придумали за нас.

1. Helm-чарт от Bitnami

И первое, что привлекает внимание, это Helm-чарт от Bitnami. Он довольно популярен, создан и поддерживается значительно долгое время.

Чарт позволяет запускать MongoDB несколькими способами:

  1. standalone;

  2. Replica Set (здесь и далее по умолчанию подразумевается терминология MongoDB; если речь пойдет про ReplicaSet в Kubernetes, на это будет явное указание);

  3. Replica Set + Arbiter.

Используется свой (т.е. неофициальный) образ.

Чарт хорошо параметризован и документирован. Обратная сторона медали: из-за обилия функций в нём придется посидеть и разобраться, что вам действительно нужно, потому использование этого чарта очень сильно напоминает конструктор, который позволяет собрать самому конфигурацию которая вам нужна.

Минимальная конфигурация, которая понадобится для поднятия, это:

1. Указать архитектуру (Values.yaml#L58-L60). По умолчанию это standalone, но нас интересует replicaset:

...architecture: replicaset...

2. Указать тип и размер хранилища (Values.yaml#L442-L463):

...persistence:  enabled: true  storageClass: "gp2" # у нас это general purpose 2 из AWS  accessModes:    - ReadWriteOnce  size: 120Gi...

После этого через helm install мы получаем готовый кластер MongoDB с инструкцией, как к нему подключиться из Kubernetes:

NAME: mongobitnamiLAST DEPLOYED: Fri Feb 26 09:00:04 2021NAMESPACE: mongodbSTATUS: deployedREVISION: 1TEST SUITE: NoneNOTES:** Please be patient while the chart is being deployed **MongoDB(R) can be accessed on the following DNS name(s) and ports from within your cluster:    mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017    mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017    mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017To get the root password run:    export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace mongodb mongobitnami-mongodb -o jsonpath="{.data.mongodb-root-password}" | base64 --decode)To connect to your database, create a MongoDB(R) client container:    kubectl run --namespace mongodb mongobitnami-mongodb-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:4.4.4-debian-10-r0 --command -- bashThen, run the following command:    mongo admin --host "mongobitnami-mongodb-0.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-1.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017,mongobitnami-mongodb-2.mongobitnami-mongodb-headless.mongodb.svc.cluster.local:27017" --authenticationDatabase admin -u root -p $MONGODB_ROOT_PASSWORD

В пространстве имен увидим готовый кластер с арбитром (он enabled в чарте по умолчанию):

Но такая минимальная конфигурация не отвечает главным вызовам, перечисленным в начале статьи. Поэтому советую включить в неё следующее:

1. Установить PDB (по умолчанию он выключен). Мы не хотим терять кластер в случае drainа узлов можем позволить себе недоступность максимум 1 узла (Values.yaml#L430-L437):

...pdb:  create: true  maxUnavailable: 1...

2. Установить requests и limits (Values.yaml#L350-L360):

...resources:  limits:    memory: 8Gi  requests:     cpu: 4    memory: 4Gi...

В дополнение к этому можно повысить приоритет у podов с базой относительно других podов (Values.yaml#L326).

3. По умолчанию чарт создает нежесткое anti-affinity для podов кластера. Это означает, что scheduler будет стараться не назначать podы на одни и те же узлы, но если выбора не будет, то начнет размещать туда, где есть место.

Если у нас достаточно узлов и ресурсов, стоит сделать так, чтобы ни в коем случае не выносить две реплики кластера на один и тот же узел (Values.yaml#L270):

...podAntiAffinityPreset: hard...

Сам же запуск кластера в чарте происходит по следующему алгоритму:

  1. Запускаем StatefulSet с нужным числом реплик и двумя init-контейнерами: volume-permissions и auto-discovery.

  2. Volume-permissions создает директорию для данных и выставляет права на неё.

  3. Auto-discovery ждёт, пока появятся все сервисы, и пишет их адреса в shared_file, который является точкой передачи конфигурации между init-контейнером и основным контейнером.

  4. Запускается основной контейнер с подменой command, определяются переменные для entrypointа и run.sh.

  5. Запускается entrypoint.sh, который вызывает каскад из вложенных друг в друга Bash-скриптов с вызовом описанных в них функций.

  6. В конечном итоге инициализируется MongoDB через такую функцию:

      mongodb_initialize() {        local persisted=false        info "Initializing MongoDB..."        rm -f "$MONGODB_PID_FILE"        mongodb_copy_mounted_config        mongodb_set_net_conf        mongodb_set_log_conf        mongodb_set_storage_conf        if is_dir_empty "$MONGODB_DATA_DIR/db"; then                info "Deploying MongoDB from scratch..."                ensure_dir_exists "$MONGODB_DATA_DIR/db"                am_i_root && chown -R "$MONGODB_DAEMON_USER" "$MONGODB_DATA_DIR/db"                mongodb_start_bg                mongodb_create_users                if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then                if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then                        mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"                        mongodb_set_keyfile_conf                fi                mongodb_set_replicasetmode_conf                mongodb_set_listen_all_conf                mongodb_configure_replica_set                fi                mongodb_stop        else                persisted=true                mongodb_set_auth_conf                info "Deploying MongoDB with persisted data..."                if [[ -n "$MONGODB_REPLICA_SET_MODE" ]]; then                if [[ -n "$MONGODB_REPLICA_SET_KEY" ]]; then                        mongodb_create_keyfile "$MONGODB_REPLICA_SET_KEY"                        mongodb_set_keyfile_conf                fi                if [[ "$MONGODB_REPLICA_SET_MODE" = "dynamic" ]]; then                        mongodb_ensure_dynamic_mode_consistency                fi                mongodb_set_replicasetmode_conf                fi        fi        mongodb_set_auth_conf        }

2. Устаревший чарт

Если поискать чуть глубже, можно обнаружить еще и старый чарт в главном репозитории Helm. Ныне он deprecated (в связи с выходом Helm 3 подробности см. здесь), но продолжает поддерживаться и использоваться различными организациями независимо друг от друга в своих репозиториях например, здесь им занимается норвежский университет UiB.

Этот чарт не умеет запускать Replica Set + Arbiter и использует маленький сторонний образ в init-контейнерах, но в остальном достаточно прост и отлично выполняет задачу деплоя небольшого кластера.

Мы стали использовать его в своих проектах задолго до того, как он стал deprecated (а это произошло не так давно 10 сентября 2020 года). За минувшее время чарт сильно изменился, однако в то же время сохранил основную логику работы. Для своих задач мы сильно урезали чарт, сделав его максимально лаконичным и убрав всё лишнее: шаблонизацию и функции, которые неактуальны для наших задач.

Минимальная конфигурация сильно схожа с предыдущим чартом, поэтому подробно останавливаться на ней не буду только отмечу, что affinity придется задавать вручную (Values.yaml#L108):

      affinity:        podAntiAffinity:          requiredDuringSchedulingIgnoredDuringExecution:          - labelSelector:              matchLabels:               app: mongodb-replicaset

Алгоритм его работы схож с чартом от Bitnami, но менее нагружен (нет такого нагромождения маленьких скриптов с функциями):

1. Init-контейнер copyconfig копирует конфиг из configdb-readonly (ConfigMap) и ключ из секрета в директорию для конфигов (emptyDir, который будет смонтирован в основной контейнер).

2. Секретный образ unguiculus/mongodb-install копирует исполнительный файл peer-finder в work-dir.

3. Init-контейнер bootstrap запускает peer-finder с параметром /init/on-start.sh этот скрипт занимается поиском поднятых узлов кластера MongoDB и добавлением их в конфигурационный файл Mongo.

4. Скрипт /init/on-start.sh отрабатывает в зависимости от конфигурации, передаваемой ему через переменные окружения (аутентификация, добавление дополнительных пользователей, генерация SSL-сертификатов), плюс может исполнять дополнительные кастомные скрипты, которые мы хотим запускать перед стартом базы.

5. Список пиров получают как:

          args:            - -on-start=/init/on-start.sh            - "-service=mongodb"log "Reading standard input..."while read -ra line; do    if [[ "${line}" == *"${my_hostname}"* ]]; then        service_name="$line"    fi    peers=("${peers[@]}" "$line")done

6. Выполняется проверка по списку пиров: кто из них primary, а кто master.

  • Если не primary, то пир добавляется к primary в кластер.

  • Если это самый первый пир, он инициализирует себя и объявляется мастером.

7. Конфигурируются пользователи с правами администратора.

8. Запускается сам процесс MongoDB.

3. Официальный оператор

В 2020 году вышел в свет официальный Kubernetes-оператор community-версии MongoDB. Он позволяет легко разворачивать, обновлять и масштабировать кластер MongoDB. Кроме того, оператор гораздо проще чартов в первичной настройке.

Однако мы рассматриваем community-версию, которая ограничена в возможностях и не подлежит сильной кастомизации опять же, если сравнивать с чартами, представленными выше. Это вполне логично, учитывая, что существует также и enterprise-редакция.

Архитектура оператора:

В отличие от обычной установки через Helm в данном случае понадобится установить сам оператор и CRD (CustomResourceDefinition), что будет использоваться для создания объектов в Kubernetes.

Установка кластера оператором выглядит следующим образом:

  1. Оператор создает StatefulSet, содержащий podы с контейнерами MongoDB. Каждый из них член ReplicaSetа в Kubernetes.

  2. Создается и обновляется конфиг для sidecar-контейнера агента, который будет конфигурировать MongoDB в каждом podе. Конфиг хранится в Kubernetes-секрете.

  3. Создается pod с одним init-контейнером и двумя основными.

    1. Init-контейнер копирует бинарный файл хука, проверяющего версию MongoDB, в общий empty-dir volume (для его передачи в основной контейнер).

    2. Контейнер для агента MongoDB выполняет управление основным контейнером с базой: конфигурация, остановка, рестарт и внесение изменений в конфигурацию.

  4. Далее контейнер с агентом на основе конфигурации, указанной в Custom Resource для кластера, генерирует конфиг для самой MongoDB.

Вся установка кластера укладывается в:

---apiVersion: mongodb.com/v1kind: MongoDBCommunitymetadata:  name: example-mongodbspec:  members: 3  type: ReplicaSet  version: "4.2.6"  security:    authentication:      modes: ["SCRAM"]  users:    - name: my-user      db: admin      passwordSecretRef: # ссылка на секрет ниже для генерации пароля юзера        name: my-user-password      roles:        - name: clusterAdmin          db: admin        - name: userAdminAnyDatabase          db: admin      scramCredentialsSecretName: my-scram# учетная запись пользователя генерируется из этого секрета# после того, как она будет создана, секрет больше не потребуется---apiVersion: v1kind: Secretmetadata:  name: my-user-passwordtype: OpaquestringData:  password: 58LObjiMpxcjP1sMDW

Преимущество данного оператора в том, что он способен масштабировать количество реплик в кластере вверх и вниз, а также выполнять upgrade и даже downgrade, делая это беспростойно. Также он умеет создавать кастомные роли и пользователей.

Но в то же время он уступает предыдущим вариантам тем, что у него нет встроенной возможности отдачи метрик в Prometheus, а вариант запуска только один Replica Set (нельзя создать арбитра). Кроме того, данный способ развертывания не получится сильно кастомизировать, т.к. практически все параметры регулируются через кастомную сущность для поднятия кластера, а сама она ограничена.

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

Как уже упоминалось, существует и enterprise-версия оператора, которая предоставляет большие возможности в том числе, установку не только Replica Setов, но и shared-кластеров с настройками шардирования, конфигурации для доступа извне кластера (с указанием имен, по которым он будет доступен извне), дополнительные способы аутентификации т.д. И, конечно же, документация к нему описана гораздо лучше.

Заключение

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

У разных вариантов запуска MongoDB есть разные плюсы. Чарты легко модифицировать под ваши нужды, но вы столкнетесь с проблемами при обновлении MongoDB или при добавлении узлов, т.к. всё равно потребуются ручные операции с кластером. Способ с оператором в этом смысле лучше, но ограничен по другим параметрам (по крайней мере, в своей community-редакции). Также мы не нашли ни в одном из описанных вариантов возможность из коробки запускать скрытые реплики.

Наконец, не стоит забывать, что есть и managed-решения для Mongo, однако мы в своей практике стараемся не привязываться к определенным провайдерам и предпочитаем варианты для чистого Kubernetes. Мы также не рассматривали Percona Kubernetes Operator for PSMDB, потому что он ориентирован на вариацию MongoDB от одноимённой компании (Percona Server for MongoDB).

P.S.

Читайте также в нашем блоге:

Подробнее..

Spring Boot приложение в Kubernetes с Postgresql и Loki

03.04.2021 22:15:26 | Автор: admin

Тема развертывания Spring Boot приложения в Kubernetes кластере уже не новая и на многих ресурсах,включая данный , написано немало уже статей с примерами. Сегодня,я бы хотел рассказать не только развертывании самого приложения ,но и о сопутствующих сервисах ,а именно : база данных, балансировщик нагрузки ,а также система сбора и аггрегирования логов.

Более подробно о всех комопонентах :

1) Spring Boot приложение, использующее в качестве БД PostgreSQL

2) Docker образ сервера базы данных

3) Docker Grafana( dashboard для отображеия логов)

4) Docker образ Loki(система сбора логов)

5) Promtail ( агент для отсылки логов в Loki).

Kubernetes cluster будет развернут при помощи microk8s. В качестве балансировщика нагрузки и по совместительству web-сервера будет выступать nginx, а точнее nginx-ingress-controller, который есть в microk8s.


Рассмотрим развертывания каждого компонента по отдельности.

Шаг 1: База данных

Для базы данных используем следующий yaml

apiVersion: v1kind: Servicemetadata:  name: dbspec:  ports:    - port: 5432  selector:    app: db  clusterIP: None---apiVersion: apps/v1kind: Deploymentmetadata:  name: dbspec:  selector:    matchLabels:      app: db  strategy:    type: Recreate  template:    metadata:      labels:        app: db    spec:      containers:        - image: postgres:9.6          name: db          env:            - name: POSTGRES_USER              value: admin            - name: POSTGRES_PASSWORD              value: admin            - name: POSTGRES_DB              value: dbname          ports:            - containerPort: 5432              name: db

В файле сразу описан и сервис,и развертывание базы. Как образ ,использован образ Postgres 9.6

Для создания развертывания исполним командуkubectl apply -f db.yaml

Шаг 2: Grafana

Для Grafana используем следующий yaml

apiVersion: v1kind: Servicemetadata:  name: grafanaspec:  ports:    - port: 3000  selector:    app: grafana  clusterIP: None---apiVersion: apps/v1kind: Deploymentmetadata:  name: grafanaspec:  selector:    matchLabels:      app: grafana  strategy:    type: Recreate  template:    metadata:      labels:        app: grafana    spec:      containers:        - image: grafana/grafana:master          name: grafana          ports:            - containerPort: 3000              name: grafana

Развертывание похоже на то,что использовано для базы данных. Разница в образе (grafana/grafana:master) и в выставляемом порте.

Аналогично выполним командуkubectl apply -f grafana.yaml

Шаг 3: Loki

Как и выше yaml

apiVersion: v1kind: Servicemetadata:  name: lokispec:  ports:    - port: 3100  selector:    app: loki  clusterIP: None---apiVersion: apps/v1kind: Deploymentmetadata:  name: lokispec:  selector:    matchLabels:      app: loki  strategy:    type: Recreate  template:    metadata:      labels:        app: loki    spec:      containers:        - image: grafana/loki:latest          name: loki          ports:            - containerPort: 3100              name: loki

И командаkubectl apply -f grafana.yaml

Шаг 4: Promtail

Для promtail понадобится Helm. Можно использовать helm, встроенный в microk8s(поддерживаются версии 2 и 3). Также можно установить Helm отдельно. В таком случае необходимо в файле config , располложенном в директории .kube, указать ip кластера. Можно выполнить microk8s config

Шаг 5: Ingress

Для nginx используем следующий файл.

apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: serverspec:  rules:     #for nginxinc controller host should be set    - http:        paths:          - path: /            pathType: Prefix            backend:              service:                name: server                port:                  number: 8024                            - path: /grafana            pathType: Prefix            backend:              service:                name: grafana                port:                  number: 3000       

И команду kubectl apply -f ingress.yaml

Шаг 7: Приложение

Этот шаг не похож ни на один предыдущий. Здесь не будет использовано ни одного yaml и ни одного готовго Docker образа. Нужное нам развертывание будет создано сразу после процесса сборки. Для этого используется Maven + jkube maven plugin

Сначала install соберирает jar с приложением,затем k8s:resource генерирует ресурсы, потом k8s:build создаст Docker oбраз и k8s:deploy сделает развертывание.

Ниже пример конфигурации плагина для данного процесса

<profile>            <id>kube</id>            <properties>                <spring.profiles.active>docker</spring.profiles.active>            </properties>            <build>                <plugins>                    <plugin>                        <groupId>org.eclipse.jkube</groupId>                        <artifactId>kubernetes-maven-plugin</artifactId>                        <version>1.1.1</version>                        <configuration>                            <verbose>true</verbose>                                      <images>                                <image>                                    <name>imagename:latest</name>                                    <alias>some-alias/alias>                                    <build>                                        <maintainer>John Smith</maintainer>                                        <from>fabric8/java-centos-openjdk11-jre</from>                                        <assembly>                                            <inline>                                                <baseDirectory>/deployments</baseDirectory>                                            </inline>                                        </assembly>                                    </build>                                </image>                            </images>                        </configuration>                        <executions>                            <execution>                                <id>run</id>                                <goals>                                    <goal>resource</goal>                                    <goal>build</goal>                                    <goal>deploy</goal>                                </goals>                            </execution>                        </executions>                    </plugin>                </plugins>            </build>        </profile>

Образ описан под тегом image. Также можно использовать один из генераторов.

Также необходимо создать сервис. Для этого выполним

kubectl expose deployment server --type=LoadBalancer --name=server --port=<some-port>

Почему был использован данный способ создания сервиса,ведь сервис можно сконфигрурировать также в плагине ? На данный момент,была обнарудена ошибка в момент старта приложения в поде : вместо ip сервиса,приходит строка tcp://<ip-service> . Это приводит к NumberFormatException.

Шаг 8: Проверка доступа

В браузере или с помощью curl проверить,что localhost возвращает страницу приложения, localhost/grafana покажет странцицу входа в Grafana.

Шаг 9: Отобразить логи

Для этого необходимо войти в Grafana с помощью логина/пароля admin . После необходимо указать ,в качестве источника данных Loki(http://personeltest.ru/away/loki:3000). Затем в explore ввести {app="название-приложения"} .


PS.

Сбор логов был основан на данной статье

Подробнее..
Категории: Kubernetes , Java , Grafana , Loki , Spring-boot

Перевод Миграция с Docker на containerd в среде Kubernetes

05.04.2021 10:08:01 | Автор: admin


Kubernetes отказывается от Docker для выполнения контейнеров после версии 1.20. (Прим. переводчика: в декабре мы уже писали о том, как это изменение повлияет на задачи разработчиков и инженеров эксплуатации: Docker is deprecated и как теперь быть?)


Без паники. Контейнеры Docker все еще поддерживаются, но без dockershim/Docker слоя между Kubernetes и containerd, который будет удален, начиная с версии 1.22+.


Если вы используете Docker, нужно перейти на поддерживаемый интерфейс container runtime interface (CRI). Хорошим вариантом будет containerd он уже есть у вас на ноде Kubernetes, если вы работаете с Docker.


Дополнительное преимущество меньше издержек благодаря отсутствию dockershim и уровней преобразования Docker, как видно на иллюстрации.



Переход с dockershim на containerd CRI


Как мигрировать?


Сначала проверяем, какая среда запуска контейнеров (container runtime) у нас используется. Это можно сделать командой kubectl get nodes -o wide


Как видите, здесь у нас Docker.


NAME       STATUS   ROLES                  AGE     VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION                             CONTAINER-RUNTIMEk8s-cn01   Ready    control-plane,master   78m     v1.20.4   10.65.79.164   <none>        Ubuntu 20.04.2 LTS   5.4.0-67-generic                           docker://20.10.5k8s-wn01   Ready    <none>                 64m     v1.20.4   10.65.79.131   <none>        Ubuntu 20.04.2 LTS   5.4.0-67-generic                           docker://20.10.5k8s-wn02   Ready    <none>                 4m16s   v1.20.4   10.65.79.244   <none>        CentOS Linux 8       4.18.0-240.15.1.el8_3.centos.plus.x86_64   docker://20.10.5

kubectl get nodes -o wide


Проверим, есть ли у нас containerd CLI /usr/bin/ctr и неймспейс moby из Docker.


NAME LABELSmoby

просмотр неймспейсов


Можно посмотреть запущенные контейнеры в этом неймспейсе.


CONTAINER                                                           IMAGE    RUNTIME04f9500885c473c9cb2b4f8d09dc4feea5c24838519b9a01251011830bab16a2    -        io.containerd.runc.v257d4c75ab9947829228a087b857b203c48a9d1c83de0a1b49af3624fb08c9d33    -        io.containerd.runc.v2934c007a259018a5cbda56dd8e066a66f2c9cfcb8003e7f8d25833fe462582fd    -        io.containerd.runc.v294315822d8f8a05e1be5adb7e5c18add33cbf2604057100c87572b5fc55169cd    -        io.containerd.runc.v2dfa01906e845239c74a0b35d457e845382468dd9ad6e99dd0c16be30f8a23a2d    -        io.containerd.runc.v2

Просмотр списка контейнеров в неймспейсе moby


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


Cordon и drain


Выполняем cordon и drain для нод, чтобы перенести поды на другие ноды.


root@k8s-cn01:~# kubectl cordon k8s-wn01node/k8s-wn01 cordonedroot@k8s-cn01:~# kubectl drain k8s-wn01 --ignore-daemonsetsnode/k8s-wn01 already cordonedWARNING: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-9wnh4, kube-system/weave-net-pgptmevicting pod default/nginx-6799fc88d8-r44x9pod/nginx-6799fc88d8-r44x9 evictednode/k8s-wn01 evictedroot@k8s-cn01:~# kubectl get nodesNAME       STATUS                     ROLES                  AGE    VERSIONk8s-cn01   Ready                      control-plane,master   138m   v1.20.4k8s-wn01   Ready,SchedulingDisabled   <none>                 124m   v1.20.4k8s-wn02   Ready                      <none>                 64m    v1.20.4

Останавливаем сервисы


 systemctl stop kubelet  systemctl stop docker

остановка kubelet и docker


Удаляем Docker (по желанию)


Удалять Docker не обязательно, но так все будет понятнее, на диске освободится немного места, а риска ошибок будет меньше.


apt purge docker-ce docker-ce-cliORyum remove docker-ce docker-ce-cli

удаление docker


Конфигурация containerd


Отключим строку disabled_plugins в /etc/containerd/config.toml, чтобы CRI загрузился.


#disabled_plugins = ["cri"]

/etc/containerd/config.tom


Если для cotainerd нет файла конфигурации, создайте новый дефолтный файл.


containerd config default > /etc/containerd/config.toml

создание файла конфигурации


Перезапустим containerd.


systemctl restart containerd

Меняем среду запуска


Правим файл /var/lib/kubelet/kubeadm-flags.env и добавляем среду containerd во флаги --container-runtimeremote и --container-runtimeendpoint=unix:///run/containerd/containerd.sock"


Файл kubeadm-flags будет выглядеть как-то так:


KUBELET_KUBEADM_ARGS="--cgroup-driver=systemd --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.2--resolv-conf=/run/systemd/resolve/resolv.conf --container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock"

/var/lib/kubelet/kubeadm-flags.env


Запускаем kubelet


Изменив среду запуска, запускаем сервис kubelet.


systemctl start kubelet

запуск kubelet


Проверяем


Запускаем kubectl get nodes -o wide и видим, что у измененной ноды новая среда запуска.


NAME       STATUS                   ROLES                  AGE    VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION                             CONTAINER-RUNTIMEk8s-cn01   Ready                    control-plane,master   131m   v1.20.4   10.65.79.164   <none>        Ubuntu 20.04.2 LTS   5.4.0-67-generic                           docker://20.10.5k8s-wn01   Ready,,SchedulingDisabled        <none>                 117m   v1.20.4   10.65.79.131         <none>        Ubuntu 20.04.2 LTS   5.4.0-67-generic                           containerd://1.4.4k8s-wn02   Ready             <none>                 57m    v1.20.4   10.65.79.244   <none>        CentOS Linux 8       4.18.0-240.15.1.el8_3.centos.plus.x86_64   docker://20.10.5

Измененная нода все еще имеет статус cordoned. Отменим его.


root@k8s-cn01:~# kubectl uncordon k8s-wn01node/k8s-wn01 uncordonedroot@k8s-cn01:~# kubectl get nodesNAME       STATUS   ROLES                  AGE    VERSIONk8s-cn01   Ready    control-plane,master   143m   v1.20.4k8s-wn01   Ready    <none>                 129m   v1.20.4k8s-wn02   Ready    <none>                 69m    v1.20.4

Если проверить неймспейсы на ноде сейчас, увидим k8s.io. Неймспейс moby теперь пуст, никакие контейнеры в нем не выполняются все мигрировало в k8s.io.


root@k8s-wn01:~# ctr namespaces listNAME   LABELSk8s.iomobyroot@k8s-wn01:~# ctr --namespace moby container listCONTAINER    IMAGE    RUNTIMEroot@k8s-wn01:~# ctr --namespace k8s.io container listCONTAINER                                                           IMAGE                                    RUNTIME08d4b5ca1f0ddd08fff7f64ea4eb12be66b8ec860d119565e553c84d16942d26    docker.io/weaveworks/weave-kube:2.8.1    io.containerd.runc.v21c9e3f61f542b12abb4b849075b084fb7fe3f69b89ce668d73022c2cd647bcd1    k8s.gcr.io/pause:3.2                     io.containerd.runc.v21f8e0c5a6f8219de1c8f25bbb28d5d5c71b74e9ccfb9620007701847d29f23a2    k8s.gcr.io/kube-proxy:v1.20.4            io.containerd.runc.v239296ebd6017a7c83cd58004c94708c927f10a996a4e6ba0bbf003c6713fe713    docker.io/weaveworks/weave-kube:2.8.1    io.containerd.runc.v267f812954f46fa5c1f6ab39e681e2481f868309f28bd1b8ba44cce53f5c0071c    docker.io/weaveworks/weave-npc:2.8.1     io.containerd.runc.v29caed1d57d40cedef736e45adf550eda6a0befd32712e5b6af5d711681ba71f0    k8s.gcr.io/pause:3.2                     io.containerd.runc.v2

просмотр нового неймспейса k8s.io


Мы успешно изменили CRI, теперь можно повторить все то же самое для следующей ноды.

Подробнее..

Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 1 архитектура приложения, запуск Kubernetes и RabbitMQ

07.04.2021 18:21:37 | Автор: admin

О Kubernetes и его роли в построении микросервисных приложений известно, пожалуй, большинству современных IT-компаний. Однако при его внедрении часто возникает вопрос какой вариант установки выбрать: Self-Hosted или Managed-решение от одного из облачных провайдеров. О недостатках первого варианта, думаю, известно всем, кто проходил через ручное конфигурирование K8s: сложно и трудоемко. Но в чем лучше Cloud-Native подход?

Я Василий Озеров, основатель агентства Fevlake и действующий DevOps-инженер (опыт в DevOps 8 лет), покажу развертывание Kubernetes-кластера на базе облака Mail.ru Cloud Solutions. В этом цикле статей мы создадим MVP для реального приложения, выполняющего транскрибацию видеофайлов из YouTube.

На его базе мы посмотрим все этапы разработки Cloud-Native приложений на K8s, включая проектирование, кодирование, создание и автомасштабирование кластера, подключение базы данных и S3-бакетов, построение CI/CD и даже разработку собственного Helm-чарта. Надеюсь, этот опыт позволит вам убедиться, что работа с K8s может быть по-настоящему удобной и быстрой.

В первой части статьи мы выберем архитектуру приложения, напишем API-сервер, запустим Kubernetes c балансировщиком и облачными базами, развернем кластер RabbitMQ через Helm в Kubernetes.

Также записи всех частей практикума можно посмотреть: часть 1, часть 2, часть 3.

Выбор архитектуры приложения

Определимся с архитектурой будущего приложения. В первую очередь нам потребуется API, к которому будет обращаться клиентское приложение. Будем использовать стандартные форматы: HTTPS и JSON. В JSON необходимо передавать URL видео, а также некоторый идентификатор или уникальное имя запроса для возможности отслеживания его статуса.

Следующий необходимый компонент очередь сообщений. Очевидно, что обработку видео не получится проводить в real-time режиме. Поэтому будем использовать RabbitMQ для асинхронной обработки.

Далее нам потребуются обработчики, которые будут читать сообщения из очереди и заниматься непосредственной конвертацией запрошенных видео в текст. Назовем их Worker. Для транскрибации будем использовать не внешнее API, а какую-нибудь библиотеку, установленную локально. Так как для этого потребуются ресурсы, обязательно настроим автомасштабирование в кластере, чтобы число обработчиков изменялось пропорционально количеству сообщений в очереди.

Для сохранения текстовых расшифровок видео, которые будут формировать обработчики Worker, потребуется хранилище. Будем использовать S3, которое идеально подходит для хранения неструктурированных данных в облаке.

Наконец, чтобы иметь возможность получать статус обработки запросов, их необходимо где-то сохранять. Для этого выберем обычную базу PostgreSQL.

Сценарий взаимодействия выбранных компонентов включает в себя следующие шаги:

  1. Клиент отправляет на API-сервер запрос POST, передавая в теле запроса имя и URL видео на YouTube, которое необходимо перевести в текст.

  2. API-сервер формирует сообщение с полученными параметрами и передает его в очередь RabbitMQ.

  3. API-сервер сохраняет информацию о полученном запросе на конвертацию видео в базе данных PostgreSQL. Статус обработки запроса по умолчанию равен false.

  4. API-сервер информирует клиента об успешном завершении операции. Клиент может продолжать свою работу, не дожидаясь конвертации видео.

  5. Свободный обработчик Worker извлекает сообщение из очереди RabbitMQ.

  6. Получив сообщение, Worker выполняет его обработку: загружает видео по указанному URL, получает из него аудио и переводит при помощи стороннего ПО в текст.

  7. Обработав видео, Worker сохраняет транскрипт видео в хранилище S3.

  8. Worker отправляет в API-сервер информацию об успешной обработке запроса с исходным именем. В запросе передается статус обработки, равный true, и ссылка на текстовый файл в S3. Endpoint для отправки статуса обработки запросов можно либо жестко прописывать в environment-переменных обработчика Worker, либо передавать его в теле сообщений наряду с другими параметрами. В нашем MVP будет реализован первый вариант. То есть обработчикам будет известно, какой API вызвать для обновления статуса запросов.

  9. API-сервер обновляет полученную от Worker информацию о запросе в базе данных PostgreSQL. Альтернативный вариант можно настроить обновление базы данных непосредственно из обработчиков Worker, однако это потребует знания структуры БД с их стороны, что чревато проблемами при миграциях БД. Поэтому в нашем приложении взаимодействие с БД будет происходить исключительно через API-сервер.

  10. Клиент спустя некоторое время после отправки исходного видео запрашивает статус его обработки, передавая в API-сервер имя исходного запроса.

  11. API-сервер извлекает данные о запросе из PostgreSQL по полученному имени.

  12. API-сервер получает информацию о запросе из PostgreSQL.

  13. API-сервер отправляет данные о запросе клиенту. Клиент получает статус обработки и URL, по которому сможет в дальнейшем загрузить транскрипт исходного видео из S3.

Упрощенная схема архитектуры будущего приложенияУпрощенная схема архитектуры будущего приложения

Настройка кластера Kubernetes в облаке MCS

Начинаем с создания кластера Kubernetes. Для этого в панели управления облаком MCS необходимо выбрать пункт меню Контейнеры Кластеры Kubernetes и добавить новый кластер.

На первом шаге настраивается конфигурация будущего кластера. Можно выбрать тип среды и один или несколько предустановленных сервисов. Мы выберем среду Dev и сразу добавим Ingress Controller Nginx для управления внешним доступом к кластеру:

На следующем шаге вводим название кластера и выбираем тип виртуальной машины для ноды Master. Оставим стандартную конфигурацию с 2 CPU и 4 ГБ памяти. Далее можно указать зону доступности мы оставим для нее автоматическое заполнение:

Далее на этом же шаге выбирается тип и размер диска. Нам достаточно HDD размером 20 Гб. Оставляем одну Master-ноду, выбираем предварительно добавленную подсеть и назначаем внешний IP для удобного доступа к кластеру извне:

На следующем шаге создаются группы рабочих узлов. В рамках проекта нам потребуются две группы. Сейчас создадим первую для развертывания API и RabbitMQ, а впоследствии добавим еще одну, для обработчиков Worker.

Вводим название группы узлов и указываем конфигурацию: 2 CPU и 4ГБ памяти. Для зоны доступности вновь выбираем автоматический выбор:

Чтобы обеспечить работу RabbitMQ, выбираем более производительный тип дисков SSD размером 50 ГБ. Оставляем один узел, автомасштабирование пока не указываем его рассмотрим позднее на примере другой группы узлов:

На последнем шаге запускается процесс формирования кластера, который может занять некоторое время: от 5 до 20 минут.

При успешном добавлении кластера на экране отобразится информация о его параметрах:

Для последующей работы с кластером необходимо:

  1. Установить локальный клиент kubectl и запустить его.

  2. Экспортировать в локальный клиент конфигурационный файл созданного кластера с расширением .yaml командой export KUBECONFIG=<путь к файлу>.

  3. Для безопасного подключения к кластеру запустить proxy-сервер командой kubectl proxy.

Эта инструкция отображается под списком параметров кластера после его добавления.

У нас kubectl установлен поэтому берем из загрузок сформированный конфигурационный файл kub-vc-dev_kubeconfig.yaml и экспортируем его в kubectl:

После экспорта конфигурационного файла можно убедиться в работоспособности кластера:

  1. Сначала смотрим доступные контексты: kubectl config get-contexts

    Видим, что у нас создался кластер kub-vc-dev:

  2. Смотрим доступные ноды: kubectl get nodes

    В кластере создались две ноды master и workload:

  3. Смотрим доступные Namespace: kubectl get ns

    Получаем ответ:

  4. Смотрим доступные поды: kubectl -n ingress-nginx get pods

    В Namespace ingress-nginx запущены поды для Nginx Controller:

  5. Смотрим доступные сервисы: kubectl -n ingress-nginx get svс

В списке сервисов также отображается Nginx Controller, для которого указан внешний адрес, который мы сможем прописывать в DNS, чтобы попадать в наши сервисы извне:

Разработка API-сервера на Go

Следующий шаг написать API для отправки запросов на конвертацию видео и получения статуса их обработки. С полной версией исходного кода можно ознакомиться здесь.

Ниже отображена структура проекта. Это стандартное Go-приложение. В файлах go.mod, go.sum описываются зависимости, в папке migrations миграции для базы данных PostgreSQL. В main.go содержится основная логика программы, в requests.go реализация API на добавление, редактирование, удаление и выборку запросов. И есть Dockerfile.

Структура API-сервераСтруктура API-сервера

Остановимся подробнее на содержимом main.go.

Вначале импортируем нужные зависимости. В первую очередь, это migrate для автоматического осуществления миграций, database/sql для работы с базами данных, go-env для работы с переменными окружения, web-фреймворк Gorilla и AMQP для работы с RabbitMQ:

package mainimport (    "encoding/json"    "os"    "github.com/golang-migrate/migrate/v4"    "github.com/golang-migrate/migrate/v4/database/postgres"    _ "github.com/golang-migrate/migrate/v4/source/file"    "database/sql"    env "github.com/Netflix/go-env"    _ "github.com/lib/pq"    "log"    "net/http"    "github.com/gorilla/handlers"    "github.com/gorilla/mux"    "github.com/streadway/amqp")

Далее идут environment, которые мы будем использовать. PGSQL_URI и RABBIT_URI нужны для того, чтобы подключиться к PostgreSQL и RabbitMQ соответственно, LISTEN номер порта, на котором необходимо слушать входящие запросы:

type environment struct {    PgsqlURI  string `env:"PGSQL_URI"`    Listen    string `env:"LISTEN"`    RabbitURI string `env:"RABBIT_URI"`}

Далее следует функция main, которая занимается инициализацией. Сначала происходит чтение environment-переменных, подключение к базе данных PostgreSQL и запуск миграций:

func main() {var err error// Getting configurationlog.Printf("INFO: Getting environment variables\n")cnf := environment{}_, err = env.UnmarshalFromEnviron(&cnf)if err != nil {    log.Fatal(err)}// Connecting to databaselog.Printf("INFO: Connecting to database")db, err = sql.Open("postgres", cnf.PgsqlURI)if err != nil {    log.Fatalf("Can't connect to postgresql: %v", err)}// Running migrationsdriver, err := postgres.WithInstance(db, &postgres.Config{})if err != nil {    log.Fatalf("Can't get postgres driver: %v", err)}m, err := migrate.NewWithDatabaseInstance("file://./migrations", "postgres", driver)if err != nil {    log.Fatalf("Can't get migration object: %v", err)}m.Up()

Затем следует подключение к RabbitMQ и инициализация работы с ним:

// Initialising rabbit mq// Initing rabbitmqconn, err := amqp.Dial(cnf.RabbitURI)if err != nil {    log.Fatalf("Can't connect to rabbitmq")}defer conn.Close()ch, err = conn.Channel()if err != nil {    log.Fatalf("Can't open channel")}defer ch.Close()err = initRabbit()if err != nil {    log.Fatalf("Can't create rabbitmq queues: %s\n", err)}

И в завершение запускается web-сервер. При этом каждому из возможных API-запросов сопоставляется функция обработки, описанная в отдельном файле requests.go:

// Setting handlers for querylog.Printf("INFO: Starting listening on %s\n", cnf.Listen)router := mux.NewRouter().StrictSlash(true)// PROJECTSrouter.HandleFunc("/requests", authMiddleware(getRequests)).Methods("GET")router.HandleFunc("/requests", authMiddleware(addRequest)).Methods("POST")router.HandleFunc("/requests/{name}", authMiddleware(getRequest)).Methods("GET")router.HandleFunc("/requests/{name}", authMiddleware(updRequest)).Methods("PUT")router.HandleFunc("/requests/{name}", authMiddleware(delRequest)).Methods("DELETE")http.ListenAndServe(cnf.Listen, handlers.LoggingHandler(os.Stdout, router))

Далее следует аутентификация в сильно упрощенном варианте, так как на стадии MVP этого достаточно. Разумеется, при разработке Enterprise-решений указание токенов и прочих переменных в явном виде неприемлемо:

func authMiddleware(next http.HandlerFunc) http.HandlerFunc {    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        tokenString := r.Header.Get("X-API-KEY")        if tokenString != "804b95f13b714ee9912b19861faf3d25" {            w.WriteHeader(http.StatusUnauthorized)            w.Write([]byte("Missing Authorization Header\n"))            return        }        next(w, r)    })}

Переходим к инициализации RabbitMQ. Тут мы будем использовать два Exchange и три очереди.

Первый Exchange VideoParserExchange. К нему подключены две очереди:

  • VideoParserWorkerQueue это основная очередь, которую будут слушать обработчики (на иллюстрации для примера приведен один обработчик Worker-0).

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

У VideoParserExchange тип fanout, это значит, что все сообщения из него будут отправляться во все подключенные очереди одновременно.

Второй Exchange VideoParserRetryExchange, к нему подключена очередь VideoParserWorkerRetryQueue. К ней не подключены обработчики.

Архитектура очередей сообщенийАрхитектура очередей сообщений

Цель такого решения отложить попытки отправки сообщений на вышедшие из строя Worker до момента, когда они с большей долей вероятности смогут вернуться к обработке.

Например, если во время обработки сообщения из основной очереди обработчик по какой-то причине отключится и не обработает сообщение, то оно отправится в VideoParserRetryExchange. Этот переход настроен при помощи параметра x-dead-letter-exchange.

Далее VideoParserRetryExchange отправит сообщение в очередь VideoParserWorkerRetryQueue. В ней при помощи параметра x-message-ttl ограничено время хранения сообщения. Также при помощи параметра x-dead-letter-exchange мы указываем, что по прошествии таймаута сообщение должно вернуться в VideoParserExchange для последующей обработки.

Алгоритм работы очередей сообщенийАлгоритм работы очередей сообщений

Вся эта логика описана в функции initRabbit. Сначала мы объявляем два Exchange:

func initRabbit() error {    err := ch.ExchangeDeclare(        "VideoParserExchange", // name        "fanout",              // type        true,                  // durable        false,                 // auto delete        false,                 // internal        false,                 // no wait        nil,                   // arguments    )    if err != nil {        return err    }    err = ch.ExchangeDeclare(        "VideoParserRetryExchange", // name        "fanout",                   // type        true,                       // durable        false,                      // auto delete        false,                      // internal        false,                      // no wait        nil,                        // arguments    )    if err != nil {        return err    }

Далее инициализируются три очереди:

args := amqp.Table{"x-dead-letter-exchange": "VideoParserRetryExchange"}    queue, err = ch.QueueDeclare(        "VideoParserWorkerQueue", // name        true,                     // durable - flush to disk        false,                    // delete when unused        false,                    // exclusive - only accessible by the connection that declares        false,                    // no-wait - the queue will assume to be declared on the server        args,                     // arguments -    )    if err != nil {        return err    }    args = amqp.Table{"x-dead-letter-exchange": "VideoParserExchange", "x-message-ttl": 60000}    queue, err = ch.QueueDeclare(        "VideoParserWorkerRetryQueue", // name        true,                          // durable - flush to disk        false,                         // delete when unused        false,                         // exclusive - only accessible by the connection that declares        false,                         // no-wait - the queue will assume to be declared on the server        args,                          // arguments -    )    if err != nil {        return err    }    queue, err = ch.QueueDeclare(        "VideoParserArchiveQueue", // name        true,                      // durable - flush to disk        false,                     // delete when unused        false,                     // exclusive - only accessible by the connection that declares        false,                     // no-wait - the queue will assume to be declared on the server        nil,                       // arguments -    )    if err != nil {        return err    }

И далее очереди связываются с соответствующими Exchange: VideoParserExchange с очередями VideoParserWorkerQueue и VideoParserArchiveQueue, а VideoParserRetryExchange с очередью VideoParserWorkerRetryQueue:

err = ch.QueueBind("VideoParserWorkerQueue", "*", "VideoParserExchange", false, nil)    if err != nil {        return err    }    err = ch.QueueBind("VideoParserArchiveQueue", "*", "VideoParserExchange", false, nil)    if err != nil {        return err    }    err = ch.QueueBind("VideoParserWorkerRetryQueue", "*", "VideoParserRetryExchange", false, nil)    if err != nil {        return err    }    return nil}

Переходим к файлам миграций БД. Они находятся в отдельной папке migrations:

Devices_up.sql предназначен для создания таблицы requests. В ней содержатся следующие поля:

  • id уникальный идентификатор запроса;

  • name уникальное имя, которое мы будем передавать в API при создании нового запроса и в дальнейшем использовать его для поиска нужного запроса;

  • description описание запроса;

  • video_url ссылка на исходное видео на YouTube, в котором необходимо распарсить текст;

  • text_url ссылка на место хранения результирующего текстового файла в S3;

  • processed логический признак того, что обработка запроса успешно завершена;

  • archived логический признак того, что запись таблицы архивирована. Будем использовать вместо физического удаления для сохранения истории;

  • created_at, updated_at временные метки для сохранения времени создания и последнего редактирования, соответственно.

Итак, создаем таблицу requests:

CREATE TABLE IF NOT EXISTS requests (    id SERIAL,    name VARCHAR(256),    description VARCHAR(2048),    video_url VARCHAR(64),    text_url VARCHAR(64),    processed BOOL DEFAULT FALSE,    archived BOOL DEFAULT FALSE,    created_at TIMESTAMP DEFAULT now(),    updated_at TIMESTAMP DEFAULT null,    UNIQUE(name));

В devices_down.sql описывается удаление таблицы requests:

DROP TABLE requests;

Переходим к файлу requests.go. В нем содержатся функции, которые обрабатывают запросы:

  • addRequest для добавления запроса;

  • updRequest для редактирования запроса;

  • delRequest для удаления запроса;

  • getRequest для получения запроса по имени;

  • getRequests для получения всех запросов.

Все функции довольно простые, в них выполняется проверка входных данных и отправка SQL-запроса в PostgreSQL. Поэтому приведем только фрагмент кода основной функции addRequest. Остальные функции можно посмотреть по ссылке выше.

Здесь происходит попытка отправить сообщение в VideoParserExchange, вывод сообщения в случае ошибки и добавление новой записи в таблицу requests, рассмотренную выше:

func addRequest(w http.ResponseWriter, r *http.Request) {    // Parsing event    req := postRequestRequest{}    err := json.NewDecoder(r.Body).Decode(&req)    if err != nil {        log.Printf("WARNING: Can't parse incoming request: %s\n", err)        returnResponse(400, "Can't parse json", nil, w)        return    }    request := Request{}    if req.Name == nil {        returnResponse(400, "name can't be null", nil, w)        return    }    request.Name = *req.Name    if req.Description != nil {        request.Description = *req.Description    }    if req.Processed != nil {        request.Processed = *req.Processed    }    if req.VideoURL != nil {        request.VideoURL = *req.VideoURL    }    if req.TextURL != nil {        request.TextURL = *req.TextURL    }    // Publishing data to rabbitmq    msg, err := json.Marshal(request)    if err != nil {        log.Printf("ERROR: Marshaling request: %s\n", err)        returnResponse(500, "Can't marshal request ", nil, w)        return    }    err = ch.Publish(        "VideoParserExchange", // exchange        "",                    // routing key        false,                 // mandatory - could return an error if there are no consumers or queue        false,                 // immediate        amqp.Publishing{            DeliveryMode: amqp.Persistent,            ContentType:  "application/json",            Body:         msg,        })    if err != nil {        log.Printf("ERROR: Publishing to rabbit: %s\n", err)        returnResponse(500, "Can't publish to rabbit ", nil, w)        return    }    stmt := `INSERT INTO requests (name, description, processed, video_url, text_url) VALUES ($1, $2, $3, $4, $5) RETURNING id`    err = db.QueryRow(stmt, &request.Name, &request.Description, &request.Processed, &request.VideoURL, &request.TextURL).Scan(&request.ID)    if err != nil {        log.Printf("ERROR: Adding new request to database: %s\n", err)        returnResponse(500, "Can't add new request ", nil, w)        return    }    returnResponse(200, "Successfully added new request", nil, w)}

В завершение рассмотрим Dockerfile, с помощью которого можно собрать приложение. Здесь используется образ golang-alpine, выполняется статическая компиляция, затем берется чистый alpine, куда переносится приложение со всеми миграциями и необходимыми файлами:

FROM golang:1.15-alpine AS build# Installing requirementsRUN apk add --update git && \    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*# Creating workdir and copying dependenciesWORKDIR /go/src/appCOPY . .# Installing dependenciesRUN go getENV CGO_ENABLED=0RUN go build -o api main.go requests.goFROM alpine:3.9.6RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \    apk add --update bash && \    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*WORKDIR /appCOPY --from=build /go/src/app/api /app/apiCOPY ./migrations/ /app/migrations/CMD ["/app/api"]

Создание БД PostgreSQL в облаке MCS

Базу данных для хранения статуса обработки запросов на конвертацию видео будем создавать из консоли управления облаком MCS. Для этого нужно выбрать пункт меню Базы данных и добавить БД PostgreSQL:

На первом шаге определяется конфигурация. Выберем последнюю версию PostgreSQL и тип конфигурации Single: для среды Dev нам достаточно единичного инстанса:

На следующем шаге указываем имя инстанса БД и выбираем конфигурацию виртуальной машины. Нам достаточно 1 CPU и 2 ГБ памяти. Для зоны доступности оставляем автоматический выбор:

В качестве диска выберем SSD размером 20 ГБ. Сеть можно создать отдельную, мы возьмем текущую. Внешний IP назначать не будем: база будет во внутренней сети. В настройках Firewall при необходимости можно указать ограничения на доступ, нам пока они не нужны все разрешаем. Создание реплики нам также не нужно. Ключ для доступа по SSH создаем свой. И устанавливаем периодичность резервного копирования раз в сутки:

На следующем шаге указываем имя БД, имя пользователя и генерируем пароль:

Далее запускается процесс создания инстанса, который займет некоторое время. После успешного создания параметры БД будут выведены на экран, в том числе внутренний IP-адрес сети, который впоследствии нам понадобится:

Установка RabbitMQ через Helm в Kubernetes

Для установки RabbitMQ воспользуемся Helm-чартом bitnami/rabbitmq. Достоинство чартов в том, что не нужно устанавливать по отдельности все необходимые сервису ресурсы: можно установить их одновременно в рамках общего релиза. А при изменениях в любом из ресурсов можно вынести новый релиз, в котором все обновления будут собраны воедино.

Создадим папку helm, добавим в нее репозиторий bitnami и найдем нужный нам Helm Chart bitnami/rabbitmq:

mkdir helmcd helmhelm repo add bitnami https://charts.bitnami.com/bitnamihelm search repo bitnami

Теперь мы нашли нужный чарт:

Копируем его имя, загружаем и распаковываем:

helm pull bitnami/rabbitmqtar zxv

Переходим в папку rabbitmq/templates. Здесь находятся все ресурсы, которые нужно будет создать в Kubernetes для корректной работы RabbitMQ: конфигурация, Ingress, сертификаты, сетевые политики, сервисные аккаунты, секреты, правила Prometheus и так далее. И Helm позволяет это сделать единой командой, без установки каждого файла по отдельности:

Возвращаемся в родительскую папку helm, чтобы посмотреть возможность настройки файла values.yaml. Скопируем содержимое rabbitmq/values.yaml в наш собственный файл values.dev.yaml и откроем его для редактирования:

cp rabbitmq/values.yaml ./values.dev.yamlvi values.dev.yaml

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

В данном файле содержится очень много параметров, которые можно настраивать под нужды своего проекта: режим debug, плагины RabbitMQ для подключения, необходимость включения TLS и memoryHighWatermark, аутентификация через LDAP, количество реплик, nodeSelector для создания RabbitMQ на нодах с определенной меткой, требования к CPU и памяти и многое другое.

Нас в первую очередь интересуют настройки Ingress. Находим секцию ingress, устанавливаем в enabled значение true и прописываем в поле hostname имя rabbitmq.stage.kis.im. Эта настройка необходима для внешнего доступа к RabbitMQ, без нее он будет доступен только внутри кластера. Kis.im это мой существующий домен:

Далее переходим непосредственно к развертыванию RabbitMQ. Создаем новый namespace stage и применяем к нему созданный файл values.stage.yaml (изменив dev на stage в названии для единообразия):

kubectl create ns stagehelm instal -n stage rabbitmq -f values.dev.yamlmv values.dev.yaml values. stage. yamlhelm install -n stage rabbitmq -f values.stage.yanl ./rabbitmq/

Вот, что получилось, когда Namespace создан:

После успешной установки можно посмотреть список подов и сервисов в Namespace stage rabbitmq успешно добавлен. Он имеет кластерный IP 10.254.178.84. Но так как наше приложение будет находиться в том же Namespace, мы сможем обращаться к нему по имени rabbitmq.

Еще один сервис rabbitmq-headless не имеет кластерного IP. Он используется при добавлении нескольких RabbitMQ для их автообнаружения и объединения в кластер с помощью kubectl -n stage get svc:

С помощью Helm можно получить дополнительные сведения о релизе: время последнего обновления, статус, название чарта, версию приложения, используем helm -n stage list:

Кроме этого, можно посмотреть Persistent Volumes, выделенные RabbitMQ, с помощью kubectl get pv. В нашем случае Volume имеет размер 8 ГБ и Storage Class csi-hdd:

При необходимости нужный Storage Class можно было прописать непосредственно в YAML-файле:

Список всех возможных классов можно вывести командой kubectl get storageclasses:

Здесь важен параметр RECLAIMPOLICY: в зависимости от его значения при удалении запроса на данный ресурс (PVC, Persistent Volume Claim) сам Persistent Volume будет удален или сохранен для будущего использования.

Осталось обеспечить внешний доступ к нашему сервису. Проверяем добавление ресурса Ingress для RabbitMQ командой kubectl -n stage get ingress:

Затем получаем внешний адрес Ingress Controller с помощью kubectl -n ingress-nginx get svc:

В Cloudflare прописываем DNS для RabbitMQ, связывая его внешний Hostname и IP-адрес Ingress Controller:

После этого RabbitMQ становится доступен по адресу rabbitmq.stage.kis.im:

Имя пользователя user. Пароль сохранился в переменные окружения после развертывания RabbitMQ, его можно получить с помощью команды env | grep RABBITMQ_PASSWORD.

Развертывание и предварительная проверка API

RabbitMQ мы развернули с помощью Helm. Для нашего приложения с API в последующем мы также создадим собственный Helm Chart, но пока посмотрим, как выполняется развертывание приложения вручную на основе YAML-файлов.

Образ приложения мною уже создан при помощи Dockerfile, который мы рассматривали ранее.

Далее определим необходимые ресурсы. Очевидно, что локальное хранилище приложению не нужно, так как приложение уже взаимодействует с PostgreSQL и RabbitMQ, размещенными в облаке. Поэтому Persistent Volumes создавать не будем. Основные ресурсы, которые нам потребуются, описывают файлы deployment.yaml, ingress.yaml и svc.yaml:

Начнем с deployment.yaml. Здесь описывается ресурс Deployment. Тут мы описываем шаблон пода, который будем запускать. Указываем, что будем запускать контейнер с именем api, образ vozerov/video-api:v1 (этот образ я уже залил на hub.docker.com).

Далее в блоке env указываем переменные, используемые в нашем API:

  • В переменной RABBIT_URI вводим сформированные при создании RabbitMQ имя и пароль пользователя, название сервиса rabbitmq и номер порта 5672 (имя сервиса можно проверить с помощью команды kubectl -n stage get svc).

  • В переменной LISTEN устанавливаем номер порта 8080.

  • В переменной PGSQL_URI заполняем сформированные при создании PostgreSQL имя и пароль пользователя, внутренний адрес БД 10.0.0.10, номер порта 5432 и название БД vc-dev. Все параметры БД можно найти в консоли управления облаком.

deployment.yaml: описываем шаблон подаdeployment.yaml: описываем шаблон пода

По хорошему, пароли нельзя хранить тут в открытом виде. Но как я уже говорил ранее, это MPV, и для упрощения мы сейчас сделаем так.

Применяем сформированный файл:

kubectl -n stage apply -f deployment.yamlkubectl -n stage get deploy

Video-api создан:

И проверяем создание нового пода с помощью kubectl -n stage get pods:

После успешного применения deployment.yaml можно зайти в RabbitMQ и убедиться в создании всех необходимых очередей и Exchange.

Созданные очередиСозданные очередиСозданные ExchangeСозданные Exchange

Следующий ресурс, который нам необходимо добавить для доступа к сервису извне это Service. Он описывается в файле svc.yaml. Мы указываем, что приложение video-api будет принимать входящие соединения на порт 8080 и пробрасывать их в контейнер на порт 8080. Применяем svc.yaml стандартной командой kubectl apply -n stage -f svc.yaml:

Последний ресурс, который необходим для нашего сервиса Ingress. В файле ingress.yaml мы указываем правила, по которым нужно направлять запросы к сервису. Заполняем внешнее имя api.stage.kis.im и в блоке path указываем, что все корневые запросы направляем на сервис video-api-svc, созданный на прошлом шаге. Применяем сформированный файл kubectl apply -n stage -f Ingress.yaml:

Убеждаемся в добавлении Ingress для нашего сервиса с помощью kubectl -n stage get ingress:

Затем добавляем запись в DNS аналогично тому, как делали это ранее для RabbitMQ:

Теперь можно провести первое тестирование API, используя отправку запросов через curl. В заголовках всех запросов нужно передавать X-API-KEY со значением токена из кода программы main.go.

Для начала с помощью метода GET получим список всех записей requests:

curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .

На текущий момент он пуст:

Отправим новый запрос на конвертацию видео, используя метод POST. В имени запроса (name) укажем test1. В ссылке на видео (video_url) введем тестовое значение, так как у нас пока нет обработчиков Worker:

curl -X POST -d '{"name": "test1", "video_url": "https://google.com" }' -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .

Запрос успешно создан:

Далее можно получить запрос по имени test1 и убедиться в наличии всех переданных при создании параметров:

curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests/request1 | jq .

Запрос создан, все параметры верные:

В очереди RabbitMQ сообщение также будет добавлено. Заходим в очередь:

Видим сообщение:

Осталось зайти в базу PostgreSQL и проверить ее структуру. Внешний доступ мы не настраивали поэтому можно подключиться, например, через psql из отдельно запущенного пода. Мы видим наличие таблицы requests, а в ней добавленный нами запрос:

Таким образом, проверка работы API пройдена.

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

Новым пользователям платформы Mail.ru Cloud Solutions доступны 3000 бонусов после полной верификации аккаунта. Вы сможете повторить сценарий из статьи или попробовать другие облачные сервисы.

И обязательно вступайте в сообществоRebrain в Telegram там постоянно разбирают различные проблемы и задачи из сферы Devops, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.

Что еще почитать по теме:

  1. Как развернуть кластер Kubernetes на платформе MCS.

  2. Запускаем etcd-кластер для Kubernetes.

  3. Как устроен Kubernetes aaS на платформе Mail.ru Cloud Solutions.

Подробнее..

Kubernets 1.21 неожиданно много изменений

12.04.2021 16:18:43 | Автор: admin

Новая эмблема символизирует распределение членов команды выпуска релиза по земному шару от UTC-8 до UTC+8 (похоже, ни японцев, ни корейцев в команде нет). Эмблему нарисовал Aravind Sekar, независимый дизайнер из Индии. На мой взгляд, котики были круче.

Но давайте перейдем к чтению changelog и особенно моему любимому разделу Urgent Upgrade Notes.


CronJob

Сообщение в блоге гласит, что CronJob объявлены stable, но далее есть небольшое уточнение стабильным объявлена версия API, то есть структура манифеста kind: cronJob, а вот с контроллером, который и отвечает за реализацию логики работы, все намного интереснее.

В версии 1.20 был добавлен CronJob контроллер версии 2. В новой версии 1.21 его перевели в стадию бета и включили по умолчанию. В версии 1.22 планируется удалить код старого CronJob контроллера. Очень, очень быстрые изменения, обычно не свойственные циклам релизов в Kubernetes.

Причем в документации все Cronjob limitations, о которых я писал на Хабре, остались без изменений.

Зачем тогда делали новый контроллер, если все проблемы с кронджобами остались нерешенными? Ответ есть в этой статье старый контроллер излишне нагружал API Kubernetes и не успевал создавать Job, если в кластере было больше 1000 Cronjob манифестов. Новая версия контроллера написана согласно последним гайдлайнам и намного быстрее.

Immutable Secret and ConfigMap

Добавили возможность создавать защищенные от изменений секреты и конфиг мапы. Видимо, защита от джунов, которые "pushed bad configuration". На мой взгляд, ConfigMap надо деплоить через helm чарты, а секреты хранить в Vault. Там, где есть история изменений, а ваш CI/CD не должен позволять выкатывать нерабочие конфиги на прод.

IPv4/IPv6 Dual-Stack support

Поддержка IPv6 теперь включена по умолчанию, единственная тонкость ваш CNI также должен уметь в Dual-Stack. Calico умеет)

Graceful Node Shutdown

Kubelet научился определять ситуацию, когда узел выключается командой shutdown, и теперь посылает подам sigterm. TODO: Протестировать, не завершается ли container runtime быстрее kubelet, и что будет при простом systemctl shutdown kubelet.

PodSecurityPolicy Deprecation

Еще одна неоднозначная новость. PSP объявлены устаревшими и запланированы к удалению в версии 1.25. Но при этом PSP Replacement Policy (полиси для замены полиси) находятся в состоянии проекта, а альфа-версию обещают показать только в Kubernetes 1.22. Кратко ознакомиться, что же там проектируется, можно в KEP #2582. Самое странное из того, что там написано, на мой взгляд, это предложение использовать namespace label, чтобы определять, по каким правилам проверять манифесты подов. Получается, что, выдав кому-либо права на редактирование неймспейса, вы даете ему и простой способ получить права администратора кластера.

Подождем и посмотрим, что же будет в итоге, а пока нам предлагают плавно переходить на использование стандартных PSP, аналоги которых в виде встроенных профилей будут захардкожены в новый admission plugin PSPv2.

Или переходить на использование сторонних решений, таких как Open Policy Agent Gatekeeper.

Urgent Upgrade Notes

По умолчанию теперь используется cgroupDriver systemd. Не забывайте проверять настройки своего containerd при установке нового кластера или добавлении узлов. Но и это еще не все. В версии 1.22 обещают принудительную смену cgroup driver в kubelet на systemd при обновлении кластера, поэтому пора уже почитать руководство по миграции и начать смену драйвера.

Много мелких изменений в районе CSI и PV: перестают использоваться старые метки, флаги и метрики. В принципе ничего страшного, скорее всего, надо будет просто обновить используемые CSI драйверы.

Команды kubeadm kubeconfig user, certs и debug переведены из экспериментальных в постоянные, и теперь их надо указывать без слова alpha.

Продолжают урезать функционал команды kubectl run. Убрали целый набор ключей для создания service и cronjob, объявили устаревшими ключи для установки реквестов и лимитов, сервисаккаунта и использования hostport. В общем активно заставляют использовать только готовые yaml-манифесты для создания объектов кластера.

К большому моему сожалению, окончательно убрали поддержку ключа kubectl --export. А как было удобно с помощью этого ключа получать из готового объекта кластера манифест для создания его копии, например, секрет с TLS сертификатом скопировать в другой namespace.

Всем, кто использует vSphere версии меньшей, чем 67u3, рекомендуют обновиться, время есть до выхода kubernetes 1.24.

Интересные мелкие новшества

В NetworkPolicy добавили поле endPort для поддержки диапазонов портов. Радуйтесь, любители запускать Asterisk в кластере.

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

В команды kubectl exec и portforward добавили keepalive пинги, и теперь промежуточные HTTP-балансировщики не будут обрывать соединение, если в нем нет активности.

В Job добавили поле suspend и написали про это целую статью в блоге. Вот только я не понял, какой в этом смысл имитация работы Kafka или Rabbitmq?

Появилась возможность выбирать неймспейсы по их имени. Просто в манифест неймспейса автоматически добавляют метку kubernetes.io/metadata.name.

В Service добавили поле InternalTrafficPolicy. Если указать в нем значение Local, трафик будет направляться только на поды, расположенные на том же узле кластера, что и под, отправивший запрос. Пока в альфа-статусе, для использования надо включить featureGate = ServiceInternalTrafficPolicy.

Наконец-то включили TTL Controller, который позволяет удалять манифесты завершившихся Job.

В манифест подов добавили аннотацию kubectl.kubernetes.io/default-container, с помощью которой можно указать, в какой контейнер пода делать exec, чьи логи смотреть и тому подобное, если при вызове не указан ключ -c.

Подробнее..

Рекомендации по запуску приложений в OpenShift Service Mesh

15.04.2021 12:10:33 | Автор: admin

В этом посте мы собрали советы и рекомендации, которые стоит изучить, прежде чем переносить свои приложения в сервисную сетку OpenShift Service Mesh (OSSM). Если вы никогда не сталкивались с сервисными сетками Service Mesh, то для начала можно глянуть страницу OSSM на сайте Red Hat и почитать о том, как система Istio реализована на платформе OpenShift.

Начав изучать Istio, вы скорее всего столкнетесь с приложением bookinfo, которое почти повсеместно используется в качеств наглядного пособия, или же с более продвинутым вариантом в виде приложения Travel Agency. Разбирая эти и другие примеры, вы сможете лучшее понять, как устроена mesh-сетка, и затем уже переносить в нее свои приложения

Сначала о главном

Начать стоит с официальная документация OpenShift Service Mesh 2.0 (OSSM), в ней можно найти массу полезных материалов, в том числе:

Когда дойдет до интеграции вашего приложения в mesh-сетку, надо будет копнуть поглубже и заглянуть в документацию по Istio. Также стоит ознакомиться с release notes соответствующих версий компонентов, входящих Red Hat OSSM.

Если еще не сделали это, то протестируйте свою mesh-сетку с помощью приложения-примера Bookinfo. Если все пройдет нормально, то в нее уже можно будет добавлять ваше приложение.

Первое, что надо сделать при добавлении в mesh-сетку своего приложения убедиться, что sidecarы проксей Envoy правильно внедрены в podы вашего приложения. В OSSM такое внедрение делается довольно просто и хорошо описывается в документации.

Потом воспользуйтесь примером, где описывается, как настраивать ingress-шлюз для приложения Bookinfo, и примените эту конфигурацию к своему приложению, чтобы к нему можно было получать доступ не только изнутри кластера OpenShift, но и извне.

Выбор протоколов

Важно четко понимать, как Istio определяет, какие протоколы использует ваше приложение. Для этого изучите все, что связано с Protocol Selection и app and version labelsв разделе документации Pods and Services.

В противном случае скорее всего произойдет следующий казус. Допустим, вы внедряете в свое приложение sidecarы проксей Istio, загружаете его тестовым трафиком и идёте смотреть граф Kiali. И видите там совсем не то, что ожидали (рис. ниже). Почему? Потому что Kiali и Istio не смогли правильно определить, какие протоколы используют наши сервисы, и отобразили соединения между ними как TCP, а не HTTP.

На графе Kiali есть только TCP-соединенияНа графе Kiali есть только TCP-соединения

Istio должен точно знать, какой протокол используется. Если Istio не может определить протокол автоматически, то трактует трафик как обычный (plain) TCP. Если у вас какие-то другие протоколы, их надо вручную прописать в определениях служб Kubernetes Service вашего приложения. Подробнее об этом написано в документации, раздел Protocol Selection.

Чтобы вручную задать, какой протокол использует ваш сервис, надо соответствующим образом настроить объекты Kubernetes Service. В нашем случае в них по умолчанию отсутствовало значение параметра spec -> ports -> name. Если прописать "name: http" для сервисов A, B и C, то граф отобразит эти соединения как HTTP.

Kiali

Kiali это отличный инструмент для того, чтобы начать работать с OpenShift Service Mesh. Можно даже сказать, что именно на нем и надо сосредоточиться, когда вы начинаете работать с mesh-сеткой.

Kiali визуализирует метрики, генерируемые Istio, и помогает понять, что происходит в mesh-сетке. В качестве одной из первоочередных задач мы настоятельно рекомендуем изучить документацию Kiali.

Kiali не только пригодится для визуализации вашего приложения, но и поможет с созданием, проверкой и управлением конфигурациями mesh-сетки. Поначалу изучение конфигураций Istio может быть делом сложным, и Kiali тут сильно поможет.

В Kiali есть много полезных вещей, поэтому мы очень советуем изучить список ее возможностей и FAQ. Например, там есть следующие интересные вещи:

Другая важная вещь умение маркировать сервисы приложения с помощью меток (label). Istio, а следовательно и Kiali, требует, чтобы маркировка велась строго определенным образом, который поначалу отнюдь не кажется очевидным, особенно когда весь ваш опыт исчерпывается работой с приложением-примером Bookinfo, где все метки уже есть и всё прекрасно работает из коробки.

Развертывания с использованием меток app и version это важно, поскольку они добавляет контекстную информацию к метрикам и телеметрии, которые собираются Istio и затем используются в Kiali и Jaeger.

Istio может показать такие связи между внутренними и внешними сервисами, о существовании которых вы даже не догадывались. Это полезно, но иногда может раздражать при просмотре графа. К счастью, с графа всегда можно убрать эти Неизвестные узлы.

Еще одна полезная вещь в Kiali это то, что она может провести проверку вашей mesh-сетки, что особенно полезно, когда вы сами создаете конфигурации.

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

Jaeger-выборки

При первоначальном тестировании своего приложения в mesh-сетке вам, скорее всего, захочется, чтобы частота трассировки была больше 50%, желательно, 100%, чтобы отслеживать все тестовые запросы, проходящие через приложение. В этом случае Jaeger и Kiali быстрее наберут необходимые данные, а вам не придется долго ждать обновления информации.

Иначе говоря, нам надо, чтобы sample rate был равен 100% (тут есть соответствие: 10000 = 100%).

Для этого надо подредактировать объект ServiceMeshControlPlane (обычно называется basic-install) в вашем проекте Control Plane (обычно istio-system) и добавить или изменить там следующее значение:

spec: tracing:  sampling: 10000 # 100%

Понятно, что после запуска приложения в продакшн уже не надо мониторить каждый запрос и частоту выборки стоит понизить процентов до пяти или ниже.

Распространение заголовков контекста трассировки

Jaeger помогает убрать одну из проблем, которая возникает при переходе на микросервисную архитектуру, но для этого все сервисы вашего приложения должны правильно распространять заголовки трассировки (trace headers).

Очень полезно отслеживать, как запросы ходят через сервисы (или даже множество сервисов) в вашей mesh-сетке. OSSM может здесь помочь за счет сбора данных в форме spanов и трасс (trace). Просмотр трасс очень помогает понять сериализацию, параллелизм и источники задержек в вашем приложении. Вкратце, span это интервал от начала выполнения единицы работы до ее завершения (например, полная отработка запроса клиент-сервер). Трасса это путь, по которому запрос проходит, двигаясь по mesh-сети, или, другими словами, по мере того, как он передается от одного сервиса вашего приложения к другому. Подробнее об этом можно и нужно почитать в документации OSSM.

Обратите внимание, что в OSSM spanы (единицы работы) автоматически генерируются средствами Istio, а вот трассы нет. Поэтому чтобы распределенные трассы (distributed traces) были полностью просматриваемыми, разработчик должен изменить код так, чтобы любые существующие trace-заголовки правильно копировались при передаче запроса между сервисами. К счастью, вы не обязаны сами генерировать эти заголовки. Если изначально их нет, то они будут автоматически сгенерированы и добавлены первым Envoy-прокси, который встретится на пути запроса (обычно это прокси на ingress-шлюзе).

Вот список заголовков для распространения:

  • x-request-id

  • x-b3-traceid

  • x-b3-spanid

  • x-b3-parentspanid

  • x-b3-sampled

  • x-b3-flags

  • x-ot-span-context

Распространение заголовков может выполняться вручную или с использованием клиентских библиотек Jaeger, реализующих OpenTracing API.

Вот как делается ручное распространение trace-контекста на Java:

HttpHeaders upstreamHttpHeaders = new HttpHeaders();if (downstreamHttpHeaders.getHeader(headerName: "x-request-id") != null)   upstreamHttpHeaders.set("x-request-id", downstreamHttpHeaders.getHeader( headerName: "x-request-id"));

Примечание: это надо повторить для всех заголовков из списка выше.

Мастера Kiali и редактор YAML

Проверки

Ручная настройка yaml-манифестов дело утомительное, особенно если значения в одном yaml должны совпадать со значениями в другом, а еще ведь есть жесткие правила отступов. К счастью, в Kiali есть отличная функция, которая поможет при создании и проверке конфигураций.

Создание Istio-ресурсов с помощью Kiali-мастеров

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

YAML-редактор

Kiali имеет собственный редактор YAML для просмотра и редактирования конфигурационных ресурсов Istio напрямую, который также выявляет некорректные конфигурации.

Часто бывает так, что граф Kiali вдруг выявляет в вашем приложении неизвестные ранее (в том числе и разработчикам) коммуникационные пути. Другими словами, Kiali помогает найти и выявить все существующие пути во время тестирования вашего приложения. Это, конечно, полезно, но иногда может и раздражать. В этом случае их можно просто не отображать на графе, введя "node=unknown" в поле ввода над графом Kiali.

Уберите из кода шифрование коммуникаций

Если вы уже защитили соединения между своими сервисами и/или (скорее всего) используете TLS для внешних соединений, то при переводе приложения в mesh-сетку их надо будет в обязательном порядке выключить и переключиться на чистый HTTP без шифрования. А всем шифрованием теперь займутся Envoy-прокси.

Если ваши сервисы будут связываться с внешними сервисами по TLS, то Istio не сможет инспектировать трафик и Kiali будет отображать эти соединения только как TCP.

В общем, используйте для взаимодействия сервисов только HTTP, но не HTTPS.

Также про внешние сервисы надо поставить в известность и вашу mesh-сетку (см. ниже Настройка внешних сервисов).

Упростите код

Самое время пересмотреть код вашего приложения и убрать из него лишнее.

Одно из преимуществ Service Mesh состоит в том, что реализацию многих вещей по сервисам можно убрать из приложения и отдать на откуп платформе, что помогает разработчикам сосредоточиться на бизнес-логике и упростить код. Например, можно рассмотреть следующие доработки:

  • Как сказано выше, убрать HTTPS-шифрование.

  • Убрать всю логику обработки таймаутов и повторных попыток.

  • Убрать все ставшие ненужными библиотеки.

  • Помните, что mesh-сетка увеличивает количество пулов подключений. Если раньше два сервиса связывались напрямую, то теперь у каждого из них есть свой прокси-посредник. То есть фактически вместо одного пула подключений появляются три:

    1. От вашего первого сервиса к локальному для него sidecarу Envoy (расположены в одном и том же podе).

    2. От этого sidecarа к другому sidecarу Envoy, который обслуживает второй сервис и расположен в одном podе с этим сервисом.

    3. И наконец, от того другого sidecarа, собственно, ко второму сервису. Поэтому, возможно, стоит упростить конфигурацию, чтобы не раздувать пулы подключений. Подробнее об этом хорошо написано здесь.

Еще один плюс оптимизации кода это возможность уменьшить размер сервисов и (возможно) поднять их производительность, убрав из них те вещи, которые теперь реализуются на уровне mesh-сетки.

Объекты Service

Убедитесь, что все сервисы вашего приложения взаимодействуют друг с другом через имена объектов Kubernetes Service, а не через OpenShift Routes.

Просто проверьте, вдруг ваши разработчики используют OpenShift Routes (конечные точки ingress на кластере) для организации коммуникаций между сервисами в пределах одного кластера. Если эти сервисы должны входить в одну и ту же mesh-сетку, то разработчиков надо заставить поменять конфигурации/манифесты своих приложений, чтобы вместо конечных точек OpenShift Route использовались имена объектов Kubernetes Service.

Функции аварийного переключения (fallback)

В коде вашего приложения, возможно, потребуется поменять еще кое-что. Envoy-прокси конечно заботятся о таймаутах и повторах, чтобы межсервисные коммуникации были более надежными, но иногда сервисы падают полностью, и такие отказы можно отработать только на уровне приложения. Для этого в коде приложения надо реализовать функции аварийного переключения (fallback), чтобы оно уже само решало, как быть дальше, когда Envoy-прокси уже ничем не может помочь.

Настройка внешних сервисов

Envoy-прокси могут работать не только внутри кластера, но и отправлять трафик за его пределы, если зарегистрировать внешние сервисы в mesh-сетке.

Скорее всего, ваше приложение общается с сервисами за пределами mesh-сетки. Поскольку это внешние по отношению mesh-сетке сервисы, то толку от нее здесь не будет, да? А вот и нет. Эти внешние сервисы можно прописать в Service Mesh и использовать часть её функций и для них.

Подробнее и с примерами можно почитать об этом в документации OSSM. Есть и подробный разбор, как визуализировать внешний трафик Istio в Kiali, и как использовать TLS originationдля зашифрованного egress-трафика.

Вот некоторые из функций Istio, которые можно использовать при работе с внешними сервисами:

  1. Шифрование (и простое, и Mutual TLS).

  2. Таймауты и повторы.

  3. Circuit breakerы.

  4. Маршрутизация трафика.

Заключение

С OpenShift Service Mesh вы можете лучше понять, как устроена ваша mesh-сетка, сделать ее более просматриваемой, что, в свою очередь, помогает поднять общий уровень сложности микросервисной архитектуры. Бонусом идет возможность реализовать больше функций и возможностей на уровне самой платформе OpenShift, а не кодировать их на уровне отдельных приложений, что облегчает жизнь разработчикам. Еще один плюс реализация вещей, которые раньше казались неподъемными, например, канареечное развертывание, A/B-тестирование и т.п. Кроме того, вы получаете целостный подход к управлению микросервисными приложениями на всех своих кластерах OpenShift, что хорошо с точки зрения преемственности людей и непрерывности процессов. В конечном итоге, это поможет перейти от монолитных приложений к распределенной микросервисной архитектуре и работать в большей степени на уровне конфигураций, чем кода.

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru