# GATEWAY Service — Path-based DNS-less Router > **Репозиторий:** `gateway` (шаблонный, создан 2026-07-20) > **URL:** `https://gateway.nodejsk8s.services.ngcloud.ru/` > **Назначение:** Замена всех DNS-записей `*.kube5s.ru` единым path-based роутером > **Статус:** СПЛАНИРОВАНО, НЕ НАЧАТО --- ## 1. ЗАЧЕМ ЭТО НУЖНО ### Проблема Сейчас каждый сервис живёт на своём поддомене `*.kube5s.ru`: | Поддомен | Сервис | Ingress | TLS-сертификат | |----------|--------|---------|-----------------| | `registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru -->` | Terraform Registry | отдельный | отдельный | | `something1.kube5s.ru` | Сервис 1 | отдельный | отдельный | | `something2.kube5s.ru` | Сервис 2 | отдельный | отдельный | | ... | ... | ... | ... | **Проблемы:** - Домен `kube5s.ru` — **срочный**, скоро истекает - Продление — **дорого** - Каждый поддомен = DNS-запись + Ingress + сертификат — **администрировать заебало** ### Решение **Один домен, один Ingress, один сертификат.** Все сервисы — под path-префиксами: ``` https://gateway.nodejsk8s.services.ngcloud.ru/record01/* → Сервис 1 https://gateway.nodejsk8s.services.ngcloud.ru/record02/* → Сервис 2 https://gateway.nodejsk8s.services.ngcloud.ru/record03/* → Сервис 3 ``` Домен `gateway.nodejsk8s.services.ngcloud.ru` — **облачный managed service**, назначается облаком, не требует покупки/продления. ### Выгода | Было (DNS-based) | Стало (Path-based) | |---|---| | N DNS-записей | 0 дополнительных DNS-записей | | N Ingress'ов | 1 Ingress | | N TLS-сертификатов | 1 TLS-сертификат | | Домен `kube5s.ru` нужен | Домен `kube5s.ru` не нужен | --- ## 2. АРХИТЕКТУРА ```mermaid graph TD subgraph "Внешний мир" T[Terraform CLI] C[Клиенты API] end subgraph "Kubernetes (облако Nubes)" I["Ingress
gateway.nodejsk8s.services.ngcloud.ru
(cert-manager + LE)"] G["GATEWAY Service
(NodeJS: Express + http-proxy-middleware)"] I --> G G -->|"/record01/*"| R1["registry-server
(Terraform Registry)"] G -->|"/record02/*"| R2["Сервис 2
(будущий)"] G -->|"/record03/*"| R3["Сервис 3
(будущий)"] G -->|"/"| FALLBACK["Fallback / health-check"] end T -->|"source = gateway...nubes.ru/nubes/nubes"| I ``` ### Как это работает (для Terraform Registry) 1. Terraform читает `.tf`: ```hcl source = "gateway.nodejsk8s.services.ngcloud.ru/nubes-test/nubes" ``` 2. Terraform идёт в: ``` GET https://gateway.nodejsk8s.services.ngcloud.ru/.well-known/terraform.json ``` 3. GATEWAY проксирует на registry-server, который отвечает: ```json { "providers.v1": "/record01/v1/providers/" } ``` 4. Terraform дальше сам идёт по `/record01/v1/providers/nubes-test/nubes/versions` 5. Registry-server генерит download-url с префиксом `/record01/v1/proxy?...` 6. Profit. Terraform скачивает провайдер. --- ## 3. КОНФИГУРАЦИЯ GATEWAY ### Формат конфига (`config.yaml` или env-переменные) ```yaml # gateway/config.yaml routes: - id: "registry" prefix: "/record01" description: "Terraform Provider Registry" upstream: "http://registry-server.terra.svc.cluster.local:8080" stripPrefix: false # НЕ отрезать /record01 — registry сам знает свой префикс timeout: 30000 # 30s (registry может долго листить S3) - id: "future-service-2" prefix: "/record02" description: "Будущий сервис — TODO" upstream: "http://service2.namespace.svc.cluster.local:8080" stripPrefix: true # Отрезать /record02 перед отправкой в upstream timeout: 10000 - id: "future-service-3" prefix: "/record03" description: "Будущий сервис — TODO" upstream: "http://service3.namespace.svc.cluster.local:3000" stripPrefix: true timeout: 10000 ``` ### Конфиг через env (альтернатива, проще для K8s) ```bash # Формат: ROUTE_XX_ID, ROUTE_XX_PREFIX, ROUTE_XX_UPSTREAM, ROUTE_XX_STRIP, ROUTE_XX_TIMEOUT ROUTE_01_ID=registry ROUTE_01_PREFIX=/record01 ROUTE_01_UPSTREAM=http://registry-server.terra.svc.cluster.local:8080 ROUTE_01_STRIP=false ROUTE_01_TIMEOUT=30000 ROUTE_02_ID=future-svc ROUTE_02_PREFIX=/record02 ROUTE_02_UPSTREAM=http://service2.namespace.svc.cluster.local:8080 ROUTE_02_STRIP=true ROUTE_02_TIMEOUT=10000 ``` ### Важно: `stripPrefix` | Режим | Когда использовать | |-------|--------------------| | `stripPrefix: false` | Upstream **знает** о своём префиксе и сам его обрабатывает (как registry-server с `PATH_PREFIX=/record01`) | | `stripPrefix: true` | Upstream **не знает** о префиксе — GATEWAY отрезает его перед проксированием. `/record01/api/users` → `/api/users` | --- ## 4. КОД GATEWAY (план) ### Стек - **NodeJS** + **Express** — HTTP-фреймворк - **http-proxy-middleware** — реверс-прокси - **Dotenv / js-yaml** — конфигурация ### Структура проекта ``` gateway/ ├── package.json ├── server.js # Главный файл ├── config/ │ ├── routes.yaml # Конфиг маршрутов │ └── index.js # Загрузчик конфига ├── lib/ │ ├── proxy.js # Фабрика proxy-middleware'ов из конфига │ ├── health.js # Health/readiness probes │ └── logger.js # Логирование ├── Dockerfile ├── .dockerignore └── README.md ``` ### `server.js` (псевдокод) ```javascript const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const { loadRoutes } = require('./config'); const { healthHandler, readyHandler } = require('./lib/health'); const app = express(); const routes = loadRoutes(); // из env или routes.yaml // Health probes (без прокси) app.get('/healthz', healthHandler); app.get('/readyz', readyHandler); // Root — информационная страница app.get('/', (req, res) => { res.json({ service: 'gateway', version: process.env.APP_VERSION || '1.0.0', routes: routes.map(r => ({ prefix: r.prefix, description: r.description })) }); }); // Динамическая регистрация прокси-маршрутов for (const route of routes) { const proxy = createProxyMiddleware({ target: route.upstream, changeOrigin: true, pathRewrite: route.stripPrefix ? { [`^${route.prefix}`]: '' } : undefined, timeout: route.timeout, onError: (err, req, res) => { console.error(`[${route.id}] Proxy error:`, err.message); res.status(502).json({ error: 'Upstream unavailable', route: route.id }); } }); app.use(route.prefix, proxy); console.log(`Route: ${route.prefix} → ${route.upstream} (strip: ${route.stripPrefix})`); } // 404 для всего остального app.use((req, res) => { res.status(404).json({ error: 'Not Found', path: req.path }); }); app.listen(process.env.PORT || 3000); ``` --- ## 5. КАК ДОБАВИТЬ НОВЫЙ СЕРВИС (RECORD) ### Шаг 1: Создать сервис в K8s ```yaml # Пример: новый сервис my-api apiVersion: v1 kind: Service metadata: name: my-api namespace: default spec: ports: - port: 8080 selector: app: my-api ``` ### Шаг 2: Добавить route в конфиг gateway ```yaml # Добавить в config/routes.yaml: - id: "my-api" prefix: "/record04" description: "Мой новый API" upstream: "http://my-api.default.svc.cluster.local:8080" stripPrefix: true timeout: 10000 ``` ### Шаг 3: Передеплоить gateway ```bash kubectl rollout restart deployment/gateway -n terra ``` ### Шаг 4: Готово Сервис доступен по адресу: ``` https://gateway.nodejsk8s.services.ngcloud.ru/record04/ ``` **Никаких DNS, Ingress'ов, сертификатов.** Только конфиг gateway. --- ## 6. ЧТО НУЖНО ПОМЕНЯТЬ В REGISTRY-SERVER Файл: `~/tf_registry/server/main.go` ### Добавить env-переменную ```go var pathPrefix = os.Getenv("PATH_PREFIX") // "/record01" или "" (пустая строка = без префикса, обратная совместимость) ``` ### Изменить `main()` — регистрация handlers ```go // БЫЛО: http.HandleFunc("/.well-known/terraform.json", discoveryHandler) http.HandleFunc("/v1/providers/", router) http.HandleFunc("/v1/proxy", proxyHandler) http.HandleFunc("/docs/", docsHandler) // СТАЛО: http.HandleFunc(pathPrefix+"/.well-known/terraform.json", discoveryHandler) http.HandleFunc(pathPrefix+"/v1/providers/", router) http.HandleFunc(pathPrefix+"/v1/proxy", proxyHandler) http.HandleFunc(pathPrefix+"/docs/", docsHandler) ``` ### Изменить `discoveryHandler()` — ответ с префиксом ```go // БЫЛО: json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"}) // СТАЛО: json.NewEncoder(w).Encode(Discovery{ProvidersV1: pathPrefix + "/v1/providers/"}) ``` ### Изменить `downloadVersion()` — ссылки с префиксом ```go // БЫЛО: baseURL := "https://" + hostname downloadLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(fullKey)) // СТАЛО: baseURL := "https://" + hostname + pathPrefix downloadLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(fullKey)) ``` ### Изменить `listVersions()` — S3-префикс ```go // БЫЛО: prefix := fmt.Sprintf("%s/%s/%s/", hostname, namespace, pType) // СТАЛО (hostname теперь = gateway.nodejsk8s.services.ngcloud.ru): // Если PATH_PREFIX непустой, можно использовать его как основу для S3-пути, // либо продолжать использовать hostname. Зависит от того, как лежат файлы в S3. // Пока оставляем hostname — он просто поменяется на новый домен. prefix := fmt.Sprintf("%s/%s/%s/", hostname, namespace, pType) ``` ### Новые env-переменные для registry-server | Переменная | Было | Стало | |------------|------|-------| | `REGISTRY_HOSTNAME` | `terra.k8c.ru` / `registry.kube5s.ru ` | `gateway.nodejsk8s.services.ngcloud.ru` | | `PATH_PREFIX` | — | `/record01` | --- ## 7. КАК ЭТО ВЫГЛЯДИТ ДЛЯ КЛИЕНТОВ (terraform init) ### Было ```hcl terraform { required_providers { nubes = { source = "registry.kube5s.ru /nubes-test/nubes" version = "5.1.13" } } } ``` ### Стало ```hcl terraform { required_providers { nubes = { source = "gateway.nodejsk8s.services.ngcloud.ru/nubes-test/nubes" version = "5.1.13" } } } ``` **Всё.** Terraform сам разберётся: 1. Пойдёт на `.well-known/terraform.json` 2. Получит путь `/record01/v1/providers/` 3. Скачает провайдер через `/record01/v1/proxy?...` --- ## 8. ПЛАН ВНЕДРЕНИЯ ### Фаза 1: Подготовка GATEWAY - [ ] Склонировать шаблонный репо - [ ] Написать `server.js` (прокси-роутер) - [ ] Написать `config/routes.yaml` с первым роутом (`/record01` → registry) - [ ] Dockerfile - [ ] Helm chart / K8s manifests (Deployment, Service) - [ ] Ingress: `gateway.nodejsk8s.services.ngcloud.ru` → gateway-service - [ ] TLS через cert-manager ### Фаза 2: Подготовка Registry-Server - [ ] Добавить `PATH_PREFIX` в `server/main.go` - [ ] Изменить `REGISTRY_HOSTNAME` на `gateway.nodejsk8s.services.ngcloud.ru` - [ ] Собрать новый Docker-образ - [ ] Задеплоить registry-server с новыми env - [ ] Убедиться что registry отвечает на `/record01/.well-known/terraform.json` - [ ] Убедиться что download-url содержат `/record01/` ### Фаза 3: Тестирование - [ ] `curl https://gateway.nodejsk8s.services.ngcloud.ru/record01/.well-known/terraform.json` → OK - [ ] `curl https://gateway.nodejsk8s.services.ngcloud.ru/record01/v1/providers/nubes-test/nubes/versions` → OK - [ ] `terraform init` с новым source → OK ### Фаза 4: Миграция клиентов - [ ] Обновить `provider/main.go` — `address` по умолчанию на новый домен - [ ] Обновить profile.env (dev/test/prod) — `REGISTRY_HOSTNAME` - [ ] Обновить все `.tf` файлы в `PROD_STAND/`, `TEST_STAND/`, `DEV_STAND/` - [ ] Обновить CI/CD (`.github/workflows/`, build scripts) - [ ] Старый `registry.kube5s.ru ` — 301 редирект или удалить ### Фаза 5: Добавление остальных сервисов - [ ] Выявить все текущие `*.kube5s.ru` (через grep по всем репо) - [ ] Для каждого: добавить route в gateway - [ ] Перенести сервис за gateway - [ ] Удалить старые DNS/Ingress/сертификаты --- ## 9. ТЕКУЩИЕ ЗАПИСИ `kube5s.ru` (НАЙТИ ВСЕ) ```bash # Выполнить чтобы найти все использования kube5s.ru: grep -r "kube5s\.ru" /home/naeel/ --include="*.{tf,yaml,yml,go,sh,env,js,json,md,cfg,ejs,html}" -l ``` **Известные на 2026-07-20:** | Запись | Сервис | Приоритет миграции | |--------|--------|--------------------| | `registry.kube5s.ru ` | Terraform Registry Server | **#1 (критично)** | | ??? | ??? | Найти grep'ом | --- ## 10. КОНФИГУРАЦИЯ KUBERNETES ### Deployment ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: gateway namespace: terra spec: replicas: 1 selector: matchLabels: app: gateway template: metadata: labels: app: gateway spec: containers: - name: gateway image: registry.kube5s.ru /gateway:1.0.0 ports: - containerPort: 3000 env: - name: PORT value: "3000" - name: APP_VERSION value: "1.0.0" # ROUTES - name: ROUTE_01_ID value: "registry" - name: ROUTE_01_PREFIX value: "/record01" - name: ROUTE_01_UPSTREAM value: "http://registry-server.terra.svc.cluster.local:8080" - name: ROUTE_01_STRIP value: "false" - name: ROUTE_01_TIMEOUT value: "30000" livenessProbe: httpGet: path: /healthz port: 3000 initialDelaySeconds: 5 readinessProbe: httpGet: path: /readyz port: 3000 initialDelaySeconds: 3 ``` ### Service ```yaml apiVersion: v1 kind: Service metadata: name: gateway namespace: terra spec: ports: - port: 80 targetPort: 3000 selector: app: gateway ``` ### Ingress ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gateway-ingress namespace: terra annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/proxy-body-size: "100m" spec: ingressClassName: nginx tls: - hosts: - gateway.nodejsk8s.services.ngcloud.ru secretName: gateway-tls rules: - host: gateway.nodejsk8s.services.ngcloud.ru http: paths: - path: / pathType: Prefix backend: service: name: gateway port: number: 80 ``` --- ## 11. ССЫЛКИ | Ресурс | URL | |--------|-----| | GATEWAY (production) | `https://gateway.nodejsk8s.services.ngcloud.ru/` | | Репозиторий GATEWAY | `~/tf_provider/gateway/` (создать) или отдельный репо | | Registry Server | `~/tf_registry/server/main.go` | | Terraform Provider | `~/tf_provider/provider/main.go` | | S3 (registry files) | `s3.msk-1.ngcloud.ru`, bucket `terraform-registry` | --- ## 12. КОНТРОЛЬНЫЙ СПИСОК ПРИ ДОБАВЛЕНИИ НОВОГО RECORD 1. [ ] Сервис задеплоен в K8s, есть Service 2. [ ] Сервис работает (проверить через `kubectl port-forward` или напрямую) 3. [ ] Определён `stripPrefix`: upstream знает о префиксе? (как registry) или нет? 4. [ ] Добавлен route в конфиг gateway 5. [ ] Gateway передеплоен 6. [ ] Проверен curl через gateway: `curl https://gateway...ngcloud.ru/recordXX/...` 7. [ ] Клиенты обновлены на новый URL --- *Документ создан 2026-07-20. Планирование завершено, реализация — позже.*