From a6ef511ca6314f59edf08d47914ff3c8f6a58e85 Mon Sep 17 00:00:00 2001 From: arcbjorn Date: Sat, 2 Aug 2025 17:52:53 -0300 Subject: [PATCH] feat(k8s): add k8s web UI manifests --- k8s/k8s-webui/README.md | 162 +++++++++++++++++++++++++ k8s/k8s-webui/backend-deployment.yaml | 86 +++++++++++++ k8s/k8s-webui/deploy.sh | 105 ++++++++++++++++ k8s/k8s-webui/frontend-deployment.yaml | 62 ++++++++++ k8s/k8s-webui/ingress.yaml | 45 +++++++ k8s/k8s-webui/namespace.yaml | 6 + k8s/k8s-webui/postgres.yaml | 89 ++++++++++++++ k8s/k8s-webui/rbac.yaml | 48 ++++++++ k8s/k8s-webui/redis.yaml | 70 +++++++++++ k8s/k8s-webui/secrets.yaml | 21 ++++ 10 files changed, 694 insertions(+) create mode 100644 k8s/k8s-webui/README.md create mode 100644 k8s/k8s-webui/backend-deployment.yaml create mode 100755 k8s/k8s-webui/deploy.sh create mode 100644 k8s/k8s-webui/frontend-deployment.yaml create mode 100644 k8s/k8s-webui/ingress.yaml create mode 100644 k8s/k8s-webui/namespace.yaml create mode 100644 k8s/k8s-webui/postgres.yaml create mode 100644 k8s/k8s-webui/rbac.yaml create mode 100644 k8s/k8s-webui/redis.yaml create mode 100644 k8s/k8s-webui/secrets.yaml diff --git a/k8s/k8s-webui/README.md b/k8s/k8s-webui/README.md new file mode 100644 index 0000000..dd3e84e --- /dev/null +++ b/k8s/k8s-webui/README.md @@ -0,0 +1,162 @@ +# K8s WebUI Deployment + +This directory contains Kubernetes manifests for deploying the K8s WebUI application to manage the base-infrastructure services. + +## Overview + +The K8s WebUI provides a web interface for monitoring and managing the Kubernetes infrastructure services including: + +- **Gitea** - Git repository hosting +- **Umami** - Web analytics platform +- **Memos** - Notes and memos service +- **Uptime Kuma** - Uptime monitoring dashboard +- **FileBrowser** - File browser and management +- **Dozzle** - Container logs viewer +- **PostgreSQL** - Database server + +## Architecture + +The application consists of: + +- **Frontend**: Svelte 5 application serving the web interface +- **Backend**: Bun.js API server with Kubernetes client integration +- **PostgreSQL**: Database for audit logs and application data +- **Redis**: Session storage and caching + +## Deployment + +### Prerequisites + +1. Kubernetes cluster with nginx-ingress controller +2. cert-manager for SSL certificates (optional) +3. DNS configured for `k8s.arcbjorn.com` + +### Build Images + +First, build the Docker images for the frontend and backend: + +```bash +# From the hellir/k8s-webui directory +cd ../../hellir/k8s-webui + +# Build backend image +docker build -t k8s-webui-backend:latest ./backend + +# Build frontend image +docker build -t k8s-webui-frontend:latest ./frontend + +# Tag and push to your registry if needed +# docker tag k8s-webui-backend:latest your-registry/k8s-webui-backend:latest +# docker push your-registry/k8s-webui-backend:latest +``` + +### Deploy to Kubernetes + +Apply the manifests in order: + +```bash +# Create namespace and RBAC +kubectl apply -f namespace.yaml +kubectl apply -f rbac.yaml + +# Create secrets and config +kubectl apply -f secrets.yaml + +# Deploy database and cache +kubectl apply -f postgres.yaml +kubectl apply -f redis.yaml + +# Wait for database to be ready +kubectl wait --for=condition=ready pod -l app=k8s-webui-postgres -n k8s-webui --timeout=120s + +# Deploy application +kubectl apply -f backend-deployment.yaml +kubectl apply -f frontend-deployment.yaml + +# Create ingress +kubectl apply -f ingress.yaml +``` + +### Verify Deployment + +Check that all pods are running: + +```bash +kubectl get pods -n k8s-webui +kubectl get services -n k8s-webui +kubectl get ingress -n k8s-webui +``` + +## Access + +Once deployed, the application will be available at: + +- **URL**: https://k8s.arcbjorn.com +- **Credentials**: admin / admin (change in production) + +## Configuration + +### Environment Variables + +Key configuration is managed through: + +- `k8s-webui-config` ConfigMap +- `k8s-webui-secrets` Secret + +### RBAC Permissions + +The application uses a ServiceAccount with minimal required permissions: + +- Read access to pods, services, deployments, statefulsets, ingresses +- Read access to configmaps and secrets (data redacted) +- Limited write access for scaling deployments +- Access to pod logs + +### Security Notes + +- Generate a secure 32-byte PASETO key for production (run `bun run generate-key` in backend/) +- Update database passwords +- Review RBAC permissions based on your security requirements +- Consider using external secrets management +- PASETO tokens provide better security than JWT (immune to algorithm confusion attacks) + +## Monitoring + +The application provides: + +- Real-time WebSocket updates for cluster status +- Health check endpoints at `/health` +- Prometheus-compatible metrics (if metrics-server is installed) + +## Troubleshooting + +### Common Issues + +1. **Images not found**: Ensure Docker images are built and available in your cluster +2. **RBAC errors**: Verify ServiceAccount and ClusterRole are applied +3. **Database connection**: Check postgres StatefulSet is running and ready +4. **Ingress not working**: Verify nginx-ingress controller and DNS configuration + +### Logs + +```bash +# Backend logs +kubectl logs -f deployment/k8s-webui-backend -n k8s-webui + +# Frontend logs +kubectl logs -f deployment/k8s-webui-frontend -n k8s-webui + +# Database logs +kubectl logs -f statefulset/k8s-webui-postgres -n k8s-webui +``` + +## Development + +For local development, use the docker-compose setup in the hellir/k8s-webui directory: + +```bash +cd ../../hellir/k8s-webui +docker-compose up -d +``` + +This will start the application with hot reload and development databases. \ No newline at end of file diff --git a/k8s/k8s-webui/backend-deployment.yaml b/k8s/k8s-webui/backend-deployment.yaml new file mode 100644 index 0000000..82637ba --- /dev/null +++ b/k8s/k8s-webui/backend-deployment.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8s-webui-backend + namespace: k8s-webui +spec: + replicas: 1 + selector: + matchLabels: + app: k8s-webui-backend + template: + metadata: + labels: + app: k8s-webui-backend + spec: + serviceAccountName: k8s-webui + containers: + - name: backend + image: k8s-webui-backend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3001 + name: http + env: + - name: PORT + value: "3001" + - name: NODE_ENV + valueFrom: + configMapKeyRef: + name: k8s-webui-config + key: NODE_ENV + - name: K8S_NAMESPACE + valueFrom: + configMapKeyRef: + name: k8s-webui-config + key: K8S_NAMESPACE + - name: REDIS_URL + valueFrom: + configMapKeyRef: + name: k8s-webui-config + key: REDIS_URL + - name: DATABASE_URL + valueFrom: + configMapKeyRef: + name: k8s-webui-config + key: DATABASE_URL + - name: PASETO_SECRET_KEY + valueFrom: + secretKeyRef: + name: k8s-webui-secrets + key: PASETO_SECRET_KEY + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + restartPolicy: Always + +--- +apiVersion: v1 +kind: Service +metadata: + name: k8s-webui-backend + namespace: k8s-webui +spec: + selector: + app: k8s-webui-backend + ports: + - port: 3001 + targetPort: http + name: http + type: ClusterIP \ No newline at end of file diff --git a/k8s/k8s-webui/deploy.sh b/k8s/k8s-webui/deploy.sh new file mode 100755 index 0000000..25360a5 --- /dev/null +++ b/k8s/k8s-webui/deploy.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# K8s WebUI Deployment Script +# This script deploys the K8s WebUI to the Kubernetes cluster + +set -e + +echo "🚀 Deploying K8s WebUI to Kubernetes cluster..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + print_error "kubectl is not installed or not in PATH" + exit 1 +fi + +# Check if we can connect to the cluster +if ! kubectl cluster-info &> /dev/null; then + print_error "Cannot connect to Kubernetes cluster. Check your kubeconfig." + exit 1 +fi + +print_status "Connected to Kubernetes cluster" + +# Apply namespace and RBAC first +print_status "Creating namespace and RBAC..." +kubectl apply -f namespace.yaml +kubectl apply -f rbac.yaml + +# Apply secrets and configuration +print_status "Creating secrets and configuration..." +kubectl apply -f secrets.yaml + +# Deploy database and cache +print_status "Deploying PostgreSQL database..." +kubectl apply -f postgres.yaml + +print_status "Deploying Redis cache..." +kubectl apply -f redis.yaml + +# Wait for PostgreSQL to be ready +print_status "Waiting for PostgreSQL to be ready..." +kubectl wait --for=condition=ready pod -l app=k8s-webui-postgres -n k8s-webui --timeout=300s + +print_status "Waiting for Redis to be ready..." +kubectl wait --for=condition=ready pod -l app=k8s-webui-redis -n k8s-webui --timeout=180s + +# Deploy application services +print_status "Deploying backend API..." +kubectl apply -f backend-deployment.yaml + +print_status "Deploying frontend..." +kubectl apply -f frontend-deployment.yaml + +# Wait for deployments to be ready +print_status "Waiting for backend to be ready..." +kubectl wait --for=condition=available deployment/k8s-webui-backend -n k8s-webui --timeout=300s + +print_status "Waiting for frontend to be ready..." +kubectl wait --for=condition=available deployment/k8s-webui-frontend -n k8s-webui --timeout=300s + +# Apply ingress +print_status "Creating ingress..." +kubectl apply -f ingress.yaml + +# Show deployment status +echo "" +print_status "Deployment completed successfully! 🎉" +echo "" +echo "📊 Deployment Status:" +kubectl get pods -n k8s-webui +echo "" +kubectl get services -n k8s-webui +echo "" +kubectl get ingress -n k8s-webui +echo "" + +print_status "K8s WebUI should be available at: https://k8s.arcbjorn.com" +print_warning "Default credentials: admin / admin (please change in production)" + +echo "" +echo "🔍 To check logs:" +echo " Backend: kubectl logs -f deployment/k8s-webui-backend -n k8s-webui" +echo " Frontend: kubectl logs -f deployment/k8s-webui-frontend -n k8s-webui" +echo "" +echo "🗑️ To remove:" +echo " kubectl delete namespace k8s-webui" \ No newline at end of file diff --git a/k8s/k8s-webui/frontend-deployment.yaml b/k8s/k8s-webui/frontend-deployment.yaml new file mode 100644 index 0000000..26de8f6 --- /dev/null +++ b/k8s/k8s-webui/frontend-deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8s-webui-frontend + namespace: k8s-webui +spec: + replicas: 1 + selector: + matchLabels: + app: k8s-webui-frontend + template: + metadata: + labels: + app: k8s-webui-frontend + spec: + containers: + - name: frontend + image: k8s-webui-frontend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + name: http + env: + - name: NODE_ENV + value: "production" + - name: BACKEND_URL + value: "http://k8s-webui-backend:3001" + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + restartPolicy: Always + +--- +apiVersion: v1 +kind: Service +metadata: + name: k8s-webui-frontend + namespace: k8s-webui +spec: + selector: + app: k8s-webui-frontend + ports: + - port: 3000 + targetPort: http + name: http + type: ClusterIP \ No newline at end of file diff --git a/k8s/k8s-webui/ingress.yaml b/k8s/k8s-webui/ingress.yaml new file mode 100644 index 0000000..525d88a --- /dev/null +++ b/k8s/k8s-webui/ingress.yaml @@ -0,0 +1,45 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: k8s-webui-ingress + namespace: k8s-webui + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" # If using cert-manager + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "k8s-webui-backend" +spec: + tls: + - hosts: + - k8s.arcbjorn.com + secretName: k8s-webui-tls + rules: + - host: k8s.arcbjorn.com + http: + paths: + # API routes go to backend + - path: /api + pathType: Prefix + backend: + service: + name: k8s-webui-backend + port: + number: 3001 + # WebSocket routes go to backend + - path: /ws + pathType: Prefix + backend: + service: + name: k8s-webui-backend + port: + number: 3001 + # Everything else goes to frontend + - path: / + pathType: Prefix + backend: + service: + name: k8s-webui-frontend + port: + number: 3000 \ No newline at end of file diff --git a/k8s/k8s-webui/namespace.yaml b/k8s/k8s-webui/namespace.yaml new file mode 100644 index 0000000..6f60f9f --- /dev/null +++ b/k8s/k8s-webui/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: k8s-webui + labels: + name: k8s-webui \ No newline at end of file diff --git a/k8s/k8s-webui/postgres.yaml b/k8s/k8s-webui/postgres.yaml new file mode 100644 index 0000000..a2328bb --- /dev/null +++ b/k8s/k8s-webui/postgres.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: k8s-webui-postgres + namespace: k8s-webui +spec: + serviceName: k8s-webui-postgres-headless + replicas: 1 + selector: + matchLabels: + app: k8s-webui-postgres + template: + metadata: + labels: + app: k8s-webui-postgres + spec: + containers: + - name: postgres + image: postgres:15 + ports: + - containerPort: 5432 + name: postgres + env: + - name: POSTGRES_DB + value: k8s_webui + - name: POSTGRES_USER + value: k8s_user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: k8s-webui-secrets + key: POSTGRES_PASSWORD + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - /bin/sh + - -c + - pg_isready -U k8s_user -d k8s_webui + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - pg_isready -U k8s_user -d k8s_webui + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: postgres-data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: k8s-webui-postgres + namespace: k8s-webui +spec: + selector: + app: k8s-webui-postgres + ports: + - port: 5432 + targetPort: postgres + name: postgres + type: ClusterIP + +--- +apiVersion: v1 +kind: Service +metadata: + name: k8s-webui-postgres-headless + namespace: k8s-webui +spec: + selector: + app: k8s-webui-postgres + ports: + - port: 5432 + targetPort: postgres + name: postgres + clusterIP: None \ No newline at end of file diff --git a/k8s/k8s-webui/rbac.yaml b/k8s/k8s-webui/rbac.yaml new file mode 100644 index 0000000..743c8df --- /dev/null +++ b/k8s/k8s-webui/rbac.yaml @@ -0,0 +1,48 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8s-webui + namespace: k8s-webui + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: k8s-webui-reader +rules: +# Allow reading most resources in base-infrastructure namespace +- apiGroups: [""] + resources: ["pods", "services", "configmaps", "secrets", "namespaces"] + verbs: ["get", "list", "watch"] +- apiGroups: ["apps"] + resources: ["deployments", "statefulsets", "replicasets"] + verbs: ["get", "list", "watch"] +- apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get", "list", "watch"] +# Allow limited write operations for scaling +- apiGroups: ["apps"] + resources: ["deployments/scale", "statefulsets/scale"] + verbs: ["get", "patch"] +# Allow reading pod logs +- apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +# Allow reading metrics if metrics-server is available +- apiGroups: ["metrics.k8s.io"] + resources: ["nodes", "pods"] + verbs: ["get", "list"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: k8s-webui-reader +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: k8s-webui-reader +subjects: +- kind: ServiceAccount + name: k8s-webui + namespace: k8s-webui \ No newline at end of file diff --git a/k8s/k8s-webui/redis.yaml b/k8s/k8s-webui/redis.yaml new file mode 100644 index 0000000..727d849 --- /dev/null +++ b/k8s/k8s-webui/redis.yaml @@ -0,0 +1,70 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8s-webui-redis + namespace: k8s-webui +spec: + replicas: 1 + selector: + matchLabels: + app: k8s-webui-redis + template: + metadata: + labels: + app: k8s-webui-redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + name: redis + volumeMounts: + - name: redis-data + mountPath: /data + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: k8s-webui-redis-data + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: k8s-webui-redis-data + namespace: k8s-webui +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: k8s-webui-redis + namespace: k8s-webui +spec: + selector: + app: k8s-webui-redis + ports: + - port: 6379 + targetPort: redis + name: redis + type: ClusterIP \ No newline at end of file diff --git a/k8s/k8s-webui/secrets.yaml b/k8s/k8s-webui/secrets.yaml new file mode 100644 index 0000000..985fc11 --- /dev/null +++ b/k8s/k8s-webui/secrets.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Secret +metadata: + name: k8s-webui-secrets + namespace: k8s-webui +type: Opaque +stringData: + PASETO_SECRET_KEY: "change-this-32-byte-secret-in-production-please-use-random-key" + POSTGRES_PASSWORD: "k8s_webui_password" + REDIS_PASSWORD: "" # Redis without password for simplicity +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-webui-config + namespace: k8s-webui +data: + NODE_ENV: "production" + K8S_NAMESPACE: "base-infrastructure" + REDIS_URL: "redis://k8s-webui-redis:6379" + DATABASE_URL: "postgres://k8s_user:k8s_webui_password@k8s-webui-postgres:5432/k8s_webui" \ No newline at end of file