427 lines
15 KiB
Markdown
427 lines
15 KiB
Markdown
# 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
|