From a22a37bef3ebb0265552203e9da600f21e7ab7a3 Mon Sep 17 00:00:00 2001 From: Naeel Date: Tue, 7 Apr 2026 13:24:40 +0300 Subject: [PATCH] =?UTF-8?q?doc:=20SQS=20Operator=20plan=20=E2=80=94=20Elas?= =?UTF-8?q?ticMQ=20per-tenant,=2010=20=D1=8D=D1=82=D0=B0=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/sqs-operator-plan.md | 426 +++++++++++++++++++++++++++++++++++++ doc/thinking/2026-04-07.md | 25 +++ 2 files changed, 451 insertions(+) create mode 100644 doc/sqs-operator-plan.md create mode 100644 doc/thinking/2026-04-07.md diff --git a/doc/sqs-operator-plan.md b/doc/sqs-operator-plan.md new file mode 100644 index 0000000..cccaffd --- /dev/null +++ b/doc/sqs-operator-plan.md @@ -0,0 +1,426 @@ +# SQS Operator — План реализации +# Дата: 2026-04-07 +# Агент: Claude Opus 4.6 + +## Цель +Managed SQS-совместимый сервис очередей сообщений. +Каждый тенант облачного провайдера получает изолированный инстанс (ElasticMQ). +Работает через стандартный AWS SDK (Go/Python/Java/JS) — меняется только endpoint. + +## Решения (согласованы с пользователем) +- **Модель**: инстанс на тенанта (Вариант A) — изоляция, падение одного не влияет на остальных +- **Backend**: ElasticMQ Native (GraalVM) — `softwaremill/elasticmq-native` +- **Routing**: path-based — `sqs.kube5s.ru/sqs/{tenant}/...` +- **Auth**: Bearer token (существующий у тенанта), абстрагирован для будущей замены на ЛК +- **Namespace**: существующий `sless-fn-{tenant}` — ElasticMQ pod рядом с функциями тенанта +- **DNS**: `sqs.kube5s.ru` → 185.247.187.147 (создано, резолвится) +- **Persistence**: H2 (встроенная в ElasticMQ), через PVC +- **Config**: `SQS_EXTERNAL_HOST` в ConfigMap оператора — настраиваемый хост (dev → prod) + +--- + +## Архитектура + +``` +Terraform "sless_queue_service" + → REST API (sless-operator :9090) + → POST /api/v1/queue-services + → создаёт CRD QueueService в K8s + → QueueServiceReconciler (контроллер в sless-operator) + → создаёт в namespace sless-fn-{tenant}: + - ConfigMap (elasticmq.conf) + - PVC (persistence H2) + - Deployment (ElasticMQ Native pod) + - Service (ClusterIP :9324) + - Secret (accessKey/secretKey для тенанта) + → Ingress на sqs.kube5s.ru/sqs/{tenant}/ → Service :9324 + → Status.Endpoint = https://sqs.kube5s.ru/sqs/{tenant} + → Status.Phase = Ready +``` + +Клиент использует: +```python +import boto3 +sqs = boto3.client(sqs, + endpoint_url=https://sqs.kube5s.ru/sqs/my-tenant, + aws_access_key_id=xxx, + aws_secret_access_key=yyy, + region_name=ru-msk-1) +queue = sqs.create_queue(QueueName=my-queue) +sqs.send_message(QueueUrl=queue[QueueUrl], MessageBody=hello) +``` + +--- + +## Этапы реализации + +### Этап 1: CRD QueueService +**Файл**: `api/v1alpha1/queueservice_types.go` + +```go +// QueueServiceSpec — желаемое состояние инстанса очередей тенанта +type QueueServiceSpec struct { + // TenantID — уникальный ID тенанта облачного провайдера + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9-]*[a-z0-9]$` + TenantID string `json:"tenantId"` + + // MemoryMB — лимит RAM для ElasticMQ (default: 64) + // +kubebuilder:default=64 + // +kubebuilder:validation:Minimum=32 + // +kubebuilder:validation:Maximum=1024 + MemoryMB int32 `json:"memoryMB,omitempty"` + + // StorageMB — размер PVC для H2 persistence (default: 512) + // +kubebuilder:default=512 + // +kubebuilder:validation:Minimum=128 + // +kubebuilder:validation:Maximum=10240 + StorageMB int32 `json:"storageMB,omitempty"` + + // Persistence — включить сохранение сообщений на диск (default: true) + // Если false — только in-memory, сообщения теряются при рестарте + // +kubebuilder:default=true + Persistence bool `json:"persistence"` +} + +// QueueServicePhase — фаза жизненного цикла инстанса +type QueueServicePhase string +const ( + QueueServicePhasePending QueueServicePhase = "Pending" + QueueServicePhaseProvisioning QueueServicePhase = "Provisioning" + QueueServicePhaseReady QueueServicePhase = "Ready" + QueueServicePhaseFailed QueueServicePhase = "Failed" + QueueServicePhaseDeleting QueueServicePhase = "Deleting" +) + +// QueueServiceStatus — наблюдаемое состояние +type QueueServiceStatus struct { + Phase QueueServicePhase `json:"phase,omitempty"` + Endpoint string `json:"endpoint,omitempty"` // https://sqs.kube5s.ru/sqs/{tenantId} + SecretName string `json:"secretName,omitempty"` // имя Secret с credentials + Message string `json:"message,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ReadyAt *metav1.Time `json:"readyAt,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="TenantID",type=string,JSONPath=`.spec.tenantId` +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.status.endpoint` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +type QueueService struct { ... } +type QueueServiceList struct { ... } +``` + +**Действия**: +1. Создать файл `api/v1alpha1/queueservice_types.go` +2. Добавить `init()` → `SchemeBuilder.Register(&QueueService{}, &QueueServiceList{})` +3. Запустить `make manifests` — сгенерирует CRD YAML + deepcopy +4. Применить CRD: `kubectl apply -f config/crd/bases/` + +--- + +### Этап 2: ElasticMQ Config Generator +**Файл**: `internal/sqs/elasticmq_config.go` + +Генерирует HOCON конфиг для ElasticMQ: +```go +func GenerateElasticMQConfig(tenantID, externalHost string, persistence bool) string +``` + +Содержимое конфига: +```hocon +include classpath("application.conf") +node-address { + protocol = https + host = {SQS_EXTERNAL_HOST} + port = 443 + context-path = "/sqs/{tenantID}" +} +rest-sqs { + enabled = true + bind-port = 9324 + bind-hostname = "0.0.0.0" + sqs-limits = strict +} +messages-storage { + enabled = {persistence} // true/false + uri = "jdbc:h2:/data/elasticmq" +} +aws { + region = ru-msk-1 + accountId = {tenantID} +} +``` + +--- + +### Этап 3: Credentials Generator +**Файл**: `internal/sqs/credentials.go` + +```go +// GenerateSQSCredentials — создаёт пару accessKey/secretKey для тенанта. +// accessKey: SQSAK{tenantID}_{random8} +// secretKey: crypto/rand 32 bytes → base64 +func GenerateSQSCredentials(tenantID string) (accessKey, secretKey string, err error) +``` + +--- + +### Этап 4: Controller +**Файл**: `controllers/queueservice_controller.go` + +Структура: +```go +type QueueServiceReconciler struct { + client.Client + Scheme *runtime.Scheme + KubeClient kubernetes.Interface + SQSExternalHost string // из env SQS_EXTERNAL_HOST + Log logr.Logger +} +``` + +Reconcile loop: +``` +1. GET QueueService CR +2. IF deleting: + a. Delete Deployment sqs-{tenantId} + b. Delete Service sqs-svc-{tenantId} + c. Delete ConfigMap sqs-cfg-{tenantId} + d. Delete Secret sqs-creds-{tenantId} + e. НЕ удалять PVC (данные сохраняются, удаляются вручную) + f. Remove finalizer sless.kube5s.ru/sqs-finalizer + g. RETURN +3. IF no finalizer → add finalizer, set Phase=Pending +4. IF Phase=Pending: + a. Ensure namespace sless-fn-{tenantId} exists + b. Generate credentials → create Secret sqs-creds-{tenantId} + c. Generate elasticmq.conf → create ConfigMap sqs-cfg-{tenantId} + d. Create PVC sqs-data-{tenantId} (StorageMB) + e. Set Phase=Provisioning, requeue +5. IF Phase=Provisioning: + a. Create/Update Deployment sqs-{tenantId}: + - image: softwaremill/elasticmq-native:1.7.1 + - container port: 9324 + - volumeMounts: + - sqs-cfg-{tenantId} → /opt/elasticmq/custom.conf (subPath) + - sqs-data-{tenantId} → /data + - env: JAVA_TOOL_OPTIONS=-Dconfig.file=/opt/elasticmq/custom.conf + - resources: requests 10m/32Mi, limits 500m/{MemoryMB}Mi + - readinessProbe: httpGet /health :9324 (period: 5s) + - livenessProbe: httpGet /health :9324 (period: 10s) + b. Create Service sqs-svc-{tenantId} → port 9324 + c. Check: is Deployment Ready? (availableReplicas >= 1) + - No → requeue after 3s + - Yes → set Phase=Ready, Endpoint, ReadyAt +6. IF Phase=Ready: + a. Check Deployment health (availableReplicas) + b. If unhealthy → Phase=Failed + Message +7. IF Phase=Failed: + a. Check if Deployment recovered → Phase=Ready + b. Else requeue after 30s +``` + +RBAC markers: +```go +//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=queueservices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=queueservices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=sless.kube5s.ru,resources=queueservices/finalizers,verbs=update +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch;create +``` + +SetupWithManager — watch QueueService, own Deployment/Service/ConfigMap/Secret/PVC. + +--- + +### Этап 5: Ingress +**Подход**: один Ingress на `sqs.kube5s.ru` с path-based routing. + +Варианты: +A) Контроллер создаёт отдельный Ingress на каждого тенанта: +```yaml +# Ingress sqs-ing-{tenantId} в ns sless-fn-{tenantId} +spec: + rules: + - host: sqs.kube5s.ru + http: + paths: + - path: /sqs/{tenantId} + pathType: Prefix + backend: + service: + name: sqs-svc-{tenantId} + port: 9324 + tls: + - hosts: [sqs.kube5s.ru] + secretName: sqs-kube5s-ru-tls +``` + +B) Один Ingress + nginx rewrite в оператор, оператор проксирует. + +**Рекомендация**: вариант A — по Ingress на тенанта. Nginx Ingress Controller мержит +правила автоматически. + +RBAC добавить: `networking.k8s.io/ingresses` + +--- + +### Этап 6: Регистрация в main.go + +1. Добавить в `internal/config/config.go`: +```go +SQSExternalHost string // env SQS_EXTERNAL_HOST, default: "sqs.kube5s.ru" +``` + +2. В `main.go` — зарегистрировать контроллер: +```go +if err = (&controllers.QueueServiceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + KubeClient: kubernetes.NewForConfigOrDie(mgr.GetConfig()), + SQSExternalHost: cfg.SQSExternalHost, +}).SetupWithManager(mgr); err != nil { + log.Error("unable to create controller", "controller", "QueueService", "err", err) + os.Exit(1) +} +``` + +--- + +### Этап 7: REST API Handlers +**Файл**: `internal/api/handler/queueservice_handler.go` + +Эндпоинты: +``` +POST /api/v1/queue-services — создать QueueService CR +GET /api/v1/queue-services — список QueueService для тенанта (по namespace) +GET /api/v1/queue-services/{name} — статус конкретного инстанса +DELETE /api/v1/queue-services/{name} — удалить QueueService CR +``` + +POST body: +```json +{ + "name": "my-queues", + "memoryMB": 64, + "storageMB": 512, + "persistence": true +} +``` + +GET response: +```json +{ + "name": "my-queues", + "phase": "Ready", + "endpoint": "https://sqs.kube5s.ru/sqs/my-tenant", + "accessKey": "SQSAKmy-tenant_a1b2c3d4", + "secretKey": "...", + "createdAt": "2026-04-07T13:00:00Z" +} +``` + +**Auth**: существующий middleware (Bearer token → namespace mapping). + +Добавить routes в `internal/api/router.go`. + +--- + +### Этап 8: Deployment +**Файл**: `deployments/k8s/operator.yaml` + +Добавить в ConfigMap: +```yaml +SQS_EXTERNAL_HOST: sqs.kube5s.ru +SQS_ELASTICMQ_IMAGE: softwaremill/elasticmq-native:1.7.1 +``` + +RBAC: обновить ClusterRole (или использовать `make manifests` → `config/rbac/role.yaml`). + +Применить новый CRD: +```bash +kubectl apply -f config/crd/bases/sless.kube5s.ru_queueservices.yaml +``` + +--- + +### Этап 9: Сборка + Деплой + Тест +1. `make manifests` — генерация CRD + RBAC +2. `go build -o bin/sless-operator .` → Docker build → push +3. `kubectl apply -f deployments/k8s/operator.yaml` +4. Тест: +```bash +# Создать инстанс через API +curl -X POST https://sless.kube5s.ru/api/v1/queue-services \ + -H "Authorization: Bearer $TOKEN" \ + -d memoryMB:64 + +# Дождаться Ready +curl https://sless.kube5s.ru/api/v1/queue-services/test-qs \ + -H "Authorization: Bearer $TOKEN" + +# Проверить SQS API через AWS CLI +aws sqs create-queue \ + --queue-name test-queue \ + --endpoint-url https://sqs.kube5s.ru/sqs/test-tenant \ + --region ru-msk-1 + +aws sqs send-message \ + --queue-url https://sqs.kube5s.ru/sqs/test-tenant/queue/test-queue \ + --message-body "hello from managed SQS" \ + --endpoint-url https://sqs.kube5s.ru/sqs/test-tenant \ + --region ru-msk-1 +``` + +--- + +### Этап 10: Terraform Resource (отдельная репа) +```hcl +resource "sless_queue_service" "main" { + name = "production-queues" + memory_mb = 128 + storage_mb = 1024 +} + +output "sqs_endpoint" { + value = sless_queue_service.main.endpoint +} +output "sqs_access_key" { + value = sless_queue_service.main.access_key + sensitive = true +} +``` + +--- + +## Файловая карта (новые файлы) + +| # | Файл | Назначение | +|---|------|-----------| +| 1 | `api/v1alpha1/queueservice_types.go` | CRD types | +| 2 | `internal/sqs/elasticmq_config.go` | Генератор HOCON конфига | +| 3 | `internal/sqs/credentials.go` | Генератор accessKey/secretKey | +| 4 | `controllers/queueservice_controller.go` | Reconciler | +| 5 | `internal/api/handler/queueservice_handler.go` | REST API handlers | +| 6 | `internal/api/router.go` | Добавить routes (модификация) | +| 7 | `internal/config/config.go` | Добавить SQSExternalHost (модификация) | +| 8 | `main.go` | Регистрация контроллера (модификация) | +| 9 | `deployments/k8s/operator.yaml` | ConfigMap + RBAC (модификация) | + +## Зависимости (go.mod) +- Новых зависимостей НЕТ. Всё уже есть: controller-runtime, client-go, kubernetes. + +## Открытые вопросы (для будущего) +- Auth sidecar (AWS Signature V4) — пока Bearer token, потом если надо +- Мониторинг (Prometheus metrics per tenant) — после MVP +- Autoscaling (вертикальный — увеличить memory по нагрузке) — после MVP +- Backup/restore PVC — после MVP diff --git a/doc/thinking/2026-04-07.md b/doc/thinking/2026-04-07.md new file mode 100644 index 0000000..6cdff27 --- /dev/null +++ b/doc/thinking/2026-04-07.md @@ -0,0 +1,25 @@ + +--- +# Агент: Claude Opus 4.6 | SQS Operator Planning + +## Контекст +Пользователь попросил создать managed SQS-совместимый сервис очередей для облачного провайдера. + +## Анализ вариантов backend +1. **ElasticMQ** — Scala, 2.8k stars, persistence (H2), SQS API, native image (GraalVM), Apache-2.0, активная разработка (v1.7.1 — 2 дня назад). ПОБЕДИТЕЛЬ. +2. **GoAWS** — Go, 834 stars, НЕТ persistence, только in-memory. Не production. +3. **LocalStack** — Python, 56k stars, эмуляция всего AWS. Избыточно. + +## Анализ архитектуры +- **Вариант A (инстанс на тенанта)** — выбран. Изоляция, отказоустойчивость, ~30MB RAM idle. +- **Вариант B (shared)** — отклонён: "если наебнётся — то у всех". + +## Ключевые решения +- Routing: path-based `sqs.kube5s.ru/sqs/{tenant}/` (не subdomain — не нужен wildcard DNS) +- Auth: Bearer token (существующий), абстрагирован для замены на ЛК +- Namespace: `sless-fn-{tenant}` (уже создаётся для serverless функций) +- Config: `SQS_EXTERNAL_HOST` — настраиваемый, для смены при деплое в облако +- DNS: создана A-запись `sqs.kube5s.ru` → 185.247.187.147 + +## Результат +Создан подробный план `doc/sqs-operator-plan.md` — 10 этапов, 9 файлов, готов для реализации Sonnet.