Кратко для платформенной команды

Что этоColloq — совместные тетради Jupyter для занятий: веб-приложение, брокер среды выполнения и отдельный под с ядром Python для каждой комнаты. В кластер ставится Helm-чартом oci://ghcr.io/colloq-edu/charts/colloq в одно пространство имён. Открытый код, лицензия MIT.
Что работаетDeployment приложения, одна реплика: база — SQLite. Deployment брокера, одна реплика: только он обращается к API Kubernetes. Под и Service на каждую работающую комнату. Второй под и Service комнаты — для личных тетрадей студентов. Короткоживущие поды соревнований: посылка, метрика, подготовка пакетов. Поды комнат и соревнований создаёт брокер, а не чарт.
Что нужноОдно пространство имён с правами его администратора; Role брокера из раздела «Права»; StorageClass — ReadWriteOnce обязательно, ReadWriteMany желательно; ingress-контроллер и сертификат; зеркало реестра с образами выпуска.
Чего не нужно никогдаcluster-admin и кластерных объектов: Namespace, PersistentVolume, ClusterRole, CRD, DaemonSet, RuntimeClass. Привилегированных контейнеров, hostPath, hostNetwork и hostPID. Root: все контейнеры работают под uid 1000. Выхода в интернет из кластера.
СетьВходящие — только от ingress-контроллера к приложению, порт 3000. Внутри пространства имён: приложение → брокер, приложение и брокер → комнаты. Исходящие: брокер → API Kubernetes, поды → DNS кластера; по желанию приложение → модель ИИ и исходящий прокси. Коду студентов доступен только DNS, посылкам соревнований — ничего.
ДанныеДва PVC: colloq-data — база SQLite, ключи, файлы страниц занятий, данные и посылки соревнований, снимки базы; colloq-workspace — файлы комнат.
Резервные копииСнимки томов вашими средствами (Velero, CSI) и согласованные снимки базы, которые приложение раз в сутки само пишет в том colloq-data.
Браузеры студентов и преподавателейIngress-контроллер кластера, TLSПриложение · Deployment colloq-app, 1 репликаБрокер · Deployment colloq-runtime, 1 реплика
Комната A · под и ServiceКомната B · под и ServiceЛичные тетради комнатыПосылка соревнования

Приложение — сервер на Node.js с собранным веб-интерфейсом и базой SQLite — хранит всё на двух томах и к API Kubernetes не обращается. Когда в комнату входят или запускают ячейку, приложение просит брокер поднять её под. Брокер создаёт под и Service с ClusterIP по шаблону, зашитому в его код, дожидается Jupyter и возвращает приложению адрес и токен; дальше приложение говорит с Jupyter комнаты напрямую, на порту 8888. Под комнаты живёт, пока в ней кто-то есть, и ещё 2 часа после того, как все ушли. Перезапуск и обновление приложения и брокера поды комнат переживают: новый брокер находит их по меткам, и переменные Python остаются.

Что мы предполагаем о кластере

Это список для вашей команды: пройдите его до установки. На каждый пункт ответ — «да, так и есть» или значение чарта, которое нужно задать.

ЧтоМы предполагаемЕсли у вас иначе
Версия Kubernetes1.30 или новее, совместимый с upstream: ванильный, Deckhouse и подобные. Менять память и ядра работающей комнаты без перезапуска можно с 1.33 и при праве pods/resize.Версия ниже 1.33 или право не выдаётся — runtime.inPlaceResize: false. Правила нет в Role, изменение ресурсов живой комнаты отвечает «не поддерживается», без сбоя, а новое число получит следующий под этой комнаты.
Пространство имёнНам выдают одно пространство имён с правами его администратора, и в нём одна установка Colloq: имена объектов фиксированы (colloq-app, colloq-runtime…). Кластерных объектов чарт не создаёт.Пространство имён, его метки и квоту создаёт ваша команда. Role и RoleBinding создаёт сам чарт, а Kubernetes не даёт выдать право, которого нет у того, кто его выдаёт: ставить чарт должен тот, у кого есть все глаголы из раздела «Права».
Pod Security и политикиДействует Pod Security Admission restricted, а Kyverno или Gatekeeper требуют requests и limits у каждого контейнера, пробы, runAsNonRoot и readOnlyRootFilesystem, запрещают privileged, hostPath и hostNetwork, пускают образы только из внутреннего реестра и только по дайджесту и требуют стандартных меток app.kubernetes.io/*.Все поды Colloq проходят restricted; подробности и исключения — в разделе «Безопасность». Свои метки добавляет commonLabels (всем объектам чарта и подам брокера), метки и аннотации только подам брокера — rooms.podLabels и rooms.podAnnotations.
Сетевые политикиNetworkPolicy исполняются (Calico, Cilium), и пространство имён может быть закрыто по умолчанию (default-deny).Чарт объявляет каждый нужный поток, входящий и исходящий; они перечислены в разделе «Сеть». Задайте, где живут ingress-контроллер (networkPolicy.ingressController.from) и DNS кластера (networkPolicy.dns.to), и адреса API-сервера (networkPolicy.kubeApiServer).
Ingress и TLSВход через ingress-nginx; класс и аннотации настраиваются. TLS — сертификат организации из готового Secret или от cert-manager. Пользователи приходят из корпоративной сети, через VPN или, если студенты внешние, из интернета.Другой контроллер — ingress.className и ingress.annotations с тем же смыслом: тело запроса от 256 МБ, тайм-ауты от часа, без буферизации ответов. Или ingress.enabled: false: опубликуйте Service colloq-app:3000 своим объектом и задайте config.publicUrl.
ВходСтуденты входят по ссылке занятия, преподаватели — по личной ссылке. Перед Colloq может стоять прокси с SSO, который передаёт подписанный JWT: Teleport Application Access, oauth2-proxy или Pomerium перед вашим IdP.config.sso.*: преподаватели входят корпоративной учётной записью, студенты с ней — под своим именем; см. «Вход через корпоративный SSO». Без прокси ничего задавать не нужно.
ХранилищеStorageClass с ReadWriteOnce есть всегда, с ReadWriteMany (CephFS) — может быть.По умолчанию чарт рассчитан на ReadWriteOnce: всё, что монтирует тома Colloq, работает на одном узле. Есть ReadWriteMany — persistence.accessMode: ReadWriteMany, и поды комнат расходятся по кластеру; см. «Хранилище».
Без интернетаИз кластера нет интернета. Образы идут через зеркало реестра (Harbor, Nexus), пакеты PyPI — через зеркало в Nexus, оракул — через внутреннюю модель с API, совместимым с OpenAI, или выключен. Возможны исходящий HTTP-прокси и внутренний CA.global.imageRegistry — зеркало для всех образов, включая образы ядер; dependencies.indexUrl и dependencies.mirrorEgress — зеркало PyPI; config.ai.baseUrl — модель; outbound.httpsProxy, outbound.noProxy, outbound.extraCa. См. «Образы и зеркало реестра».
Секреты и GitOpsСекреты приходят из Vault через External Secrets, развёртывание идёт через ArgoCD.Каждый секрет берётся из готового Secret: config.existingSecret, secrets.runtimeToken.existingSecret, secrets.roomSecret.existingSecret, metrics.existingSecret. Когда они заданы, чарт не порождает при рендеринге ни одного случайного значения, и ArgoCD не видит вечного расхождения.
Мониторинг и журналыPrometheus Operator; журналы собираются со stdout и stderr.metrics.enabled включает /metrics с токеном, metrics.serviceMonitor.enabled — ServiceMonitor; без Prometheus Operator собирайте /metrics своими средствами. config.logFormat: json — журнал по объекту JSON в строке.
Резервные копииСнимки томов: Velero, CSI.Приложение само пишет согласованные снимки базы в том colloq-data (config.dbSnapshotHours, config.dbSnapshotKeep), так что снимок тома всегда несёт целую копию базы.
GPUУзлы с GPU необязательны.Для GPU-комнат задаются RuntimeClass, nodeSelector и tolerations: gpu.runtimeClassName, gpu.nodeSelector, gpu.tolerations; см. «Эксплуатация».

Права

Права в кластере есть только у брокера: Role и RoleBinding colloq-runtime в этом пространстве имён. Ни одной ClusterRole, никакого доступа к узлам, к Secret, к Deployment и к другим пространствам имён.

РесурсГлаголыЗачем
podsget, list, create, deleteПоды комнат, личных тетрадей и соревнований. list нужен для переписи: перезапущенный брокер находит работающие поды по меткам и подхватывает их, а не создаёт заново.
servicesget, list, create, deleteService с ClusterIP для каждой комнаты и её личных тетрадей — постоянное имя, по которому приложение ходит в Jupyter, — и для прокси подготовки пакетов. Удалённая насовсем комната оставляет «надгробие»: headless Service без селектора и без ClusterIP, чтобы её идентификатор нельзя было открыть снова.
pods/resizepatchПоменять память и ядра работающей комнаты без перезапуска (Kubernetes 1.33+): патч касается только memory и cpu контейнера kernel. Необязательно: при runtime.inPlaceResize: false этого правила нет.
persistentvolumeclaimsgetПеред соревнованием убедиться, что том с данными на месте.
networkpolicies (networking.k8s.io)getПеред соревнованием убедиться, что политики изоляции посылок существуют. Без них посылки не принимаются, а преподаватель видит причину.

Ни patch, ни update на поды и Service нет: живой объект брокер не правит — меняет только ресурсы комнаты через pods/resize, а всё остальное удаляет и создаёт заново, с предусловием по UID. RBAC не умеет ограничивать поля пода, поэтому шаблоны подов зашиты в код брокера, а его HTTP API принимает только идентификатор комнаты, окружение из каталога, ревизию образа, память и ядра: ни произвольного шаблона, ни hostPath, ни образа не из каталога. Остальное держат PSA restricted и ваши политики.

ServiceAccountТокен APIКто
colloq-appнетПриложение. К API Kubernetes не обращается вовсе.
colloq-runtimeда: под брокера монтирует спроецированный токен, который kubelet обновляет самБрокер; связан с Role выше.
colloq-kernelнетПоды комнат и личных тетрадей: у кода студентов нет ключей от API.
учётная запись по умолчаниюнетПоды соревнований: токен не монтируется.

У всех трёх ServiceAccount стоит automountServiceAccountToken: false; токен включает только под брокера.

Безопасность

Pod Security. Каждый под Colloq — приложение, брокер, комнаты, личные тетради, соревнования — проходит профиль restricted: runAsNonRoot, uid и gid 1000, seccomp RuntimeDefault, allowPrivilegeEscalation: false, capabilities.drop: [ALL], readOnlyRootFilesystem: true. Писать можно только в тома: emptyDir для /tmp, домашнего каталога и /dev/shm (в памяти: 64 МиБ, у GPU-комнат 1 ГиБ) и в PVC. Метку pod-security.kubernetes.io/enforce: restricted на пространство имён ставит ваша команда: чарт им не владеет.

Движки политик.

  • requests и limits есть у каждого контейнера, включая init-контейнер и sidecar подов соревнований. У ядра комнаты requests равны limits (QoS Guaranteed): память и ядра резервируются целиком, плюс 2 ГиБ эфемерного диска.
  • Пробы. У приложения — HTTP: startupProbe и livenessProbe на /api/livez, readinessProbe на /api/readyz. У брокера — TCP 8787. У ядра комнаты и личных тетрадей — пробы готовности и живости по TCP 8888; проба живости срабатывает только после пяти минут отказов: под с restartPolicy: Never при этом завершается вместе с переменными занятия. Контейнеры посылок исполняются до конца и проб не имеют; у их выгрузчика и у прокси пакетов есть проба готовности. Если правило о пробах не подходит подам, которые работают до завершения, делайте исключение по метке app.kubernetes.io/managed-by: colloq-runtime.
  • Метки. Объекты чарта несут app.kubernetes.io/name: colloq, instance, component, version, part-of, managed-by: Helm и helm.sh/chart. Поды, которые создаёт брокер, — app.kubernetes.io/name, instance и part-of, app.kubernetes.io/managed-by: colloq-runtime и colloq.dev/role: kernel (у пода личных тетрадей ещё colloq.dev/kernel: own), competition-job, competition-resolver, competition-proxy. Ключи app.kubernetes.io/* и colloq.* принадлежат чарту и брокеру, переопределить их нельзя.
  • Дайджесты. Образы приложения и брокера чарт всегда пишет как реестр/путь@sha256:… — ни тега, ни latest. Образы ядер в каталоге принимаются только в том же виде: ссылку с одним тегом не пропустят ни чарт, ни брокер.
  • Реестр. global.imageRegistry переписывает реестр каждого образа, включая ядра из каталога, выгрузчик и прокси соревнований.
  • Мутирующие вебхуки. Под комнаты брокер после создания сверяет с тем, что просил. Если вебхук изменил контейнеры, тома или настройки безопасности — внедрил sidecar, переписал imagePullPolicy, — брокер откажется от пода, и комната не запустится; в журнале брокера будут названы изменённые поля. Исключите поды с меткой app.kubernetes.io/managed-by: colloq-runtime из таких вебхуков; для Istio достаточно rooms.podLabels: {sidecar.istio.io/inject: "false"}. Размещение, которое дописывает сам кластер — PodNodeSelector, PodTolerationRestriction, приоритет, RuntimeClass, Kueue, — сверке не мешает.

Какой код под кем работает.

ПодОбраз и кодТокен APIТомаСеть
Приложениеcolloq-app: сервер Colloq на Node.jsнетcolloq-data и colloq-workspaceвход от ingress-контроллера; выход к брокеру, комнатам, DNS и, по желанию, к модели ИИ и прокси
Брокерcolloq-runtime: брокер на Node.jsданетвход от приложения; выход к API Kubernetes, комнатам, посылкам и DNS
Комнатаcolloq-kernel окружения комнаты: Jupyter и код студентовнетсвой подкаталог colloq-workspaceвход от приложения и брокера на 8888; выход — только DNS
Личные тетрадиcolloq-kernel: код студентов в личных тетрадях, без GPUнетподкаталог своей комнаты, по тому же путикак у комнаты
Посылкаcolloq-kernel: тетрадь участника или метрика; colloq-runtime: выгрузчик результатанетподкаталоги colloq-data, входные — только на чтениеникакой; внутрь — только брокер к выгрузчику на 8765
Подготовка пакетовcolloq-kernel: pip скачивает «свои пакеты» участниковнетподкаталоги colloq-dataтолько к своему прокси на 3128 и DNS
Прокси пакетовcolloq-runtime: CONNECT-проксинетнетвход от подготовки; выход — DNS и индекс пакетов: публичный PyPI на 443 или ваше зеркало

Изоляция комнат. У каждой комнаты свой под: свои процессы, свои emptyDir, свой токен Jupyter — брокер выводит его из секрета комнат через HMAC и отдаёт только приложению. Под монтирует лишь свой подкаталог тома colloq-workspace (subPath), файлов других комнат в нём нет. Сетевая политика пускает к порту 8888 комнаты только приложение и брокер, а из комнаты не выпускает никуда, кроме DNS: соседние комнаты, приложение и API коду студентов недоступны. Внутри комнаты участники делят ядро, файлы и пользователя Linux — так задумано: это общее рабочее место. Стандартная NetworkPolicy не гарантирует, что под не достучится до служб своего узла: проверьте это из комнаты (см. проверки после установки) и при необходимости закройте хостовыми политиками вашего CNI.

Секреты.

SecretЧто в нёмКто читает
config.existingSecret или colloq-app-configКлючи — переменные приложения: OPENAI_API_KEY, SESSION_SECRET, прокси с паролем в HTTPS_PROXY или любая другая настройкаприложение
colloq-runtime-auth, ключ runtime-tokenBearer, с которым приложение обращается к брокеруприложение и брокер
colloq-room-secret, ключ room-secretКлюч, из которого брокер выводит токены Jupyter комнат и токены выгрузки посылоктолько брокер
colloq-metrics, ключ metrics-tokenBearer для /metrics, если метрики включеныприложение и Prometheus
global.imagePullSecrets.dockerconfigjson зеркала; первый из них получают и поды брокераkubelet

Каждый можно взять из готового Secret — например, его создаёт External Secrets из Vault. Токен брокера, секрет комнат и токен метрик, если их не задали, чарт создаёт сам: при установке через helm CLI — один раз, дальше читая обратно из кластера через lookup. ArgoCD и другие инструменты, которые рендерят чарт через helm template, прочитать их не могут и создавали бы новые значения при каждой синхронизации, перезапуская комнаты, — там задайте existingSecret. Токен брокера и секрет комнат — от 43 до 128 символов [A-Za-z0-9_-] из 32 случайных байт и больше: openssl rand -hex 32. Держите секреты неизменными: новый SESSION_SECRET делает недействительными все выданные ссылки и входы, новый секрет комнат пересоздаёт поды комнат при следующем запуске, и переменные Python пропадают. Если SESSION_SECRET не задан, приложение один раз создаёт его само и хранит в томе colloq-data. Переходя с созданного чартом секрета на existingSecret, сначала перенесите его значение в новый Secret: свой чарт удалит.

Секреты лежат и в томе colloq-data: setup-token, ключи входа преподавателей в базе, ключ модели, если его ввели в панели. Снимки этого тома храните так же закрыто, как Secret.

Сеть

Все потоки, которые нужны Colloq. Чарт объявляет их сетевыми политиками, входящими и исходящими, поэтому установка работает и в пространстве имён, закрытом по умолчанию.

ОткудаКудаПортЗачем
Ingress-контроллерПриложениеTCP 3000Весь трафик пользователей: страницы, API, WebSocket, server-sent events. Кого пускать, задаёт networkPolicy.ingressController.from (по умолчанию поды ingress-nginx из пространства имён ingress-nginx). Контроллер в сети узла приходит с адресов узлов — тогда укажите их ipBlock.
ПриложениеБрокерTCP 8787Поднять, изменить и остановить под комнаты, запустить посылку. Запросы с Bearer-токеном.
ПриложениеПоды комнат и личных тетрадейTCP 8888Jupyter: исполнение ячеек, терминал. С токеном комнаты.
БрокерПоды комнат и личных тетрадейTCP 8888Проверить, что Jupyter поднялся и принимает токен.
БрокерПоды посылок и подготовки пакетовTCP 8765Забрать результат у выгрузчика.
БрокерAPI-сервер Kubernetes443 и 6443Создавать и удалять поды и Service. Сетевая политика видит адрес уже после DNAT, то есть сами адреса API-серверов, поэтому по умолчанию разрешён любой адрес на этих портах. Сузьте networkPolicy.kubeApiServer.cidrs до вывода kubectl get endpointslices -n default -l kubernetes.io/service-name=kubernetes. В Cilium у API-сервера своя идентичность, которую ipBlock не ловит: включите networkPolicy.kubeApiServer.ciliumEntity — чарт добавит CiliumNetworkPolicy в этом пространстве имён.
Все поды Colloq, кроме посылокDNS кластераUDP и TCP 53Имена Service. Где DNS, задаёт networkPolicy.dns.to (по умолчанию k8s-app: kube-dns в kube-system). У комнат DNS можно убрать: rooms.network: none.
Подготовка пакетовСвой прокси пакетовTCP 3128pip ходит только через прокси, который пускает к одному индексу пакетов.
Прокси пакетовЗеркало PyPI или публичный PyPI443 или порт зеркалаИндекс и файлы «своих пакетов» соревнований. Политика знает адреса, а не имена: адреса и порты зеркала — в dependencies.mirrorEgress. В закрытой сети выключите общее правило для публичных адресов на 443: dependencies.publicEgress: false.
ПриложениеМодель ИИ, исходящий прокси, GitHubпорт модели или проксиПо желанию: оракул и импорт тетрадей с GitHub. По умолчанию приложению наружу нельзя никуда: адреса и порты — в networkPolicy.app.egress.
PrometheusПриложениеTCP 3000, /metricsПо желанию, с metrics.enabled: кого пускать, задаёт networkPolicy.monitoring.from.
Узлы (kubelet)Зеркало реестраTCP 443Образы. Сетевыми политиками не описывается.

Если в кластере работает NodeLocal DNSCache, запросы DNS уходят на локальный адрес узла, а не к подам kube-dns: добавьте в networkPolicy.dns.to этот адрес, например ipBlock: {cidr: 169.254.20.10/32}.

Код студентов наружу не выходит: у подов комнат и личных тетрадей исходящих разрешений нет, кроме DNS, а rooms.network: none убирает и его. Поэтому pip install в ячейке не работает: библиотеки приходят в образах ядер, см. «Образы и зеркало реестра». Посылки соревнований не получают сети вовсе, даже DNS, и это проверяется до запуска кода: init-контейнер пробует выйти наружу и пропускает посылку, только если трижды подряд получил отказ, а если сеть ответила — посылка завершается ошибкой.

Адрес клиента: TRUSTED_PROXIES=private. Пределы на адрес — 60 новичков в комнате за 10 минут, 20 вопросов оракулу в минуту, входы и вступления в соревнования — должны видеть адрес студента, а не ingress-контроллера. Приложение верит заголовку X-Forwarded-For только от соединений с адресов из TRUSTED_PROXIES; чарт ставит private (config.inbound.trustedProxies) — все частные диапазоны, откуда бы ни пришёл под контроллера. Это безопасно, потому что к порту 3000 приложения допускается только ingress-контроллер: сетевая политика не пускает туда больше никого, в том числе код студентов, так что подделать заголовок изнутри кластера некому. Клиентом считается самый правый адрес в заголовке, который сам не из доверенных; если доверенные все — а в корпоративной сети адреса пользователей и сами частные, — берётся самый левый, то есть тот, что записал контроллер. Поэтому контроллер должен заменять заголовок, а не дописывать свой адрес к присланному клиентом: у ingress-nginx так по умолчанию, пока use-forwarded-headers и compute-full-forwarded-for выключены. Если перед контроллером стоит балансировщик, который подменяет адрес источника, все студенты окажутся одним адресом: сохраните адрес клиента (externalTrafficPolicy: Local у Service контроллера или PROXY protocol). Если много людей приходит с одного адреса — через NAT или концентратор VPN, — перечислите его в config.inbound.sharedAddresses: пределы на адрес к нему не применяются, пределы на комнату и на человека остаются.

ingress-nginx. Чарт ставит на свой Ingress такие аннотации (ingress.annotations; ключ со значением null убирает аннотацию):

nginx.ingress.kubernetes.io/proxy-body-size: 256m       # данные соревнования: до 200 МБ одним запросом
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"  # WebSocket и server-sent events живут часами
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"      # события должны приходить по одному

Без первой строки ingress-nginx примет не больше 1 МБ, и загрузка данных соревнования или файла в комнату получит 413. WebSocket (/collab/, /control/, /file/) контроллер пропускает сам; сервер шлёт ping каждые 25 с, а в потоках server-sent events — пульс каждые 20 с и заголовок X-Accel-Buffering: no. Приложение обслуживает корень своего имени и из подкаталога не работает: Ingress чарта — по имени хоста ingress.host, путь /. Файл в комнату — до 50 МБ по умолчанию (config.maxUploadMb); поднимете предел — поднимите и proxy-body-size.

TLS и адрес. Сертификат организации — готовый Secret типа kubernetes.io/tls: ingress.tls.secretName, по умолчанию colloq-tls. Или cert-manager: ingress.tls.certManager.clusterIssuer добавит аннотацию с вашим выпускающим центром. PUBLIC_URL — внешний адрес, из которого строятся все ссылки, которые раздаёт Colloq, включая ссылку владельца; по умолчанию чарт берёт https://<ingress.host>, другой задаёт config.publicUrl. Когда контроллер сообщает X-Forwarded-Proto: https, cookies получают флаг Secure, а приложение шлёт Strict-Transport-Security. ingress-nginx по умолчанию добавляет такой заголовок и сам; чтобы он не дублировался, выключите заголовок приложения: config.inbound.hsts: false.

Журналы доступа ingress-контроллера содержат адреса комнат, ключи входа преподавателей (/admin/k/…, /admin/t/…) и токены комнат в адресах WebSocket (?token=…). Храните их так же закрыто, как базу.

Вход через корпоративный SSO

Если перед Colloq стоит прокси, который сам проверяет человека и передаёт в каждом запросе подписанный JWT, — Teleport Application Access, oauth2-proxy или Pomerium перед вашим IdP, — Colloq принимает этот вход. Подпись проверяется по открытым ключам прокси (JWKS), срок действия и аудитория обязательны, издатель — если задан. Аудитория обязательна потому, что один прокси подписывает токены всех своих приложений: без неё токен, выданный для другого приложения, вошёл бы и сюда; * принимает любую. Принимаются только асимметричные алгоритмы (RS, PS, ES, EdDSA); none и HMAC отклоняются всегда. Запрос без токена или с токеном, который не прошёл проверку, обслуживается как раньше: ссылки продолжают работать, и прокси не может запереть класс.

  • Преподаватели. Токен с адресом почты из списка преподавателей входит как этот преподаватель: Colloq выдаёт обычную cookie, и панель, комната и сокеты видят вошедшего. Владелец по-прежнему добавляет коллег по почте, но ссылку им отправлять не нужно. Роли прокси в config.sso.teacherRoles и config.sso.ownerRoles добавляют человека в список при первом заходе; роль того, кто уже в списке, они не меняют. С этими ролями список решает прокси: удалённый в панели вернётся, пока прокси даёт ему роль, — убирайте роль у прокси. Незанятую установку занимает только роль владельца. Cookie, оставшаяся в общем браузере от предыдущего человека, не перебивает того, за кого поручился прокси.
  • Студенты. Имя в комнате берётся из токена, вводить его не нужно. Один человек — один участник комнаты с любого устройства. Пределы «на адрес» считаются на человека: за Teleport у всего класса один адрес — адрес прокси.
  • Соревнования. Участники пока входят по своему ключу, как и без прокси.

Teleport. Приложение в Teleport указывает на Service Colloq, а не на Ingress:

# teleport-kube-agent, app_service
apps:
  - name: colloq
    uri: http://colloq-app.colloq.svc:3000
    public_addr: colloq.teleport.corp.example

Значения чарта:

ingress:
  enabled: false                            # вход — через Teleport
config:
  publicUrl: https://colloq.teleport.corp.example
  sso:
    jwksUrl: https://teleport.corp.example/.well-known/jwks.json
    issuer: teleport.corp.example           # имя кластера Teleport
    audience: http://colloq-app.colloq.svc:3000   # uri приложения: его Teleport пишет в aud
    teacherRoles: colloq-teacher            # необязательно
networkPolicy:
  ingressController:
    from:                                   # поды агента Teleport вместо ingress-nginx
      - namespaceSelector:
          matchLabels: {kubernetes.io/metadata.name: teleport}
        podSelector:
          matchLabels: {app: teleport-kube-agent}
  app:
    egress:
      - cidrs: [10.20.0.15/32]              # прокси Teleport — за ключами
        ports: [443]

Ключи можно не забирать по сети, а передать документом JWKS в config.sso.jwks: чарт смонтирует его из ConfigMap colloq-sso-jwks, исходящий доступ не нужен, но при ротации ключей в Teleport значение нужно обновить. Заголовок по умолчанию — Teleport-Jwt-Assertion; другой задаёт config.sso.header. Authorization не подходит: в нём браузер передаёт токен комнаты, и прокси, который пишет туда свой, сломал бы файлы, историю и оракул. oauth2-proxy с --pass-access-token передаёт токен в X-Forwarded-Access-Token — годится, если IdP выдаёт access token в виде JWT (так делает Keycloak); его аудитория — та, что IdP пишет в access token.

Поля токена. По умолчанию: кто это — sub или username; имя — name, traits.name, traits.full_name, traits.display_name, username; почта — email, traits.email, traits.mail, username; роли — roles, groups, traits.groups. Другие пути задаёт config.sso.claims.*, первый непустой выигрывает. Если в токене только логин, config.sso.emailDomain превращает его в адрес, по которому узнаются преподаватели. Строка в журнале при старте говорит, откуда берутся ключи и кого Colloq добавляет; отклонённый токен попадает в журнал раз в минуту с причиной.

Студенты без учётной записи. Через Teleport проходит только тот, у кого есть учётная запись. Если у студентов её нет, оставьте им обычный вход: Ingress на тот же Service по публичному адресу и ссылки занятий, а config.publicUrl — этот публичный адрес, из него строятся ссылки. Преподаватели при этом открывают и панель, и комнаты по адресу Teleport — ссылки для студентов панель всё равно строит от публичного адреса: проверка токена одна и та же, откуда бы ни пришёл запрос, а страница на адресе Teleport проходит проверку источника, когда агент Teleport указан в config.inbound.trustedProxies (чарт ставит private).

Что остаётся на прокси. Токен — это пропуск до своего срока: если Colloq доступен и в обход прокси, украденный токен действует, пока не истечёт exp. Держите срок жизни токена коротким и не пишите заголовок с токеном в журналы доступа.

Хранилище

PVCЧто в нёмКто монтируетРазмер по умолчанию
colloq-dataБаза SQLite (colloq.db с журналом WAL), ключ подписи ссылок, setup-token, картинки вывода, файлы страниц занятий в page-files/, данные, скрытые ответы и посылки соревнований, наборы пакетов, снимки базы в snapshots/приложение; поды соревнований — свои подкаталоги10 ГиБ; больше для соревнований с большими данными и около 0,3–0,8 ГиБ на каждый курс из 33 занятий со страницами
colloq-workspaceФайлы комнат, <комната>/ на каждуюприложение; поды комнаты и её личных тетрадей — только её подкаталог100 ГиБ

Класс хранения, размер и режим доступа задаются для обоих томов сразу (persistence.storageClass, persistence.accessMode) или для каждого отдельно (persistence.data.*, persistence.workspace.*); готовый PVC подключает existingClaim. С persistence.retain: true — так по умолчанию — PVC переживают и helm uninstall, и удаление приложения ArgoCD.

ReadWriteMany — предпочтительно. С RWX у обоих томов поды комнат расходятся по узлам кластера, куда их поставит планировщик, и ёмкость установки ограничена квотой, а не одним узлом. Подходят CephFS или NFSv4, где блокировки файлов поддерживает сам протокол; писатель у SQLite один — приложение. Сама SQLite предупреждает, что на многих реализациях NFS блокировки ненадёжны: если сомневаетесь, держите том с базой на блочном хранилище (persistence.data.accessMode: ReadWriteOnce), но тогда вернётся размещение всех подов с томами на одном узле.

ReadWriteOnce — один узел. Так чарт работает по умолчанию. Том RWO подключается к одному узлу, поэтому всё, что монтирует тома Colloq, должно работать там же: если в режиме RWO хотя бы один из томов, чарт ставит брокеру RUNTIME_COLOCATE_WITH_APP=1, и каждый под, который монтирует том, — комнаты, личные тетради, посылки, подготовка пакетов — получает обязательную podAffinity к поду приложения (метка colloq.dev/role: app, топология kubernetes.io/hostname). Сам под приложения предпочитает узел, где уже работают комнаты: после обновления новому поду нужен именно тот узел, к которому подключён том. Брокер томов не монтирует и может работать где угодно. Следствия: ёмкость всей установки — свободные ресурсы одного узла; комната сверх них не встанет, и через 20 секунд ожидания преподаватель увидит, чего не хватило, — памяти, ядер или GPU; обслуживание этого узла — перерыв для всего Colloq. Выбирайте узел с запасом, см. «Ресурсы и квоты».

Права на файлы. Все поды пишут под uid и gid 1000. Под приложения задаёт fsGroup: 1000, и новый блочный том становится доступен группе 1000 сам. Для томов RWX драйвер CSI по умолчанию fsGroup не применяет (fsGroupPolicy: ReadWriteOnceWithFSType касается только томов RWO с файловой системой): корень такого тома должен быть доступен на запись uid 1000 со стороны хранилища.

Квоты диска на комнату нет. Том colloq-workspace общий: студент, заполнивший его, остановит сохранение файлов у всех. Следите за заполнением PVC — метрики kubelet kubelet_volume_stats_used_bytes и kubelet_volume_stats_capacity_bytes или colloq_disk_free_bytes из /metrics, тревога на 80 %. Если StorageClass разрешает расширение томов, PVC можно увеличить.

Образы и зеркало реестра

ОбразЧто в нём
ghcr.io/colloq-edu/colloq-app:vX.Y.ZПриложение.
ghcr.io/colloq-edu/colloq-runtime:vX.Y.ZБрокер; он же выгрузчик результатов посылок и прокси пакетов.
ghcr.io/colloq-edu/colloq-kernel:vX.Y.Z-base, …-kaggle-baseЯдра комнат и посылок: окружения base и kaggle-base, только CPU.

Все образы собраны только для amd64. У выпуска и чарта один номер. В опубликованном чарте значения по умолчанию несут дайджест каждого образа выпуска: приложения и брокера — в image.app.digest и image.runtime.digest, ядер — в каталоге окружений catalog.environments. Те же значения отдельным файлом values-X.Y.Z.yaml приложены к выпуску на GitHub: по нему удобно копировать образы в зеркало и сравнивать выпуски в репозитории GitOps. Чарт из исходного дерева дайджестов не несёт и без них рендериться откажется.

Окружение, которое выбирают в занятии, — это строка каталога: имя, образ по дайджесту, признак GPU. Своё окружение — другие библиотеки или GPU — собирается вне кластера из каталога kernel/ репозитория с KERNEL_ENV=<имя>, кладётся в ваш реестр и добавляется в catalog.environments: из панели на Kubernetes окружения не собираются. Образ base-gpu (около 9 ГБ колёс CUDA) не публикуется, GPU-окружение собирайте так же.

Зеркало реестра. global.imageRegistry заменяет реестр в каждой ссылке на образ — у приложения, брокера, ядер из каталога, выгрузчика и прокси соревнований — и сохраняет путь и дайджест. Значение может содержать и путь: прокси-кэш ghcr.io в Harbor с проектом ghcr подходит как есть, harbor.corp.example/ghcr даёт harbor.corp.example/ghcr/colloq-edu/colloq-app@sha256:…. Значит, в зеркале образы должны лежать по тем же путям. Секреты для скачивания — global.imagePullSecrets: их получают поды приложения и брокера, а первый из них — и все поды, которые создаёт брокер.

Без интернета. На машине с доступом к ghcr.io:

# чарт выпуска
helm pull oci://ghcr.io/colloq-edu/charts/colloq --version X.Y.Z

# все образы, на которые он ссылается, включая ядра из каталога
helm template colloq colloq-X.Y.Z.tgz --set ingress.enabled=false \
  | grep -oE '[a-z0-9.-]+/[a-z0-9._/-]+@sha256:[a-f0-9]{64}' | sort -u

# каждый образ — в зеркало по тому же пути, с тем же дайджестом
crane copy ghcr.io/colloq-edu/colloq-app@sha256:… harbor.corp.example/colloq-edu/colloq-app:vX.Y.Z

# чарт — в ваш OCI-реестр
helm push colloq-X.Y.Z.tgz oci://harbor.corp.example/charts

Пакет чарта colloq-X.Y.Z.tgz приложен и к выпуску на GitHub, рядом с values-X.Y.Z.yaml. Дайджест сохраняет любой инструмент, который копирует манифест как есть: crane copy, skopeo copy --all --preserve-digests или репликация Harbor. Без интернета не работают импорт тетрадей с GitHub, оракул без своей модели в сети и «свои пакеты» соревнований без зеркала PyPI. Всё остальное работает: коду студентов интернет на Kubernetes и не даётся.

Из чего собраны образы. Приложение и брокер — на node:22-trixie-slim (Debian 13): при сборке ставятся все исправления, которые Debian успел выпустить, а npm, npx, corepack и yarn из образа удалены — во время работы их ничто не запускает. Ядра — на python:3.11-slim-bookworm (Debian 12). К каждому образу в реестре приложены SBOM и provenance — аттестации BuildKit: docker buildx imagetools inspect ghcr.io/colloq-edu/colloq-app:vX.Y.Z --format '{{json .SBOM}}'. Инструменты, которые копируют индекс образа целиком (crane copy, skopeo copy --all), переносят их в зеркало вместе с образом. На день выпуска сканер вроде Trivy не находит в образах приложения и брокера ни одной критической уязвимости. В ядрах на Debian 12 остаются критические находки, для которых у Debian нет исправления (perl, zlib, sqlite); если ваш реестр не отдаёт образы с такими находками, скажите нам — перевод ядер на Debian 13 следующий шаг.

Установка

Пространство имён с метками Pod Security и квотой создаёт ваша команда; дальше — Helm или ArgoCD.

helm install colloq oci://ghcr.io/colloq-edu/charts/colloq --version X.Y.Z \
  --namespace colloq --values colloq-values.yaml

Значения для кластера вроде банковского: ReadWriteMany на CephFS, прокси-кэш Harbor, секреты из Vault, внутренняя модель ИИ и зеркало PyPI в Nexus, интернета нет.

global:
  imageRegistry: harbor.corp.example/ghcr       # прокси-кэш ghcr.io
  imagePullSecrets: [harbor-pull]

ingress:
  host: colloq.corp.example
  tls:
    secretName: colloq-tls                      # сертификат организации, тип kubernetes.io/tls

persistence:
  accessMode: ReadWriteMany                     # без RWX оставьте ReadWriteOnce: всё на одном узле
  storageClass: cephfs

config:
  existingSecret: colloq-app-secrets            # OPENAI_API_KEY, SESSION_SECRET — из Vault
  timezone: Europe/Moscow
  institution: Корпоративный университет
  logFormat: json
  ai:
    provider: vllm
    baseUrl: https://llm.corp.example/v1
    model: corp-llm

dependencies:
  indexUrl: https://nexus.corp.example/repository/pypi/simple
  mirrorEgress:
    - cidrs: [10.20.0.15/32]                    # адреса Nexus: политика знает адреса, не имена
      ports: [443]
  publicEgress: false

secrets:
  runtimeToken:
    existingSecret: colloq-internal             # ключ runtime-token
  roomSecret:
    existingSecret: colloq-internal             # ключ room-secret

rooms:
  maxMemory: 16Gi

networkPolicy:
  kubeApiServer:
    cidrs: [10.0.0.11/32, 10.0.0.12/32, 10.0.0.13/32]
    ports: [6443]
  app:
    egress:
      - cidrs: [10.30.0.20/32]                  # внутренняя модель ИИ
        ports: [443]

metrics:
  enabled: true
  existingSecret: colloq-metrics                # ключ metrics-token
  serviceMonitor:
    enabled: true

Секреты из Vault — через External Secrets. В colloq-app-secrets ключи — это имена переменных приложения (OPENAI_API_KEY, по желанию SESSION_SECRET); тогда config.ai.apiKey и config.sessionSecret оставьте пустыми. colloq-metrics с ключом metrics-token — так же, как colloq-internal:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: colloq-internal
  namespace: colloq
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: vault
  target:
    name: colloq-internal
  data:
    - secretKey: runtime-token                  # openssl rand -hex 32, один раз
      remoteRef: {key: education/colloq, property: runtime-token}
    - secretKey: room-secret
      remoteRef: {key: education/colloq, property: room-secret}

ArgoCD. Чарт — OCI-артефакт; в ArgoCD он подключается как репозиторий Helm с enableOCI, адрес — без oci://.

apiVersion: v1
kind: Secret
metadata:
  name: corp-charts
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: helm
  name: corp-charts
  url: harbor.corp.example/charts
  enableOCI: "true"
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: colloq
  namespace: argocd
spec:
  project: education
  source:
    repoURL: harbor.corp.example/charts
    chart: colloq
    targetRevision: X.Y.Z
    helm:
      releaseName: colloq
      valuesObject:
        # те же значения, что выше; или файл значений из Git вторым источником
        ingress:
          host: colloq.corp.example
  destination:
    server: https://kubernetes.default.svc
    namespace: colloq
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

С persistence.retain: true (по умолчанию) PVC чарта помечены argocd.argoproj.io/sync-options: Prune=false,Delete=false, так что удаление приложения в ArgoCD данных не удалит. Поды комнат и соревнований создаёт брокер, в Git их нет. Метки app.kubernetes.io/instance на них нет нарочно: по ней ArgoCD до 3.0 отслеживал свои объекты и с prune удалил бы комнаты посреди занятия как лишние. Ваши метки для них — rooms.podLabels; не добавляйте туда app.kubernetes.io/instance.

Ссылка владельца.

kubectl -n colloq exec deploy/colloq-app -- cat /data/setup-token

Откройте https://colloq.corp.example/admin/t/<токен>, введите имя и почту — и вы владелец. Пока у сервера нет владельца, та же ссылка печатается в журнал приложения при каждом старте: kubectl -n colloq logs deploy/colloq-app | grep /admin/t/. Когда владелец появился, токен заменяется новым и больше не печатается. Ссылка — ключ от сервера: не пересылайте её в общий чат и займите сервер сразу после установки, пока журнал с ней не разошёлся по системе сбора журналов. Преподавателей добавьте в панели, в разделе «Преподаватели»: каждый получит личную ссылку входа.

Проверки после установки.

  1. kubectl -n colloq get deploy,pods — colloq-app и colloq-runtime в состоянии Ready.
  2. curl -sS https://colloq.corp.example/api/readyz отвечает {"ok":true,"database":true,"workspace":true}.
  3. Полная проверка — изнутри пода, где видны и причины ошибок: kubectl -n colloq exec deploy/colloq-app -- node -e 'fetch("http://127.0.0.1:3000/api/health").then(r => r.text()).then(console.log)'. В ответе должны быть "ok":true и "isolation":"broker"; поле capabilities скажет, готовы ли соревнования.
  4. Две комнаты с двух устройств из сети студентов: совместная правка, запуск ячейки в каждой, загрузка файла. kubectl -n colloq get pods -l colloq.dev/role=kernel -o wide покажет два пода и их узлы.
  5. Изоляция — из ячейки комнаты: соединения с внешним адресом, с Service приложения и с адресом узла должны получить отказ.
    import socket
    for host, port in [("1.1.1.1", 443), ("colloq-app", 3000), ("<адрес узла>", 10250)]:
        try:
            socket.create_connection((host, port), 3).close()
            print(host, port, "ОТКРЫТО: проверьте сетевые политики")
        except OSError as error:
            print(host, port, "закрыто:", error)
  6. Если нужны соревнования — пробная посылка до конца; строки лидерборда должны появляться сами, без перезагрузки страницы.
  7. До первой группы — восстановление из резервной копии в отдельное пространство имён.

Значения чарта

Главные значения и переменные, которые они задают. Полный список с пояснением к каждому — в values.yaml чарта: helm show values oci://ghcr.io/colloq-edu/charts/colloq --version X.Y.Z. Остальные настройки приложения из таблицы «Настройки» руководства по VM передаются в config.extraEnv или ключами Secret из config.existingSecret.

ЗначениеПеременная и что задаётПо умолчанию
Адрес и вход
ingress.hostИмя хоста; обязательно, если Ingress включён.—
config.publicUrlPUBLIC_URL — внешний адрес, из которого строятся ссылки.https://<ingress.host>
ingress.className, ingress.annotationsКласс контроллера и аннотации.nginx, аннотации из раздела «Сеть»
ingress.tls.secretName, ingress.tls.certManager.*Secret с сертификатом или выпускающий центр cert-manager.colloq-tls
config.inbound.trustedProxiesTRUSTED_PROXIES — чьему X-Forwarded-For верить.private
config.inbound.sharedAddressesSHARED_ADDRESSES — адреса NAT и VPN, к которым не применяются пределы на адрес.пусто
config.inbound.hstsHSTS — заголовок Strict-Transport-Security от приложения.true
Образы и каталог
global.imageRegistryРеестр, можно с путём, вместо ghcr.io для всех образов, включая каталог ядер.пусто
global.imagePullSecretsSecret для скачивания образов; первый из них получают поды брокера (RUNTIME_IMAGE_PULL_SECRET).—
image.app.digest, image.runtime.digestДайджесты образов приложения и брокера.дайджесты выпуска
catalog.environments, catalog.defaultEnvironmentKERNEL_CATALOG_FILE, RUNTIME_CATALOG_FILE — каталог окружений: имя, образ по дайджесту, GPU, несколько ревизий одного имени с одной current: true.base и kaggle-base выпуска
commonLabelsМетки всех объектов чарта и всех подов брокера (входят в RUNTIME_POD_LABELS).—
Секреты
config.existingSecretГотовый Secret, чьи ключи становятся переменными приложения.—
secrets.runtimeToken.*, secrets.roomSecret.*Токен брокера и секрет комнат: existingSecret и existingSecretKey или value.создаются чартом один раз (только helm CLI)
Хранилище
persistence.accessModeРежим доступа томов. Если хотя бы один том ReadWriteOnce, брокер получает RUNTIME_COLOCATE_WITH_APP=1: все поды с томами — на узле приложения.ReadWriteOnce
persistence.storageClass, persistence.data.size, persistence.workspace.sizeКласс хранения и размеры томов; existingClaim — готовый PVC.класс кластера; 10Gi и 100Gi
persistence.retainОставлять PVC при удалении релиза и приложения ArgoCD.true
Комнаты
rooms.memoryRUNTIME_KERNEL_MEMORY — память комнаты, если в занятии не задана своя.4Gi
rooms.maxMemoryRUNTIME_KERNEL_MEMORY_MAX — потолок памяти комнаты. Задайте явно: без него брокер берёт память своего узла за вычетом 1 ГиБ.память узла брокера − 1 ГиБ
rooms.cpuRUNTIME_KERNEL_CPU — ядра комнаты, если в занятии не заданы свои.2
rooms.ephemeralStorageRUNTIME_KERNEL_EPHEMERAL — место под /tmp и домашний каталог комнаты.2Gi
rooms.networkCOLLOQ_ROOM_NETWORK — с none у комнат нет и DNS.только DNS
rooms.nodeSelector, rooms.tolerationsRUNTIME_ROOM_NODE_SELECTOR, RUNTIME_ROOM_TOLERATIONS — где работают поды комнат и личных тетрадей; поды соревнований их не получают.—
rooms.priorityClassNameRUNTIME_PRIORITY_CLASS — класс приоритета всех подов, которые создаёт брокер.—
rooms.podLabels, rooms.podAnnotationsRUNTIME_POD_LABELS, RUNTIME_POD_ANNOTATIONS — метки и аннотации всех подов брокера; к меткам чарт сам добавляет app.kubernetes.io/name, instance и part-of.—
runtime.inPlaceResizeRUNTIME_IN_PLACE_RESIZE — с false ресурсы живой комнаты не меняются, а правила pods/resize нет в Role.true
gpu.runtimeClassNameRUNTIME_GPU_RUNTIME_CLASS — RuntimeClass GPU-комнат; пусто — без него.nvidia
gpu.nodeSelector, gpu.tolerationsRUNTIME_GPU_NODE_SELECTOR, RUNTIME_GPU_TOLERATIONS — добавляются GPU-комнатам.—
Сеть и исходящие
networkPolicy.ingressController.fromКто допускается к порту 3000 приложения.ingress-nginx
networkPolicy.dns.toГде DNS кластера.kube-dns в kube-system
networkPolicy.kubeApiServer.*Адреса и порты API-сервера для брокера; ciliumEntity — для Cilium.любой адрес на 443 и 6443
networkPolicy.app.egressКуда приложению можно наружу: модель, прокси, GitHub.никуда
networkPolicy.monitoring.fromКто собирает /metrics.Prometheus в monitoring
outbound.httpsProxy, outbound.httpProxy, outbound.noProxyHTTPS_PROXY, HTTP_PROXY, NO_PROXY — исходящий прокси приложения. Имена Service (.svc, имена без точек) и частные адреса идут мимо него сами.—
outbound.extraCa.pem или .existingConfigMapNODE_EXTRA_CA_CERTS — сертификаты внутреннего CA в формате PEM для приложения и подготовки пакетов.—
dependencies.indexUrl, dependencies.filesHostsDEPENDENCY_INDEX_URL, DEPENDENCY_FILES_HOSTS — зеркало PyPI для «своих пакетов» соревнований: https, без учётных данных.публичный PyPI
dependencies.mirrorEgress, dependencies.publicEgressАдреса и порты зеркала для прокси пакетов; общее правило для публичных адресов на 443.—; true
Занятия и оракул
config.timezoneTZ — часовой пояс: границы дня для дневных норм, даты.Europe/Moscow
config.uiLanguageUI_LANGUAGE — язык интерфейса, пока владелец не выберет его в панели.ru
config.maxUploadMb, config.maxSessionMbMAX_UPLOAD_MB, MAX_SESSION_MB — предел одного файла и всех загрузок в комнату.50 и 1024
config.ai.provider, .baseUrl, .model, .apiKeyAI_PROVIDER, OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY — модель оракула; для модели внутри сети — vllm, ollama или custom. Ключ лучше держать в config.existingSecret.openai, https://api.openai.com/v1, gpt-4o-mini
config.extraEnvДругие настройки приложения: NAME: значение.—
Наблюдение, копии, ресурсы
metrics.enabled, metrics.serviceMonitor.enabledMETRICS_TOKEN — адрес /metrics за Bearer-токеном и ServiceMonitor, который берёт тот же токен.выключено
config.logFormatLOG_FORMAT — с json каждая строка журнала — объект JSON.text
config.dbSnapshotHours, config.dbSnapshotKeepDB_SNAPSHOT_HOURS, DB_SNAPSHOT_KEEP — как часто писать согласованный снимок базы в /data/snapshots и сколько хранить.24 ч и 7
app.resources, runtime.resourcesЗапросы и пределы приложения и брокера.см. «Ресурсы и квоты»

Ресурсы и квоты

Цифры выведены из пределов в коде и из того же расчёта, что для одной VM; здесь они переведены в requests и limits подов. Главное отличие от VM: под комнаты резервирует память и ядра целиком, а не делит их с соседями.

ПодСколькоCPU: запрос / пределПамять: запрос / пределЭфемерный диск
Приложение1250m / 2512Mi / 2Gi—
Брокер1100m / 1128Mi / 512Mi—
Комнатана каждую работающую2 / 24Gi / 4Gi2Gi
Личные тетрадина комнату, где ими пользуютсякак у комнаты или своё число класса, запрос равен пределу; нужно около 250m на студентакак у комнаты или «Память на класс»; нужно 1–1,5 ГиБ на студента2Gi
Посылкана слотядра посылки + 100m / + 500mпамять посылки + 64Mi / + 256Mi128Mi / 320Mi
Подготовка пакетовпока идёт1100m / 1500m, прокси 100m / 500m2112Mi / 2304Mi, прокси 64Mi / 128Miдо 320Mi

Минимальная квота для одного занятия за раз (комната 2 ядра и 4 ГиБ), в ключах ResourceQuota — requests.cpu, requests.memory, limits.cpu, limits.memory, pods:

СценарийЗапрос CPUЗапрос памятиПредел CPUПредел памятиПодов
(a) 30 студентов, одна общая тетрадь2,354,6 ГиБ56,5 ГиБ3
(b) 30 студентов, у каждого личная тетрадь (1,5 ГиБ и 0,25 ядра на студента)9,8549,6 ГиБ12,551,5 ГиБ4
(c) 30 студентов и соревнование, 7 слотов по 2 ядра и 2 ГиБ17,0519,1 ГиБ22,522,25 ГиБ10
(e) 100 человек в лекционной комнате2,354,6 ГиБ56,5 ГиБ3

Комната держит свой под, пока в ней кто-то есть, и ещё 2 часа после того, как все ушли, поэтому занятия подряд накладываются: на каждое следующее занятие в пределах двух часов добавьте ещё комнату. Слоты соревнований — одна очередь на всю установку. Пример квоты для сценария (c) с запасом на комнату следующего занятия и на подготовку пакетов:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: colloq
  namespace: colloq
spec:
  hard:
    requests.cpu: "22"
    requests.memory: 28Gi
    limits.cpu: "30"
    limits.memory: 32Gi
    requests.ephemeral-storage: 10Gi
    pods: "25"
    services: "100"
    persistentvolumeclaims: "2"
    requests.storage: 150Gi
  • Под сверх квоты API не создаст, и комната или посылка получит отказ. Держите квоту выше суммы, а число слотов — в её пределах: «Исполнителей одновременно» во вкладке «Ресурсы» или COMPETITION_SLOTS в config.extraEnv. Автоматическое число считается по ядрам и памяти узла, где работает приложение, и о вашей квоте ничего не знает.
  • Service копятся: каждая комната, удалённая насовсем, оставляет headless Service без ClusterIP, поэтому предел на число Service ставьте с запасом.
  • LimitRange с максимумом на контейнер должен пропускать комнату (2 ядра и 4 ГиБ, GPU-комнату — с её памятью) и самую большую посылку, которую вы разрешите в соревнованиях: по умолчанию 2 ядра и 4 ГиБ.
  • С ReadWriteOnce вся квота должна поместиться на один узел: для сценария (c) — узел, где свободно не меньше 24 vCPU и 32 ГиБ, для (b) — не меньше 16 vCPU и 64 ГиБ.

Замер приложения (30.09.2026, VM 21 vCPU / 49 ГБ, nginx перед приложением, make load): 500 человек вошли в одну комнату за минуту без единого отказа, вход — 7 мс в середине распределения. Процесс приложения держал 243 МБ памяти и 20 % одного ядра в покое и 50 % ядра, когда двадцать человек печатали одновременно; правка доходила до остальных за 0,17 с (p95). На 300 человек — 198 МБ и те же доли ядра. Замер сделан на VM, а не в кластере, но процесс тот же: запрос чарта в 512 МиБ вдвое больше замеренного, а пределы в 2 ядра и 2 ГиБ оставляют большой запас.

Эксплуатация

Обновление. helm upgrade colloq oci://ghcr.io/colloq-edu/charts/colloq --version X.Y.Z -n colloq -f colloq-values.yaml или новый targetRevision в ArgoCD. Приложение и брокер обновляются стратегией Recreate: старый под останавливается до старта нового, потому что писатель у SQLite один. Перерыв — от секунд до минуты; на старт с миграцией базы отведено до 10 минут. Открытые комнаты переподключаются сами. Поды комнат обновление переживают: брокер при остановке их не трогает, новый подхватывает, и переменные Python остаются. Настройки размещения — rooms.nodeSelector, rooms.tolerations, класс приоритета, режим хранилища — достаются следующему поду комнаты, а работающий остаётся, где стоял. И всё же обновляйтесь между занятиями.

Ревизии ядер. Комната остаётся на той ревизии образа ядра, с которой начала, и запускает только её. При helm upgrade чарт сам оставляет в каталоге ревизии прежнего выпуска с current: false (не больше catalog.maxRetainedPerEnvironment, по умолчанию трёх на окружение): он читает их из живого ConfigMap colloq-catalog, и комнаты, начатые раньше, продолжают на своём Python. ArgoCD рендерит без доступа к кластеру и прежний каталог не видит: там комната, чьей ревизии больше нет, при следующем запуске переходит на текущую ревизию своего окружения, а в журнале приложения остаётся строка об этом. Чтобы и под ArgoCD держать прежние ревизии, перечислите их в catalog.environments с current: false. Образы прежних ревизий держите в зеркале, пока ими пользуются комнаты.

Версия данных. Если новый выпуск поднимает версию схемы базы, приложение перед миграцией само пишет снимок /data/snapshots/pre-schema-<было>-to-<стало>-<время>.db; такие снимки сами не удаляются. Прежний выпуск на базе с более новой схемой не запустится: в журнале будет «colloq.db … was written by a newer Colloq», под уйдёт в CrashLoopBackOff. Поэтому откат в таком случае — это прежний чарт вместе с восстановлением базы: остановите приложение, из служебного пода положите снимок на место colloq.db и удалите colloq.db-wal и colloq.db-shm, затем установите прежнюю версию чарта. Всё, что сделано после обновления, пропадёт. COLLOQ_ALLOW_SCHEMA_DOWNGRADE=1 в config.extraEnv запускает старый выпуск на новой базе без восстановления — только если вы знаете, что миграция для него безвредна.

kubectl -n colloq scale deploy/colloq-app --replicas=0
kubectl -n colloq apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: colloq-maintenance
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    seccompProfile: {type: RuntimeDefault}
  containers:
    - name: shell
      image: <образ colloq-app установленной версии>
      command: ["sleep", "3600"]
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities: {drop: ["ALL"]}
      resources:
        requests: {cpu: 100m, memory: 128Mi}
        limits: {cpu: 500m, memory: 256Mi}
      volumeMounts: [{name: data, mountPath: /data}]
  volumes:
    - name: data
      persistentVolumeClaim: {claimName: colloq-data}
EOF
kubectl -n colloq exec colloq-maintenance -- ls -l /data/snapshots/
kubectl -n colloq exec colloq-maintenance -- sh -c \
  'cp /data/snapshots/pre-schema-….db /data/colloq.db && rm -f /data/colloq.db-wal /data/colloq.db-shm'
kubectl -n colloq delete pod colloq-maintenance
helm rollback colloq <ревизия до обновления> -n colloq

Резервные копии. Снимайте оба PVC вашими средствами — Velero со снимками CSI или снимками хранилища — и Secret пространства имён, если они не из Vault: тогда копией секретов служит Vault. Снимок живого тома SQLite — это снимок как при внезапном отключении: база по нему обычно восстанавливается, но полагаться на это не стоит. Поэтому приложение каждые config.dbSnapshotHours часов (в чарте — 24) само пишет согласованный снимок /data/snapshots/colloq-<время>.db и хранит последние config.dbSnapshotKeep (7): в любом снимке тома есть целая копия базы не старше суток. Тома снимаются по отдельности, так что файлы комнат и база в копии могут расходиться на минуты. Копии тома colloq-data содержат ключи входа преподавателей, setup-token и ключ модели, если его вводили в панели, — храните их как секреты. Восстановление в отдельное пространство имён проверяйте хотя бы раз в семестр. Команды colloq-host backup и restore — для VM, в кластере они не используются.

Наблюдение. Проба живости — /api/livez, готовности — /api/readyz: она проверяет только базу и рабочие файлы, так что сбой брокера или API не выводит сайт из ротации, а трудности с ядрами видны там, где запускают код, и в /api/health. С metrics.enabled адрес /metrics отдаёт метрики в текстовом формате Prometheus тому, кто пришёл с Authorization: Bearer <токен>; без настройки он отвечает 404. Для тревог пригодятся свободное место на томах (colloq_disk_free_bytes), время последнего снимка базы (colloq_db_snapshot_last_timestamp_seconds), сбои сохранения тетрадей и запуска ядер (colloq_notebook_save_failures_total, colloq_kernel_start_failures_total). Журнал — stdout и stderr; с config.logFormat: json каждая строка — объект JSON с полями time, level, msg и контекстом. В журнале есть адреса комнат, а адрес комнаты — это ссылка для входа: доступ к журналу равен доступу к комнатам. Преподавателям доступен GET /api/instance/operations: возраст очереди, повторы, сбои сохранения тетрадей, резервы памяти, свободный диск.

Узлы с GPU. GPU-комната просит nvidia.com/gpu: 1 и RuntimeClass из gpu.runtimeClassName (по умолчанию nvidia, пусто — без него); узлы выбирают gpu.nodeSelector и gpu.tolerations. Драйвер, device plugin или GPU Operator и сам RuntimeClass — кластерные объекты, их ставит ваша команда. Одна карта на комнату, без разделения, если ваш device plugin не делит карты сам. Личные тетради и посылки соревнований GPU не получают никогда. Образ GPU-окружения соберите сами, см. «Образы и зеркало реестра».

Обслуживание узлов. Поды комнат — одиночные поды без контроллера и с emptyDir: kubectl drain удалит их только с --force --delete-emptydir-data, а cluster-autoscaler не освободит узел с ними, пока вы не добавите аннотацию cluster-autoscaler.kubernetes.io/safe-to-evict: "true" через rooms.podAnnotations. Удалённый под комната поднимет снова при следующем запуске: тетради и файлы на месте, переменные Python пропадают. С ReadWriteOnce обслуживание узла приложения — перерыв для всей установки.

Удаление. helm uninstall убирает объекты чарта, кроме PVC — с persistence.retain: true они остаются, — и не трогает поды и Service, которые создал брокер: они не принадлежат Helm. Удалите их по метке: kubectl -n colloq delete pod,svc -l app.kubernetes.io/managed-by=colloq-runtime.

Честные ограничения

  • Одна реплика приложения. База — SQLite, писатель один: отказоустойчивости нет, обновление и отказ узла прерывают работу на время перезапуска пода. Чтобы расти, ставят отдельные установки, по одной на пространство имён, — например, по одной на подразделение.
  • SSO — только через прокси. Своего входа через SSO, LDAP и второго фактора у Colloq нет: его отдают прокси, который подписывает JWT, — см. «Вход через корпоративный SSO». Без такого прокси владелец и преподаватели входят по личным ссылкам; панель можно закрыть на ingress-контроллере — например, oauth2-proxy с корпоративным IdP через аннотации внешней аутентификации ingress-nginx на отдельном Ingress для путей /admin и /api/admin. Участники соревнований и с прокси входят по своему ключу.
  • Ссылка комнаты — общая ссылка. Кто её получил, тот войдёт и по умолчанию сможет запускать код; отозвать её нельзя. Для индивидуально оцениваемых и конфиденциальных работ комнаты не подходят: участники делят ядро, файлы и пользователя Linux.
  • ReadWriteOnce — это один узел. Вся установка помещается на один узел, и его обслуживание — перерыв для всех.
  • У кода студентов нет интернета. pip install в ячейке не работает; библиотеки — только в образах ядер из каталога, которые собираются вне кластера.
  • Нет квоты диска на комнату: том colloq-workspace общий.
  • Сетевые политики — не вся изоляция. Стандартная NetworkPolicy не гарантирует, что под не достучится до служб своего узла, а поды делят ядро Linux узла.
  • Только amd64.
  • Ядра — на Debian 12. Сканер найдёт в них критические находки, которые Debian не исправил; см. «Образы и зеркало реестра».
  • Путь новый. Чарт появился в этом выпуске: проверьте установку в непроизводственном пространстве имён, прежде чем вести в нём занятия.