- PVSM.RU - https://www.pvsm.ru -
Прим. перев.: Этот материал, озаглавленный в оригинале как «What happens when… Kubernetes edition!» и написанный Jamie Hannaford из компании Rackspace, является отличной иллюстрацией работы многих механизмов Kubernetes, которые зачастую скрыты от нашего глаза, но весьма полезны для лучшего понимания устройства этой Open Source-системы, алгоритма работы и взаимосвязей её компонентов. Поскольку вся статья весьма объёмна, её перевод разбит на две части. В первой речь идёт про работу kubectl, kube-apiserver, etcd и initializers.
P.S. Некоторые оригинальные ссылки на код в master-ветках были заменены на последние к моменту перевода коммиты, чтобы актуальность номеров строк, к которым отсылает автор, сохранялась долгое время.
Представим, что я хочу задеплоить nginx в кластере Kubernetes. Я введу в терминале нечто такое:
kubectl run --image=nginx --replicas=3
… и нажму на Enter. Через несколько секунд увижу 3 пода с nginx, распределённые по всем рабочим узлам. Работает — словно по волшебству, и это здорово! Но что на самом деле происходит под капотом?
Одно из замечательных свойств Kubernetes — как эта система обслуживает развёртывание рабочих нагрузок в инфраструктуре через дружелюбные к пользователям API. Вся сложность скрыта простой абстракцией. Однако для того, чтобы полностью осознать ценность, которую приносит K8s, полезно понимать внутреннюю кухню. Эта статья проведёт вас через весь жизненный цикл запроса от клиента до kubelet, при необходимости ссылаясь на исходный код для иллюстрации происходящего.
Это живой документ. Если вы найдёте, что улучшить или переписать, изменения приветствуются! (Речь, конечно, об оригинальной англоязычной статье в GitHub [1] — прим. перев.)
Итак, начнём. Мы только что нажали на Enter в терминале. И что теперь?
В первую очередь kubectl выполнит валидацию на стороне клиента. Он убедится, что нерабочие запросы (например, создание ресурса, который не поддерживается, или использование образа с неправильно указанным названием [2]) быстро прервутся и не будут отправлены в kube-apiserver. Так улучшается производительность системы — благодаря снижению ненужной нагрузки.
После валидации kubectl начинает составлять HTTP-запрос, который будет отправлен kube-apiserver. Все попытки получить доступ к состоянию или изменить его в системе Kubernetes проходят через сервер API, который в свою очередь общается с etcd. И kubectl здесь не исключение. Чтобы составить HTTP-запрос, kubectl использует так называемые генераторы (generators [3]) — абстракцию, реализующую сериализацию.
Не совсем очевидным здесь может показаться то, что в kubectl run
допускается указание множества типов ресурсов, не только Deployments. Чтобы это работало, kubectl вычисляет [4] тип ресурса, если имя генератора не было специально указано через флаг --generator
.
Например, ресурсы, у которых есть --restart-policy=Always
, рассматриваются как Deployments, а ресурсы с --restart-policy=Never
— как поды. Также kubectl выяснит, какие другие действия необходимо предпринять — например, запись команды (для выкатов или аудита) — и является ли эта команда пробным прогоном (по наличию флага --dry-run
).
Разобравшись, что мы хотим создать Deployment, kubectl воспользуется генератором DeploymentV1Beta1
для создания runtime-объекта [5] из предоставленных параметров. Runtime object — это обобщающий термин для ресурса.
Перед тем, как продолжим, важно отметить, что Kubernetes использует версионный API, который классифицируется по группам API (API groups). Группа API предназначена для отнесения в одну категорию схожих ресурсов, чтобы с ними было проще взаимодействовать. Кроме того, она является хорошей альтернативой единому монолитному API. Группа API для Deployment называется apps
и её последняя версия — v1beta2
. Именно это вы указываете вверху определений Deployment: apiVersion: apps/v1beta2
.
(Прим. перев.: Как мы рассказывали в анонсе Kubernetes 1.8 [6], сейчас в проекте идёт работа над созданием новой группы Workload API, в которую войдут Deployments и другие API, относящиеся к «рабочим нагрузкам».)
В общем, после того, как kubectl сгенерировал runtime-объект, он начинает искать [7] соответствующую ему группу API и версию, после чего собирает [8] клиента нужной версии — в нём учитывается различная REST-семантика для ресурса. Этот этап обнаружения и называется «переговором о версии» (version negotiation), включает в себя сканирование содержимого /apis
на удалённом API для получения всех возможных групп API. Поскольку kube-apiserver выдаёт структурный документ (в формате OpenAPI) по этому пути (/apis
), клиентам легко выполнять обнаружение.
Для улучшения производительности kubectl также кэширует [9] OpenAPI-схему в директории ~/.kube/schema
. Если хотите посмотреть на обнаружение API в действии, попробуйте удалить этот каталог и запустить команду с максимальным значением флага -v
. Вы увидите все HTTP-запросы, пытающиеся найти версии API. И их много!
Финальный шаг — отправка [10] HTTP-запроса. Когда она сделана и получен успешный ответ, kubectl выведет успешное сообщение с учётом [11] предпочтительного формата вывода.
На прошлом шаге мы не упомянули аутентификацию клиента (она происходит до отправки HTTP-запроса) — рассмотрим и её.
Для успешной отправки запроса kubectl необходимо аутентифицироваться. Учётные данные пользователя почти всегда хранятся в файле kubeconfig
, хранимом на диске, однако он может находиться в разных местах. Для его поиска kubectl делает следующее:
--kubeconfig
— использует его;$KUBECONFIG
— использует её;~/.kube
и использует первый найденный файл.
После парсинга файла определяются текущий контекст, текущий кластер и аутентификационные сведения для текущего пользователя. Если пользователь задал специальные значения через флаги (такие, как --username
), приоритет отдаётся им и они переписывают значения, указанные в kubeconfig
. Когда информация получена, kubectl устанавливает конфигурацию клиента, делая её соответствующей потребностям HTTP-запроса:
tls.TLSConfig
[13] (root CA тоже входит сюда);Authorization
;Итак, запрос был отправлен, ура! Что дальше? В дело вступает kube-apiserver. Как упоминалось выше, kube-apiserver — основной интерфейс, используемый клиентами и системными компонентами для сохранения и получения состояния кластера. Для выполнения этой функции необходимо верифицировать запрашивающую сторону, убедившись, что она соответствует тому, за кого себя выдаёт. Этот процесс называется аутентификацией.
Как apiserver аутентифицирует запросы? Когда сервер впервые запускается, он проверяет все предоставленные пользователем консольные флаги [16] и собирает список подходящих аутентификаторов (authenticators). Рассмотрим пример: если был передан --client-ca-file
, будет добавлен аутентификатор x509; если указан --token-auth-file
— к списку добавится аутентификатор токенов. Каждый раз при получении запроса он прогоняется через цепочку аутентификаторов, пока один из них не сработает успешно:
Authorization
) существует в файле на диске, указанном директивой --token-auth-file
;
Если ни один из аутентификаторов не завершится с успехом, запрос не сработает [20] и вернёт агрегированную ошибку. Если аутентификация прошла успешно, заголовок Authorization
убирается из запроса и сведения о пользователе добавляются [21] в его контекст. Это даёт доступ к установленной ранее идентичности пользователя на последующих этапах (таких, как авторизация и admission controllers).
Окей, запрос отправлен, kube-apiserver успешно верифицировал, что мы являемся теми, кем представляемся. Какое облегчение! Однако это ещё не всё. Мы можем и быть теми, кем представляемся, однако есть ли у нас права на выполнение этой операции? Идентичность и право доступа — это всё-таки не одно и то же. Чтобы продолжить, kube-apiserver должен авторизовать нас.
Способ, которым kube-apiserver выполняет авторизацию, очень схож с аутентификацией: из значений флагов он собирает цепочку авторизаторов (authorizers), которые будут использоваться для каждого входящего запроса. Если все авторизаторы запрещают запрос, он завершится [22] с ответом Forbidden
и остановится на этом. Если хоть один авторизатор одобрит запрос, он пройдёт дальше.
Примеры авторизаторов, входящих в состав релиза Kubernetes v1.8:
Посмотрите на метод Authorize
у каждого из них, чтобы увидеть, как они работают.
Окей, мы аутентифицированы и авторизованы kube-apiserver. Что осталось? Сам kube-apiserver доверяет нам и разрешает продолжить, но у других частей системы в Kubernetes могут быть собственные глубокие убеждения по поводу того, что разрешено, а что — нет. Здесь вступают в дело admission controllers [27].
Если авторизация отвечает на вопрос, имеет ли пользователь право, admission controllers проверяют запрос на соответствие более широкому спектру ожиданий и правил в кластере. Они являются последним оплотом контроля перед тем, как объект передаётся в etcd, и отвечают за оставшиеся в системе проверки, задающиеся целью убедиться, что действие не приведёт к неожиданным или негативным последствиям.
Принцип, по которому работают эти контроллеры, схож с аутентификаторами и авторизаторами, но имеет одно отличие: для admission controllers достаточно единственного отказа в цепочке контроллеров, чтобы прервать эту цепочку и признать запрос неудачным.
В архитектуре admission controllers прекрасна ориентация на способствование расширяемости. Каждый контроллер хранится как плагин в директории plugin/pkg/admission
и создаётся для реализации потребностей маленького интерфейса. Каждый из них компилируется в главный бинарный файл Kubernetes.
Обычно admission controllers разбиты по категориям управления ресурсами, безопасности, установок по умолчанию и эталонной консистентности. Вот некоторые примеры контроллеров, занимающихся управлением ресурсов:
InitialResources
устанавливает лимиты по умолчанию для ресурсов контейнера, основываясь на предыдущем использовании;LimitRanger
устанавливает значения по умолчанию для запросов и лимитов контейнера, гарантирует верхние границы для определённых ресурсов (512 Мб памяти по умолчанию, но не более 2 Гб);ResourceQuota
считает количество объектов (подов, rc, балансировщиков нагрузки сервисов) и общие потребляемые ресурсы (процессор, память, диск) в пространстве имён и предотвращает их превышение.К этому моменту Kubernetes полностью одобрил входящий запрос и разрешил двигаться дальше. Следующим шагом kube-apiserver десериализует HTTP-запрос, создаёт runtime-объекты из него (нечто вроде обратного процесса тому, что делают генераторы kubectl) и сохраняет их хранилище данных. Посмотрим на это в деталях.
Откуда kube-apiserver знает, что делать, принимая наш запрос? Для этого следует довольно сложная последовательность шагов, предваряющих обработку любых запросов. Посмотрим с самого начала — когда бинарный файл впервые запускается:
К этому моменту kube-apiserver знает, какие существуют маршруты и имеет внутренний mapping, указывающий на то, какие обработчики и поставщики хранилища должны быть вызваны при соответствии запроса. Предположим, в него попал наш HTTP-запрос:
/apis
). Если зарегистрированных обработчиков для этого пути нет, вызывается обработчик not found [37], возвращающий 404.createHandler
[34]. Что он делает? В первую очередь, он декодирует HTTP-запрос и выполняет базовую валидацию, такую как проверка соответствия предоставленных JSON-данных с ожиданиями для ресурса из API нужной версии.<namespace>/<name>
, но это настраивается.get
, проверяя, что объект был действительно создан. Затем он вызывает все обработчики, назначенные на момент после создания (post-create), и декораторы, если требуется дополнительная финализация.Много шагов! Удивительно вот так следовать за apiserver, потому что понимаешь, как много работы он в действительности делает. Итак, резюмируя: ресурс Deployment теперь существует в etcd. Но мало так вот просто его туда поместить — вы всё ещё не увидите его на данном этапе…
Когда объект сохранён в хранилище данных, он не является полностью видимым apiserver и не попадает в планировщик, пока не отработает набор инициализаторов [42] (intializers). Инициализатор — это контроллер, ассоциированный с типом ресурса и исполняющий логику на ресурсе до того, как он становится доступным для внешнего мира. Если у типа ресурса нет зарегистрированных инициализаторов, этот шаг пропускается и ресурсы видны мгновенно.
Как написано [43] во многих блогах, это мощная возможность, позволяющая выполнять общие операции «начальной загрузки» (bootstrap). Примеры могут быть такие:
Объекты initializerConfiguration
позволяют определять, какие инициализаторы должны запускаться для определённых типов ресурсов. Представьте, что мы хотим запускать свой инициализатор при каждом случае создания пода. Тогда мы сделаем нечто такое:
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
name: custom-pod-initializer
initializers:
- name: podimage.example.com
rules:
- apiGroups:
- ""
apiVersions:
- v1
resources:
- pods
После создания этого конфига в поле ожидания (metadata.initializers.pending
) каждого пода будет добавлен custom-pod-initializer
. Контроллер инициализатора будет уже развёрнут и начнёт регулярно сканировать кластер на новые поды. Когда инициализатор обнаружит под со своим (т.е. инициализатора) названием в поле ожидания, он исполнит свои действия. После завершения работы он удалит своё название из списка ожидания. Только инициализаторы, названия которых являются первыми в списке, могут управлять ресурсами. Когда все инициализаторы отработали и список ожидания пуст, объект будет считать инициализированным.
Самые наблюдательные читатели могли заметить потенциальную проблему. Как может контроллер из пользовательского пространства обрабатывать ресурсы, если kube-apiserver ещё не сделал их видимыми? Для этого у kube-apiserver есть специальный параметр запроса ?includeUninitialized
, позволяющий возвращать все объекты, в том числе и неинициализированные.
Вторую часть статьи опубликуем в ближайшее время (на этой неделе). В ней рассмотрена работа контроллеров Deployments и ReplicaSets, информеров, планировщика, kubelet.
Читайте также в нашем блоге:
Автор: Андрей Сидоров
Источник [47]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-administrirovanie/268736
Ссылки в тексте:
[1] GitHub: https://github.com/jamiehannaford/what-happens-when-k8s
[2] неправильно указанным названием: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L163
[3] generators: https://kubernetes.io/docs/user-guide/kubectl-conventions/#generators
[4] вычисляет: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L231-L257
[5] runtime-объекта: https://github.com/kubernetes/kubernetes/blob/7650665059e65b4b22375d1e28da5306536a12fb/pkg/kubectl/run.go#L59
[6] анонсе Kubernetes 1.8: https://habrahabr.ru/company/flant/blog/338230/
[7] начинает искать: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L580-L597
[8] собирает: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L598
[9] кэширует: https://github.com/kubernetes/kubernetes/blob/7650665059e65b4b22375d1e28da5306536a12fb/pkg/kubectl/cmd/util/factory_client_access.go#L117
[10] отправка: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L628
[11] с учётом: https://github.com/kubernetes/kubernetes/blob/701d6536a2584fbf4decdee38c81bc443d21df74/pkg/kubectl/cmd/run.go#L403-L407
[12] предполагаемый: https://github.com/kubernetes/client-go/blob/1a014f453972bd8c8183f6f105cc9a623ccc6ba3/tools/clientcmd/loader.go#L52
[13] tls.TLSConfig
: https://github.com/kubernetes/client-go/blob/82aa063804cf055e16e8911250f888bc216e8b61/rest/transport.go#L80-L89
[14] отправляются: https://github.com/kubernetes/client-go/blob/c6f8cf2c47d21d55fa0df928291b2580544886c8/transport/round_trippers.go#L314
[15] отправляются: https://github.com/kubernetes/client-go/blob/c6f8cf2c47d21d55fa0df928291b2580544886c8/transport/round_trippers.go#L223
[16] консольные флаги: https://kubernetes.io/docs/admin/kube-apiserver/
[17] обработчик x509: https://github.com/kubernetes/apiserver/blob/51bebaffa01be9dc28195140da276c2f39a10cd4/pkg/authentication/request/x509/x509.go#L60
[18] обработчик токенов: https://github.com/kubernetes/apiserver/blob/51bebaffa01be9dc28195140da276c2f39a10cd4/pkg/authentication/request/bearertoken/bearertoken.go#L38
[19] обработчик basicauth: https://github.com/kubernetes/apiserver/blob/51bebaffa01be9dc28195140da276c2f39a10cd4/plugin/pkg/authenticator/request/basicauth/basicauth.go#L37
[20] не сработает: https://github.com/kubernetes/apiserver/blob/20bfbdf738a0643fe77ffd527b88034dcde1b8e3/pkg/authentication/request/union/union.go#L71
[21] добавляются: https://github.com/kubernetes/apiserver/blob/e30df5e70ef9127ea69d607207c894251025e55b/pkg/endpoints/filters/authentication.go#L71-L75
[22] завершится: https://github.com/kubernetes/apiserver/blob/e30df5e70ef9127ea69d607207c894251025e55b/pkg/endpoints/filters/authorization.go#L60
[23] webhook: https://github.com/kubernetes/apiserver/blob/d299c880c4e33854f8c45bdd7ab599fb54cbe575/plugin/pkg/authorizer/webhook/webhook.go#L143
[24] ABAC: https://github.com/kubernetes/kubernetes/blob/77b83e446b4e655a71c315ad3f3890dc2a220ccf/pkg/auth/authorizer/abac/abac.go#L223
[25] RBAC: https://github.com/kubernetes/kubernetes/blob/8db5ca1fbb280035b126faf0cd7f0420cec5b2b6/plugin/pkg/auth/authorizer/rbac/rbac.go#L43
[26] Node: https://github.com/kubernetes/kubernetes/blob/dd9981d038012c120525c9e6df98b3beb3ef19e1/plugin/pkg/auth/authorizer/node/node_authorizer.go#L67
[27] admission controllers: https://kubernetes.io/docs/admin/admission-controllers/#what-are-they
[28] создаёт: https://github.com/kubernetes/kubernetes/blob/d12d711ba67af9c63c6497a3d73357729a76e9ab/cmd/kube-apiserver/app/server.go#L119
[29] создаётся: https://github.com/kubernetes/kubernetes/blob/e19257f2ec87d8091defb7935bb3a161fbb229d0/cmd/kube-apiserver/app/server.go#L151
[30] наполняет: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/server/config.go#L149
[31] настраивает: https://github.com/kubernetes/kubernetes/blob/c7a1a061c3dc5acabcc0c35b3b96a6935dccf546/pkg/master/master.go#L410
[32] устанавливаются: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/groupversion.go#L92
[33] регистрируется: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/installer.go#L710
[34] обработчику создания ресурса: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/handlers/create.go#L37
[35] будет вызван: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/server/handler.go#L153
[36] обработчик: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/server/mux/pathrecorder.go#L248
[37] обработчик not found: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/server/mux/pathrecorder.go#L254
[38] Происходит: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/handlers/create.go#L93-L104
[39] сохраняется: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/handlers/create.go#L111
[40] делегирования: https://github.com/kubernetes/apiserver/blob/19667a1afc13cc13930c40a20f2c12bbdcaaa246/pkg/registry/generic/registry/store.go#L327
[41] Создаётся: https://github.com/kubernetes/apiserver/blob/7001bc4df8883d4a0ec84cd4b2117655a0009b6c/pkg/endpoints/handlers/create.go#L131-L142
[42] инициализаторов: https://kubernetes.io/docs/admin/extensible-admission-controllers/#initializers
[43] написано: https://ahmet.im/blog/initializers/
[44] Наш опыт с Kubernetes в небольших проектах: https://habrahabr.ru/company/flant/blog/331188/
[45] Как на самом деле работает планировщик Kubernetes?: https://habrahabr.ru/company/flant/blog/335552/
[46] Инфраструктура с Kubernetes как доступная услуга: https://habrahabr.ru/company/flant/blog/341760/
[47] Источник: https://habrahabr.ru/post/342658/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.