Як загнати Kubernetes у браузер або знайомтеся з webernetes

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Натрапив в інтернеті на історію про те, як Сем Роуз, Senior Developer Educator в ngrok, за два місяці портував Kubernetes у браузер, згенерувавши майже 100 000 рядків коду на TypeScript. Проєкт називається webernetes.

Можна було б подумати, що він просто скомпілював вихідний код у WebAssembly, але не тут то було. По-перше, оригінальний K8s постійно звертається до системних API ОС, яких у браузері просто немає. А по-друге, тоді проєкт замість 140 КБ, які він важить зараз, важив би набагато більше

Тому автор пішов шляхом переписування. Він створив частковий порт бінарника kubelet і ключових контролерів. Для комунікації подів він написав браузерний Container Network Interface, а замість Docker реалізував власне середовище виконання контейнерів, яке спілкується з кастомним kubelet через стандартний Container Runtime Interface.

Замість реальних образів з Docker Hub використовуються класи TypeScript, які успадковуються від базового класу та перехоплюють HTTP-запити на віртуальних портах.

import * as w8s from "@ngrok/webernetes";
class HelloWorld extends w8s.BaseImage {
  static readonly imageName = "hello-world";
  static readonly imageVersion = "1.0";
  async exec(ctx: w8s.ProcessContext, argv: string[]): Promise<number> {
    ctx.listenHttp(8080, async (_ctx, request) => {
      return { status: 200, body: "Hello, world!" };
    });
    return await ctx.waitUntilKilled();
  }
}

Розгортається такий образ за допомогою звичного маніфесту Deployment після ініціалізації віртуального кластера.

const cluster = new w8s.Cluster();
await cluster.registerImage(HelloWorld);
const [pod] = await cluster.apply([
  {
    apiVersion: "apps/v1",
    kind: "Deployment",
    metadata: { name: "hello-world-deployment" },
    spec: {
      replicas: 1,
      selector: { matchLabels: { app: "hello-world-pod" } },
      template: {
        metadata: { labels: { app: "hello-world-pod" } },
        spec: {
          containers: [{ name: "hello-world-container", image: "hello-world:1.0" }],
        },
      },
    },
  },
]);

Далі з кластером можна повноцінно взаємодіяти через API, відстежувати події та робити запити до віртуальних IP-адрес подів.

const pods = await cluster.api.corev1.listNamespacedPod({ namespace: "default" });
const informer = cluster.informer("pods", (type, pod) => {
  console.log(`pod ${type}: ${pod.metadata?.name}`);
});
await informer.stop();
cluster.on("request", (event) => console.log(`request: ${event.request.method} ${event.request.url}`));
cluster.on("response", (event) => console.log(`response: ${event.response?.status}`));
const pod = pods.items[0];
const resp = await cluster.fetch(`http://${pod.status?.podIP}:8080/`);
console.log(resp.body); // "Hello, world!"

Левову частку коду для проєкту писали LLM, але всі рядки уважно рев’ювалися автором (якщо вірити його словам). Оскільки Go та JavaScript мають різні середовища, Сему довелося з нуля імплементувати в TypeScript специфічні речі на кшталт каналів, м’ютексів та інструкцій select.

Для контролю результату він також написав понад 200 інтеграційних тестів через бібліотеку kubernetes-client/javascript. Кожен тест запускався паралельно — спочатку проти справжнього кластера k3s у Node.js, а потім проти webernetes у браузері.

import { expect, it } from "vitest";
import { kubernetes } from "../../test/harnesses/kubernetes";
kubernetes.describe("Pods", (context) => {
  const { core } = context;
  const { getTestNamespace, waitFor } = context.helpers;
  it("should be able to delete a pod", async () => {
    const namespace = await getTestNamespace();
    await core.createNamespacedPod({
      namespace,
      body: {
        metadata: { name: "delete-test" },
        spec: { 
          containers: [{ name: "pause", image: "registry.k8s.io/pause:3.10" }] 
        },
      },
    });
    await waitFor(async () => {
      const pods = await core.listNamespacedPod({ namespace });
      expect(pods.items.find((pod) => pod.metadata?.name === "delete-test")).toBeDefined();
    });
    await core.deleteNamespacedPod({ name: "delete-test", namespace });
    await waitFor(async () => {
      const pods = await core.listNamespacedPod({ namespace });
      expect(pods.items.find((pod) => pod.metadata?.name === "delete-test")).toBeUndefined();
    });
  });
});

Під кінець розробки Сем вирішив прискорити інтеграцію об’єктів Deployment і нацькував на це завдання цілий рій ШІ-агентів. За кілька днів вони спалили понад 2 мільярди токенів, що обійшлося йому приблизно у 1800 доларів за використання API.

Як потестити новинку

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

Для розробників, які хочуть інтегрувати віртуальний кластер у свої проєкти чи поекспериментувати локально, варто зробити наступне:

Встановіть пакет за допомогою npm у ваш JS/TS проєкт:

npm install @ngrok/webernetes

Створіть власний образ контейнера.

import { BaseImage, type ProcessContext } from "@ngrok/webernetes";

class MyImage extends BaseImage {

	static readonly imageName = "my-image";
	static readonly imageVersion = "1.0";

	readonly defaultCommand = ["server"];

        override async exec(ctx: ProcessContext, argv: readonly string[]): Promise<number> {
		if (argv[0] !== "server") {
			return await super.exec(ctx, argv);
		}

		ctx.listenHttp(8080, async (request) => {
			return {
				statusCode: 200,
				body: "hello, world\n",
			};
		});

		return await ctx.waitUntilKilled();
	}
}

Ініціалізуйте кластер і застосуйте маніфести.

import { Cluster } from "@ngrok/webernetes";
const cluster = new Cluster();
cluster.registerImage(MyImage);
await cluster.init();
await cluster.apply([
	{
		apiVersion: "v1",
		kind: "Pod",
		metadata: {
			name: "my-pod",
			labels: { app: "my-pod" },
		},
		spec: {
			containers: [
				{
					name: "my-container",
					image: "my-image:1.0",
				},
			],
		},
	},
]);

Після розгортання ви можете створити NodePort Service і робити HTTP-запити до кластера за допомогою методу cluster.fetch().

Детальніше про проєкт читайте у блозі автора та в офіційному репозиторії.

Як вам такий проєктик?

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Чим би дитя не тішилось, аби не плакало

Підписатись на коментарі