Одна з тих великих революційних технологій, що змінили підхід розробників до роботи з хмарною інфраструктурою, — це 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).
Контролери відповідають за управління життєвим циклом об'єктів Kubernetes, таких як Pods, Deployments і Services. Кожен контролер зазвичай обробляє один або кілька типів ресурсів і виконує завдання, як-от створення, оновлення або видалення ресурсів на основі заявлених специфікацій.
Ось розбір того, як працює контролер Kubernetes:
1.
Спостереження (Observe): Контролер спостерігає за сервером API на предмет змін у ресурсах, за які він відповідає. Він моніторить ці ресурси, періодично запитуючи Kubernetes API або через потік подій (event stream).
-
Аналіз (Analyze): Контролер порівнює фактичний стан (поточні умови ресурсів) з бажаним станом (вказаним у конфігураціях).
-
Дії (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. Це контрольний цикл, який моніторить стан кластера і вносить зміни для наближення його до бажаного стану.
Кожен контролер спостерігає за певними ресурсами, чи то вбудованими (наприклад, 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:
- https://github.com/jim-junior/crane-operator/blob/main/yaml/examples/mysql.yaml
- https://github.com/jim-junior/crane-operator/blob/main/yaml/examples/wordpress.yml
- https://github.com/jim-junior/crane-operator/blob/main/yaml/examples/wp-secrets.yml
Конфігурації в цих файлах призначені для застосунку 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