Перейти до вмісту

Модуль 1.1: Kubernetes API та архітектура розширюваності

Складність: [СЕРЕДНЯ] — розуміння механіки API

Час на проходження: 3 години

Передумови: CKA або еквівалентний досвід роботи з Kubernetes, базові навички програмування на Go та доступ до кластера Kubernetes 1.35+

Результати навчання

Розділ «Результати навчання»

Після завершення цього модуля ви зможете:

  1. Простежити шлях запиту kubectl крізь автентифікацію, авторизацію, контроль допуску, валідацію та збереження в etcd.
  2. Реалізувати програму на Go, яка створює, спостерігає та змінює ресурси Kubernetes за допомогою інформерів і черг роботи client-go.
  3. Оцінити, що краще підходить для конкретної вимоги до розширення: CRD, вебхуки допуску, ValidatingAdmissionPolicy чи агрегація API.
  4. Діагностувати збої запитів до API Server, пов’язуючи поля журналу аудиту, коди HTTP-відповідей, версії ресурсу та власність полів server-side apply.

Чому цей модуль важливий

Розділ «Чому цей модуль важливий»

Гіпотетичний сценарій: ваша платформна команда розгортає контролер, який маркує Поди для розподілу витрат, команда безпеки додає політику допуску, що блокує ризиковані образи, а команда розробників раптом не може створити Деплоймент, який учора працював. Под так і не з’являється, журнали контролера мовчать, а kubectl повертає коротку помилку, що згадує заборонені поля. Корисний інженер не починає з вгадування, який саме компонент зламався; він простежує запит крізь конвеєр API Server і визначає, на якому саме етапі було ухвалено рішення.

Kubernetes відчувається як набір компонентів, але операційно поводиться як система, побудована навколо API. Планувальник, kubelet, контролери, kubectl, вебхуки допуску та власні оператори — усі вони взаємодіють, читаючи й записуючи об’єкти API. Якщо API Server приймає об’єкт, решта площини управління реагує на цей збережений намір; якщо API Server відхиляє його, ніщо нижче за течією не може це компенсувати. Саме тому грамотність у роботі з API є основою для кожного механізму розширення, який ви вивчите в цьому напрямку.

Цей модуль вибудовує таку грамотність шарами. Спершу ви простежите запит від автентифікації клієнта крізь авторизацію, допуск, валідацію, збереження та спостереження. Потім ви взаємодіятимете з API безпосередньо, щоб форма REST була видимою, а не прихованою за kubectl. Нарешті, ви реалізуєте патерни client-go, які використовують справжні контролери: list, watch, кеш, lister і черга роботи з обмеженням швидкості. Мета полягає не в тому, щоб запам’ятати кожен внутрішній пакет; мета полягає в тому, щоб мислити ясно й упевнено тоді, коли розширення змінює поверхню API або змінює те, як саме допускаються до системи наявні об’єкти.

Аналогія з аеропортовою службою безпеки корисна, з однією важливою поправкою. API Server Kubernetes — це не просто охоронець біля дверей; це водночас стійка реєстрації, рушій політики багажу, сканер посадки та система обліку рейсів. Автентифікація запитує, хто робить запит, авторизація запитує, чи може ця особа спробувати виконати дію, допуск запитує, чи слід змінити або відхилити запитуваний об’єкт, а збереження записує прийнятий стан в etcd. Кожен етап має власні докази, власні коди збоїв і власні точки розширення, тож хороша діагностика означає чітке знання того, на яке саме питання відповідає кожен етап і де треба шукати свідчення про ухвалене рішення.

Базова концепція 1: Конвеєр обробки запитів API Server

Розділ «Базова концепція 1: Конвеєр обробки запитів API Server»

Коли ви виконуєте kubectl create deployment nginx --image=nginx:1.27, kubectl будує HTTP-запит, підписує його обліковими даними з вашого kubeconfig і надсилає його до API Server. API Server не записує Деплоймент в etcd одразу. Спершу він перетворює мережевий запит на особу Kubernetes і запитуване дієслово, перевіряє, чи дозволено цій особі виконувати це дієслово над цим ресурсом, застосовує логіку допуску, валідує отриманий об’єкт і лише тоді зберігає прийнятий об’єкт.

Порядок має значення, бо кожен етап отримує запит у різній формі. Автентифікація бачить облікові дані й видає інформацію про користувача. Авторизація бачить користувача, дієслово, групу API, ресурс, простір імен та ім’я. Мутаційний допуск бачить об’єкт до фінальної валідації й може додати усталені значення або сайдкари. Валідаційний допуск бачить остаточний кандидатний об’єкт і може прийняти чи відхилити його, але не може його переписати. Збереження записує схвалений стан і просуває версію ресурсу, яку клієнти спостереження використовують, щоб залишатися синхронізованими.

Така послідовність — це також причина, чому одна видима користувачеві помилка може ввести вас в оману, якщо ви пропустите модель конвеєра. Відхилене створення Поду може бути спричинене відсутністю RBAC, тайм-аутом вебхука, правилом валідації CEL, помилкою схеми або проблемою зберігання в etcd, і кожен збій лишає різні докази. Найшвидший шлях — запитати, які докази існують для кожного етапу: дійсність облікових даних для автентифікації, kubectl auth can-i для авторизації, повідомлення допуску для допуску, помилки схеми для валідації та журнали аудиту чи API Server для збереження.

API Server навмисно зроблений stateless щодо довговічних даних кластера. Ви можете запускати кілька реплік API Server за балансувальником навантаження, бо прийнятий стан зберігається в etcd, тоді як кожен процес API Server виконує обробку запитів, виклики допуску, конвертацію, валідацію та доставлення подій спостереження. Такий stateless-дизайн покращує доступність, але не робить кожен запит дешевим. Запити list, фан-аут спостереження, виклики вебхуків і серіалізація — усе це споживає ресурс площини управління, тож авторам розширень доводиться проєктувати себе як громадян API, а не вважати сервер безмежною кінцевою точкою бази даних.

┌─────────────────────────────────────────────────────────────────────┐
│ API Server Request Pipeline │
│ │
│ kubectl / client-go / curl │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. AUTHENTICATION (AuthN) │ │
│ │ Who are you? │ │
│ │ • X.509 client certificates │ │
│ │ • Bearer tokens (ServiceAccount, OIDC) │ │
│ │ • Authenticating proxy │ │
│ │ → Result: User info (name, groups, UID) │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2. AUTHORIZATION (AuthZ) │ │
│ │ Are you allowed to do this? │ │
│ │ • RBAC (most common) │ │
│ │ • ABAC (legacy) │ │
│ │ • Webhook (external policy engines) │ │
│ │ • Node authorizer (kubelet-specific) │ │
│ │ → Result: Allow / Deny │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3. MUTATING ADMISSION │ │
│ │ Modify the request before validation │ │
│ │ • MutatingAdmissionWebhooks (YOUR extension point) │ │
│ │ • Built-in mutating controllers │ │
│ │ → Can modify the object │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4. SCHEMA VALIDATION │ │
│ │ Is the object structurally valid? │ │
│ │ • OpenAPI schema validation │ │
│ │ • CRD structural schema checks │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 5. VALIDATING ADMISSION │ │
│ │ Final policy check with no modification allowed │ │
│ │ • ValidatingAdmissionWebhooks (YOUR extension point) │ │
│ │ • ValidatingAdmissionPolicies (CEL-based, in-tree) │ │
│ │ → Can only Accept / Reject │ │
│ └────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 6. PERSISTENCE │ │
│ │ Write to etcd │ │
│ │ • Serialize to storage format (protobuf for built-in │ │
│ │ resources; JSON for CustomResources) │ │
│ │ • Apply resource version │ │
│ │ • Notify watchers via etcd watch │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Автентифікація навмисно зроблена модульною, бо кластери інтегруються з багатьма системами ідентифікації. Локальний адміністратор може використовувати клієнтський сертифікат X.509 у kubeconfig, Под зазвичай використовує проєктований токен ServiceAccount, а людина-користувач у корпоративному кластері може автентифікуватися через OIDC. API Server запускає налаштовані автентифікатори, доки один із них не спрацює, а потім додає до запиту ім’я користувача, групи та необов’язковий UID. Якщо жоден автентифікатор не спрацював, конвеєр зупиняється ще до того, як Kubernetes узагалі оцінить RBAC.

АвтентифікаторЯк він працюєТипове застосування
Клієнтські сертифікати X.509CN сертифіката = ім’я користувача, O = групаkubeconfig для адміністраторів і потоків початкового завантаження
Bearer-токенТокен у заголовку AuthorizationServiceAccounts та автоматизація
OIDCJWT-токен від постачальника ідентифікаціїЛюдський SSO та корпоративна ідентифікація
Проксі автентифікаціїДовірені заголовки запиту, такі як X-Remote-UserШлюзи API та фронт-проксі

Авторизація — це місце, де трапляється багато практичних збоїв, бо особа дійсна, але запитуване дієслово не дозволене. RBAC оцінює Roles, ClusterRoles, RoleBindings та ClusterRoleBindings щодо запиту на кшталт «чи може цей ServiceAccount патчити Поди в просторі імен default?». Відповідь 403 Forbidden після успішної автентифікації означає, що запит не дійшов до допуску чи зберігання. Ця відмінність важлива на практиці, коли команда звинувачує вебхук допуску в збої, хоча насправді саме RBAC заблокував запит набагато раніше, ще до того, як черга дійшла до допуску.

Terminal window
# Перевірте, чи може ваша поточна особа створювати Деплойменти.
kubectl auth can-i create deployments --namespace=default
# Перевірте від імені конкретного ServiceAccount.
kubectl auth can-i create pods --as=system:serviceaccount:default:my-sa
# Перелічіть дозволи, які Kubernetes може визначити для поточної особи.
kubectl auth can-i --list

Допуск — це насичена розширеннями частина конвеєра. Мутаційний допуск може змінити вхідний об’єкт, додаючи мітки, впроваджуючи контейнери, встановлюючи усталені значення або нормалізуючи поля. Валідаційний допуск оцінює остаточний об’єкт і повертає «прийняти» чи «відхилити». Кластери Kubernetes 1.35 можуть використовувати вебхуки допуску для зовнішньої логіки та ValidatingAdmissionPolicy (стабільна з Kubernetes 1.30) для вбудованих виразів CEL, тож вибір дизайну більше не зводиться до «вебхук або нічого». Використовуйте найпростіший механізм, який може виразити політику, і резервуйте вебхуки для логіки, що потребує зовнішніх даних, складних викликів або мутації.

Мутацію слід використовувати обережно, бо вона змінює об’єкт, який бачить кожен наступний етап. Інжектор сайдкарів, що додає контейнери, може випадково спрацювати валідаційну політику, яка рахує контейнери, а вебхук з усталеними значеннями, що додає мітки, може вплинути на селектори, квоти чи поведінку контролера. Валідацію легше осягнути, бо вона не переписує запит, але вона все одно може створити операційний ризик, якщо вебхук повільний або недосяжний. У продакшені дизайн допуску включає тайм-аут, політику збою, вибір простору імен, вибір об’єкта та план розгортання, а не лише код вебхука.

Зробіть паузу й передбачте: якщо користувач має дозвіл RBAC на створення Подів, але валідаційний вебхук допуску відхиляє Поди, що використовують тег образу latest, який саме етап дає збій і які попередні етапи мали вже успішно пройти? Запит успішно автентифікувався, пройшов авторизацію, можливо, пройшов мутаційний допуск і валідацію схеми, а потім дав збій під час валідаційного допуску. Залежно від відповіді допуску, користувач зазвичай побачить відповідь «forbidden» або «invalid» із повідомленням від політики, що відхилила запит.

Карта розширень нижче — це практичний спосіб упорядкувати вибір. Деякі точки розширення живуть безпосередньо в шляху запиту, що робить їх потужними, але чутливими до затримки. Інші створюють нові типи API або реагують після збереження, що зазвичай безпечніше для бізнес-процесів, бо повільний контролер не блокує кожен запит на створення чи оновлення. Розглядайте розширення в шляху запиту як контроль допуску, а контролери після збереження — як узгодження.

Різниця між розширеннями в шляху запиту та після збереження — це різниця між брамою та робітником. Брама мусить вирішувати швидко, бо користувач, контролер або kubelet чекають на відповідь API. Робітник може повторити спробу, відступити, записати статус і продовжити пізніше, бо бажаний стан уже збережено. Коли вимога звучить як «цей об’єкт ніколи не повинен існувати», допуск є природним вибором; коли вона звучить як «зробити так, щоб зовнішній світ відповідав цьому об’єкту», узгодження зазвичай безпечніше.

Точка розширенняЕтап конвеєраМеханізмМодуль
Власний автентифікаторAuthNВебхук token reviewПоза цим напрямком
Власний авторизаторAuthZВебхук авторизаціїПоза цим напрямком
Мутаційний вебхукДопускMutatingAdmissionWebhookМодуль 1.6
Валідаційний вебхукДопускValidatingAdmissionWebhookМодуль 1.6
Валідаційна політикаДопускValidatingAdmissionPolicy з CELМодуль 1.6
Власні ресурсиПоверхня APICRD або агрегація APIМодулі 1.2 і 1.8
Власні контролериПісля збереженняПатерн контролераМодулі 1.3 і 1.4
Плагіни планувальникаПлануванняФреймворк плануванняМодуль 1.7
CNI / CSI / CRIРівень вузлаІнтерфейси плагінівПоза цим модулем

Базова концепція 2: Сира взаємодія з API та семантика спостереження

Розділ «Базова концепція 2: Сира взаємодія з API та семантика спостереження»

kubectl зручний, але він може приховувати той факт, що Kubernetes — це структурований HTTP API з кінцевими точками виявлення, URL ресурсів, узгодженням вмісту та потоковими спостереженнями. Сира взаємодія з API — це не те, що ви робите щодня в продакшені, проте це один із найшвидших способів діагностувати заплутану поведінку. Коли ви можете оглянути шлях запиту безпосередньо, ви можете відокремити форматування на боці клієнта від поведінки API і побачити, чи повертає сервер список, таблицю, подію спостереження, об’єкт статусу чи відхилення допуску.

Кожен ресурс Kubernetes належить або до основної групи API, або до іменованої групи API. Основні ресурси, такі як Поди й Сервіси, живуть під /api/v1, тоді як іменовані групи, такі як Деплойменти, використовують /apis/apps/v1. CustomResourceDefinitions живуть під групою API apiextensions.k8s.io, а екземпляри ваших власних ресурсів отримують власні виявлені шляхи ресурсів. Форма достатньо регулярна, щоб динамічні клієнти могли виявляти ресурси під час виконання й оперувати типами, які не були скомпільовані в програму.

/api/v1/namespaces/{namespace}/pods/{name} # Core API group
/apis/apps/v1/namespaces/{namespace}/deployments # Named API group
/apis/apiextensions.k8s.io/v1/customresourcedefinitions # CRD API

Найпростіший безпечний експеримент — запустити kubectl proxy, який слухає локально й пересилає запити до API Server, використовуючи ваші облікові дані kubeconfig. У прикладах нижче .venv/bin/python використовується лише для гарного форматування JSON, бо цей репозиторій стандартизовано на локальному віртуальному середовищі. Самі запити до API — це звичайні HTTP-запити до проксі. Перш ніж це запускати, який результат ви очікуєте від /apis порівняно з /api/v1 і чому власні ресурси з’являються в одному дереві виявлення, а не в іншому?

Виявлення — це більше, ніж зручність для інструментів командного рядка. Контролери, утиліти резервного копіювання, сканери політик і дашборди використовують виявлення, щоб зрозуміти, які ресурси існують і які дієслова підтримуються. Саме тому CRD відчуваються рідними після встановлення: вони беруть участь у виявленні, авторизації, спостереженні та публікації схеми OpenAPI. Якщо інструмент жорстко кодує лише вбудовані ресурси, він пропустить власні API, які вводять оператори, що є серйозною прогалиною в кластерах, де CRD представляють бази даних, сертифікати, шлюзи, політики та платформи застосунків.

Terminal window
# Запустіть kubectl proxy для обробки автентифікації.
kubectl proxy --address=127.0.0.1 --port=8080 &
# Виявіть усі іменовані групи API.
curl -s http://127.0.0.1:8080/apis | .venv/bin/python -m json.tool | head -40
# Перелічіть основні ресурси API.
curl -s http://127.0.0.1:8080/api/v1 | .venv/bin/python -m json.tool | head -30
# Перелічіть усі Поди в просторі імен default.
curl -s http://127.0.0.1:8080/api/v1/namespaces/default/pods | .venv/bin/python -m json.tool
# Отримайте конкретний Под.
curl -s http://127.0.0.1:8080/api/v1/namespaces/default/pods/my-pod | .venv/bin/python -m json.tool
# Спостерігайте за Подами як за потоковою відповіддю API.
curl -s "http://127.0.0.1:8080/api/v1/namespaces/default/pods?watch=true"

Прямий доступ до API без проксі корисний для розуміння токенів ServiceAccount і поведінки TLS, але він також демонструє, чому існують клієнтські бібліотеки. Ви маєте знайти кінцеву точку API Server, отримати токен, передати заголовок Authorization, вирішити, як перевіряти сертифікат сервера, і самостійно обробляти об’єкти відповіді. Прапорець -k нижче пропускає перевірку сертифіката лише для локального навчального кластера; продакшен-інструменти мають використовувати CA кластера з kubeconfig або внутрішньокластерну конфігурацію.

Цей прямий шлях також виявляє межу між автентифікацією та авторизацією. Дійсний bearer-токен доводить, що API Server може автентифікувати того, хто викликає, але сам по собі не надає жодних дозволів. RoleBinding у прикладі навмисно надає широкі права edit для лабораторного простору імен, щоб сирий запит на створення міг успішно пройти. У справжньому контролері ви створили б вузький ServiceAccount, прив’язали лише необхідні дієслова та ресурси й перевірили б ці дозволи через kubectl auth can-i ще до того, як діагностувати клієнтський код.

Terminal window
# Надайте усталеному ServiceAccount дозвіл керувати Подами для цієї лабораторної роботи.
kubectl create rolebinding default-edit --clusterrole=edit --serviceaccount=default:default
# Отримайте URL API Server і короткочасний токен ServiceAccount.
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
TOKEN=$(kubectl create token default)
# Прямий виклик API з пропущеною перевіркою сертифіката лише для локального dev-кластера.
curl -s -k -H "Authorization: Bearer $TOKEN" \
"$APISERVER/api/v1/namespaces/default/pods" | .venv/bin/python -m json.tool | head -20
# Створіть Под через сирий API.
curl -s -k -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$APISERVER/api/v1/namespaces/default/pods" \
-d '{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "api-test"},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:1.27"
}]
}
}'

Найважливіше поле в робочих процесах list і watch — це metadata.resourceVersion. Це не версія застосунку на кшталт v1 чи v2, і ви не повинні порівнювати її, наче це семантична версія. Це непрозоре значення, що ідентифікує точку в історії сховища Kubernetes для колекції ресурсів. Клієнт перелічує об’єкти, записує повернену версію ресурсу, а потім запускає спостереження від цієї точки, щоб отримувати лише ті зміни, які відбулися після переліку.

Патерн list-and-watch розв’язує класичну проблему синхронізації. Якщо клієнт просто перелічить усі Поди, а потім почне спостереження без точки безперервності, оновлення може статися між цими двома операціями й ніколи не дійти до клієнта. Записавши версію ресурсу з відповіді переліку й використавши її для спостереження, клієнт створює міст між початковим знімком і потоком подій. Саме цей міст дозволяє контролеру підтримувати кеш, який зрештою стає узгодженим з API Server, без повторного виконання повних переліків.

Terminal window
# Отримайте поточну версію ресурсу для колекції Подів.
RV=$(curl -s http://127.0.0.1:8080/api/v1/namespaces/default/pods \
| .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['metadata']['resourceVersion'])")
# Спостерігайте від цієї точки далі, отримуючи лише нові зміни.
curl -s "http://127.0.0.1:8080/api/v1/namespaces/default/pods?watch=true&resourceVersion=$RV"

Події спостереження надходять як JSON-об’єкти, розділені переходами рядка. Тип події каже вам, що сталося, а об’єкт містить повний стан ресурсу в той момент. Надійний спостерігач має обробляти події в порядку, пам’ятати останню версію ресурсу, яку він успішно застосував, і відновлюватися, коли сервер повідомляє, що стара версія більше недоступна. Якщо спостереження роз’єднано достатньо довго, щоб історичну ревізію було ущільнено, клієнт мусить виконати новий перелік і перебудувати свій локальний кеш.

Саме тут багато написаних вручну спостерігачів стають непомітно неправильними. Вони обробляють щасливий шлях ADDED, MODIFIED та DELETED, але не обробляють перепідключення, протерміновані версії ресурсу, закладки, переліки чи збої декодування об’єктів. Контролер із неправильним локальним кешем може бути гіршим за контролер, який очевидно не працює, бо він ухвалює рішення на основі застарілого стану. Рефлектор client-go не просто заощаджує набирання тексту; він кодує роки обробки збоїв навколо контракту спостереження Kubernetes.

{"type":"ADDED","object":{"kind":"Pod","metadata":{"name":"new-pod"},"spec":{},"status":{}}}
{"type":"MODIFIED","object":{"kind":"Pod","metadata":{"name":"new-pod"},"spec":{},"status":{}}}
{"type":"DELETED","object":{"kind":"Pod","metadata":{"name":"new-pod"},"spec":{},"status":{}}}

Зупиніться й подумайте: якщо ваш скрипт втрачає мережеве з’єднання під час спостереження за Подами, що станеться, якщо він перепідключиться без останньої обробленої resourceVersion? Скрипт може створити розрив між своїм локальним станом і реальністю кластера, бо події, що сталися під час роз’єднання, не відтворюються з правильної точки. Саме тому продакшен-контролери не пишуть цей цикл вручну необачно; вони використовують рефлектори та інформери client-go, які реалізують поведінку list-and-watch, відновлення через переліки та синхронізацію кешу.

Базова концепція 3: client-go, інформери, lister’и та черги роботи

Розділ «Базова концепція 3: client-go, інформери, lister’и та черги роботи»

Ви могли б побудувати контролер, роблячи сирі HTTP-виклики, але кожному серйозному контролеру потрібні ті самі складні частини: завантаження kubeconfig, внутрішньокластерна автентифікація, декодування об’єктів, поведінка повторних спроб, перепідключення спостереження, обмеження швидкості та локальний кеш. Офіційна бібліотека client-go упаковує ці турботи в патерни, що використовуються в усьому Kubernetes. Раннє вивчення цих патернів запобігає найпоширенішій помилці контролерів: ставленню до API Server як до бази даних, яку треба опитувати, замість потоку стану, який треба спостерігати й узгоджувати.

Слово «контролер» може зробити це складнішим, ніж є насправді. Контролер — це цикл, який спостерігає поточний стан, порівнює його з бажаним станом і вживає заходів, щоб зменшити різницю. Kubernetes робить цей цикл потужним, бо бажаний стан виражається як об’єкти API, а поточний стан спостерігається через той самий API. client-go постачає механіку, що дозволяє циклу працювати ефективно: спільний кеш для спостереження, типізовані клієнти для записів і черги для повторної спроби роботи без блокування доставлення подій.

Базовий Clientset дає вам типізовані клієнти для вбудованих ресурсів, таких як Поди, Деплойменти й Сервіси. Він чудовий, коли ваша програма знає типи ресурсів під час компіляції й хоче Go-структури з доступом до полів і перевірками під час компіляції. DynamicClient, який з’явиться далі в напрямку, обмінює цю типобезпеку на виявлення під час виконання та неструктуровані об’єкти. Для цього модуля типізованого клієнта достатньо, бо ми спостерігаємо за Подами й використовуємо офіційні структури corev1.Pod.

┌─────────────────────────────────────────────────────────────────────┐
│ client-go Architecture │
│ │
│ ┌─────────────┐ ┌───────────────────────────────────┐ │
│ │ API Server │◄───────│ Reflector │ │
│ │ │ WATCH │ • List + Watch resources │ │
│ └─────────────┘ │ • Pushes events to DeltaFIFO │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ DeltaFIFO │ │
│ │ • Queue of Added/Updated/Deleted │ │
│ │ • Deduplicates by key │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ Indexer (In-Memory Store/Cache) │ │
│ │ • Thread-safe local store │ │
│ │ • Indexed for fast lookups │ │
│ └──────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────┼───────────────────┐ │
│ │ SharedIndexInformer │ │ │
│ │ ▼ │ │
│ │ Lister ◄──── reads from cache (no API call!) │ │
│ │ │ │
│ │ EventHandlers ──► OnAdd / OnUpdate / OnDelete │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Workqueue ──► Your controller logic processes items │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
КомпонентЩо він робитьЧому це важливо
ReflectorПерелічує, потім спостерігає за типом ресурсуТримає локальний кеш синхронізованим з API
DeltaFIFOСтавить зміни в чергу з дедуплікацієюЗапобігає обробці застарілих проміжних подій
IndexerЗберігає об’єкти в пам’яті з індексамиДозволяє швидкий пошук без викликів API
InformerПоєднує Reflector, DeltaFIFO, Indexer та обробникиСтандартний спосіб спостерігати за ресурсами в контролерах
ListerЧитає з кешу IndexerЗменшує навантаження на API Server під час узгодження
WorkqueueЧерга з обмеженням швидкості для обробки ключівВідокремлює отримання подій від повільної роботи чи повторних спроб

Налаштуйте приклад модуля з клієнтськими бібліотеками Kubernetes 1.35, щоб код відповідав версії кластера, на яку націлено цей навчальний план. Номер версії client-go використовує схему модулів v0.x, тож Kubernetes 1.35 відповідає сімейству client-go v0.35.x. У справжньому проєкті закріплюйте точні патч-версії в go.mod і оновлюйте їх свідомо; для лабораторної роботи команди нижче тримають рядок залежності чітким.

Terminal window
mkdir -p ~/extending-k8s/pod-watcher
cd ~/extending-k8s/pod-watcher
go mod init github.com/example/pod-watcher
go get k8s.io/client-go@v0.35.0
go get k8s.io/apimachinery@v0.35.0
go get k8s.io/api@v0.35.0

Почніть із простої програми переліку, перш ніж додавати інформери. Перелік навмисно нудний: побудуйте конфігурацію REST з kubeconfig, створіть clientset, викличте API Подів і виведіть об’єкти. Цей нудний шлях цінний, бо він показує форму типізованого клієнта до того, як у картину входить механіка кешу. Якщо ця програма дає збій, помилка зазвичай у завантаженні kubeconfig, автентифікації, авторизації чи досяжності кластера, а не в логіці інформера.

Той самий крок побудови конфігурації змінюється, коли код виконується всередині Поду. Внутрішньокластерний контролер зазвичай використовує змонтований токен ServiceAccount і CA кластера через rest.InClusterConfig(), тоді як локальний інструмент зазвичай читає kubeconfig через clientcmd. Багато прикладів містять резервний варіант, який спершу пробує внутрішньокластерну конфігурацію, а потім kubeconfig для локальної розробки. Цей модуль тримає першу програму простою, але діагностична звичка та сама: довести, що клієнт може автентифікуватися, перелічити ресурс і отримати очікувану помилку, перш ніж додавати асинхронний потік керування.

main.go
package main
import (
"context"
"fmt"
"os"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// Build config from kubeconfig.
home, _ := os.UserHomeDir()
kubeconfig := filepath.Join(home, ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Error building kubeconfig: %v\n", err)
os.Exit(1)
}
// Create the clientset.
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating clientset: %v\n", err)
os.Exit(1)
}
// List Pods in all namespaces.
pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing pods: %v\n", err)
os.Exit(1)
}
fmt.Printf("Found %d pods across all namespaces:\n\n", len(pods.Items))
for _, pod := range pods.Items {
fmt.Printf(" %-40s %-20s %s\n", pod.Name, pod.Namespace, pod.Status.Phase)
}
}
Terminal window
go run main.go

Інформер змінює модель з «питати сервер щоразу» на «підтримувати синхронізований локальний вигляд». Він виконує початковий перелік, відкриває потік спостереження, зберігає об’єкти в indexer’і та викликає обробники подій, коли об’єкти додаються, оновлюються чи видаляються. Lister потім читає з кешу, що означає: цикл узгодження може оглядати пов’язані об’єкти, не запитуючи API Server повторно. Це одна з причин, чому Kubernetes може підтримувати багато контролерів одночасно без того, щоб кожен контролер довбав etcd крізь шар API.

Кеш — це не лазівка навколо коректності; це навмисна модель узгодженості. Контролер читає з кешу, який може трохи відставати від API Server, а потім записує зміни через API із семантикою, що враховує версію ресурсу. Це прийнятно, бо контролери базуються на рівні стану: вони продовжують узгоджувати до бажаного стану, замість того щоб покладатися на одну ідеальну подію. Якщо кеш ненадовго відстає, наступна подія спостереження, повторна синхронізація чи повторна спроба з черги повертають контролер до останнього спостережуваного стану.

informer-example/main.go
package main
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
home, _ := os.UserHomeDir()
kubeconfig := filepath.Join(home, ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Error building kubeconfig: %v\n", err)
os.Exit(1)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating clientset: %v\n", err)
os.Exit(1)
}
// Create a shared informer factory with a 30-second resync period.
factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
// Get the Pod informer.
podInformer := factory.Core().V1().Pods().Informer()
// Register event handlers.
if _, err := podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*corev1.Pod)
fmt.Printf("[ADDED] %s/%s (Phase: %s)\n",
pod.Namespace, pod.Name, pod.Status.Phase)
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldPod := oldObj.(*corev1.Pod)
newPod := newObj.(*corev1.Pod)
if oldPod.Status.Phase != newPod.Status.Phase {
fmt.Printf("[UPDATED] %s/%s Phase: %s -> %s\n",
newPod.Namespace, newPod.Name,
oldPod.Status.Phase, newPod.Status.Phase)
}
},
DeleteFunc: func(obj interface{}) {
pod := obj.(*corev1.Pod)
fmt.Printf("[DELETED] %s/%s\n", pod.Namespace, pod.Name)
},
}); err != nil {
fmt.Fprintf(os.Stderr, "Failed to register event handler: %v\n", err)
os.Exit(1)
}
// Start the informer in background goroutines.
stopCh := make(chan struct{})
factory.Start(stopCh)
// Wait for the initial cache sync.
fmt.Println("Waiting for informer cache to sync...")
if !cache.WaitForCacheSync(stopCh, podInformer.HasSynced) {
fmt.Fprintln(os.Stderr, "Failed to sync informer cache")
os.Exit(1)
}
fmt.Println("Cache synced! Watching for Pod changes...")
// Use the Lister to read from cache, not from the API Server.
lister := factory.Core().V1().Pods().Lister()
pods, err := lister.List(labels.Everything())
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing from cache: %v\n", err)
} else {
fmt.Printf("Cache contains %d Pods\n\n", len(pods))
}
// Wait for shutdown signal.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
close(stopCh)
fmt.Println("\nShutting down...")
}

Справжні контролери не повинні виконувати повільну роботу всередині обробника подій інформера. Обробник виконується на шляху, який зливає події з інформера, тож тривалі виклики бази даних, виклики хмарних API чи складне узгодження можуть затримати обробку подій і змусити контролер відстати. Черга роботи розв’язує це, дозволяючи обробнику поставити в чергу стабільний ключ об’єкта, такий як namespace/name, і одразу повернутися. Робочі горутини потім обробляють ключі з повторними спробами та обмеженням швидкості, що є набагато кращою межею збою.

Постановка в чергу ключів замість повних об’єктів — ще один невеликий вибір дизайну з великими наслідками. Об’єкт, який ви отримуєте в події, може вже бути застарілим на момент, коли робітник його обробляє, особливо під час швидких оновлень. Ключ дозволяє робітнику запитати в lister’а останній кешований об’єкт, коли узгодження справді починається. Якщо об’єкт було видалено, промах lister’а стає частиною бажаної поведінки, і ваш контролер може очистити зовнішній стан або просто записати, що нічого не залишилося робити.

import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
// Create a rate-limited workqueue.
queue := workqueue.NewTypedRateLimitingQueue(
workqueue.DefaultTypedControllerRateLimiter[string](),
)
// In event handlers, enqueue the object key.
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
return
}
queue.Add(key) // key is "namespace/name"
}
// Process items from the queue.
// Simplified for teaching — see the complete program below for bounded retries and Forget.
func processNextItem(queue workqueue.TypedRateLimitingInterface[string]) bool {
key, shutdown := queue.Get()
if shutdown {
return false
}
defer queue.Done(key)
// Your reconciliation logic here.
err := syncHandler(key)
if err != nil {
// Re-enqueue with rate limiting on failure.
queue.AddRateLimited(key)
return true
}
// Tell the queue this item was processed successfully.
queue.Forget(key)
return true
}

Створення й зміна ресурсів усе ще використовують clientset безпосередньо. Інформери оптимізовані для читань і доставлення подій; записи — це явні операції API. Оновлюючи наявний об’єкт, спершу отримайте останню версію або використайте у повноцінному контролері помічники retry-on-conflict. Kubernetes використовує оптимістичний контроль конкурентності через resourceVersion, тож сліпе оновлення старого об’єкта може перезаписати чийсь новіший намір або бути відхиленим із конфліктом.

Server-side apply часто є кращим шляхом запису для контролерів, які володіють чітко визначеним набором полів. Замість того щоб прочитати об’єкт, відредагувати повну структуру й надіслати оновлення, що може включати непов’язані поля, контролер надсилає поля, якими він має намір керувати, з ім’ям менеджера полів. API Server відстежує цю власність і повідомляє про конфлікти, коли інший менеджер володіє тим самим полем. Це робить розв’язання конфліктів явним і не дає контролерам випадково брати на себе відповідальність за поля, які вони лише скопіювали з попереднього читання.

// Create a Pod.
newPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "example-pod"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "nginx", Image: "nginx:1.27"}},
},
}
_, _ = clientset.CoreV1().Pods("default").Create(context.TODO(), newPod, metav1.CreateOptions{})
// Modify a Pod after fetching the latest stored version.
pod, _ := clientset.CoreV1().Pods("default").Get(context.TODO(), "example-pod", metav1.GetOptions{})
pod.Annotations = map[string]string{"updated": "true"}
_, _ = clientset.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{})

Зупиніться й подумайте: якщо логіка обробки вашого контролера натрапляє на тимчасову мережеву помилку під час виклику зовнішнього API, що станеться, якщо ви повернете помилку до черги роботи проти підтвердження елемента й відкидання помилки? Повернення помилки тримає бажаний стан чесним, бо елемент повторюється з відступом. Відкидання помилки може зробити так, що метрики виглядатимуть спокійно, тоді як зовнішня система залишається неправильною, а це саме той вид прихованого дрейфу, який контролер має виправляти.

Базова концепція 4: Групи API, версіонування, apply, справедливість та докази аудиту

Розділ «Базова концепція 4: Групи API, версіонування, apply, справедливість та докази аудиту»

Групи API та версії Kubernetes дозволяють проєкту розвиватися без зміни кожного ресурсу одночасно. Основна група містить ресурси, такі як Поди, ConfigMaps та Сервіси, тоді як іменовані групи містять спеціалізовані ресурси, такі як Деплойменти під apps/v1. Мітки версій повідомляють очікування щодо стабільності: альфа-API експериментальні, бета-API ближчі до завершеності, але все ще змінні, а стабільні v1-API несуть сильні очікування сумісності. Власні ресурси дотримуються того самого патерну, коли ви визначаєте обслуговувані версії в CRD.

Версіонування — це не лише документаційна мітка; це контракт API з наслідками для зберігання та конвертації. CRD може обслуговувати кілька версій, зберігаючи одну канонічну версію, а клієнти можуть продовжувати використовувати старішу обслуговувану версію під час міграції. Вбудовані API Kubernetes дотримуються подібних принципів сумісності, тому скрипти мають уникати залежності від нестабільних альфа-полів, якщо операційний ризик не є навмисним. Для авторів розширень зміна версії — це обіцянка користувачам про те, наскільки безпечно їхні маніфести й клієнти продовжуватимуть працювати.

СтадіяЗначенняСтабільність
v1alpha1Експериментальна, може бути вилучена або перепроєктованаНе використовуйте для довговічних продакшен-API
v1beta1Достатньо повна для ширшого тестуванняВикористовуйте обережно й плануйте міграції
v1Стабільна з гарантіями сумісностіБезпечний усталений вибір для продакшен-контрактів API
Terminal window
# Перегляньте всі версії API, доступні в кластері.
kubectl api-versions
# Перегляньте всі ресурси та їхні групи API.
kubectl api-resources -o wide
# Перевірте деталі API конкретного ресурсу.
kubectl explain deployment --api-version=apps/v1

Узгодження вмісту — ще одне нагадування, що kubectl get — це не сам API. Сервер може повертати JSON для загальних клієнтів, протокольні буфери для ефективних Kubernetes-нативних клієнтів і табличне представлення, що живить зручний для людини вивід kubectl get. Діагностуючи власний клієнт, перевірте заголовок Accept, перш ніж припускати, що сервер повернув неправильну форму. Можливо, ви запитуєте представлення, оптимізоване для іншого споживача.

Табличне представлення особливо корисно зрозуміти, бо воно пояснює, чому kubectl get може показувати лаконічні стовпці без завантаження кожної деталі так, як це робить дамп YAML. Власні ресурси можуть визначати додаткові стовпці для друку, і API Server може презентувати ці стовпці клієнтам, що запитують табличну форму. Це означає, що автор CRD впливає не лише на валідацію й зберігання, а й на повсякденний досвід оператора зі складання списків і перегляду ресурсів під час інцидентів.

Terminal window
# JSON — це усталене представлення.
curl -s -H "Accept: application/json" http://127.0.0.1:8080/api/v1/pods
# Протокольні буфери ефективніші для Kubernetes-нативних клієнтів.
curl -s -H "Accept: application/vnd.kubernetes.protobuf" \
http://127.0.0.1:8080/api/v1/pods -o pods.pb
# Табличний формат — це те, що kubectl використовує для виводу в стилі get.
curl -s -H "Accept: application/json;as=Table;g=meta.k8s.io;v=v1" \
http://127.0.0.1:8080/api/v1/namespaces/default/pods

Dry run і server-side apply — це функції API, а не лише зручності kubectl. Server-side dry run просить API Server виконати допуск і валідацію без збереження об’єкта, що корисно перед увімкненням суворої політики. Server-side apply просить API Server відстежувати власність полів у managedFields, що дозволяє кільком учасникам безпечно керувати різними частинами того самого об’єкта. Контролери, що використовують apply, можуть уникнути затоптування полів, якими володіють люди чи інші контролери.

Dry run — це також безпечний спосіб протестувати поведінку допуску перед розгортанням. Якщо нове валідаційне правило відхилило б наявні патерни деплойменту, server-side dry run може виявити цю проблему без створення чи оновлення об’єкта. Це достовірніше за локальну валідацію YAML, бо запит проходить через схему, усталені значення й ланцюг допуску API Server. Це все одно не доводить, що робоче навантаження працюватиме коректно після збереження, але звужує питання до етапу прийняття API.

Terminal window
# Створіть маніфест деплойменту локально.
kubectl create deployment my-app --image=nginx:1.27 --dry-run=client -o yaml > deployment.yaml
# Server-side dry run валідує через API Server без збереження.
kubectl apply -f deployment.yaml --dry-run=server
# Server-side apply записує власність полів для цього менеджера.
kubectl apply -f deployment.yaml --server-side --field-manager=my-controller
# Перегляньте власність полів.
kubectl get deployment my-app -o yaml | head -40
# Шукайте секцію managedFields.

API Priority and Fairness важлива, щойно ви пишете контролери, бо не весь трафік API має однакову операційну важливість. Контролер із неправильною поведінкою, що швидко перелічує великі ресурси, не повинен виголоджувати kubelet’и, контролери чи людей, які реагують на інциденти. APF класифікує запити за схемами потоків і рівнями пріоритету, потім ставить їх у чергу й диспетчеризує за правилами справедливості. Якщо ваш контролер бачить тротлінг, це не просто незручність; це сигнал, що ваш дизайн спостереження, кешу чи повторних спроб може бути надто дорогим.

Хороші контролери ввічливі під час збоїв. Вони використовують спостереження замість повторюваних переліків, обмежують швидкість повторних спроб, уникають необмежених пулів робітників і експонують метрики, що показують глибину черги й частоту помилок. APF захищає кластер від найгірших ефектів галасливих клієнтів, але це не ліцензія на генерування марнотратного трафіку. Якщо ваш контролер працює лише тоді, коли може робити необмежені виклики API, він не готовий до спільної площини управління.

Terminal window
# Перегляньте схеми потоків, що класифікують запити.
kubectl get flowschemas
# Перегляньте рівні пріоритету, що використовуються API Priority and Fairness.
kubectl get prioritylevelconfigurations
# Метрики API Server, коли вони експоновані, показують, чи запити ставляться в чергу чи відхиляються.

Журнали аудиту — це паперовий слід конвеєра запитів. Вони записують, хто зробив запит, які дієслово й ресурс було націлено, звідки прийшов запит, який етап було зафіксовано та який код відповіді було повернено. Політика аудиту визначає, скільки деталей фіксується, тож кластери відрізняються, але полів нижче достатньо, щоб пов’язати видимий користувачеві збій з етапом конвеєра. Відповідь 201 означає, що збереження пройшло успішно; 403 може означати, що авторизація чи допуск відхилили запит, а навколишні поля кажуть вам, який шлях імовірніший.

Докази аудиту найсильніші, коли поєднані з навмисним відтворенням. Якщо користувач повідомляє, що Деплоймент не можна створити, відтворіть це з server-side dry run, перевірте kubectl auth can-i для точної особи й простору імен, а потім огляньте події аудиту щодо URI запиту й статусу відповіді. Така послідовність запобігає поширеній пастці, коли команди читають помилку вебхука з одного запиту й застосовують її до іншої особи, простору імен чи версії API. Модель конвеєра тримає докази прив’язаними до фактичного запиту.

{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"1234-abcd","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/default/pods","verb":"create","user":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]},"sourceIPs":["192.168.1.100"],"userAgent":"kubectl/v1.35.0","objectRef":{"resource":"pods","namespace":"default","name":"nginx","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":201}}

Читайте цю подію аудиту як хронологію. verb і requestURI показують запит на створення Подів у просторі імен default. Об’єкт user ідентифікує автентифіковану особу й групи, тоді як sourceIPs та userAgent допомагають відрізнити людський трафік kubectl від трафіку контролера. stage зі значенням ResponseComplete означає, що відповідь було надіслано, а responseStatus.code каже вам остаточний результат. Якщо той самий користувач може створювати Поди в одному просторі імен, але не в іншому, ваша наступна зупинка — прив’язки RBAC або допуск на рівні простору імен.

Базова концепція 5: Створення спостерігача анотацій

Розділ «Базова концепція 5: Створення спостерігача анотацій»

Програма-вправа спостерігає за всіма Подами й повідомляє про зміни анотацій. Це може звучати дрібно, але вона використовує ту саму архітектуру, що й серйозніші контролери: типізований clientset, спільну фабрику інформерів, обробники подій, що ставлять у чергу ключі об’єктів, lister, що читає з кешу, і чергу роботи з обмеженням швидкості, що обробляє повторні спроби. Доменна логіка навмисно проста, щоб ви могли зосередитися на патерні площини управління, а не на зовнішніх системах, схемах CRD чи побічних ефектах узгодження.

Зміни анотацій — корисна навчальна ціль, бо їх легко спричинити й легко спостерігати, проте вони все ще задіюють справжню семантику оновлень. Додавання анотації, перезапис значення та видалення ключа — усе це створює оновлення API з новими версіями ресурсу. Спостерігач бачить ці оновлення крізь потік інформера, порівнює старі й нові карти анотацій і обробляє лише ті зміни, що його цікавлять. Це віддзеркалює продакшен-контролери, що ігнорують більшість подій, але ставлять у чергу підмножину, релевантну їхньому контракту узгодження.

pod-annotation-watcher/
├── go.mod
├── go.sum
└── main.go

Повна програма нижче довша за мінімальний приклад, бо вона тримає видимими важливі продакшен-звички. Вона чекає на синхронізацію кешу перед читанням, використовує контекст для завершення роботи, обробляє події оновлення, порівнюючи старі й нові карти анотацій, повторює елементи черги обмежену кількість разів і читає об’єкти з lister’а інформера замість виклику Get до API Server усередині гарячого шляху. Ці звички масштабуються від цього крихітного спостерігача до контролерів, що узгоджують хмарні балансувальники навантаження, сертифікати, бази даних чи стан політик.

Поведінка з обмеженою кількістю повторних спроб навмисно консервативна. Нескінченні повторні спроби без обмеження швидкості можуть приховати постійну ваду, споживаючи CPU та обсяг журналів, але негайне відкидання збоїв може лишити зовнішній стан зламаним назавжди. Справжній контролер зазвичай записує помилки в метрики й умови статусу, потім повторює з відступом, доки або бажаного стану не буде досягнуто, або оператор не матиме достатньо доказів для втручання. Черга роботи дає вам механічну основу для такої поведінки без блокування інформера.

main.go
package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
)
// AnnotationWatcher watches Pods for annotation changes.
type AnnotationWatcher struct {
clientset kubernetes.Interface
informer cache.SharedIndexInformer
queue workqueue.TypedRateLimitingInterface[string]
factory informers.SharedInformerFactory
}
// NewAnnotationWatcher creates a new watcher.
func NewAnnotationWatcher(clientset kubernetes.Interface) *AnnotationWatcher {
factory := informers.NewSharedInformerFactoryWithOptions(
clientset,
60*time.Second, // resync period
informers.WithNamespace(metav1.NamespaceAll),
)
podInformer := factory.Core().V1().Pods()
queue := workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{
Name: "annotation-watcher",
},
)
w := &AnnotationWatcher{
clientset: clientset,
informer: podInformer.Informer(),
queue: queue,
factory: factory,
}
if _, err := podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*corev1.Pod)
if len(pod.Annotations) > 0 {
w.enqueue(obj)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
oldPod := oldObj.(*corev1.Pod)
newPod := newObj.(*corev1.Pod)
if !annotationsEqual(oldPod.Annotations, newPod.Annotations) {
w.enqueue(newObj)
}
},
DeleteFunc: func(obj interface{}) {
w.enqueue(obj)
},
}); err != nil {
klog.Fatalf("Failed to register event handler: %v", err)
}
return w
}
func (w *AnnotationWatcher) enqueue(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
klog.Errorf("Failed to get key for object: %v", err)
return
}
w.queue.Add(key)
}
// Run starts the informer and processes the workqueue.
func (w *AnnotationWatcher) Run(ctx context.Context) error {
defer utilruntime.HandleCrash()
defer w.queue.ShutDown()
w.factory.Start(ctx.Done())
klog.Info("Waiting for informer cache to sync...")
if !cache.WaitForCacheSync(ctx.Done(), w.informer.HasSynced) {
return fmt.Errorf("failed to sync informer cache")
}
klog.Info("Cache synced successfully!")
// Report initial state from cache.
lister := w.factory.Core().V1().Pods().Lister()
pods, err := lister.List(labels.Everything())
if err != nil {
return fmt.Errorf("listing Pods from cache: %w", err)
}
annotatedCount := 0
for _, pod := range pods {
if len(pod.Annotations) > 0 {
annotatedCount++
}
}
klog.Infof("Initial state: %d total Pods, %d with annotations",
len(pods), annotatedCount)
klog.Info("Starting workers...")
go wait.Until(w.runWorker, time.Second, ctx.Done())
<-ctx.Done()
klog.Info("Context cancelled, shutting down")
return nil
}
func (w *AnnotationWatcher) runWorker() {
for w.processNextItem() {
}
}
func (w *AnnotationWatcher) processNextItem() bool {
key, shutdown := w.queue.Get()
if shutdown {
return false
}
defer w.queue.Done(key)
err := w.handleItem(key)
if err != nil {
if w.queue.NumRequeues(key) < 3 {
klog.Warningf("Error processing %s (will retry): %v", key, err)
w.queue.AddRateLimited(key)
return true
}
klog.Errorf("Giving up on %s after 3 retries: %v", key, err)
}
w.queue.Forget(key)
return true
}
func (w *AnnotationWatcher) handleItem(key string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return fmt.Errorf("invalid key %q: %w", key, err)
}
// Read from the cache, not directly from the API Server.
pod, err := w.factory.Core().V1().Pods().Lister().Pods(namespace).Get(name)
if err != nil {
klog.Infof("[DELETED] %s/%s", namespace, name)
return nil
}
// Report annotations.
fmt.Printf("\n--- Pod: %s/%s ---\n", pod.Namespace, pod.Name)
fmt.Printf(" Phase: %s | Node: %s\n", pod.Status.Phase, pod.Spec.NodeName)
if len(pod.Annotations) == 0 {
fmt.Println(" Annotations: (none)")
} else {
fmt.Printf(" Annotations (%d):\n", len(pod.Annotations))
for key, value := range pod.Annotations {
display := value
if len(display) > 80 {
display = display[:80] + "..."
}
fmt.Printf(" %s = %s\n", key, display)
}
}
return nil
}
// annotationsEqual checks if two annotation maps are the same.
func annotationsEqual(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for key, value := range a {
if b[key] != value {
return false
}
}
return true
}
func main() {
klog.InitFlags(nil)
home, _ := os.UserHomeDir()
kubeconfig := filepath.Join(home, ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
klog.Fatalf("Error building kubeconfig: %v", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
klog.Fatalf("Error creating clientset: %v", err)
}
watcher := NewAnnotationWatcher(clientset)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle graceful shutdown.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigCh
klog.Infof("Received signal %v, shutting down", sig)
cancel()
}()
fmt.Println(strings.Repeat("=", 60))
fmt.Println(" Pod Annotation Watcher")
fmt.Println(" Press Ctrl+C to exit")
fmt.Println(strings.Repeat("=", 60))
if err := watcher.Run(ctx); err != nil {
klog.Fatalf("Error running watcher: %v", err)
}
}

Зверніть увагу на форму обробки збоїв. Видалений об’єкт не є помилкою для цього спостерігача, бо відсутність Поду сама по собі є зміною стану, про яку повідомляється. Тимчасова помилка обробки, однак, повертається до черги, щоб елемент можна було повторити з обмеженням швидкості. Ця відмінність між очікуваними переходами стану та помилками, які можна повторити, — одна з ключових навичок дизайну в розробці контролерів.

Патерни та антипатерни

Розділ «Патерни та антипатерни»

Найнадійніші розширення Kubernetes ставляться до API Server як до межі координації, а не як до місця, де можна сховати довільну логіку застосунку. Вони зберігають бажаний стан декларативно, валідують вхід близько до шляху запиту й узгоджують побічні ефекти після збереження. Патерни нижче навмисно сформульовані як рішення дизайну, а не як рецепти бібліотек, бо те саме міркування застосовне незалежно від того, чи пишете ви контролер на client-go, controller-runtime, Java, Python чи іншому клієнті Kubernetes.

Корисний огляд дизайну починається із запису того, яка частина системи володіє кожним рішенням. API Server володіє автентифікацією, авторизацією, порядком допуску, валідацією схеми, зберіганням і доставленням спостереження. Ваш контролер володіє доменним узгодженням після того, як об’єкт існує. Ваша політика допуску володіє швидкими рішеннями під час запиту про те, чи об’єкт є прийнятним. Коли ці обов’язки розмиваються, розширення стає важко експлуатувати, бо збій в одній частині системи з’являється як загадковий симптом десь в іншому місці.

ПатернКоли використовуватиЧому це працюєМіркування масштабування
List-then-watch з кешем інформераБудь-який контролер, що реагує на об’єкти KubernetesБудує узгоджений локальний вигляд і уникає повторних повних переліківПерепідключення спостереження й переліки обробляє бібліотека, але повторні синхронізації все ще коштують CPU
Ставити в чергу ключі, а не об’єктиУзгодження може бути повільним, повторюваним або залежним від зовнішніх системТримає обробники подій швидкими й дозволяє робітникам отримувати поточний станГлибина черги й метрики повторних спроб стають важливими сигналами здоров’я
Server-side apply з іменованим менеджером полівКілька учасників керують різними полями того самого об’єктаAPI Server записує власність і явно виявляє конфліктиВласність полів потребує стабільних імен менеджерів і ретельної обробки конфліктів
Допуск для політики під час запитуНевалідні об’єкти мають бути заблоковані до збереженняКористувачі отримують негайний зворотний зв’язок, і поганий стан ніколи не потрапляє у сховищеВебхуки мають бути швидкими, високодоступними, а політику збою треба обирати обережно

Антипатерни зазвичай починаються як скорочення, що працюють у малому кластері. Опитування щосекунди здається простішим за інформери, пряма обробка об’єктів здається простішою за черги роботи, а широкі вебхуки допуску здаються простішими за точні політики. Ці скорочення стають збоями, коли кластер зростає, бо вони перетворюють кожен контролер на конкурентного генератора навантаження API або перетворюють кожен запит на створення на залежність від ненадійного зовнішнього сервісу.

Безпечніша звичка — проєктувати під режим збою до того, як писати щасливий шлях. Запитайте, що станеться, коли API Server тимчасово тротлить, коли вебхук не працює, коли спостереження перепідключається, коли два учасники оновлюють те саме поле і коли зовнішній API, який викликає ваш контролер, повертає помилку. Якщо відповідь — «запит користувача блокується» або «контролер тихо відкидає елемент», дизайн потребує ще однієї межі. Kubernetes дає вам допуск, статус, події, черги й керовані поля, щоб ви могли зробити ці межі явними.

АнтипатернЩо йде не такКраща альтернатива
Опитування великих списків ресурсів у цикліAPI Server та etcd витрачають роботу, надсилаючи той самий стан повторноВикористовуйте спільні інформери та lister’и
Виконання повільної роботи всередині обробників подійОбробка інформера накопичується, і події затримуютьсяСтавте в чергу ключі й обробляйте їх робітниками
Використання вебхуків допуску для побічних ефектів після створенняЗатримка запиту й доступність залежать від зовнішніх системЗбережіть намір, потім узгодьте побічні ефекти контролером
Оновлення застарілих об’єктів без обробки конфліктівКонкурентні зміни відхиляються або перезаписуютьсяОтримуйте останній стан і використовуйте retry-on-conflict або server-side apply
Трактування всіх відповідей 403 як RBACДопуск також може відхилити з відповіддю у стилі forbiddenОглядайте поля аудиту, повідомлення допуску й перевірки авторизації разом

Фреймворк прийняття рішень

Розділ «Фреймворк прийняття рішень»

Найшвидший спосіб обрати точку розширення — запитати, який вид зміни ви вносите в кластер. Якщо вам потрібен новий декларативний тип API, почніть із CRD. Якщо вам потрібно відхиляти чи задавати усталені значення наявних ресурсів під час запиту, використовуйте допуск. Якщо вам потрібна повна власна реалізація REST, власне сховище чи нестандартна семантика API, розгляньте агрегацію API лише після того, як CRD виявляться недостатніми. Якщо вам потрібно створювати реальні побічні ефекти, напишіть контролер, що спостерігає за збереженим наміром і узгоджує його.

ВимогаПеревагаЧомуУникайте
Зберігати новий декларативний стан, такий як Database чи BackupPolicyCRD плюс контролерРідні виявлення, валідація, RBAC, спостереження й статусАгрегації API, доки семантики CRD достатньо
Блокувати Поди без обов’язкових мітокValidatingAdmissionPolicy або валідаційний вебхукВідхиляє невалідний стан до збереженняОчищення лише контролером після того, як погані об’єкти вже існують
Впроваджувати сайдкари чи усталені значення в ПодиМутаційний вебхук допускуМутація має статися до валідації схеми й збереженняМутації контролером після створення, що змагаються з робочими навантаженнями
Викликати хмарний API для створення зовнішніх ресурсівКонтролер із чергою роботиЗовнішні виклики можуть повторюватися без блокування записів APIВебхука допуску, що залежить від доступності хмарного API
Надавати власне сховище чи незвичайну поведінку RESTАгрегація APIДозволяє обслуговувати API через шар агрегації KubernetesCRD, коли вам потрібна поведінка, яку CRD не можуть змоделювати

Використовуйте цей текстовий потік, коли проєктуєте розширення: спершу запитайте, чи потрібен вам новий тип ресурсу, потім запитайте, чи має запит бути зміненим або відхиленим до зберігання, потім запитайте, чи можуть побічні ефекти статися асинхронно після зберігання. Який підхід ви б обрали тут і чому: політика, що відхиляє Деплойменти без app.kubernetes.io/name, ресурс Database, що розгортає керовану базу даних, та інжектор сайдкарів для спостережуваності? Імовірні відповіді — валідаційна політика чи вебхук, CRD плюс контролер та мутаційний вебхук відповідно, бо кожна вимога живе в різній точці конвеєра.

Граничні випадки бувають, але усталені варіанти мають бути консервативними. CRD — це нормальний шлях для нового декларативного стану, бо вони дозволяють Kubernetes обробляти виявлення, RBAC, валідацію, зберігання й спостереження. ValidatingAdmissionPolicy приваблива, коли CEL може виразити правило, бо вона уникає зовнішньої мережевої залежності в шляху запиту. Вебхуки залишаються необхідними для мутації та складних перевірок, але вони заслуговують на продакшен-інженерію навколо доступності й тайм-аутів. Агрегація API потужна, але це просунутий вибір, бо ви експлуатуєте повноцінний сервер API за шаром агрегації Kubernetes.

  • API Kubernetes навмисно зроблено таким, що його можна виявляти. Клієнт може викликати кінцеві точки виявлення, щоб знайти групи, версії та ресурси під час виконання, тому загальні інструменти можуть перелічувати CRD, встановлені вже після того, як інструмент було скомпільовано.
  • Server-side apply став загальнодоступним у Kubernetes 1.22. Його власність керованих полів — одна з головних причин, чому сучасні контролери можуть співпрацювати над тим самим об’єктом, не покладаючись лише на оновлення за принципом «останній запис перемагає».
  • API Priority and Fairness стабільна з Kubernetes 1.29. Вона замінила прості ліміти max-in-flight класифікацією запитів, чергами й справедливістю, щоб важливий трафік площини управління мав менше шансів бути виголодженим галасливими клієнтами.
  • client-go використовує ту саму основу list-watch, на яку покладаються контролери Kubernetes усередині. Розуміння рефлекторів, інформерів, lister’ів і черг роботи готує вас читати вищорівневі фреймворки контролерів замість того, щоб ставитися до них як до магії.
ПомилкаЧому вона трапляєтьсяЯк її виправити
Опитування API Server у щільному цикліОпитування здається простим і працює в крихітному кластері, але воно повторно серіалізує повні переліки й обходить ефективність спостереженняВикористовуйте інформери з потоками спостереження й читайте з lister’ів, коли можливо
Обробка подій безпосередньо в обробникуУ першого прототипу немає повільної залежності, тож обробник стає зручним місцем для бізнес-логікиСтавте в чергу ключі namespace/name й обробляйте їх у робочих горутинах з обмеженням швидкості
Читання до синхронізації кешуLister існує одразу, тож легко забути, що початковий перелік може ще триватиЗавжди викликайте WaitForCacheSync перед використанням lister’ів на основі інформера
Трактування resourceVersion як семантичної версіїНазва натякає на версіонування застосунку, але значення — це непрозорий маркер сховищаЗберігайте й передавайте її лише як токен API для безперервності list-watch
Жорстке кодування конфігурації лише з kubeconfigЛокальна розробка використовує ~/.kube/config, тож програма дає збій при розгортанні як ПодВикористовуйте внутрішньокластерну конфігурацію з резервним kubeconfig у контролерах, що розгортаються
Використання викликів API в гарячих шляхах узгодженняПрямі виклики Get звичні й здаються нешкідливими під час тестуванняНадавайте перевагу читанням lister’а з кешу інформера й резервуйте прямі виклики клієнта для записів
Ігнорування конфліктів оновлення й власності полівТести з одним записувачем приховують конкурентні оновлення від людей та інших контролерівОтримуйте останній стан, використовуйте retry-on-conflict або server-side apply зі стабільним менеджером полів
Звинувачення неправильного етапу конвеєраКоротка помилка kubectl часто приховує, чи запит відхилив RBAC, допуск, валідація чи сховищеПоєднуйте kubectl auth can-i, server-side dry run, журнали аудиту й повідомлення допуску
Сценарій: Контролер отримує `403 Forbidden` під час патчингу Подів, але токен ServiceAccount дійсний. Який етап API Server слід дослідити першим і чому?

Дослідіть авторизацію першою, бо дійсна автентифікація лише доводить, що особу ServiceAccount було прийнято. 403 під час патчу часто означає, що RBAC не дозволяє дієслово patch для Подів у цільовому просторі імен, тож запит ніколи не дійшов до мутаційного допуску, валідаційного допуску чи збереження в etcd. Підтвердьте через kubectl auth can-i patch pods --as=system:serviceaccount:<namespace>:<name> --namespace=<namespace>. Якщо RBAC дозволяє запит, тоді огляньте журнали аудиту й повідомлення допуску, бо допуск також може відхилити запит після успішної авторизації.

Сценарій: Молодший інженер пропонує `kubectl get pods -o json` щосекунди для моніторингу змін фази Подів. Як ви оцінюєте такий дизайн?

Відхиліть дизайн з опитуванням для будь-чого, окрім швидкого локального експерименту, бо він повторно просить API Server перелічити й серіалізувати ту саму колекцію. Кращий патерн — інформер, який виконує початковий перелік, відкриває потік спостереження й підтримує локальний кеш, який можна читати через lister. Це зменшує навантаження на API Server та etcd, водночас оперативно доставляючи зміни. Компроміс полягає в тому, що програма має дотримуватися патернів контролера, таких як синхронізація кешу, обробники подій і відновлення спостереження, замість простого циклу.

Сценарій: `UpdateFunc` інформера виконує повільний запит до бази даних перед поверненням. Якого архітектурного компонента бракує і який режим збою це створює?

Бракує черги роботи, що відокремлює отримання подій від роботи з узгодження. Виконання повільних операцій усередині обробника подій блокує шлях інформера, що зливає й диспетчеризує події, тож контролер може відстати під час сплесків чи затримок зовнішніх залежностей. Черга роботи дозволяє обробнику швидко поставити в чергу ключ об’єкта й повернутися, тоді як робітники обробляють ключі з повторними спробами та обмеженням швидкості. Це також дає вам метрики, такі як глибина черги й кількість повторних спроб, якими набагато легше керувати, ніж прихованою затримкою обробника.

Сценарій: Сире з'єднання спостереження обривається, і ваш локальний кеш пропускає кілька оновлень Подів після перепідключення. Як `resourceVersion` змінює вашу стратегію відновлення?

Відстежуйте останню версію ресурсу з останньої події, яку ваша програма успішно застосувала, потім перепідключіть спостереження від цієї версії, щоб API Server міг надіслати пізніші зміни. Якщо збережена ревізія застара через те, що ущільнення вилучило історію, сервер може відповісти 410 Gone, і ваш клієнт мусить виконати свіжий перелік перед початком нового спостереження. Це причина, чому продакшен-контролери покладаються на рефлектори client-go, а не на самописні цикли спостереження. Стратегія відновлення така: перелічити, запам’ятати версію ресурсу колекції, спостерігати звідти й перелічити заново, коли історія спостереження більше недоступна.

Сценарій: Мутаційний вебхук впроваджує сайдкар, потім валідаційна політика відхиляє Поди з більш ніж одним контейнером. Що станеться, коли користувач створює Под з одним контейнером?

Запит відхиляється після мутації, бо валідаційний допуск оцінює остаточний кандидатний об’єкт, а не вихідний об’єкт, який надіслав користувач. Мутаційний вебхук спершу додає сайдкар, що змінює Под у польоті так, що він містить два контейнери. Валідаційна політика потім бачить об’єкт із двома контейнерами й відхиляє його, тож Под ніколи не зберігається в etcd. Виправлення — не переупорядкувати етапи; команди мають узгодити правила мутації та валідації або точніше обмежити одне з них.

Сценарій: Вам потрібен новий API `Database` з бажаною специфікацією, спостережуваним статусом, RBAC, спостереженнями та виявленням `kubectl`. Який механізм розширення слід оцінити першим?

Оцініть CRD з контролером першим, бо вимога — це новий декларативний тип ресурсу, що зберігається через API Kubernetes. CRD дають вам виявлення, валідацію схеми, інтеграцію RBAC, підтримку спостереження й субресурси статусу без написання повноцінного агрегованого сервера API. Агрегацію API варто оцінювати лише тоді, коли вам потрібне власне сховище чи поведінка REST, яку CRD не можуть представити. Вебхуки допуску все ще можуть підтримати дизайн, валідуючи чи задаючи усталені значення власного ресурсу, але вони не є основним механізмом зберігання.

Сценарій: Подія аудиту показує `ResponseComplete`, `verb=create`, `objectRef` Поду й код відповіді `201`. Що ви можете зробити висновок, а що — ні?

Ви можете зробити висновок, що запит на створення завершився успішно й об’єкт було прийнято крізь конвеєр і збережено. Автентифікований користувач, URI запиту, вихідні IP та user agent ідентифікують, хто зробив запит і на що націлився. Ви не можете зробити висновок, що Под став Ready, бо планування, завантаження образів, виконання kubelet і проби готовності відбуваються після збереження в API. Щоб діагностувати стан під час виконання, простежте збережений Под крізь події планувальника й kubelet, а не залишайтеся в межах доказів допуску й аудиту.

Сценарій вправи: ви будуєте легкий діагностичний інструмент для платформної команди, що хоче бачити зміни анотацій Подів у міру того, як вони стаються. Інструмент не повинен опитувати API Server і не повинен виконувати читання API щоразу, коли надходить подія. Він має використовувати кеш інформера для читань, ставити роботу в чергу за ключем і чисто завершуватися при перериванні. Ця вправа навмисно близька до повного прикладу вище, щоб ви могли зосередитися на запуску, спостереженні та модифікації патерну контролера.

Завдання: Побудуйте й запустіть програму на Go, що використовує інформер client-go для спостереження за Подами в усіх просторах імен і повідомлення про зміни анотацій у реальному часі.

Terminal window
# Переконайтеся, що у вас запущено локальний кластер.
kind create cluster --name extending-k8s
# Контрольна точка: перевірте, що кластер досяжний.
kubectl cluster-info
# Створіть проєкт.
mkdir -p ~/extending-k8s/pod-annotation-watcher
cd ~/extending-k8s/pod-annotation-watcher
go mod init github.com/example/pod-annotation-watcher
go get k8s.io/client-go@v0.35.0
go get k8s.io/apimachinery@v0.35.0
go get k8s.io/api@v0.35.0
go get k8s.io/klog/v2@latest

Поступові завдання

Розділ «Поступові завдання»
  1. Скопіюйте повний спостерігач анотацій із Базової концепції 5 у main.go, виконайте gofmt -w main.go і огляньте імпорти, щоб ви могли пояснити, чому потрібні кожен із labels, cache та workqueue.

  2. Зберіть і запустіть спостерігач в одному терміналі. Тримайте його запущеним, щоб ви могли спостерігати початкову синхронізацію кешу й подальшу обробку подій.

Terminal window
go build -o pod-watcher .
./pod-watcher
  1. В іншому терміналі створіть Под, дочекайтеся, доки він стане Ready, додайте анотації, змініть анотацію, видаліть анотацію, а потім видаліть Под. Спостерігайте за першим терміналом і пов’яжіть кожен виведений рядок з подією додавання, оновлення чи видалення.
Terminal window
# Створіть Под.
kubectl run test-pod --image=nginx:1.27
# Контрольна точка: дочекайтеся, доки Под стане Ready.
kubectl wait --for=condition=Ready pod/test-pod --timeout=120s
# Додайте анотації.
kubectl annotate pod test-pod team=backend priority=high
# Змініть анотацію.
kubectl annotate pod test-pod priority=critical --overwrite
# Видаліть анотацію.
kubectl annotate pod test-pod team-
# Видаліть Под.
kubectl delete pod test-pod
  1. Перевірте, що початковий звіт кешу показує Поди кластера перед тим, як ви створите test-pod. Поясніть, чому це читання має надходити з lister’а інформера, а не з прямого виклику Clientset.Get чи Clientset.List усередині шляху подій.

  2. Змініть період повторної синхронізації з 60*time.Second на коротший інтервал для локального експерименту, перезапустіть спостерігач і подивіться, чи незмінні Поди обробляються повторно. Потім відновіть початкове значення, щоб лабораторна робота не залишила по собі галасливий контролер.

  3. Натисніть Ctrl+C і підтвердьте, що шлях скасування контексту зупиняє інформер і зливає процес без паніки. Чисте завершення роботи є частиною коректності контролера, бо Kubernetes надсилає сигнали припинення під час розгортань і обслуговування вузлів.

  4. Приберіть локальний кластер, коли закінчите.

Terminal window
kind delete cluster --name extending-k8s
Нотатки до розв'язку

Спостерігач має вивести повідомлення про синхронізацію кешу перед тим, як повідомляти про зміни анотацій. Події додавання без анотацій можуть не ставитися в чергу, бо приклад ставить у чергу додані Поди лише тоді, коли анотації вже існують, тоді як події оновлення мають ставитися в чергу, коли карти анотацій відрізняються. Події видалення повідомляються, коли lister більше не може знайти кешований об’єкт. Якщо програма дає збій до синхронізації кешу, перевірте kubeconfig, досяжність кластера й RBAC, перш ніж змінювати логіку інформера.

  • Програма компілюється й запускається без помилок.
  • Початкова синхронізація кешу завершується й повідомляє кількість Подів.
  • Створення нового Поду спостерігається крізь потік подій інформера.
  • Додавання, зміни й видалення анотацій запускають обробку оновлення.
  • Видалення Поду виявляється й повідомляється без паніки.
  • Ctrl+C запускає чисте завершення роботи через скасування контексту.
  • Читання lister’а надходять з кешу інформера, а не з прямих викликів API в гарячому шляху.

Модуль 1.2: Поглиблене вивчення Custom Resource Definitions — Визначте власні типи ресурсів Kubernetes із розширеною валідацією, версіонуванням та субресурсами.