# 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