Контроллер ведет активный мониторинг и поддерживает набор ресурсов Kubernetes в нужном состоянии. Основа самого фреймворка Kubernetes состоит из парка контроллеров, которые регулярно проверяют и согласовывают текущее состояние приложений с требуемым целевым состоянием. В этой главе вы увидите, как использовать эту базовую идею для расширения платформы под свои нужды.
Вы уже знаете, что Kubernetes — это сложная и многогранная платформа, предлагающая множество возможностей. Однако это универсальная платформа управления контейнерами охватывает далеко не все варианты использования приложений. К счастью, она поддерживает точки расширения, используя которые можно реализовать конкретные варианты, опираясь на проверенные строительные блоки Kubernetes.
Главный вопрос, который мы рассмотрим здесь: как расширить Kubernetes, не изменяя и не нарушая его работу, и как использовать его возможности для поддержки нестандартных сценариев.
В основе Kubernetes лежит декларативный API, ориентированный на ресурсы. Что подразумевается под словом декларативный? Декларативный подход, в противоположность императивному, описывает не как должен действовать Kubernetes, а каким должно быть целевое состояние. Например, масштабируя развертывание Deployment вверх, мы не создаем новые поды, требуя от Kubernetes «создать новый под», а меняем свойство replicas ресурса Deployment через Kubernetes API, присваивая ему желаемое число.
Но как создаются новые поды? Эту задачу решают внутренние контроллеры. При каждом изменении состояния ресурса (например, при изменении значения свойства replicas в развертывании Deployment) Kubernetes создает событие и передает его всем обработчикам. Затем эти обработчики реагируют на событие, изменяя, удаляя или создавая новые ресурсы, что, в свою очередь, приводит к появлению других событий, таких как событие, требующее создать новый под. Эти события затем передаются другим контроллерам, которые выполняют свои конкретные действия.
Весь этот процесс известен как согласование состояний, когда целевое состояние (требуемое количество реплик) отличается от текущего (фактическое количество запущенных экземпляров), и задача контроллера состоит в том, чтобы согласовать и достичь желаемого целевого состояния. С этой точки зрения Kubernetes представляется в роли диспетчера распределенного состояния. Вы сообщаете ему параметры желаемого состояния экземпляра компонента, а он пытается достичь этого состояния и поддерживать его.
Можно ли как-то внедриться в этот процесс согласования без изменения кода Kubernetes и создать контроллер для наших конкретных потребностей?
В состав Kubernetes входит целая коллекция встроенных контроллеров, управляющих стандартными ресурсами, такими как ReplicaSet, DaemonSet, StatefulSet, Deployment или Service. Эти контроллеры работают под управлением диспетчера контроллеров, который развертывается (как отдельный процесс или под) на главном узле. Контроллеры не знают о существовании друг друга. Они выполняют бесконечный цикл согласования, постоянно проверяя фактическое и желаемое состояние своих ресурсов и предпринимая соответствующие действия, чтобы приблизить фактическое состояние к желаемому.
Однако архитектура Kubernetes, управляемая событиями, позволяет подключать другие, нестандартные контроллеры. Эти контроллеры могут добавлять новые возможности и обрабатывать события подобно внутренним контроллерам. Все контроллеры по своей природе являются реактивными, они реагируют на события в системе и выполняют свои конкретные действия. В общем и целом процесс согласования состояния состоит из следующих основных этапов:
Определение фактического состояния путем наблюдения за событиями, которые распространяет Kubernetes при изменении контролируемого ресурса.
Выявление отличий от желаемого состояния.
Выполнение операций, необходимых для приведения текущего состояния в желаемое.
Например, контроллер ReplicaSet наблюдает за изменениями в ресурсе ReplicaSet, определяет, сколько подов должно быть запущено, и выполняет действия, посылая определения подов в API Server. После этого внутренние механизмы Kubernetes производят запуск указанного пода на узле.
На рис. 22.1 показано, как контроллер регистрирует себя, подписываясь на события с целью обнаружения изменения состояния контролируемых ресурсов, наблюдает за их текущим состоянием и обращается к API Server (если это необходимо), чтобы поддержать фактическое состояние как можно ближе к желаемому.
Рис. 22.1. Цикл наблюдение-анализ-действие
Контроллеры являются частью уровня управления в Kubernetes, и с самого начала было ясно, что они могут служить инструментом расширения платформы новыми аспектами поведения. Более того, они превратились в стандартный механизм расширения платформы и управления сложным жизненным циклом приложений. В результате родилось новое поколение более совершенных контроллеров под названием Операторы. С точки зрения эволюционного развития и сложности активные компоненты согласования состояния можно разделить на две группы:
Реализуют простой процесс согласования, контролируя и воздействуя на стандартные ресурсы Kubernetes. Чаще всего эти контроллеры создаются с целью совершенствования поведения платформы и добавления новых возможностей.
Реализуют сложный процесс согласования, который является основой паттерна Operator (Оператор), обслуживают определение нестандартных ресурсов CustomResourceDefinition (CRD). Как правило, операторы инкапсулируют сложную предметную логику и управляют полным жизненным циклом приложения. Мы подробно рассмотрим паттерн Operator (Оператор) в главе 23.
Как отмечалось выше, такое деление помогает постепенно внедрять новые идеи. В этой главе мы сосредоточимся на более простых контроллерах, а в следующей познакомимся с CRD и рассмотрим паттерн Operator (Оператор).
Чтобы исключить возможность одновременной обработки одних и тех же ресурсов, контроллеры реализуются с использованием паттерна Singleton Service (Служба-одиночка), описанного в главе 10. Большинство контроллеров развертываются с использованием ресурса Deployment, но с единственной репликой, потому что Kubernetes использует оптимистическую блокировку на уровне ресурсов для предотвращения проблем, связанных с параллельной обработкой при изменении объектов ресурсов. В конце концов, контроллер — это не что иное, как приложение, которое постоянно выполняется в фоновом режиме.
Поскольку сам фреймворк Kubernetes и клиентская библиотека для доступа к Kubernetes написаны на языке Go, многие контроллеры тоже написаны на Go. Однако при желании контроллеры, посылающие запросы в Kubernetes API Server, можно писать на любом языке. Далее, в листинге 22.1, вы увидите контроллер, написанный на языке сценариев командной оболочки.
Наиболее просто реализуются контроллеры, расширяющие возможности Kubernetes управления ресурсами. Они оперируют теми же стандартными ресурсами и выполняют те же действия, что и внутренние контроллеры Kubernetes, но невидимые пользователям кластера. Контроллеры интерпретируют определения ресурсов и выполняют некоторые действия в соответствии со сложившимися условиями. Хотя они могут отслеживать и воздействовать на любой параметр в определении ресурса, лучше всего для этой цели подходят метаданные и карты конфигураций ConfigMap. Ниже перечислены некоторые соображения, которые следует учитывать при выборе места хранения данных контроллера:
Метки, как часть метаданных ресурса, доступны для анализа любым контроллерам. Они индексируются во внутренней базе данных, благодаря чему запросы с метками выполняются очень эффективно. Метки следует использовать всегда, когда требуется реализовать функциональность селектора (например, для выявления соответствующих подов в определениях Service или Deployment). Недостаток меток в том, что в метках можно использовать только буквенно-цифровые имена и значения с некоторыми ограничениями. Описание синтаксиса меток и набор допустимых символов можно найти в документации Kubernetes.
Аннотации являются отличной альтернативой меткам. Их следует использовать вместо меток, если значения не вписываются в синтаксические ограничения меток. Аннотации не индексируются, поэтому обычно они используются для представления информации, которая не используется в качестве ключей в запросах контроллеров. Еще одно преимущество аннотаций перед метками, кроме возможности представления произвольных метаданных, — они не оказывают отрицательного влияния на внутреннюю производительность Kubernetes.
Иногда контроллерам нужна дополнительная информация, которую нельзя передать через метки и аннотации. В таких случаях для хранения определения целевого состояния можно использовать карты конфигураций ConfigMap, легко доступные контроллерам. Однако определения нестандартных ресурсов (CRD) намного лучше подходят для представления нестандартного описания целевого состояния и потому предпочтительнее, чем простые ConfigMap. Однако для регистрации CRD требуются повышенные привилегии на уровне кластера. Если у вас их нет, карты конфигураций ConfigMap останутся лучшей альтернативой CRD. Подробнее о CRD мы поговорим в главе 23 «Оператор».
Вот несколько примеров простых контроллеров, которые можно использовать для изучения способов реализации этого паттерна:
Этот контроллер (http://bit.ly/2Ushlpy) просматривает определения Service и, обнаружив в метаданных аннотацию с именем expose, автоматически создает объект Ingress для доступа к службе Service извне. Он также удаляет объект Ingress после удаления Service.
Этот контроллер (http://bit.ly/2uJ2FnI) следит за изменениями в объектах ConfigMap и обновляет связанные с ними развертывания Deployment. Его можно использовать с приложениями, которые не способны наблюдать за ConfigMap и динамически обновляться при изменении конфигурации. Это особенно верно, когда ConfigMap отображается в переменные окружения или когда приложение не может быстро и надежно обновить свою конфигурацию на лету, без перезапуска. Реализация такого контроллера в виде сценария командной оболочки будет показана в листинге 22.2.
Этот контроллер (http://bit.ly/2uFcNgX) перезагружает узел Kubernetes, обнаружив определенную аннотацию в узле.
Теперь рассмотрим конкретный пример: контроллер, состоящий из единственного сценария командной оболочки и наблюдающий за изменениями в ресурсах ConfigMap через Kubernetes API. Если снабдить ConfigMap аннотацией k8spatterns.io/podDeleteSelector, все поды, соответствующие этому значению аннотации, будут остановлены при изменении ConfigMap. Если эти поды управляются посредством высокоуровневых ресурсов, таких как Deployment или ReplicaSet, они будут перезапущены с измененной конфигурацией.
Например, наш контроллер мог бы следить за изменением следующей карты конфигурации ConfigMap и перезапускать все поды с меткой app и значением webapp. Конфигурация из ConfigMap в листинге 22.1 используется нашим веб-приложением для определения приветственного сообщения.
Листинг 22.1. Ресурс ConfigMap для веб-приложения
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector: "app=webapp"
data:
message: "Welcome to Kubernetes Patterns !"
Аннотация, которая используется контроллером из листинга 22.2 как селектор для поиска подов, требующих перезапуска.
Этот ресурс ConfigMap будет проверяться нашим контроллером, реализованным в виде сценария командной оболочки. Полный исходный код вы найдете в репозитории Git. Контроллер запускает зависающий HTTP-запрос GET, чтобы открыть бесконечный поток HTTP-ответа, через который API Server передает события жизненного цикла.
События имеют форму простых объектов JSON. Контроллер анализирует события и определяет, содержит ли изменившийся ConfigMap нашу аннотацию. Получив событие, требующее реакции, контроллер останавливает все поды, которые соответствуют селектору в значении аннотации. Давайте поближе рассмотрим работу контроллера.
Основу контроллера составляет цикл согласования, который принимает и обрабатывает события жизненного цикла ConfigMap, как показано в листинге 22.2.
Листинг 22.2. Сценарий контроллера
namespace=${WATCH_NAMESPACE:-default}
base=http://localhost:8001
ns=namespaces/$namespace
curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \
while read -r event
do
# ...
done
Пространство имен для наблюдения (или default, если не задано).
Доступ к Kubernetes API осуществляется через прокси, реализованный с использованием паттерна Ambassador (Посредник) и действующий в том же поде.
Цикл обработки событий изменения состояния ConfigMap.
Обратите внимание на параметр запроса watch = true в листинге 22.2. Этот параметр сообщает, что API Server должен оставить HTTP-соединение открытым и пересылать через него события по мере их появления (этот прием также иногда называют зависающий GET или Comet). Цикл читает и обрабатывает поступающие события по отдельности.
Переменная окружения WATCH_NAMESPACE определяет пространство имен, в котором контроллер должен следить за обновлениями ConfigMap. Эту переменную можно инициализировать в описании развертывания Deployment самого контроллера. В данном примере, чтобы извлечь переменную окружения WATCH_NAMESPACE из пространства имен, в котором развернут контроллер, используется Downward API, описанный в главе 13 «Самоанализ», как показано в листинге 22.3.
Листинг 22.3. WATCH_NAMESPACE извлекается из текущего пространства имен
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
На основе пространства имен сценарий контроллера конструирует URL конечной точки Kubernetes API для наблюдения за ConfigMap.
Как видите, наш контроллер связывается с Kubernetes API Server через локальное соединение. Этот сценарий необязательно должен развертываться непосредственно на главном узле Kubernetes API, но тогда как он сможет работать, используя локальное соединение? Как вы, наверное, догадались, здесь на сцену выходит другой паттерн. Этот сценарий разворачивается в поде вместе с контейнером-посредником, который открывает порт 8001 на локальном хосте и связывает его с настоящей службой Service в Kubernetes. Более подробно о паттерне Ambassador (Посредник) рассказывается в главе 17. Фактическое определение пода с этим посредником будет показано далее в этой главе.
Конечно, простое наблюдение за событиями — не самое надежное решение. Соединение может быть разорвано в любой момент, поэтому нужно предусмотреть возможность перезапустить цикл. Кроме того, события могут теряться, поэтому, действуя в промышленном окружении, контроллеры должны не только следить за событиями, но и время от времени запрашивать у API Server все текущее состояние и использовать его как новую основу. Но для демонстрации паттерна такого решения вполне достаточно.
В листинге 22.4 показана логика, выполняющаяся в цикле.
Листинг 22.4. Цикл согласования в контроллере
curl -N -s $base/api/v1/${ns}/configmaps?watch=true | \
while read -r event
do
type=$(echo "$event" | jq -r '.type')
config_map=$(echo "$event" | jq -r '.object.metadata.name')
annotations=$(echo "$event" | jq -r '.object.metadata.annotations')
if [ "$annotations" != "null" ]; then
selector=$(echo $annotations | \
jq -r "\
to_entries |\
.[] |\
select(.key == \"k8spatterns.io/podDeleteSelector\") |\
.value |\
@uri \
")
fi
if [ $type = "MODIFIED" ] && [ -n "$selector" ]; then
pods=$(curl -s $base/api/v1/${ns}/pods?labelSelector=$selector |\
jq -r .items[].metadata.name)
for pod in $pods; do
curl -s -X DELETE $base/api/v1/${ns}/pods/$pod
done
fi
done
Извлечь из события тип и имя ConfigMap.
Извлечь из ConfigMap все аннотации с ключом k8spatterns.io/podDeleteSelector. См. подробное описание этого выражения во врезке «Некоторые особенности jq» ниже.
Если событие уведомляет об изменении ConfigMap и в нем имеется наша аннотация, найти все поды с метками, соответствующими этому селектору.
Остановить все поды, соответствующие селектору.
Сначала сценарий извлекает тип события, определяющий произошедшее с ConfigMap. Далее извлекается ресурс ConfigMap и из него, с помощью jq, извлекаются аннотации. jq (https://stedolan.github.io/jq/) — отличный инструмент для анализа документов в формате JSON из командной строки, и данный сценарий предполагает его доступность в контейнере, где выполняется.
Если в ConfigMap есть аннотации, с помощью более сложного jq-запроса проверяется наличие среди них аннотации k8spatterns.io/podDeleteSelector. Цель этого запроса — преобразовать значение аннотации в селектор подов, который можно использовать в запросе к API на следующем шаге: аннотация k8spatterns.io/podDeleteSelector: "app = webapp" преобразуется в app%3Dwebapp — селектор подов. Это преобразование также выполняется с помощью jq, как описывается во врезке «Некоторые особенности jq» ниже.
Если сценарию удалось извлечь селектор, его можно сразу же и использовать, чтобы выбрать поды для остановки. Сначала выбираются все поды, которые соответствуют селектору, а затем останавливаются друг за другом прямыми вызовами API.
Этот контроллер в виде сценария командной оболочки, конечно, нельзя использовать в производстве (потому что, к примеру, цикл обработки событий может остановиться в любой момент), но он хорошо раскрывает основные идеи на небольшом объеме стандартного кода.
Остальная работа связана с созданием объектов ресурсов и образов контейнеров. Сам сценарий контроллера хранится в ConfigMap config-watcher-controller, и его легко можно поправить позже, если потребуется.
Некоторые особенности jq
Извлечение значения аннотации k8spatterns.io/podDeleteSelector и его преобразование в селектор подов выполняется с помощью jq. Это отличный инструмент командной строки для работы с документами JSON, но некоторые его идеи могут показаться немного необычными. Давайте рассмотрим, как работают выражения:
selector=$(echo $annotations | \
jq -r "\
to_entries |\
.[] |\
select(.key == \"k8spatterns.io/podDeleteSelector\") |\
.value |\
@uri \
")
• Переменная $annotations содержит все аннотации в форме объекта JSON, в котором имена аннотаций играют роль свойств.
• Команда to_entries преобразует объект JSON вида { "a": "b"} в массив с элементами { "key": "a", "value": "b" }. Подробности ищите в документации для jq.
• .[] выбирает элементы массива по одному.
• Из этих элементов выбираются только элементы с соответствующим ключом. После применения этого фильтра может остаться ноль или один элемент.
• Наконец, извлекается значение (.value), которое преобразуется командой @uri так, чтобы его можно было использовать как часть URI.
Это выражение преобразует структуру JSON, такую как
{
"k8spatterns.io/pattern": "Controller",
"k8spatterns.io/podDeleteSelector": "app=webapp"
}
в селектор app%3Dwebapp.
Развертывание Deployment для контроллера создает под с двумя контейнерами:
• Контейнер-посредник связывает порт 8001 на локальном хосте с Kubernetes API. Образ k8spatterns/kubeapi-proxy — это Alpine Linux с установленным локальным kubectl и запускается командой kubectl proxy с соответствующим сертификатом и токеном. Оригинальная версия kubectl-proxy была написана Марко Лукшей (Marko Lukša) и представлена в его книге «Kubernetes in Action».
• Основной контейнер, в котором выполняется сценарий, содержащийся в только что созданном ConfigMap. Для его создания используется базовый образ Alpine с установленными curl и jq.
Файлы Dockerfile с определениями образов k8spatterns/kubeapi-proxy и k8spatterns/curl-jq вы найдете в репозитории Git с примерами к книге.
Теперь, когда у нас есть образы для создания пода, осталось лишь развернуть контроллер с использованием Deployment. Основные разделы развертывания Deployment показаны в листинге 22.5 (полную версию ищите в репозитории с примерами).
Листинг 22.5. Описание развертывания контроллера
apiVersion: apps/v1
kind: Deployment
# ....
spec:
template:
# ...
spec:
serviceAccountName: config-watcher-controller
containers:
- name: kubeapi-proxy
image: k8spatterns/kubeapi-proxy
- name: config-watcher
image: k8spatterns/curl-jq
# ...
command:
- "sh"
- "/watcher/config-watcher-controller.sh"
volumeMounts:
- mountPath: "/watcher"
name: config-watcher-controller
volumes:
- name: config-watcher-controller
configMap:
name: config-watcher-controller
Учетная запись службы ServiceAccount с привилегиями, необходимыми для наблюдения за событиями и перезапуска подов.
Контейнер-посредник для доступа к Kubeserver API через локальное соединение.
Основной контейнер со всеми инструментами и смонтированным сценарием контроллера.
Команда, запускающая сценарий контроллера.
Том, отображающийся в ConfigMap со сценарием.
Монтирование тома на основе ConfigMap в главный под.
Как видите, мы монтируем config-watcher-controller-script из ConfigMap, созданный ранее, и напрямую используем его в роли команды запуска в основном контейнере. Исключительно ради простоты мы опустили все проверки работоспособности и готовности, а также объявления с лимитами ресурсов. Также нам понадобилась учетная запись ServiceAccount config-watcher-controller с привилегиями, позволяющими следить за состоянием карт конфигураций ConfigMap. Полные настройки безопасности вы найдете в репозитории с примерами.
Теперь посмотрим на этот контроллер в действии. Для этого возьмем простой веб-сервер, использующий значение переменной окружения в роли контента. Для этой цели базовый образ использует обычный nc (netcat). Файл Dockerfile с определением этого образа можно найти в нашем репозитории.
Развертывание HTTP-сервера реализовано с помощью ресурсов ConfigMap и Deployment, представленных в листинге 22.6.
Листинг 22.6. Deployment и ConfigMap с тестовым веб-приложением
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
k8spatterns.io/podDeleteSelector: "app=webapp"
data:
message: "Welcome to Kubernetes Patterns !"
---
apiVersion: apps/v1
kind: Deployment
# ...
spec:
# ...
template:
spec:
containers:
- name: app
image: k8spatterns/mini-http-server
ports:
- containerPort: 8080
env:
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: webapp-config
key: message
ConfigMap с данными для обслуживания.
Аннотация, вызывающая перезапуск пода веб-приложения.
Сообщение, которое возвращает веб-приложение в HTTP-ответах.
Описание развертывания Deployment веб-приложения.
Простой образ с веб-сервером netcat.
Переменная окружения, содержимое которой используется как тело HTTP-ответа и извлекается из ресурса ConfigMap, находящегося под наблюдением контроллера.
На этом мы завершаем пример контроллера, наблюдающего за изменениями в ConfigMap и реализованного в виде сценария командной оболочки. Это, пожалуй, самый сложный пример в книге, но он достаточно ясно показывает, что для создания простого контроллера не требуется писать много кода.
Очевидно, что для промышленного использования контроллеры должны программироваться на более мощных языках, обладающих лучшими средствами обработки ошибок и богатыми дополнительными возможностями.
Подводя итоги, можно сказать, что контроллер — это активный процесс согласования, наблюдающий за объектами, которые представляют желаемое состояние окружения. Он анализирует фактическое и желаемое состояния и посылает инструкции, пытаясь привести текущее состояние окружения к желаемому. Kubernetes реализует этот механизм во множестве своих внутренних контроллеров, и вы тоже можете использовать тот же механизм в своих собственных контроллерах. Мы увидели, что необходимо для того, чтобы создать свой контроллер, как он функционирует и расширяет возможности платформы Kubernetes.
Мы можем добавлять свои контроллеры благодаря модульной архитектуре Kubernetes, управляемой событиями. Такая архитектура естественным образом способствует созданию независимых и асинхронных контроллеров для ее расширения. Также существенным преимуществом является наличие четкой технической границы между самим фреймворком Kubernetes и любыми расширениями. Однако с асинхронной природой контроллеров связана одна проблема — их часто трудно отлаживать, потому что поток событий не всегда имеет простую структуру. Как следствие, нет простой возможности установить контрольные точки в контроллере, чтобы приостановить его и исследовать конкретную ситуацию.
В главе 23 вы познакомитесь с родственным паттерном Operator (Оператор), который основан на паттерне Controller (Контроллер) и предлагает еще более гибкий способ настройки операций.
• Пример паттерна Контроллер (http://bit.ly/2TWw6AW).
• Создание контроллеров (http://bit.ly/2HKlIWc).
• Создание своего контроллера на Python (https://red.ht/2HxC85a).
• Подробное обсуждение контроллеров Kubernetes (http://bit.ly/2ULdC3t).
• Контроллер, открывающий доступ к службам (https://github.com/jenkins-x/exposecontroller).
• Контроллер для наблюдения за ConfigMap (https://github.com/fabric8io/configmapcontroller).
• Создание собственных контроллеров (http://bit.ly/2TYgo9b).
• Создание собственных контроллеров для Kubernetes (http://bit.ly/2Cs1rS4).
• Контроллер Contour Ingress (https://github.com/heptio/contour).
• Контроллер AppController (https://github.com/Mirantis/k8s-AppController).
• Набор символов, допустимых в метках (http://bit.ly/2Q0td0M).
• Kubectl-Proxy (http://bit.ly/2FgearB).
Лукша Марко. Kubernetes в действии. М.: ДМК-Пресс. — Примеч. пер.