Як загнати Kubernetes у браузер або знайомтеся з webernetes
Натрапив в інтернеті на історію про те, як Сем Роуз, 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().
Детальніше про проєкт читайте у блозі автора та в офіційному репозиторії.
Як вам такий проєктик?
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарівЧим би дитя не тішилось, аби не плакало