Кастомный контроллер Kubernetes на Rust

от автора

Привет, Хабр!

Сегодня я расскажу, как создать кастомный контроллер для Kubernetes на Rust. Кастомные контроллеры нужны, чтобы автоматизировать действия, которые Kubernetes сам по себе не умеет. Например, представим, что вы хотите:

  • Динамически создавать ресурсы на основе пользовательских запросов.

  • Управлять сторонними сервисами, которые не понимают Kubernetes.

  • Делать всё это без лишних рук и прочих YAML‑файлов.

Сначала вы определяете свой CRD (Custom Resource Definition), который описывает, как выглядят данные. Затем пишете контроллер, который читает эти данные и выполняет соответствующие действия. В итоге получаете систему, которая работает автономно.

Почему Rust?

Почему не Go, ведь он де‑факто стандарт для Kubernetes? Go — хороший инструмент, но Rust лучше, когда дело доходит до:

  1. Безопасности: никаких гонок данных или утечек памяти.

  2. Производительности: Rust компилируется в машинный код и летает.

  3. Компактности: бинарники маленькие.

  4. Код на 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 — это описание ресурса, то контроллер — это та часть, которая оживляет объекты. Контроллер — это программа, которая работает так:

  1. Следит за объектами вашего CRD: он получает уведомления о каждом изменении в ваших PizzaOrder (создание, обновление, удаление).

  2. Реагирует на изменения: на основе данных из объекта контроллер выполняет действия, которые приводят систему в желаемое состояние. Например, создаёт Pod для приготовления бутерброда.

  3. Постоянно проверяет состояние: если что‑то пошло не так, он возвращается к объекту и пытается исправить проблему.

Проще говоря, контроллер — это цикл:

  1. Узнаёт о новом или изменённом объекте.

  2. Проверяет текущее состояние объекта.

  3. Делает действия для достижения «идеального состояния».

  4. Повторяет цикл до бесконечности.

Контроллеры работают асинхронно, используя принципы управления событиями. Это позволяет обрабатывать тысячи объектов одновременно.

Kubernetes API

Kubernetes API — это основной механизм взаимодействия с кластером. Это HTTP‑интерфейс, через который можно:

  • Получать список объектов (например, все заказы пиццы в кластере).

  • Следить за изменениями объектов в реальном времени (streaming через watch).

  • Создавать, обновлять и удалять объекты.

Контроллер подключается к API‑серверу Kubernetes через клиентскую библиотеку. В случае Rust это kube-rs. Она упрощает работу с Kubernetes API, предоставляя инструменты для:

  • Создания клиента.

  • Отслеживания событий.

  • Управления ресурсами.

Как это работает на практике?

  1. Контроллер подписывается на события через API (например, изменения в PizzaOrder).

  2. Когда пользователь создаёт новый объект PizzaOrder, API‑сервер уведомляет контроллер.

  3. Контроллер обрабатывает данные и выполняет логику, например, создаёт Pod для приготовления пиццы.

Как всё это соединяется?

  1. Вы создаёте CRD, чтобы Kubernetes понял ваш новый тип объекта.

  2. Пользователи создают объекты вашего типа.

  3. Контроллер следит за этими объектами через Kubernetes API.

  4. На основе данных из объектов контроллер выполняет действия, приводя систему в желаемое состояние.

Важные моменты:

  1. Идём на события, а не на опрос. Контроллеры не опрашивают API каждые N секунд. Они подписываются на события, чтобы реагировать мгновенно.

  2. Идём к консистентности. Если состояние объекта неожиданно изменилось, контроллер всё равно пытается вернуть его в желаемое состояние.

  3. Не блокируйте основной поток. Контроллеры должны быть максимально асинхронными, чтобы не тормозить работу системы.

  4. Не паникуйте. Даже если контроллер «упал», ничего страшного не произойдёт. Kubernetes продолжит работу, и вы сможете перезапустить контроллер.

  5. Следите за расходом ресурсов. Контроллеры могут «съесть» 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *