doc: SQS Operator plan — ElasticMQ per-tenant, 10 этапов
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user