deploy: EMQX, iot-operator, managed Postgres, Docker Hub image
- Добавлен EMQX deployment + WS ingress (адаптирован из sless) - Добавлен iot-operator deployment с RBAC (ServiceAccount, ClusterRole) - Postgres: заменён self-hosted на managed (dc5db45d namespace) - Image: naeel/iot-operator:v0.2.0 (Docker Hub) - Убран imagePullSecrets (Docker Hub публичный)
This commit is contained in:
+115
-115
@@ -13,131 +13,131 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
|
||||||
iotv1alpha1 "gitea.services.ngcloud.ru/Nail/IoT/api/v1alpha1"
|
iotv1alpha1 "gitea.services.ngcloud.ru/Nail/IoT/api/v1alpha1"
|
||||||
iotcontrollers "gitea.services.ngcloud.ru/Nail/IoT/controllers"
|
iotcontrollers "gitea.services.ngcloud.ru/Nail/IoT/controllers"
|
||||||
iotapi "gitea.services.ngcloud.ru/Nail/IoT/internal/api"
|
iotapi "gitea.services.ngcloud.ru/Nail/IoT/internal/api"
|
||||||
"gitea.services.ngcloud.ru/Nail/IoT/internal/api/handler"
|
"gitea.services.ngcloud.ru/Nail/IoT/internal/api/handler"
|
||||||
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var scheme = runtime.NewScheme()
|
var scheme = runtime.NewScheme()
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
// IoT API group iot.kube5s.ru/v1alpha1
|
// IoT API group iot.kube5s.ru/v1alpha1
|
||||||
utilruntime.Must(iotv1alpha1.AddToScheme(scheme))
|
utilruntime.Must(iotv1alpha1.AddToScheme(scheme))
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||||
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
|
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
|
||||||
|
|
||||||
log.Info("starting IoT managed service operator")
|
log.Info("starting IoT managed service operator")
|
||||||
|
|
||||||
// Controller Manager — управляет reconcile loop для IoTDevice CRD
|
// Controller Manager — управляет reconcile loop для IoTDevice CRD
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
HealthProbeBindAddress: ":8081",
|
HealthProbeBindAddress: ":8081",
|
||||||
LeaderElection: false,
|
LeaderElection: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("unable to create controller manager", "err", err)
|
log.Error("unable to create controller manager", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Регистрируем IoTDevice controller
|
// Регистрируем IoTDevice controller
|
||||||
if err = (&iotcontrollers.IoTDeviceReconciler{
|
if err = (&iotcontrollers.IoTDeviceReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
}).SetupWithManager(mgr); err != nil {
|
}).SetupWithManager(mgr); err != nil {
|
||||||
log.Error("unable to create IoTDevice controller", "err", err)
|
log.Error("unable to create IoTDevice controller", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health/ready пробы для k8s
|
// Health/ready пробы для k8s
|
||||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
log.Error("unable to set up health check", "err", err)
|
log.Error("unable to set up health check", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
log.Error("unable to set up ready check", "err", err)
|
log.Error("unable to set up ready check", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IoT Postgres — опционален (если IOT_PG_DSN не задан — телеметрия отключена)
|
// IoT Postgres — опционален (если IOT_PG_DSN не задан — телеметрия отключена)
|
||||||
iotPGStore, err := iotpg.NewFromEnv(log)
|
iotPGStore, err := iotpg.NewFromEnv(log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to init IoT Postgres", "err", err)
|
log.Error("failed to init IoT Postgres", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if iotPGStore != nil {
|
if iotPGStore != nil {
|
||||||
defer iotPGStore.Close()
|
defer iotPGStore.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// REST API handler
|
// REST API handler
|
||||||
h := &handler.Handler{
|
h := &handler.Handler{
|
||||||
K8s: mgr.GetClient(),
|
K8s: mgr.GetClient(),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
IoTPG: iotPGStore,
|
IoTPG: iotPGStore,
|
||||||
Log: log,
|
Log: log,
|
||||||
}
|
}
|
||||||
|
|
||||||
router := iotapi.NewRouter(h, log)
|
router := iotapi.NewRouter(h, log)
|
||||||
|
|
||||||
// HTTP API сервер на порту 9090
|
// HTTP API сервер на порту 9090
|
||||||
apiServer := &http.Server{
|
apiServer := &http.Server{
|
||||||
Addr: ":9090",
|
Addr: ":9090",
|
||||||
Handler: router,
|
Handler: router,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 60 * time.Second,
|
WriteTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем API сервер в горутине
|
// Запускаем API сервер в горутине
|
||||||
go func() {
|
go func() {
|
||||||
log.Info("starting REST API server", "addr", ":9090")
|
log.Info("starting REST API server", "addr", ":9090")
|
||||||
if err := apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Error("API server failed", "err", err)
|
log.Error("API server failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Запускаем controller-manager (блокирует до ctx.Done)
|
// Запускаем controller-manager (блокирует до ctx.Done)
|
||||||
go func() {
|
go func() {
|
||||||
log.Info("starting controller manager")
|
log.Info("starting controller manager")
|
||||||
if err := mgr.Start(ctx); err != nil {
|
if err := mgr.Start(ctx); err != nil {
|
||||||
log.Error("controller manager failed", "err", err)
|
log.Error("controller manager failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
log.Info("shutting down IoT operator")
|
log.Info("shutting down IoT operator")
|
||||||
|
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer shutdownCancel()
|
defer shutdownCancel()
|
||||||
if err := apiServer.Shutdown(shutdownCtx); err != nil {
|
if err := apiServer.Shutdown(shutdownCtx); err != nil {
|
||||||
log.Error("API server shutdown error", "err", err)
|
log.Error("API server shutdown error", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("IoT operator stopped")
|
fmt.Println("IoT operator stopped")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Создано: 2026-04-12
|
||||||
|
# MQTT over WebSocket через Ingress с TLS termination.
|
||||||
|
# Причина: порт 1883 заблокирован NSX-T Edge firewall на уровне облака.
|
||||||
|
# Решение: EMQX WebSocket listener (8083) проксируется через nginx-ingress с TLS.
|
||||||
|
#
|
||||||
|
# IoT устройство подключается: wss://iot.kube5s.ru/mqtt
|
||||||
|
# IoT Консоль (UI): https://iot.kube5s.ru/console
|
||||||
|
#
|
||||||
|
# DNS A-запись: iot.kube5s.ru → 185.247.187.147
|
||||||
|
# TLS: cert-manager + letsencrypt-prod, secret=iot-kube5s-ru-tls
|
||||||
|
#
|
||||||
|
# Применение: kubectl apply -f deployments/k8s/emqx-ws-ingress.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: emqx-ws
|
||||||
|
namespace: sless
|
||||||
|
# Отдельный Service — WebSocket порт для Ingress
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: emqx
|
||||||
|
ports:
|
||||||
|
- name: mqtt-ws
|
||||||
|
port: 8083
|
||||||
|
targetPort: 8083
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: emqx-mqtt-websocket
|
||||||
|
namespace: sless
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: nginx
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
# WebSocket: nginx-ingress добавляет Upgrade/Connection при proxy-http-version=1.1
|
||||||
|
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- iot.kube5s.ru
|
||||||
|
secretName: iot-kube5s-ru-tls
|
||||||
|
rules:
|
||||||
|
- host: iot.kube5s.ru
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
# MQTT over WebSocket — wss://iot.kube5s.ru/mqtt
|
||||||
|
- path: /mqtt
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: emqx-ws
|
||||||
|
port:
|
||||||
|
number: 8083
|
||||||
|
# IoT Консоль (UI) — https://iot.kube5s.ru/console
|
||||||
|
- path: /console
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: iot-operator
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Создано: 2026-04-12
|
||||||
|
# EMQX MQTT-брокер для IoT-сервиса (namespace: sless).
|
||||||
|
#
|
||||||
|
# Архитектура:
|
||||||
|
# IoT Device → MQTT CONNECT → EMQX (HTTP auth → iot-operator:9090/internal/mqtt/auth)
|
||||||
|
# EMQX → MQTT PUBLISH → iot-mqtt-bridge (paho subscriber) → shared-SQS queue iot-telemetry
|
||||||
|
# SQS → iot-sqs-consumer → Postgres
|
||||||
|
#
|
||||||
|
# EMQX 5.x конфиг через emqx.conf (HOCON формат), монтируется как ConfigMap volume.
|
||||||
|
#
|
||||||
|
# Порты:
|
||||||
|
# 1883 — MQTT (plaintext)
|
||||||
|
# 8083 — MQTT over WebSocket
|
||||||
|
# 18083 — EMQX Dashboard (admin/public по умолчанию — менять в prod!)
|
||||||
|
#
|
||||||
|
# Применение: kubectl apply -f deployments/k8s/emqx.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: emqx-config
|
||||||
|
namespace: sless
|
||||||
|
data:
|
||||||
|
# emqx.conf — HOCON конфиг для EMQX 5.5.x
|
||||||
|
# Раздел authentication: HTTP Backend для проверки MQTT credentials IoT-устройств.
|
||||||
|
# iot-operator ищет Secret iot-{deviceId} и сравнивает пароль.
|
||||||
|
emqx.conf: |
|
||||||
|
## EMQX 5.x configuration (HOCON format)
|
||||||
|
## Создано: 2026-04-12
|
||||||
|
|
||||||
|
## node — без них EMQX 5.x падает при старте
|
||||||
|
## node.cookie — секрет кластерного Erlang-соединения, для single-node любая строка
|
||||||
|
## node.data_dir — директория данных (mnesia, конфиги)
|
||||||
|
node {
|
||||||
|
name = "emqx@127.0.0.1"
|
||||||
|
cookie = "iot-emqx-cookie-mvp"
|
||||||
|
data_dir = "/opt/emqx/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
## HTTP Auth Backend для IoT-устройств
|
||||||
|
## EMQX посылает POST с {username, password, clientid} → iot-operator отвечает {"result":"allow"|"deny"}
|
||||||
|
authentication = [
|
||||||
|
{
|
||||||
|
mechanism = password_based
|
||||||
|
backend = http
|
||||||
|
enable = true
|
||||||
|
method = post
|
||||||
|
url = "http://iot-operator.sless.svc:9090/internal/mqtt/auth"
|
||||||
|
body {
|
||||||
|
username = "${username}"
|
||||||
|
password = "${password}"
|
||||||
|
clientid = "${clientid}"
|
||||||
|
}
|
||||||
|
headers {
|
||||||
|
"content-type" = "application/json"
|
||||||
|
}
|
||||||
|
connect_timeout = 5s
|
||||||
|
request_timeout = 5s
|
||||||
|
pool_size = 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
## Authorization (ACL) — HTTP backend для изоляции топиков по устройству.
|
||||||
|
## no_match = deny: если HTTP backend недоступен или не ответил — запрещаем.
|
||||||
|
## Endpoint /internal/mqtt/acl возвращает allow только для топиков {ns}/{deviceId}/#
|
||||||
|
authorization {
|
||||||
|
no_match = deny
|
||||||
|
deny_action = disconnect
|
||||||
|
cache {
|
||||||
|
enable = true
|
||||||
|
max_size = 32
|
||||||
|
ttl = 1m
|
||||||
|
}
|
||||||
|
sources = [
|
||||||
|
{
|
||||||
|
type = http
|
||||||
|
enable = true
|
||||||
|
method = post
|
||||||
|
url = "http://iot-operator.sless.svc:9090/internal/mqtt/acl"
|
||||||
|
body {
|
||||||
|
username = "${username}"
|
||||||
|
clientid = "${clientid}"
|
||||||
|
action = "${action}"
|
||||||
|
topic = "${topic}"
|
||||||
|
}
|
||||||
|
headers {
|
||||||
|
"content-type" = "application/json"
|
||||||
|
}
|
||||||
|
connect_timeout = 5s
|
||||||
|
request_timeout = 5s
|
||||||
|
pool_size = 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
## MQTT настройки
|
||||||
|
mqtt {
|
||||||
|
max_packet_size = 1MB
|
||||||
|
max_topic_levels = 10
|
||||||
|
retain_available = false
|
||||||
|
}
|
||||||
|
|
||||||
|
## Listeners — plaintext MQTT + WebSocket
|
||||||
|
listeners.tcp.default {
|
||||||
|
bind = "0.0.0.0:1883"
|
||||||
|
max_connections = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners.ws.default {
|
||||||
|
bind = "0.0.0.0:8083"
|
||||||
|
max_connections = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
dashboard {
|
||||||
|
listeners.http {
|
||||||
|
bind = 18083
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: emqx
|
||||||
|
namespace: sless
|
||||||
|
labels:
|
||||||
|
app: emqx
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: emqx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: emqx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: emqx
|
||||||
|
image: emqx/emqx:5.5.1
|
||||||
|
ports:
|
||||||
|
- name: mqtt
|
||||||
|
containerPort: 1883
|
||||||
|
- name: ws
|
||||||
|
containerPort: 8083
|
||||||
|
- name: dashboard
|
||||||
|
containerPort: 18083
|
||||||
|
volumeMounts:
|
||||||
|
- name: emqx-conf
|
||||||
|
mountPath: /opt/emqx/etc/emqx.conf
|
||||||
|
subPath: emqx.conf
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 1883
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 1883
|
||||||
|
initialDelaySeconds: 40
|
||||||
|
periodSeconds: 20
|
||||||
|
volumes:
|
||||||
|
- name: emqx-conf
|
||||||
|
configMap:
|
||||||
|
name: emqx-config
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: emqx
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: emqx
|
||||||
|
ports:
|
||||||
|
- name: mqtt
|
||||||
|
port: 1883
|
||||||
|
targetPort: 1883
|
||||||
|
- name: ws
|
||||||
|
port: 8083
|
||||||
|
targetPort: 8083
|
||||||
|
- name: dashboard
|
||||||
|
port: 18083
|
||||||
|
targetPort: 18083
|
||||||
@@ -38,7 +38,7 @@ spec:
|
|||||||
- name: mqtt-bridge
|
- name: mqtt-bridge
|
||||||
# Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge).
|
# Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge).
|
||||||
# При смене версии оператора — менять тег и здесь.
|
# При смене версии оператора — менять тег и здесь.
|
||||||
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.69
|
image: naeel/iot-operator:v0.2.0
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
command: ["/mqtt-bridge"]
|
command: ["/mqtt-bridge"]
|
||||||
env:
|
env:
|
||||||
@@ -61,5 +61,3 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
memory: "64Mi"
|
memory: "64Mi"
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
imagePullSecrets:
|
|
||||||
- name: sless-registry-auth
|
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Создано: 2026-04-12
|
||||||
|
# Deployment iot-operator — controller-manager (IoTDevice CRD) + REST API на :9090.
|
||||||
|
#
|
||||||
|
# Компоненты:
|
||||||
|
# - ServiceAccount + ClusterRole + ClusterRoleBinding (RBAC для CRD controller)
|
||||||
|
# - Deployment: naeel/iot-operator:v0.2.0
|
||||||
|
# - Service: ClusterIP :9090 (REST API, MQTT auth/acl, admin UI)
|
||||||
|
#
|
||||||
|
# iot-operator обслуживает:
|
||||||
|
# - IoTDevice CRD reconcilation (controller-runtime)
|
||||||
|
# - REST API: устройства, телеметрия, MQTT auth/acl
|
||||||
|
# - Admin UI: /iot-admin
|
||||||
|
# - Console UI: /console
|
||||||
|
#
|
||||||
|
# Секреты:
|
||||||
|
# iot-postgres-secret — IOT_PG_DSN для managed Postgres
|
||||||
|
# iot-sqs-credentials — SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY
|
||||||
|
#
|
||||||
|
# Применение: kubectl apply -f deployments/k8s/iot-operator.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: iot-operator
|
||||||
|
namespace: sless
|
||||||
|
---
|
||||||
|
# ClusterRole — права на IoTDevice CRD + Secrets (для MQTT auth)
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: iot-operator-role
|
||||||
|
rules:
|
||||||
|
# IoTDevice CRD
|
||||||
|
- apiGroups: ["iot.kube5s.ru"]
|
||||||
|
resources: ["iotdevices", "iotdevices/status", "iotdevices/finalizers"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
# Secrets — для MQTT auth (чтение device credentials)
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["secrets"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
# Events — controller-runtime записывает events
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["events"]
|
||||||
|
verbs: ["create", "patch"]
|
||||||
|
# Namespaces — для per-tenant DB provisioning
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["namespaces"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
# Leases — leader election (controller-runtime)
|
||||||
|
- apiGroups: ["coordination.k8s.io"]
|
||||||
|
resources: ["leases"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: iot-operator-rolebinding
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: iot-operator-role
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: iot-operator
|
||||||
|
namespace: sless
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iot-operator
|
||||||
|
namespace: sless
|
||||||
|
labels:
|
||||||
|
app: iot-operator
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iot-operator
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iot-operator
|
||||||
|
spec:
|
||||||
|
serviceAccountName: iot-operator
|
||||||
|
containers:
|
||||||
|
- name: operator
|
||||||
|
image: naeel/iot-operator:v0.2.0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
containerPort: 9090
|
||||||
|
- name: metrics
|
||||||
|
containerPort: 8080
|
||||||
|
- name: health
|
||||||
|
containerPort: 8081
|
||||||
|
envFrom:
|
||||||
|
# IOT_PG_DSN — managed Postgres
|
||||||
|
- secretRef:
|
||||||
|
name: iot-postgres-secret
|
||||||
|
# SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY — для admin stats
|
||||||
|
- secretRef:
|
||||||
|
name: iot-sqs-credentials
|
||||||
|
env:
|
||||||
|
- name: API_PORT
|
||||||
|
value: "9090"
|
||||||
|
# ADMIN_STATS_TOKEN — токен доступа к /iot-admin/stats
|
||||||
|
- name: ADMIN_STATS_TOKEN
|
||||||
|
value: "iot-admin-2026"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iot-operator
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: iot-operator
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
port: 9090
|
||||||
|
targetPort: 9090
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
# Создано: 2026-04-05
|
# Создано: 2026-04-05
|
||||||
# Postgres для IoT телеметрии — отдельный от sless postgres (тот для invocations логов).
|
# Изменено: 2026-04-12 (заменён self-hosted на managed Postgres через оператор)
|
||||||
# Deployment (не StatefulSet) — для dev/demo. В prod заменить на managed Postgres.
|
|
||||||
#
|
#
|
||||||
# Суперюзер iot_admin используется оператором для:
|
# Managed PostgreSQL 17 — тот же инстанс что использует shared-SQS.
|
||||||
|
# Namespace: dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5 (managed оператором)
|
||||||
|
# Host: postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local
|
||||||
|
#
|
||||||
|
# IoT оператор использует суперюзер для:
|
||||||
# - CREATE USER tenant_{ns} + CREATE DATABASE tenant_{ns}
|
# - CREATE USER tenant_{ns} + CREATE DATABASE tenant_{ns}
|
||||||
# - CREATE TABLE iot_telemetry в tenant DB
|
# - CREATE TABLE iot_telemetry в tenant DB
|
||||||
# Клиенты НЕ имеют прямого доступа — только через REST API платформы.
|
# Клиенты НЕ имеют прямого доступа — только через REST API платформы.
|
||||||
|
#
|
||||||
|
# Deployment и Service удалены — Postgres managed, только Secret с DSN.
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
@@ -13,54 +18,9 @@ metadata:
|
|||||||
name: iot-postgres-secret
|
name: iot-postgres-secret
|
||||||
namespace: sless
|
namespace: sless
|
||||||
stringData:
|
stringData:
|
||||||
# Суперпользователь — для управления tenant databases
|
# Суперпользователь managed Postgres
|
||||||
POSTGRES_USER: "iot_admin"
|
POSTGRES_USER: "super"
|
||||||
POSTGRES_PASSWORD: "iot-pg-super-2026"
|
POSTGRES_PASSWORD: "BQUF5ruECa1ZFlq4wYt3gPJUEmtBMkA9QNK4MM5Sd8al4ArMDlmT16DIKHYBPyif"
|
||||||
POSTGRES_DB: "iot_platform"
|
POSTGRES_DB: "sqsdb"
|
||||||
# DSN для оператора и mqtt-bridge (superuser к management DB)
|
# DSN для iot-operator и sqs-consumer (superuser к managed DB)
|
||||||
IOT_PG_DSN: "postgresql://iot_admin:iot-pg-super-2026@iot-postgres.sless.svc:5432/iot_platform?sslmode=disable"
|
IOT_PG_DSN: "postgresql://super:BQUF5ruECa1ZFlq4wYt3gPJUEmtBMkA9QNK4MM5Sd8al4ArMDlmT16DIKHYBPyif@postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local:5432/sqsdb?sslmode=disable"
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: iot-postgres
|
|
||||||
namespace: sless
|
|
||||||
labels:
|
|
||||||
app: iot-postgres
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: iot-postgres
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: iot-postgres
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: postgres
|
|
||||||
image: postgres:16-alpine
|
|
||||||
ports:
|
|
||||||
- containerPort: 5432
|
|
||||||
envFrom:
|
|
||||||
- secretRef:
|
|
||||||
name: iot-postgres-secret
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: "128Mi"
|
|
||||||
cpu: "100m"
|
|
||||||
limits:
|
|
||||||
memory: "512Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: iot-postgres
|
|
||||||
namespace: sless
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: iot-postgres
|
|
||||||
ports:
|
|
||||||
- port: 5432
|
|
||||||
targetPort: 5432
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: sqs-consumer
|
- name: sqs-consumer
|
||||||
# Тот же образ что и оператор — все IoT бинари в одном образе.
|
# Тот же образ что и оператор — все IoT бинари в одном образе.
|
||||||
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.69
|
image: naeel/iot-operator:v0.2.0
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
command: ["/sqs-consumer"]
|
command: ["/sqs-consumer"]
|
||||||
env:
|
env:
|
||||||
@@ -58,5 +58,3 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
memory: "64Mi"
|
memory: "64Mi"
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
imagePullSecrets:
|
|
||||||
- name: sless-registry-auth
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultNamespace — fallback namespace для dev/тестов.
|
// defaultNamespace — fallback namespace для dev/тестов.
|
||||||
@@ -26,34 +26,34 @@ const defaultNamespace = "default"
|
|||||||
|
|
||||||
// Handler содержит зависимости для всех IoT REST-обработчиков.
|
// Handler содержит зависимости для всех IoT REST-обработчиков.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
K8s client.Client
|
K8s client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
// IoTPG — хранилище IoT телеметрии (per-tenant Postgres). nil если IOT_PG_DSN не задан.
|
// IoTPG — хранилище IoT телеметрии (per-tenant Postgres). nil если IOT_PG_DSN не задан.
|
||||||
IoTPG *iotpg.IoTPostgresStore
|
IoTPG *iotpg.IoTPostgresStore
|
||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeJSON отправляет JSON-ответ с указанным статусом.
|
// writeJSON отправляет JSON-ответ с указанным статусом.
|
||||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errResp возвращает структуру ошибки для JSON.
|
// errResp возвращает структуру ошибки для JSON.
|
||||||
func errResp(msg string) map[string]string {
|
func errResp(msg string) map[string]string {
|
||||||
return map[string]string{"error": msg}
|
return map[string]string{"error": msg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pathVar читает переменную пути из gorilla/mux.
|
// pathVar читает переменную пути из gorilla/mux.
|
||||||
func pathVar(r *http.Request, key string) string {
|
func pathVar(r *http.Request, key string) string {
|
||||||
return mux.Vars(r)[key]
|
return mux.Vars(r)[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
// namespace читает {namespace} из пути URL.
|
// namespace читает {namespace} из пути URL.
|
||||||
func namespace(r *http.Request) string {
|
func namespace(r *http.Request) string {
|
||||||
if ns := mux.Vars(r)["namespace"]; ns != "" {
|
if ns := mux.Vars(r)["namespace"]; ns != "" {
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
return defaultNamespace
|
return defaultNamespace
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user