Files
sless/doc/sqs-operator-plan.md
T

15 KiB
Raw Blame History

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

Клиент использует:

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

// 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:

func GenerateElasticMQConfig(tenantID, externalHost string, persistence bool) string

Содержимое конфига:

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

// 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

Структура:

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:

//+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 на каждого тенанта:

# 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:
SQSExternalHost string  // env SQS_EXTERNAL_HOST, default: "sqs.kube5s.ru"
  1. В main.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:

{
  "name": "my-queues",
  "memoryMB": 64,
  "storageMB": 512,
  "persistence": true
}

GET response:

{
  "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:

SQS_EXTERNAL_HOST: sqs.kube5s.ru
SQS_ELASTICMQ_IMAGE: softwaremill/elasticmq-native:1.7.1

RBAC: обновить ClusterRole (или использовать make manifestsconfig/rbac/role.yaml).

Применить новый CRD:

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. Тест:
# Создать инстанс через 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 (отдельная репа)

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