Кратко для платформенной команды
| Что это | 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. |
Приложение — сервер на Node.js с собранным веб-интерфейсом и базой SQLite — хранит всё на двух томах и к API Kubernetes не обращается. Когда в комнату входят или запускают ячейку, приложение просит брокер поднять её под. Брокер создаёт под и Service с ClusterIP по шаблону, зашитому в его код, дожидается Jupyter и возвращает приложению адрес и токен; дальше приложение говорит с Jupyter комнаты напрямую, на порту 8888. Под комнаты живёт, пока в ней кто-то есть, и ещё 2 часа после того, как все ушли. Перезапуск и обновление приложения и брокера поды комнат переживают: новый брокер находит их по меткам, и переменные Python остаются.
Что мы предполагаем о кластере
Это список для вашей команды: пройдите его до установки. На каждый пункт ответ — «да, так и есть» или значение чарта, которое нужно задать.
| Что | Мы предполагаем | Если у вас иначе |
|---|---|---|
| Версия Kubernetes | 1.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 и к другим пространствам имён.
| Ресурс | Глаголы | Зачем |
|---|---|---|
pods | get, list, create, delete | Поды комнат, личных тетрадей и соревнований. list нужен для переписи: перезапущенный брокер находит работающие поды по меткам и подхватывает их, а не создаёт заново. |
services | get, list, create, delete | Service с ClusterIP для каждой комнаты и её личных тетрадей — постоянное имя, по которому приложение ходит в Jupyter, — и для прокси подготовки пакетов. Удалённая насовсем комната оставляет «надгробие»: headless Service без селектора и без ClusterIP, чтобы её идентификатор нельзя было открыть снова. |
pods/resize | patch | Поменять память и ядра работающей комнаты без перезапуска (Kubernetes 1.33+): патч касается только memory и cpu контейнера kernel. Необязательно: при runtime.inPlaceResize: false этого правила нет. |
persistentvolumeclaims | get | Перед соревнованием убедиться, что том с данными на месте. |
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-token | Bearer, с которым приложение обращается к брокеру | приложение и брокер |
colloq-room-secret, ключ room-secret | Ключ, из которого брокер выводит токены Jupyter комнат и токены выгрузки посылок | только брокер |
colloq-metrics, ключ metrics-token | Bearer для /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 8888 | Jupyter: исполнение ячеек, терминал. С токеном комнаты. |
| Брокер | Поды комнат и личных тетрадей | TCP 8888 | Проверить, что Jupyter поднялся и принимает токен. |
| Брокер | Поды посылок и подготовки пакетов | TCP 8765 | Забрать результат у выгрузчика. |
| Брокер | API-сервер Kubernetes | 443 и 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 3128 | pip ходит только через прокси, который пускает к одному индексу пакетов. |
| Прокси пакетов | Зеркало PyPI или публичный PyPI | 443 или порт зеркала | Индекс и файлы «своих пакетов» соревнований. Политика знает адреса, а не имена: адреса и порты зеркала — в 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/. Когда владелец появился, токен заменяется новым и больше не печатается. Ссылка — ключ от сервера: не пересылайте её в общий чат и займите сервер сразу после установки, пока журнал с ней не разошёлся по системе сбора журналов. Преподавателей добавьте в панели, в разделе «Преподаватели»: каждый получит личную ссылку входа.
Проверки после установки.
kubectl -n colloq get deploy,pods—colloq-appиcolloq-runtimeв состоянии Ready.curl -sS https://colloq.corp.example/api/readyzотвечает{"ok":true,"database":true,"workspace":true}.- Полная проверка — изнутри пода, где видны и причины ошибок:
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скажет, готовы ли соревнования. - Две комнаты с двух устройств из сети студентов: совместная правка, запуск ячейки в каждой, загрузка файла.
kubectl -n colloq get pods -l colloq.dev/role=kernel -o wideпокажет два пода и их узлы. - Изоляция — из ячейки комнаты: соединения с внешним адресом, с 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) - Если нужны соревнования — пробная посылка до конца; строки лидерборда должны появляться сами, без перезагрузки страницы.
- До первой группы — восстановление из резервной копии в отдельное пространство имён.
Значения чарта
Главные значения и переменные, которые они задают. Полный список с пояснением к каждому — в 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.publicUrl | PUBLIC_URL — внешний адрес, из которого строятся ссылки. | https://<ingress.host> |
ingress.className, ingress.annotations | Класс контроллера и аннотации. | nginx, аннотации из раздела «Сеть» |
ingress.tls.secretName, ingress.tls.certManager.* | Secret с сертификатом или выпускающий центр cert-manager. | colloq-tls |
config.inbound.trustedProxies | TRUSTED_PROXIES — чьему X-Forwarded-For верить. | private |
config.inbound.sharedAddresses | SHARED_ADDRESSES — адреса NAT и VPN, к которым не применяются пределы на адрес. | пусто |
config.inbound.hsts | HSTS — заголовок Strict-Transport-Security от приложения. | true |
| Образы и каталог | ||
global.imageRegistry | Реестр, можно с путём, вместо ghcr.io для всех образов, включая каталог ядер. | пусто |
global.imagePullSecrets | Secret для скачивания образов; первый из них получают поды брокера (RUNTIME_IMAGE_PULL_SECRET). | — |
image.app.digest, image.runtime.digest | Дайджесты образов приложения и брокера. | дайджесты выпуска |
catalog.environments, catalog.defaultEnvironment | KERNEL_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.memory | RUNTIME_KERNEL_MEMORY — память комнаты, если в занятии не задана своя. | 4Gi |
rooms.maxMemory | RUNTIME_KERNEL_MEMORY_MAX — потолок памяти комнаты. Задайте явно: без него брокер берёт память своего узла за вычетом 1 ГиБ. | память узла брокера − 1 ГиБ |
rooms.cpu | RUNTIME_KERNEL_CPU — ядра комнаты, если в занятии не заданы свои. | 2 |
rooms.ephemeralStorage | RUNTIME_KERNEL_EPHEMERAL — место под /tmp и домашний каталог комнаты. | 2Gi |
rooms.network | COLLOQ_ROOM_NETWORK — с none у комнат нет и DNS. | только DNS |
rooms.nodeSelector, rooms.tolerations | RUNTIME_ROOM_NODE_SELECTOR, RUNTIME_ROOM_TOLERATIONS — где работают поды комнат и личных тетрадей; поды соревнований их не получают. | — |
rooms.priorityClassName | RUNTIME_PRIORITY_CLASS — класс приоритета всех подов, которые создаёт брокер. | — |
rooms.podLabels, rooms.podAnnotations | RUNTIME_POD_LABELS, RUNTIME_POD_ANNOTATIONS — метки и аннотации всех подов брокера; к меткам чарт сам добавляет app.kubernetes.io/name, instance и part-of. | — |
runtime.inPlaceResize | RUNTIME_IN_PLACE_RESIZE — с false ресурсы живой комнаты не меняются, а правила pods/resize нет в Role. | true |
gpu.runtimeClassName | RUNTIME_GPU_RUNTIME_CLASS — RuntimeClass GPU-комнат; пусто — без него. | nvidia |
gpu.nodeSelector, gpu.tolerations | RUNTIME_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.noProxy | HTTPS_PROXY, HTTP_PROXY, NO_PROXY — исходящий прокси приложения. Имена Service (.svc, имена без точек) и частные адреса идут мимо него сами. | — |
outbound.extraCa.pem или .existingConfigMap | NODE_EXTRA_CA_CERTS — сертификаты внутреннего CA в формате PEM для приложения и подготовки пакетов. | — |
dependencies.indexUrl, dependencies.filesHosts | DEPENDENCY_INDEX_URL, DEPENDENCY_FILES_HOSTS — зеркало PyPI для «своих пакетов» соревнований: https, без учётных данных. | публичный PyPI |
dependencies.mirrorEgress, dependencies.publicEgress | Адреса и порты зеркала для прокси пакетов; общее правило для публичных адресов на 443. | —; true |
| Занятия и оракул | ||
config.timezone | TZ — часовой пояс: границы дня для дневных норм, даты. | Europe/Moscow |
config.uiLanguage | UI_LANGUAGE — язык интерфейса, пока владелец не выберет его в панели. | ru |
config.maxUploadMb, config.maxSessionMb | MAX_UPLOAD_MB, MAX_SESSION_MB — предел одного файла и всех загрузок в комнату. | 50 и 1024 |
config.ai.provider, .baseUrl, .model, .apiKey | AI_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.enabled | METRICS_TOKEN — адрес /metrics за Bearer-токеном и ServiceMonitor, который берёт тот же токен. | выключено |
config.logFormat | LOG_FORMAT — с json каждая строка журнала — объект JSON. | text |
config.dbSnapshotHours, config.dbSnapshotKeep | DB_SNAPSHOT_HOURS, DB_SNAPSHOT_KEEP — как часто писать согласованный снимок базы в /data/snapshots и сколько хранить. | 24 ч и 7 |
app.resources, runtime.resources | Запросы и пределы приложения и брокера. | см. «Ресурсы и квоты» |
Ресурсы и квоты
Цифры выведены из пределов в коде и из того же расчёта, что для одной VM; здесь они переведены в requests и limits подов. Главное отличие от VM: под комнаты резервирует память и ядра целиком, а не делит их с соседями.
| Под | Сколько | CPU: запрос / предел | Память: запрос / предел | Эфемерный диск |
|---|---|---|---|---|
| Приложение | 1 | 250m / 2 | 512Mi / 2Gi | — |
| Брокер | 1 | 100m / 1 | 128Mi / 512Mi | — |
| Комната | на каждую работающую | 2 / 2 | 4Gi / 4Gi | 2Gi |
| Личные тетради | на комнату, где ими пользуются | как у комнаты или своё число класса, запрос равен пределу; нужно около 250m на студента | как у комнаты или «Память на класс»; нужно 1–1,5 ГиБ на студента | 2Gi |
| Посылка | на слот | ядра посылки + 100m / + 500m | память посылки + 64Mi / + 256Mi | 128Mi / 320Mi |
| Подготовка пакетов | пока идёт | 1100m / 1500m, прокси 100m / 500m | 2112Mi / 2304Mi, прокси 64Mi / 128Mi | до 320Mi |
Минимальная квота для одного занятия за раз (комната 2 ядра и 4 ГиБ), в ключах ResourceQuota — requests.cpu, requests.memory, limits.cpu, limits.memory, pods:
| Сценарий | Запрос CPU | Запрос памяти | Предел CPU | Предел памяти | Подов |
|---|---|---|---|---|---|
| (a) 30 студентов, одна общая тетрадь | 2,35 | 4,6 ГиБ | 5 | 6,5 ГиБ | 3 |
| (b) 30 студентов, у каждого личная тетрадь (1,5 ГиБ и 0,25 ядра на студента) | 9,85 | 49,6 ГиБ | 12,5 | 51,5 ГиБ | 4 |
| (c) 30 студентов и соревнование, 7 слотов по 2 ядра и 2 ГиБ | 17,05 | 19,1 ГиБ | 22,5 | 22,25 ГиБ | 10 |
| (e) 100 человек в лекционной комнате | 2,35 | 4,6 ГиБ | 5 | 6,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 не исправил; см. «Образы и зеркало реестра».
- Путь новый. Чарт появился в этом выпуске: проверьте установку в непроизводственном пространстве имён, прежде чем вести в нём занятия.