КДПВ: NORA – реестр артефактов на Rust
КДПВ: NORA реестр артефактов на Rust

Нужен был реестр артефактов. Показать студентам цепочку поставки софта: сборка, тесты, push в реестр, деплой. Стандартная задача, казалось бы. "Вошли и вышли, приключение на 20 минут."

Растянулось на несколько месяцев.

В итоге написал свой реестр. Один бинарник. 7 форматов. 12 МБ RAM. Без базы данных.

Кладбище альтернатив

Nexus. Администрировал его годами на разных площадках. Знаю хорошо, где blob store хранит метаданные, как не убить OrientDB после апгрейда, а если окирпичилась – как откачать, куда смотреть, когда cleanup policy молча сожрала нужный артефакт.

С версии 3.71 Sonatype выкинули OrientDB и переделали линейку: Core (OSS, EPL, H2 embedded), Community (бесплатная, PostgreSQL, проприетарная лицензия, лимит 200K запросов/день), Pro (HA, replication, SSO). Миграция между версиями – это то ещё отдельное извраприключение.

4 гига RAM на Java-процесс. Образ 600 МБ.

Artifactory. OSS-версия формально существует (Apache 2.0), но поддерживает только Maven/Gradle/Ivy. Docker, npm, PyPI, Helm – всё платное. А платное начинается со слов «свяжитесь с отделом продаж». «Хьюстон, у нас проблема!» – «Окей, мы вас вычёркиваем.»

Harbor. Контейнерный реестр – Docker-образы, Helm charts, OCI-артефакты. npm, Maven, PyPI не завезли. PostgreSQL + Redis + 2 ГБ минимум и вот это всё.

GitLab Container Registry. Попробовал – завелось. А потом начался цирк. OCI artifacts и cosign формально работают, но через обходные решения. Buildx cache? Ставь provenance=false, иначе ломается. Мультиарх? С оговорками. Каждый нестандартный сценарий опять отдельный квест. Если бы мне платили за каждый квест...

В какой-то момент посчитал: я трачу больше времени на костыли, чем потратил бы на свой реестр.

Как появилась NORA

У меня дома жил бурундук. Маленький, шустрый, тащит всё к себе в нору. Когда понадобилось название для реестра – ну, вы поняли.

Задача на старте была скромная: Docker Registry v2, один бинарник, без внешних сервисов. Чтобы человек сделал docker run, получил рабочий реестр и через 3 секунды пушил образы.

Первая версия заработала. Push, pull, delete, каталог, теги – спецификация покрыта. А дальше случилось то, что случается с каждым «скромным» pet-проектом.

NORA написана на Rust, а крейты нужно где-то хранить. Поднимать отдельный Cargo registry ради этого не хотелось – проще дописать поддержку прямо в NORA. Sparse index по RFC 2789, config.json, cargo publish – готово.

Два формата есть. npm, Maven, PyPI? Аппетит приходит во время еды. У каждого формата свои сюрпризы. Cargo: RFC говорит «отдавай JSON по GET». На деле – 4 паттерна URL и молчаливый «crate not found». Сидишь, разговариваешь с tcpdump о вечном. Но каждый следующий формат добавлялся быстрее – архитектура настоялась.


Сейчас поддерживается 7 типов:

Реестр

Что хранит

Путь

Docker v2 + OCI

Образы, Helm charts

/v2/

Maven

JAR, WAR, POM

/maven/

npm

Node.js-пакеты

/npm/

PyPI

Python-пакеты

/pypi/

Cargo

Rust-крейты

/cargo/

Go

Go-модули

/go/

Raw

Что угодно

/raw/

Без базы данных

Нет внешней БД. Ни PostgreSQL, ни Redis, ни Elasticsearch – ничего рядом не крутится. Метаданные лежат на файловой системе рядом с артефактами.

Небольшая команда, один CI/CD, сотни артефактов – скрипач не нужен. Файловая система на SSD справляется. Каталог образов собирается обходом директории, метаданные читаются как JSON с диска.

Не надо поднимать Postgres, создавать базу, накатывать миграции.

Консистентность обеспечивается атомарным переименованием. S3-бэкенд работает. БД и HA , когда дойдёт до enterprise. Пока отлаживаем фундамент.

nora (34 МБ, Rust, 450+ тестов)
├── /v2/       — Docker Registry v2 + OCI (образы, Helm charts)
├── /maven/    — Maven (JAR, WAR, POM)
├── /npm/      — npm (Node.js-пакеты)
├── /pypi/     — PyPI (Python-пакеты)
├── /cargo/    — Cargo (Rust-крейты, sparse index RFC 2789)
├── /go/       — Go Modules
├── /raw/      — Raw (что угодно)
├── /metrics   — Prometheus
├── /health    — K8s liveness probe
├── /ready     — K8s readiness probe
├── /api-docs  — Swagger
└── /data/     — хранилище (FS или S3)

Ноль управления через веб

Кто-то зашёл в админку Nexus, поправил настройки проксирующего репозитория, переключил layout, добавил routing rule – и ни одна живая душа не знает, что изменилось. По логам восстановить, кто вчера сломал docker-group, можно, но, честно говоря, сомнительное удовольствие.

В NORA нет админки для изменения конфигурации. Совсем. Вся настройка – переменные окружения или config.toml. Нужен новый upstream для Docker? Поменял NORA_DOCKER_UPSTREAMS, перезапустил. Изменение попадает в git, в историю деплоя, в audit trail CI/CD.

Web UI есть, но read-only: просмотр артефактов, поиск, статистика. Как говорится, рыбов показываем, но не продаём – конфигурация только через файлы и CLI.

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

Аутентификация поддерживает два механизма:

htpasswd – классика из мира Apache/nginx. Файл с хешами, работает из коробки.

API-токены с RBAC. Три роли: read, write, admin. Токены отзываемые. Утёк ключ CI-раннера – отозвал за секунду. В Nexus есть локальные роли, но для нормального RBAC всё равно тянут LDAP.

Режим anonymous read: pull без авторизации, push с токеном.

TLS вешаю на reverse proxy (nginx, Caddy, Traefik) – стандартная схема. NORA слушает чистый HTTP.

Что ещё внутри

Логи. Структурированный JSON: метод, путь, статус, время, пользователь. Льёшь в Loki, OpenSearch, ClickHouse – видишь, кто, что, когда пушил. В Nexus для этого придётся ковырять access log + audit log + парсинг на коленке.

Метрики и ops. /metrics — Prometheus из коробки. /health, /ready — K8s probes. /api-docs — Swagger. Один scrape_config в Prometheus и дашборд готов.

Rate limiting. По эндпоинтам, через переменные окружения. 50 pipeline'ов одновременно дёргают docker pull и никто не страдает.

Audit log. Лог действий в формате JSONL: кто, что, когда. Пишется на диск, изменить задним числом нельзя.

GC. nora gc --dry-run – покажет осиротевшие Docker-блобы. Без --dry-run подчистит. Для остальных форматов пока в roadmap.


Бэкап. nora backup -o backup.tar.gz – один файл, полная копия. nora restore для восстановления. Ну а бэкап Nexus – это blob store + БД + надежда, что всё сойдётся.


"Отечественные"-сборки. Docker-образы на базе Astra Linux SE и RED OS в каждом релизе. Для площадок, где без бумажки ты никто.

Цифры

NORA

Nexus OSS

Artifactory

Docker-образ

34 МБ

600+ МБ

1+ ГБ

Холодный старт

~3 сек

30-60 сек

30-60 сек

RAM

12 МБ

2-4 ГБ

2-4 ГБ

Язык

Rust

Java

Java

Внешние сервисы

0

H2 (Core) / PostgreSQL (Community/Pro)

PostgreSQL + Tomcat

Лицензия

MIT

EPL-1.0 (Core) / Proprietary (Community, Pro)

Apache 2.0 (OSS, только Maven) / Proprietary

Форматов

7

20+

30+

Под нагрузкой

VM с двумя ядрами, 4 ГБ RAM, Proxmox. docker pull образа 268 МБ за 6 секунд, docker push за 19 секунд. Push и pull одновременно не мешают друг другу. 0 ошибок, 0 таймаутов. RAM под нагрузкой в районе 250 МБ (idle 12 МБ).

Вектор развития

v0.5 (сейчас)

v1.0 (ближе к лету)

GC вручную (nora gc)

Online GC, политики хранения

Single-node

Multi-node с distributed locking

7 форматов

+ NuGet, RubyGems, Conan

htpasswd + API-токены

Управление токенами через веб

Пока нет replication, LDAP/OIDC, NuGet и RubyGems.

Поднять


docker run -d \
  -p 8080:8080 \
  -v nora-data:/data \
  getnora/nora:0.5.0

Всё. Реестр на порту 8080. Веб-интерфейс, API, все 7 реестров.

Docker login + push

docker login localhost:8080 -u admin
docker tag myapp:latest localhost:8080/myapp:latest
docker push localhost:8080/myapp:latest

npm publish

npm config set registry http://localhost:8080/npm/
npm publish

pip install

pip install --index-url http://localhost:8080/pypi/simple/ mypackage

mvn deploy

mvn deploy -DaltDeploymentRepository=nora::default::http://localhost:8080/maven/

Прокси к upstream

Не только Docker Hub. NORA проксирует запросы ко всем основным источникам: Docker Hub, npmjs.org, Maven Central, pypi.org, proxy.golang.org. Образа или пакета нет локально – NORA сходит в upstream, скачает, закеширует, отдаст. Следующий запрос уже из кеша.

docker pull localhost:8080/nginx:latest        # → Docker Hub
npm install express --registry http://localhost:8080/npm/  # → npmjs.org

Для приватных upstream предусмотрен Basic Auth через конфиг.

Закрытый контур

Типовая схема: внешний контур (DMZ) с раннером, который тянет артефакты из публичных реестров, проверяет, например, по методике ФСТЭК (целостность, подпись, сканирование и др.), и перекладывает во внутренний контур. Стандартный подход – bash-скрипт на 200 строк, у каждой команды свой велосипед.

В NORA это встроено. Две инсталляции – внешняя и внутренняя – и команда переноса:

# Внешний контур: зеркалирование из upstream
nora mirror --format docker --packages "nginx:latest,redis:7" --output ./bundle

# Раннер переносит bundle во внутренний контур

# Внутренний контур: восстановление
nora restore --input ./bundle

Зеркалирование работает для Docker, npm, PyPI, Maven, Cargo. Целостность при restore проверяется по content digest (sha256).

Где мы сейчас

- Changelog | Compatibility matrix

- В Awesome Docker

- Работает в продакшене: CI/CD сборки моих проектов, учебные кластеры на Proxmox – docker push/pull, npm, maven ежедневно

- NORA хранит собственные Docker-образы и Rust-крейт nora-registry, CI тянет зависимости через NORA как proxy к crates.io

Демо: demo.getnora.io

Исходники: github.com/getnora-io/nora

Баги и пожеланияв Issues.