Привет, Хабр!
Сегодня я расскажу, как создать кастомный контроллер для Kubernetes на Rust. Кастомные контроллеры нужны, чтобы автоматизировать действия, которые Kubernetes сам по себе не умеет. Например, представим, что вы хотите:
-
Динамически создавать ресурсы на основе пользовательских запросов.
-
Управлять сторонними сервисами, которые не понимают Kubernetes.
-
Делать всё это без лишних рук и прочих YAML‑файлов.
Сначала вы определяете свой CRD (Custom Resource Definition), который описывает, как выглядят данные. Затем пишете контроллер, который читает эти данные и выполняет соответствующие действия. В итоге получаете систему, которая работает автономно.
Почему Rust?
Почему не Go, ведь он де‑факто стандарт для Kubernetes? Go — хороший инструмент, но Rust лучше, когда дело доходит до:
-
Безопасности: никаких гонок данных или утечек памяти.
-
Производительности: Rust компилируется в машинный код и летает.
-
Компактности: бинарники маленькие.
-
Код на Rust просто красивый.
Основы кастомного контроллера
Прежде, чем написать хоть строчку кода, нужно понять, как вообще работают кастомные контроллеры в Kubernetes.
CRD
CRD — это расширение Kubernetes API, которое позволяет создавать собственные типы объектов. Если стандартных сущностей недостаточно, то нужно использовать CRD:
Когда создаёте CRD, вы описываете:
-
API‑версию: например,
v1— версия вашего ресурса. -
Группу: уникальное имя для вашей сущности, например,
pizzeria.example.com. -
Имя сущности: как Kubernetes будет понимать ваш объект (например,
PizzaOrder). -
Спецификацию: поля, которые описывают ваш объект (например, размер пиццы, начинка).
CRD может выглядеть так:
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: pizzaorders.pizzeria.example.com spec: group: pizzeria.example.com names: kind: PizzaOrder plural: pizzaorders singular: pizzaorder scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: size: type: string toppings: type: array items: type: string
После применения такого CRD кластер начинает понимать новый тип объекта PizzaOrder. Теперь можно создавать такие ресурсы, как:
apiVersion: pizzeria.example.com/v1 kind: PizzaOrder metadata: name: order-123 spec: size: large toppings: - cheese - pepperoni
CRD — это новый API‑тип, который становится доступным через стандартные инструменты Kubernetes.
Контроллер
Если CRD — это описание ресурса, то контроллер — это та часть, которая оживляет объекты. Контроллер — это программа, которая работает так:
-
Следит за объектами вашего CRD: он получает уведомления о каждом изменении в ваших
PizzaOrder(создание, обновление, удаление). -
Реагирует на изменения: на основе данных из объекта контроллер выполняет действия, которые приводят систему в желаемое состояние. Например, создаёт Pod для приготовления бутерброда.
-
Постоянно проверяет состояние: если что‑то пошло не так, он возвращается к объекту и пытается исправить проблему.
Проще говоря, контроллер — это цикл:
-
Узнаёт о новом или изменённом объекте.
-
Проверяет текущее состояние объекта.
-
Делает действия для достижения «идеального состояния».
-
Повторяет цикл до бесконечности.
Контроллеры работают асинхронно, используя принципы управления событиями. Это позволяет обрабатывать тысячи объектов одновременно.
Kubernetes API
Kubernetes API — это основной механизм взаимодействия с кластером. Это HTTP‑интерфейс, через который можно:
-
Получать список объектов (например, все заказы пиццы в кластере).
-
Следить за изменениями объектов в реальном времени (streaming через
watch). -
Создавать, обновлять и удалять объекты.
Контроллер подключается к API‑серверу Kubernetes через клиентскую библиотеку. В случае Rust это kube-rs. Она упрощает работу с Kubernetes API, предоставляя инструменты для:
-
Создания клиента.
-
Отслеживания событий.
-
Управления ресурсами.
Как это работает на практике?
-
Контроллер подписывается на события через API (например, изменения в
PizzaOrder). -
Когда пользователь создаёт новый объект
PizzaOrder, API‑сервер уведомляет контроллер. -
Контроллер обрабатывает данные и выполняет логику, например, создаёт
Podдля приготовления пиццы.
Как всё это соединяется?
-
Вы создаёте CRD, чтобы Kubernetes понял ваш новый тип объекта.
-
Пользователи создают объекты вашего типа.
-
Контроллер следит за этими объектами через Kubernetes API.
-
На основе данных из объектов контроллер выполняет действия, приводя систему в желаемое состояние.
Важные моменты:
-
Идём на события, а не на опрос. Контроллеры не опрашивают API каждые N секунд. Они подписываются на события, чтобы реагировать мгновенно.
-
Идём к консистентности. Если состояние объекта неожиданно изменилось, контроллер всё равно пытается вернуть его в желаемое состояние.
-
Не блокируйте основной поток. Контроллеры должны быть максимально асинхронными, чтобы не тормозить работу системы.
-
Не паникуйте. Даже если контроллер «упал», ничего страшного не произойдёт. Kubernetes продолжит работу, и вы сможете перезапустить контроллер.
-
Следите за расходом ресурсов. Контроллеры могут «съесть» CPU и память, если не настроены должным образом. Используйте ограничения ресурсов в манифестах Deployment.
Начнем реализацию кастомного контроллера Kubernetes на Rust
Для начала создадим проект Rust, который станет основой для нашего контроллера. Выполните следующие команды:
cargo new pizza-controller cd pizza-controller
Это создаст стандартный проект Rust с базовой структурой файлов.
Теперь обновим Cargo.toml, чтобы подключить необходимые зависимости:
[dependencies] kube = "0.80" # Библиотека для взаимодействия с Kubernetes API tokio = { version = "1", features = ["full"] } # Асинхронный рантайм для обработки событий serde = { version = "1.0", features = ["derive"] } # Для сериализации и десериализации данных tracing = "0.1" # Для структурированных логов
Запустим команду cargo build, чтобы убедиться, что зависимости успешно подтянулись и проект компилируется.
Важная часть любого кастомного контроллера — это CRD. В нашем случае это будет PizzaOrder, в котором пользователь сможет указать размер пиццы и список топпингов.
Открываем файл src/main.rs и добавляем следующий код:
use kube::CustomResource; use serde::{Deserialize, Serialize}; #[derive(CustomResource, Serialize, Deserialize, Clone, Debug)] #[kube( group = "pizzeria.example.com", // Группа API для CRD version = "v1", // Версия API kind = "PizzaOrder", // Имя ресурса namespaced // Привязка к пространству имён )] pub struct PizzaOrderSpec { pub size: String, // Размер пиццы (small, medium, large) pub toppings: Vec<String>, // Список топпингов }
Этот код использует атрибуты из библиотеки kube, чтобы автоматом сгенерировать структуру CRD.
Теперь сгенерируем YAML для описания CRD:
cargo run --example crd-gen > pizzaorder-crd.yaml kubectl apply -f pizzaorder-crd.yaml
После этого Kubernetes начнёт понимать новый тип ресурса — PizzaOrder.
Пример YAML‑файла, который описывает заказ пиццы:
apiVersion: "pizzeria.example.com/v1" kind: PizzaOrder metadata: name: order-123 spec: size: large toppings: - cheese - pepperoni
Можно применить его через kubectl:
kubectl apply -f pizza-order.yaml
Теперь создадим логику, которая будет обрабатывать объекты PizzaOrder. Контроллер будет отслеживать заказы и выполнять действия в зависимости от их состояния.
Обновляем src/main.rs, чтобы он выглядел так:
use kube::{ api::{Api, PostParams}, runtime::controller::{Controller, ReconcilerAction}, Client, }; use std::sync::Arc; use tokio::time::Duration; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // Инициализируем клиента для Kubernetes API let client = Client::try_default().await?; let orders: Api<PizzaOrder> = Api::all(client); // Логика контроллера let reconciler = |order: Arc<PizzaOrder>, _| async move { println!("Обрабатываем заказ: {:?}", order.spec); // Пример логики обработки заказа if order.spec.size == "large" { println!("Готовим большую пиццу с топпингами: {:?}", order.spec.toppings); } else { println!("Готовим пиццу стандартного размера: {:?}", order.spec.size); } Ok(ReconcilerAction { requeue_after: Some(Duration::from_secs(300)), // Перезапустить через 5 минут }) }; // Обработка ошибок let error_handler = |error: &str, _| { eprintln!("Ошибка: {:?}", error); ReconcilerAction { requeue_after: Some(Duration::from_secs(60)), // Перезапустить через минуту } }; // Запуск контроллера Controller::new(orders.clone(), Default::default()) .run(reconciler, error_handler, ()) .await; Ok(()) }
Теперь нужно упаковать контроллер в Docker‑контейнер и развернуть его в Kubernetes.
Создаем Dockerfile:
FROM rust:1.70-slim WORKDIR /app COPY target/release/pizza-controller /app/ CMD ["./pizza-controller"]
Собераем и загружаем образ:
cargo build --release docker build -t myrepo/pizza-controller:v1 . docker push myrepo/pizza-controller:v1
Создаем манифест Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: pizza-controller spec: replicas: 1 selector: matchLabels: app: pizza-controller template: metadata: labels: app: pizza-controller spec: containers: - name: controller image: myrepo/pizza-controller:v1
Применяем Deployment:
kubectl apply -f deployment.yaml
А как вы используете кастомные контроллеры в Kubernetes? Делитесь своими кейсами в комментариях!
Также напоминаю об открытых уроках, которые пройдут в Otus в рамках онлайн-курсов:
-
30 января: Хранение данных в Kubernetes: Volumes, Storages, Stateful-приложения. Подробнее
-
11 февраля: Разбираем анатомию парсера на Rust. Подробнее
Список всех бесплатных уроков по IT-инфраструктуре и другим направлениям можно посмотреть в календаре.
ссылка на оригинал статьи https://habr.com/ru/articles/876194/
Добавить комментарий