Створення Kubernetes-оператора | Практичний посібник

pic

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

Незважаючи на свої вражаючі можливості, Kubernetes по суті є технологією оркестрації контейнерів. Хоча він значно спрощує розгортання і масштабування, він не вирішує всі проблеми, з якими стикаються розробники програмного забезпечення та DevOps-інженери. Щоб вирішити це, Kubernetes надає можливості для розширення і налаштування, щоб відповідати потребам вашої команди. Він надає бібліотеки клієнтів на багатьох мовах програмування.
Але ще краще — це Kubernetes Operators.

Формальне визначення Kubernetes оператора може бути таким:

Kubernetes Operator — це метод автоматизації управління складними додатками в Kubernetes. Він розширює можливості Kubernetes, використовуючи кастомні ресурси та контролери для керування життєвим циклом додатка, автоматизуючи такі завдання, як розгортання, масштабування та оновлення. Оператори кодують операційні знання про додаток в Kubernetes, полегшуючи управління станом або складними навантаженнями з меншим залученням людини.

У цій статті ми пройдемо покроковий посібник з створення власного кастомного Kubernetes оператора.
Ми охопимо різні теми, такі як Custom Resource Definitions, Controllers та ознайомимося з Kubernetes Controller Runtime.

Передумови

Є кілька речей, які ми повинні знати і мати перед тим, як продовжити з цим посібником:

  • Добре розуміння Kubernetes та як його використовувати.
  • Знання програмування на мові Go (Go Programming Language).
  • Доступ до Kubernetes кластеру (можна спробувати локально з Minikube або Kind).

Щоб налаштувати своє середовище, вам спочатку потрібно встановити Go. Kubernetes Golang Client зазвичай вимагає конкретну версію Go, тому залежно від того, коли ви читаєте цю статтю, це може змінитися, але наразі я використовую go1.21.6.
Щоб дізнатися, яку версію ви використовуєте, запустіть

go version  
# Приклад виводу: go version go1.21.6 linux/amd64

Далі вам потрібно мати доступ до Kubernetes кластеру, але для розробки я б рекомендував використовувати локальний кластер з інструментів, таких як Minikube або Kind. Ви можете відвідати їхні сайти для отримання кроків інсталяції.

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

Концепції

Custom Resource Definitions (CRDS)

Щоб зрозуміти CRD, спершу потрібно зрозуміти, що таке ресурс. Pods, Deployments, Services тощо — це все ресурси.
Офіційно

Ресурс — це кінцева точка в Kubernetes API яка зберігає колекцію API об'єктів певного типу; наприклад, вбудований ресурс pods містить колекцію об'єктів Pod.

Ресурси вбудовані в Kubernetes API. Але в нашому випадку, як ми вже згадували, одна з основних причин створення операторів — це вирішення нестандартних проблем, які Kubernetes не вирішує "з коробки". В деяких випадках нам може знадобитися визначити власні об'єкти ресурсів. Наприклад.
Уявімо, що ми створюємо оператор, який управляє базами даних PostgreSQL, і хочемо надати API для визначення конфігурацій кожної бази даних, яку ми ініціюємо. Це можна зробити, визначивши Custom Resource Definition (CRD) PGDatabase як об'єкт, що зберігає конфігурацію бази даних.

Приклад визначення Custom Resource для демонстрації

apiVersion: example.com/v1 # Кожен ресурс повинен мати версію API   
kind: PGDatabase # Назва CRD  
metadata:  
 name: mydb  
spec:  
 config:  
 port: 5432  
 user: root  
 dbname: mydb  
 volumes:  
 - pgvolume: /pgdata/  
 envFrom: wordpress-secrets

Примітка: CRD — це лише визначення фактичних об'єктів або вони представляють фактичний об'єкт у кластері Kubernetes, але не є самим об'єктом.
Наприклад, коли ви пишете файл yaml, що визначає, як має бути заплановано pod, цей файл yaml лише визначає, але сам pod не є фактичним об'єктом.

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

Контролери

Далі давайте розглянемо Контролери (Controllers) Kubernetes.
Як ми бачили вище, CRD представляють об'єкти або стан об'єктів у кластері Kubernetes, але нам потрібно щось, що буде порівнювати цей стан з фактичними ресурсами в кластері, і це саме те, для чого використовуються Контролери (Controllers).

Контролер Kubernetes — це програмний компонент у Kubernetes, який безперервно моніторить стан ресурсів кластера та вживає заходів для приведення фактичного стану кластера у відповідність до бажаного стану, вказаного в конфігураціях ресурсів (файли YAML).

pic

Контролери відповідають за управління життєвим циклом об'єктів Kubernetes, таких як Pods, Deployments і Services. Кожен контролер зазвичай обробляє один або кілька типів ресурсів і виконує завдання, як-от створення, оновлення або видалення ресурсів на основі заявлених специфікацій.
Ось розбір того, як працює контролер Kubernetes:

1.
Спостереження (Observe): Контролер спостерігає за сервером API на предмет змін у ресурсах, за які він відповідає. Він моніторить ці ресурси, періодично запитуючи Kubernetes API або через потік подій (event stream).

  1. Аналіз (Analyze): Контролер порівнює фактичний стан (поточні умови ресурсів) з бажаним станом (вказаним у конфігураціях).

  2. Дії (Act): Якщо між фактичним та бажаним станом є невідповідність, контролер виконує операції, щоб привести фактичний стан у відповідність до бажаного. Наприклад, контролер Deployment може створити або видалити Pods, щоб відповідати вказаній кількості реплік.

4.
Цикл (Loop): Контролер працює в циклі, постійно моніторячи та реагуючи на зміни, щоб забезпечити, щоб ресурси системи завжди знаходились у бажаному стані.

Controller Runtime

Kubernetes надає набір інструментів для створення рідних контролерів, і вони відомі як Controller Runtime.

Давайте глибше розглянемо Controller Runtime, оскільки саме його ми будемо використовувати для створення операторів.

Детальний погляд на Controller Runtime

Controller Runtime — це набір бібліотек та інструментів у екосистемі Kubernetes, створений для спрощення процесу побудови та управління контролерами та операторами Kubernetes.
Це частина Kubernetes Operator SDK і широко використовується для розробки кастомних контролерів, які можуть керувати ресурсами Kubernetes, включаючи кастомні ресурси (CRD).

Controller Runtime надає структуровану основу для логіки контролерів, обробляючи багато деталей низького рівня при роботі з Kubernetes API, щоб розробники могли більше зосередитися на визначенні поведінки контролера, а не на шаблонному коді.
Він написаний на Go і базується на бібліотеках Kubernetes client-go.

Щоб використовувати його, ви можете додати його до вашого проєкту Golang, імпортувавши його ось так:

package controller  

// Пакети Controller runtime  
import (  
 "sigs.k8s.io/controller-runtime"  
 "sigs.k8s.io/controller-runtime/pkg/client"  
)

Примітка: Controller runtime — це не єдиний спосіб створення Kubernetes Operator. Існує кілька способів, таких як використання Operator Framework SDK або Kubebuilder, які є фреймворками, побудованими на основі Controller runtime, і використовують його "під капотом" для допомоги при створенні складних Operator. Ви навіть можете створити застосунок, який використовує Kubernetes Rest API через клієнтські бібліотеки на різних мовах, таких як Python, Java, JavaScript тощо, залежно від вашого технологічного стека.
Знайдіть повний список клієнтських бібліотек на документації Kubernetes.

В цій статті ми будемо використовувати Controller runtime, оскільки він пропонує гнучкість і дає практичне розуміння того, як працюють Controllers (контролери) в Kubernetes. Цей підхід є ідеальним для більш глибокого розуміння внутрішньої роботи Kubernetes Operators (операторів), зберігаючи при цьому можливість розширювати або налаштовувати їх за потребою._

Основні компоненти Controller Runtime

Controller Runtime має кілька ключових компонентів, які спрощують процес створення та запуску контролерів Kubernetes.
Разом ці компоненти створюють потужну основу для створення контролерів Kubernetes.

Менеджер ініціює та керує іншими компонентами; Контролер визначає логіку погодження (reconciliation); Клієнт спрощує взаємодію з API; Кеш оптимізує доступ до ресурсів; Джерела подій та спостереження дозволяють використовувати подієво-орієнтовану поведінку; а Цикл погодження (Reconcile Loop) забезпечує постійне узгодження з бажаним станом. Ці компоненти спрощують створення контролерів та операторів, які ефективно управляють ресурсами Kubernetes, дозволяючи налаштувати автоматизацію та оркестрацію на великому масштабі.

1. Менеджер

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

Він надає спільні залежності (наприклад, клієнт Kubernetes та кеш), які можна використовувати в різних контролерах, що дозволяє об'єднати кілька контролерів в одному процесі.

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

2. Контролер

Контролер є основним компонентом, що визначає логіку погодження (reconciliation), відповідальну за коригування стану ресурсів Kubernetes. Це контрольний цикл, який моніторить стан кластера і вносить зміни для наближення його до бажаного стану.

pic

Кожен контролер спостерігає за певними ресурсами, чи то вбудованими (наприклад, Pods, Deployments), чи за користувацькими ресурсами (CRDs).
Він включає функцію Reconcile, яка спрацьовує щоразу, коли ресурс змінюється, дозволяючи контролеру привести поточний стан до бажаного.

Розробники вказують, які ресурси контролер має спостерігати, і Controller Runtime автоматично відстежує і реагує на події (такі як створення, оновлення, видалення) для цих ресурсів. У контролері для керування користувацькими ресурсами Foo функція Reconcile може створювати або видаляти пов'язані ресурси на основі специфікацій Foo.

2. Клієнт

Під час розробки оператора Kubernetes, вам потрібен інтерфейс для взаємодії з Kubernetes кластером і виконання операцій. Так само як і kubectl, командний клієнт, який ми використовуємо, Controller Runtime надає client у своїх інструментах SDK.
Цей клієнт також використовується для програмного взаємодії з Kubernetes API у вашому коді.

Клієнт є абстракцією, що спрощує взаємодію з Kubernetes API, дозволяючи виконувати операції CRUD з ресурсами.

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

Client Controller Runtime розширює базову бібліотеку Kubernetes client-go, спрощуючи виклики API, обробляючи такі деталі, як повторні спроби та кешування, за кулісами.

Використовуючи клієнт, розробники можуть створити Pod безпосередньо з логіки контролера одним рядком коду, client.Create(ctx, pod), без необхідності турбуватися про сирі API запити.

3.

Джерела подій (Event Sources) та Спостереження (Watches)

Джерела подій (Event Sources) та Спостереження (Watches) визначають ресурси, які контролер буде моніторити на наявність змін, надаючи можливість реагувати на конкретні події в кластері.

Джерела подій визначають, що спричиняє запуск циклу примирення контролера, що може базуватися на змінах певних ресурсів Kubernetes.
Спостереження (Watches) моніторять ці ресурси, дозволяючи контролеру діяти на події створення, оновлення або видалення, коли це необхідно.

Розробники можуть визначати кілька Спостережень (Watches) для одного контролера, що корисно, якщо поведінка контролера залежить від кількох ресурсів.

Контролер, який керує кастомним ресурсом App, може спостерігати за Pod'ами, Services і ConfigMaps, реагуючи на зміни в будь-якому з цих ресурсів шляхом коригування App відповідно.

4. Цикл примирення (Reconcile Loop)

Цикл примирення (Reconcile loop) є серцем контролера, реалізуючи основну логіку, що визначає кроки для приведення ресурсів до бажаного стану.
Функція Reconcile (Примирення) кожного контролера перевіряє фактичний стан ресурсу, а потім застосовує необхідні зміни для того, щоб привести його у відповідність до бажаного стану.
Цей цикл триває безкінечно, причому кожне примирення діє як механізм самовідновлення, виправляючи будь-які відхилення від специфікації.
Цикл примирення зазвичай є ідемпотентним, що означає, що його можна повторювати без небажаних побічних ефектів, гарантуючи послідовність навіть при частих оновленнях.
У функції примирення контролер може виявити, що в Deployment відсутня вказана кількість реплік, тому він оновлює конфігурацію Deployment, щоб вона відповідала бажаній кількості реплік.

Практичний приклад

Тепер, коли ви зрозуміли кілька необхідних концепцій, давайте застосуємо практичний підхід, створивши простий Kubernetes оператор і розгорнувши його в нашому Kubernetes кластері.
Це допоможе вам застосувати вищеописані концепції та краще зрозуміти процес.

У цьому розділі ми створимо простий Kubernetes оператор, який допомагає розгортати додатки за допомогою користувацької дефініції ресурсу (CRD). Цей оператор автоматизує створення Deployments і Services на основі конфігурації додатка, наданої в користувацькому ресурсі. Наприкінці ви отримаєте працюючий оператор, розгорнутий на Kubernetes кластері, і зможете зрозуміти його основні компоненти.

Увесь код буде доступний на GitHub за посиланням https://github.com/jim-junior/crane-operator

Я подбаю про те, щоб код був детально прокоментований, щоб вам було легко його читати та розуміти.

Вимоги до оператора

Наша мета — мінімізувати складність визначення ресурсів Kubernetes для розгортання додатка. Замість того, щоб писати кілька YAML файлів для Deployments, Services тощо, ми визначимо один CRD Application, який буде інкапсулювати всі необхідні конфігурації.
Оператор автоматично створюватиме необхідні ресурси Kubernetes.

Вимоги до прикладу:

  • Оператор повинен визначати CRD Application, який включатиме всю конфігурацію додатка.
  • З CRD має бути створено відповідні ресурси Kubernetes, такі як Deployments, Services.
  • Необхідно реалізувати контролер для узгодження стану ресурсів Application.
  • Використовувати контролер для створення та керування Deployments і Services.

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

Налаштування проєкту

Як було згадано раніше, ми будемо використовувати мову програмування Go для цього туторіалу. Чому Go? Перш за все, тому що Controller runtime, а також сам Kubernetes, побудовані з використанням Go. Крім того, Go спеціально розроблено з урахуванням функцій, які роблять його ідеальним для створення хмарних додатків.
Його архітектура забезпечує ефективне масштабування вашого оператора в хмарних середовищах. Ми використовуємо go1.21.6.

Тож давайте почнемо з ініціалізації нашого Go проєкту:

mkdir app-operator && cd app-operator  

go mod init https://github.com/jim-junior/crane-operator

Далі ми встановимо залежності Go, які будемо використовувати в нашому проєкті. Ви можете встановити їх, виконавши наступні команди в командному рядку.

go get sigs.k8s.io/controller-runtime  
go get k8s.io/apimachinery  
go get k8s.io/client-go

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

app-operator/  
├── api/ # Містить визначення CRD  
├── cmd/controller/   
 ├── reconciler.go # Містить логіку узгодження  
├── main.go # Точка входу для оператора  
├── config/ # Конфігураційні файли для тестування та розгортання

Визначення Custom Resource Definition (CRD)

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

  • Порт для експонування вашого додатку
  • Том, якщо ви хочете зберігати дані
  • Контейнерне зображення для розповсюдження та розгортання додатку
  • Змінні середовища

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

При визначенні Custom Resource Definition (CRD) його потрібно вказати у двох форматах. Перший — це специфікація OpenAPI у форматі yaml або json (рекомендується yaml, оскільки він більш зручний для людини), яку можна застосувати за допомогою kubectl apply, щоб встановити CRD на кластер Kubernetes.
Другий формат — це специфікація на мові Go, яка використовується у вашому коді для визначення та взаємодії з CRD програмно.

Хоча інструменти, як Operator SDK та Kubebuilder, можуть автоматично генерувати один або обидва ці формати для вас, важливо для розробника, який створює Kubernetes Operator, розуміти конфігурації, які генеруються. Це знання є безцінним для налагодження або вирішення нестандартних ситуацій, які можуть виникнути.

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

apiVersion: operator.com/v1  
kind: Application  
metadata:  
 name: mysql  
spec:  
 image: mysql:9.0  
 ports:  
 - internal: 3306  
 external: 3306  
 volumes:  
 - volume-name: /data/  
 envFrom: mysql-secrets

Перед тим як визначити Open API специфікацію, давайте подивимось, що означає кожен компонент у нашій CRD вище.

apiVersion: operator.com/v1

Це визначає версію вашого ресурсу. Кожен ресурс у Kubernetes ПОВИНЕН мати версію API. Навіть вбудовані ресурси Kubernetes мають версію. Я рекомендую версіювати ваші ресурси, дотримуючись конвенцій версіювання API в Kubernetes.

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

  • Alpha (наприклад, v1alpha1): Експериментальний, нестабільний.
  • Beta (наприклад, v1beta1): Стабільніший за alpha, але все ще в процесі активної розробки.
  • Stable (наприклад, v1): Повністю стабільний та сумісний із попередніми версіями.

API Kubernetes використовують таку конвенцію / (наприклад, apps/v1), де:

  • Group: Група API (наприклад, apps, batch, core).
  • Version: Рівень зрілості API (v1alpha1, v1beta1, v1).

Custom Resource Definitions (CRD) слідують тим самим принципам версіювання, що й основні API Kubernetes.

Перейдемо до наступного.

kind: Application

У Kubernetes, поле kind у маніфесті ресурсу вказує на тип ресурсу, який визначається або маніпулюється. Це важливий ідентифікатор, який говорить Kubernetes, який об'єкт ресурсу представляє YAML або JSON файл, що дає змогу API серверу обробляти його відповідно.
Дізнайтесь більше тут

Примітка: Значення чутливе до регістру і зазвичай пишеться у PascalCase (наприклад, ConfigMap, Deployment).

Далі йде

spec:  
 .....

Тут ви визначаєте властивості вашого CRD.

Open API Специфікація

Тепер ми можемо визначити Open API специфікацію для CRD. Вам потрібно просто перетворити наведений вище yaml на Open API специфікацію. Більше дізнатись про Open API специфікацію можна у її документації. Це досить просто.
Для наведеного вище CRD це виглядатиме ось так.

apiVersion: apiextensions.k8s.io/v1  
kind: CustomResourceDefinition  
metadata:  
 name: applications.operator.com  
spec:  
 group: operator.com  
 names:  
 kind: Application  
 plural: applications  
 singular: application  
 scope: Namespaced  
 versions:  
 - name: v1  
 served: true  
 storage: true  
 schema:  
 openAPIV3Schema:  
 type: object  
 properties:  
 apiVersion:  
 type: string  
 kind:  
 description: 'Можна додати опис'  
 type: string  
 metadata:  
 type: object  
 spec:  
 type: object  
 properties:  
 # ім'я образу  
 image:  
 type: string  
 # томи  
 volumes:  
 type: array  
 items:  
 type: object  
 properties:  
 volume-name:  
 type: string  
 path:  
 type: string  
 # налаштування портів  
 ports:  
 type: array  
 items:  
 type: object  
 properties:  
 name:  
 type: string  
 internal:  
 type: integer  
 format: int64  
 external:  
 type: integer  
 format: int64  
 # змінні середовища  
 envFrom:  
 type: string

Зверніть увагу, що навіть специфікація є ресурсом у Kubernetes типу CustomResourceDefinition

Тепер ви можете зберегти цей код у файл, розташований за адресою app-operator/config/crd.yml.

Go Lang визначення CRD

Тепер ми можемо визначити наш CRD у Go коді.
При визначенні CRD у Go, нам потрібно виконати наступне:

  • Визначити специфікацію CRD
  • Визначити функцію deepCopy, яка визначає, як Kubernetes копіює об'єкт CRD в інший об'єкт
  • Налаштувати код для реєстрації CRD

Ми використовуємо Go структури (structs) для визначення CRD. Ймовірно, це пов'язано з тим, як легко можна визначати дані, схожі на JSON, за допомогою структур.
Створіть файл у app-operator/api/v1/application.go та збережіть у ньому наступний код.

package v1  
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"  
// Це визначає екземпляр кількох ресурсів Application  
type ApplicationList struct {  
 metav1.TypeMeta `json:",inline"`  
 metav1.ListMeta `json:"metadata,omitempty"`  
 Items []Application `json:"items"`  
}  

// Це визначає наш CRD  
type Application struct {  
 metav1.TypeMeta `json:",inline"`  
 metav1.ObjectMeta `json:"metadata,omitempty"`  
 Spec ApplicationSpec `json:"spec"`  
}  

type ApplicationSpec struct {  
 Image string `json:"image"`  
 Volumes []ApplicationVolume `json:"volumes"`  
 Ports []ApplicationPortMap `json:"ports"`  
 EnvFrom string `json:"envFrom"`  
}  

type ApplicationVolume struct {  
 VolumeName string `json:"volume-name"`  
 Path string `json:"path"`  
}  

type ApplicationPortMap struct {  
 Name string `json:"name"`  
 Internal int `json:"internal"`  
 External int `json:"external"`  
}

Далі напишемо код, який визначає функції Deep Copy.
У вашому проекті створіть файл у app-operator/api/v1/deepcopy.go і додайте наступний код:

package v1  

import "k8s.io/apimachinery/pkg/runtime"  
// DeepCopyInto копіює всі властивості цього об'єкта в інший об'єкт того ж типу, 
// який наданий як вказівник.

функція (in *Application) DeepCopyInto(out *Application) {  
 out.TypeMeta = in.TypeMeta  
 out.ObjectMeta = in.ObjectMeta  
 out.Spec = ApplicationSpec{  
 Volumes: in.Spec.Volumes,  
 Ports: in.Spec.Ports,  
 EnvFrom: in.Spec.EnvFrom,  
 Image: in.Spec.Image,  
 }  
}  

// DeepCopyObject повертає загальний тип копії об'єкта  
функція (in *Application) DeepCopyObject() runtime.Object {  
 out := Application{}  
 in.DeepCopyInto(&out)  
 return &out  
}  

// DeepCopyObject повертає загальний тип копії об'єкта  
функція (in *ApplicationList) DeepCopyObject() runtime.Object {  
 out := ApplicationList{}  
 out.TypeMeta = in.TypeMeta  
 out.ListMeta = in.ListMeta  
 if in.Items != nil {  
 out.Items = make([]Application, len(in.Items))  
 для i := range in.Items {  
 in.Items[i].DeepCopyInto(&out.Items[i])  
 }  
 }  
 return &out  
}  

Останнім кроком давайте напишемо код, який визначає, як наш CRD реєструється. У вашому проекті створіть файл у app-operator/api/v1/register.go.
І додайте наступний код

package v1  

import (  
 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"  
 "k8s.io/apimachinery/pkg/runtime"  
 "k8s.io/apimachinery/pkg/runtime/schema"  
)  
// Визначаємо ім'я групи API для користувацького ресурсу  
const GroupName = "operator.com"  
// Визначаємо версію групи API для користувацького ресурсу  
const GroupVersion = "v1"  
// Створюємо об'єкт GroupVersion, який поєднує групу та версію  
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion}  

// SchemeBuilder - це runtime.SchemeBuilder, що використовується для додавання типів до схеми  
var (  
 SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // Ініціалізація SchemeBuilder з функцією addKnownTypes  
 AddToScheme = SchemeBuilder.AddToScheme // Швидкий доступ для додавання типів до схеми  
)  

// addKnownTypes реєструє типи користувацьких ресурсів в runtime.Scheme  
func addKnownTypes(scheme *runtime.Scheme) error {  
 // Реєструємо користувацькі ресурси Application та ApplicationList в схемі  
 scheme.AddKnownTypes(SchemeGroupVersion,  
 &Application{},  
 &ApplicationList{},  
 )  
 // Додаємо групу та версію до схеми для об'єктів metav1  
 metav1.AddToGroupVersion(scheme, SchemeGroupVersion)  
 return nil // Повертаємо nil для вказівки на успішне виконання  
}

Тепер можемо перейти до наступного розділу, де ми реалізуємо Контролер, який буде трансформувати стан нашого CRD у бажані об'єкти в кластері Kubernetes.

Реалізація Контролера

Тепер, коли ми реалізували визначення наших CRD, давайте перейдемо до створення контролера, який буде слідкувати за станом CRD та трансформувати його в бажані об'єкти Kubernetes, а саме в Deployments (деплойменти) та Services (сервіси).
Для досягнення цієї мети ми повернемося до кількох концепцій, які ми згадували раніше про Контролери. Ми створимо наступне:

  • Менеджер, який буде точкою входу нашого Контролера
  • Функцію Reconcile, яка приводить стан CRD до бажаного стану в кластері
  • Утилітарні функції, які виконуватимуть завдання, що наш оператор має на меті здійснити

У файлі app-operator/cmd/controller/reconciler.go вставте цей код.
Не хвилюйтесь, ми розглянемо кожен блок детально і пояснимо, що він виконує.

package controller  

import (  
 "context"  
 "errors"  
 "fmt"  
 "os"  
 "path/filepath"  
 cranev1 "github.com/jim-junior/crane-operator/api/v1"  
 craneKubeUtils "github.com/jim-junior/crane-operator/kube"  
 k8serrors "k8s.io/apimachinery/pkg/api/errors"  
 "k8s.io/apimachinery/pkg/runtime"  
 utilruntime "k8s.io/apimachinery/pkg/util/runtime"  
 "k8s.io/client-go/kubernetes"  
 "k8s.io/client-go/rest"  
 "k8s.io/client-go/tools/clientcmd"  
 "k8s.io/client-go/util/homedir"  
 ctrl "sigs.k8s.io/controller-runtime"  
 "sigs.k8s.io/controller-runtime/pkg/client"  
 "sigs.k8s.io/controller-runtime/pkg/log"  
 "sigs.k8s.io/controller-runtime/pkg/log/zap"  
)  
// Глобальні змінні для схеми Kubernetes та логера  
var (  
 scheme = runtime.NewScheme()  
 setupLog = ctrl.Log.WithName("setup")  
)  
// Ініціалізація схеми шляхом реєстрації API групи cranev1  
func init() {  
 utilruntime.Must(cranev1.AddToScheme(scheme))  
}  
// Структура Reconciler для обробки логіки реконциляції  
type Reconciler struct {  
 client.Client // Клієнт для controller-runtime  
 scheme *runtime.Scheme // Схема для управління типами API  
 kubeClient *kubernetes.Clientset // Kubernetes клієнт для безпосередніх викликів API  
}  
// Функція Reconcile обробляє основну логіку для контролера  
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {  
 log := log.FromContext(ctx).WithValues("application", req.NamespacedName) // Створення контекстного логера  
 log.Info("reconciling application")  
 // Отримання ресурсу Application за іменем та простором імен  
 var application cranev1.Application  
 err := r.Client.Get(ctx, req.NamespacedName, &application)  
 if err != nil {  
 // Якщо ресурс не знайдено, спробувати очистити пов'язані ресурси  
 if k8serrors.IsNotFound(err) {  
 err = craneKubeUtils.DeleteApplication(ctx, req, r.kubeClient)  
 if err != nil {  
 return ctrl.Result{}, fmt.Errorf("не вдалося видалити ресурси: %s", err)  
 }  
 return ctrl.Result{}, nil  
 }  
 }  
 // Створення або оновлення Kubernetes deployment для ресурсу Application  
 err = craneKubeUtils.ApplyApplication(ctx, req, application, r.kubeClient)  
 if err != nil {  
 return ctrl.Result{}, fmt.Errorf("не вдалося створити або оновити deployment: %s", err)  
 }  
 return ctrl.Result{}, nil // Реконциляція завершена успішно  
}  
// Функція RunController ініціалізує та запускає Kubernetes контролер  
func RunController() {  
 var (  
 config *rest.Config  
 err error  
 )  
 // Визначення шляху до kubeconfig файлу (використовується для локальної розробки)  
 kubeconfigFilePath := filepath.Join(homedir.HomeDir(), ".kube", "config")  
 if _, err := os.Stat(kubeconfigFilePath); errors.Is(err, os.ErrNotExist) {  
 // Якщо kubeconfig не існує, спробувати використати конфігурацію всередині кластера  
 config, err = rest.InClusterConfig()  
 if err != nil {  
 panic(err.Error()) // Вихід, якщо не знайдено валідної конфігурації  
 }  
 } else {  
 // Завантаження конфігурації з файлу kubeconfig  
 config, err = clientcmd.BuildConfigFromFlags("", kubeconfigFilePath)  
 if err != nil {  
 panic(err.Error())  
 }  
 }  
 // Створення Kubernetes clientset  
 clientset, err := kubernetes.NewForConfig(config)  
 if err != nil {  
 panic(err.Error())  
 }  
 // Налаштування логера для контролера  
 ctrl.SetLogger(zap.New())  
 // Створення нового менеджера для контролера  
 mgr, err := ctrl.NewManager(config, ctrl.Options{  
 Scheme: scheme,  
 })  
 if err != nil {  
 setupLog.Error(err, "не вдалося запустити менеджер")  
 os.Exit(1)  
 }  
 // Створення та реєстрація reconciler в менеджері  
 err = ctrl.NewControllerManagedBy(mgr).
Для (&cranev1.Application{}). // Вказуємо тип ресурсу, який контролює контролер  
 Complete(&Reconciler{  
 Client: mgr.GetClient(), // Використовуємо клієнт менеджера  
 scheme: mgr.GetScheme(), // Використовуємо схему менеджера  
 kubeClient: clientset, // Використовуємо clientset для безпосередніх викликів API  
 })  
 if err != nil {  
 setupLog.Error(err, "не вдалося створити контролер")  
 os.Exit(1)  
 }  
 // Запускаємо менеджер та обробляємо коректне завершення роботи  
 setupLog.Info("запуск менеджера")  
 if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {  
 setupLog.Error(err, "помилка при запуску менеджера")  
 os.Exit(1)  
 }  
}

Пояснення коду контролера

Давайте уважно розглянемо код вище, щоб краще зрозуміти, що відбувається в цьому файлі.

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

import (  
 "context" // Забезпечує функціональність для керування та передачі контексту, особливо корисно для операцій, що залежать від запиту.  
 "errors" // Стандартний пакет Go для створення та обробки помилок.  
 "fmt" // Забезпечує форматований ввід/вивід за допомогою функцій, подібних до printf та scanf з мови C.  
 "os" // Обробляє функціональність на рівні ОС, таку як зчитування змінних середовища та операції з файловою системою.  
 "path/filepath" // Допомагає маніпулювати та будувати шляхи до файлів у крос-платформному вигляді.  
cranev1 "github.com/jim-junior/crane-operator/api/v1" // Імпортує визначення користувацьких CRD (наприклад, Application) для цього оператора.  
 craneKubeUtils "github.com/jim-junior/crane-operator/kube" // Імпортує допоміжні утиліти для взаємодії з ресурсами Kubernetes.  
 k8serrors "k8s.io/apimachinery/pkg/api/errors" // Забезпечує утиліти для роботи з помилками API Kubernetes.  

"k8s.io/apimachinery/pkg/runtime" // Обробляє типи та схеми на етапі виконання для об'єктів Kubernetes.  
 utilruntime "k8s.io/apimachinery/pkg/util/runtime" // Містить допоміжні функції для обробки помилок на етапі виконання та відновлення.  
 "k8s.io/client-go/kubernetes" // Бібліотека client-go для Kubernetes, що дозволяє взаємодіяти з API сервером Kubernetes.  
 "k8s.io/client-go/rest" // Забезпечує інструменти для роботи з REST конфігураціями, особливо для доступу всередині кластера.  
 "k8s.io/client-go/tools/clientcmd" // Обробляє завантаження та парсинг файлів kubeconfig для доступу до Kubernetes поза кластером.  
 "k8s.io/client-go/util/homedir" // Допоміжний пакет для отримання шляху до домашнього каталогу користувача.  
 ctrl "sigs.k8s.io/controller-runtime" // Основний пакет для створення контролерів за допомогою Kubernetes Controller Runtime.  
 "sigs.k8s.io/controller-runtime/pkg/client" // Забезпечує динамічний клієнт для взаємодії з об'єктами Kubernetes.  

"sigs.k8s.io/controller-runtime/pkg/log" // Утиліти для ведення журналу в рамках Controller Runtime.  
 "sigs.k8s.io/controller-runtime/pkg/log/zap" // Забезпечує журналювання на основі Zap для Controller Runtime.  
)  

У нас є функція init, яка ініціалізує схему Kubernetes, реєструючи API групу, яку ми визначили для нашого CRD.

Далі давайте розглянемо функцію RunController в кінці файлу. Спочатку нам потрібно отримати доступ до клієнта Kubernetes або clientset, який дозволить нам взаємодіяти з API сервером Kubernetes при виконанні операцій CRUD на нашому кластері. Ось для чого і призначені ці перші 27 рядків.

Ми викликаємо функцію RunController у файлі main.go нашої програми, оскільки це буде точка входу для нашого Kubernetes Operator.

kubeconfigFilePath := filepath.Join(homedir.HomeDir(), ".kube", "config")  
 if _, err := os.Stat(kubeconfigFilePath); errors.Is(err, os.ErrNotExist) {  
 // Якщо kubeconfig не існує, намагаємося використати конфігурацію всередині кластера  
 config, err = rest.InClusterConfig()  
 if err != nil {  
 panic(err.Error()) // Завершити, якщо не знайдено дійсної конфігурації  
 }  
 } else {  
 // Завантажити конфігурацію з файлу kubeconfig  
 config, err = clientcmd.BuildConfigFromFlags("", kubeconfigFilePath)  
 if err != nil {  
 panic(err.Error())  
 }  
 }  

// Створити Kubernetes clientset  
 clientset, err := kubernetes.NewForConfig(config)  
 if err != nil {  
 panic(err.Error())  
 }  

Далі налаштовуємо журналювання за допомогою zap.
Це допоможе нам під час налагодження.

ctrl.SetLogger(zap.New())  

Далі налаштовуємо наш менеджер.

// Створюємо новий менеджер для контролера  
 mgr, err := ctrl.NewManager(config, ctrl.Options{  
 Scheme: scheme,  
 })  
 if err != nil {  
 setupLog.Error(err, "не вдалося запустити менеджер")  
 os.Exit(1)  
 }  

// Створюємо і реєструємо reconciler в менеджері  
 err = ctrl.NewControllerManagedBy(mgr).  
 For(&cranev1.Application{}).  

// Вказуємо тип ресурсу, який контролює контролер  
 Complete(&Reconciler{  
 Client: mgr.GetClient(), // Використовуємо клієнт менеджера  
 scheme: mgr.GetScheme(), // Використовуємо схему менеджера  
 kubeClient: clientset, // Використовуємо clientset для прямих API викликів  
 })  
 if err != nil {  
 setupLog.Error(err, "не вдалося створити контролер")  
 os.Exit(1)  
 }  
 // Запускаємо менеджер та обробляємо коректне завершення роботи  
 setupLog.Info("запуск менеджера")  
 if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {  
 setupLog.Error(err, "помилка при запуску менеджера")  
 os.Exit(1)  
 }  

Цей код ініціалізує та запускає основні компоненти контролера Kubernetes. Спочатку створюється менеджер (mgr) за допомогою функції ctrl.NewManager, яка служить середовищем виконання для контролера, обробляючи спільні ресурси, клієнтів та схему, яка визначає типи ресурсів, з якими може працювати менеджер. Якщо менеджер не може запуститися через проблеми з конфігурацією, помилка реєструється, і програма завершується.
Далі створюється та реєструється reconciler (відновлювач) з менеджером. Reconciler визначає логіку для забезпечення того, щоб бажаний стан кастомного ресурсу (Application) відповідав фактичному стану в кластері. Це робиться за допомогою ctrl.NewControllerManagedBy, який вказує тип ресурсу, яким буде керувати контролер, та налаштовує reconciler з клієнтом менеджера, схемою та Kubernetes clientset для прямих API викликів. Якщо контролер не може бути створений або зареєстрований, програма завершується з помилкою. Нарешті, менеджер запускається за допомогою mgr.Start, що розпочинає спостереження за вказаним ресурсом і обробку запитів на відновлення стану. Налаштовується обробник сигналів для забезпечення коректного завершення роботи. Якщо менеджер не вдається запустити, реєструється помилка, і програма завершується.
Ця конфігурація поєднує менеджер та reconciler (відновлювач), що дозволяє оператору моніторити та підтримувати бажаний стан кастомних ресурсів у Kubernetes кластері.

Далі давайте визначимо наш Reconciler та функцію Reconcile.

Ми визначаємо структуру Reconciler. Це та структура, яку ми використовували в ctrl.NewControllerManagedBy, коли ініціалізували контролер.
Структура повинна містити метод під назвою Reconcile, який обробляє основну логіку контролера, тобто він приводить бажаний стан до фактичних об'єктів Kubernetes.

type Reconciler struct {  
 client.Client  
 scheme *runtime.Scheme  
 kubeClient *kubernetes.Clientset  
}  

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {  
 log := log.FromContext(ctx).WithValues("application", req.NamespacedName) // Створення контекстного логера  
 log.Info("reconciling application")  
 // Отримуємо ресурс Application за його ім'ям та простором імен  
 var application cranev1.Application  
 err := r.Client.Get(ctx, req.NamespacedName, &application)  
 if err != nil {  
 // Якщо ресурс не знайдено, намагаємося очистити пов'язані ресурси  
 if k8serrors.IsNotFound(err) {  
 err = craneKubeUtils.DeleteApplication(ctx, req, r.kubeClient)  
 if err != nil {  
 return ctrl.Result{}, fmt.Errorf("не вдалося видалити ресурси: %s", err)  
 }  
 return ctrl.Result{}, nil  
 }  
 }  
 // Створюємо або оновлюємо Kubernetes deployment для ресурсу Application  
 err = craneKubeUtils.ApplyApplication(ctx, req, application, r.kubeClient)  
 if err != nil {  
 return ctrl.Result{}, fmt.Errorf("не вдалося створити або оновити deployment: %s", err)  
 }  
 return ctrl.Result{}, nil // Відновлення завершено успішно  
}

Примітка: Я не включаю утилітні функції, імпортовані як craneKubeUtils, тому що вони не є критичними для цієї статті, але в основному це функції, що створюють Deployments та Services з Spec CRD.
Однак, у коді, розміщеному в репозиторії на GitHub, ви можете знайти їх у цьому файлі.
https://github.com/jim-junior/crane-operator/blob/main/kube/application.go

Далі у вашому файлі main.go імпортуйте функцію RunController з пакету controller і викликайте її у функції main().

package main  

import "github.com/jim-junior/crane-operator/cmd/controller"  

func main() {  
 controller.RunController()  
}

Тестування контролера

Тепер, коли ми закінчили писати код для нашого контролера, ми можемо його протестувати. Спочатку вам потрібно, щоб ваш Kubernetes кластер був запущений і налаштований конфігураційний файл. Переконайтесь, що хост-машина, на якій ви запускаєте програму, вже підключена до Kubernetes кластера.
ви можете перевірити це, виконавши будь-яку команду kubectl, наприклад:

kubectl get nodes  
# Якщо налаштовано правильно, це може повернути щось на кшталт:  
# NAME STATUS ROLES AGE VERSION  
# minikube Ready control-plane 228d v1.27.4  

# Якщо не налаштовано, це може повернути помилку, наприклад:  
# E1222 11:35:37.597805 25720 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp [::1]:8080: connectex: No connection could be made because the target machine actively refused it.

Якщо все гаразд, можна переходити до тестування нашого контролера, запустивши:

go run main.go

Очікуваний вивід логів виглядатиме приблизно так:

{"level":"info","ts":"2024-12-22T11:08:23+03:00","logger":"setup","msg":"starting manager"}  
{"level":"info","ts":"2024-12-22T11:08:23+03:00","logger":"controller-runtime.metrics","msg":"Starting metrics server"}  
{"level":"info","ts":"2024-12-22T11:08:23+03:00","msg":"Starting EventSource","controller":"application","controllerGroup":"cloud.cranom.tech","controllerKind":"Application","source":"kind source: *v1.Application"}  
{"level":"info","ts":"2024-12-22T11:08:23+03:00","msg":"Starting Controller","controller":"application","controllerGroup":"cloud.cranom.tech","controllerKind":"Application"}  
{"level":"info","ts":"2024-12-22T11:08:23+03:00","logger":"controller-runtime.metrics","msg":"Serving metrics server","bindAddress":":8080","secure":false}  
{"level":"info","ts":"2024-12-22T11:08:23+03:00","msg":"Starting workers","controller":"application","controllerGroup":"cloud.cranom.tech","controllerKind":"Application","worker count":1}

Якщо ви отримуєте помилку або маєте труднощі з запуском контролера з якої-небудь причини, ви можете створити Issue у репозиторії на GitHub, де розміщений вихідний код, або залишити коментар, якщо ви читаєте цю статтю на Dev.to або Medium.

Якщо все добре.
Тепер ми можемо спробувати застосувати приклад CRD і перевірити, чи правильно наш оператор виконує свою очікувану функціональність.

В директорії yaml/examples репозиторію на GitHub я розмістив конфігурації для нашого користувацького ресурсу Application. Ці конфігурації призначені для розгортання інстансу WordPress з базою даних Mysql. Є три файли.
Файли: mysql.yaml, wp-secrets.yml та wordpress.yml.

Ви можете застосувати їх, виконавши команди kubectl apply в такому порядку:

# Секрети для змінних середовища  
kubectl apply yaml/examples/wp-secrets.yml  
# Інстанс Mysql  
kubectl apply yaml/examples/mysql.yml  
# Інстанс WordPress  
kubectl apply yaml/examples/wordpress.yml

Ви також можете скопіювати код з цих файлів на GitHub:

Конфігурації в цих файлах призначені для застосунку WordPress, який буде слухати на порті вузла 30080.

Розгортання Оператора

Тепер, коли ми переконалися, що Kubernetes оператор працює, давайте перейдемо до його розгортання.
текст перекладу
Щоб розгорнути це, необхідно виконати наступні кроки:

  • Створити Docker-образ для оператора
  • Опублікувати його в Docker-реєстрі
  • Після цього можемо розгорнути його в нашому кластері.

Тепер перейдемо до створення контейнерного образу для Kubernetes-оператора. Ми будемо використовувати образ golang як базовий для контейнера, оскільки це проект на Golang. Скопіюйте цей код і додайте його до файлу DockerFile в корені вашого проекту.

# Створення бінарного файлу програми  
FROM golang:1.22.5 AS build  

# `boilerplate` має бути замінено на назву вашого проекту  
WORKDIR /go/src/boilerplate  
# Копіюємо весь код та інші файли для компіляції всього  
COPY . .
текст перекладу
## Завантажуємо всі залежності заздалегідь (це можна було б і опустити, але так буде зрозуміліше)  
RUN go mod download  
# Створюємо програму як статично зібрану, щоб вона могла працювати на Alpine  
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app .  

# Переміщуємо бінарний файл у "кінцевий образ", щоб зробити його меншим  
FROM alpine:latest as release  
WORKDIR /app  

# Тут також має бути замінено на назву вашого проекту  
COPY --from=build /go/src/boilerplate/app .  
# Додаємо пакети  
RUN apk -U upgrade \  
 && apk add --no-cache dumb-init ca-certificates \  
 && chmod +x /app/app  

ENTRYPOINT ["/app/app"]

Тепер ви можете побудувати контейнер за допомогою Docker і потім опублікувати його в реєстр контейнерів на ваш вибір.

Далі переходимо до розгортання нашого кластера. Ми створимо Kubernetes-деплоймент для нашого оператора.
текст перекладу
## Нижче наведено простий об'єкт Kubernetes _Deployment_ для оператора.

apiVersion: apps/v1
kind: Deployment
metadata:
name: crane-operator
spec:
replicas: 1
selector:
matchLabels:
app: crane-operator
template:
metadata:
labels:
app: crane-operator
spec:
containers:
- name: controller
image: jimjuniorb/crane-operator:latest
```

Вітаємо!!! Тепер у вас є працюючий Kubernetes-оператор у вашому кластері. Тепер ви можете налаштувати його будь-яким зручним для вас способом.

Я б порекомендував ознайомитися з такими фреймворками, як Operator SDK або Kube builder, якщо ви хочете створювати більш складні оператори. Я також додав файл робочого процесу GitHub Action для розгортання оператора за допомогою GitHub actions щоразу, коли створюється новий тег Release.

На цьому все на сьогодні. Дякую, що слідували до кінця цієї статті. Ви можете переглянути мої інші статті на моєму Блозі.
текст перекладу

Ось кілька джерел, які я використовував, і які можуть бути корисними для вас.

Посилання

Brandon Philips, (3 листопада 2016). Introducing Operators: Putting Operational Knowledge into Software. Internet Archive Wayback Machine. https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html

CNCF TAG App-Delivery Operator Working Group, CNCF Operator White Paper — фінальна версія. Github. https://github.com/cncf/tag-app-delivery/blob/163962c4b1cd70d085107fc579e3e04c2e14d59c/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md

Документація Kubernetes, Custom Resources.
текст перекладу
https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/

Документація Kubernetes, Контролери (Controllers). https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/

Youtube, Kubernetes API Versioning: A Deep dive. https://www.youtube.com/live/-jtGM6WnF1Q?si=-6tfrlwyTf-NSizL

Перекладено з: Building a Kubernetes Operator | A Practical Guide

Leave a Reply

Your email address will not be published. Required fields are marked *