fix: k8s on-premise deployment and session persistence
Some checks failed
scrum-manager/pipeline/head There was a failure building this commit
Some checks failed
scrum-manager/pipeline/head There was a failure building this commit
Database fixes: - Add hostPath.type=DirectoryOrCreate so kubelet auto-creates /mnt/data/mysql - Add fsGroup=999 so MySQL process can write to the hostPath volume - Add MYSQL_ROOT_HOST=% to allow backend pods to authenticate as root - Fix liveness/readiness probes to include credentials (-p$MYSQL_ROOT_PASSWORD) - Increase probe initialDelaySeconds (30/60s) for slow first-run init - Add 15s grace sleep in backend initContainer after MySQL TCP is up - Add persistentVolumeReclaimPolicy=Retain to prevent accidental data loss - Explicit accessModes+resources in PVC patch to avoid list merge ambiguity - Add nodeAffinity comment in PV for multi-node cluster guidance Ingress/nginx fixes: - Remove broken rewrite-target=/ that was rewriting all paths (incl /api) to / - Route /socket.io directly to backend for WebSocket support - Add /socket.io/ proxy location to both nginx.conf and K8s ConfigMap Frontend fix: - Persist currentUser to localStorage on login so page refresh no longer clears session and redirects users back to the login page Tooling: - Add k8s/overlays/on-premise/deploy.sh for one-command deployment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
95
k8s/overlays/on-premise/deploy.sh
Executable file
95
k8s/overlays/on-premise/deploy.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── Scrum Manager — On-Premise Kubernetes Deploy Script ─────────────────────
|
||||
# Run from the project root: bash k8s/overlays/on-premise/deploy.sh
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
OVERLAY="k8s/overlays/on-premise"
|
||||
NAMESPACE="scrum-manager"
|
||||
REGISTRY="${REGISTRY:-}" # Optional: set to your registry, e.g. "192.168.1.10:5000"
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────────────
|
||||
info "Checking prerequisites..."
|
||||
command -v kubectl >/dev/null 2>&1 || error "kubectl not found"
|
||||
command -v docker >/dev/null 2>&1 || error "docker not found"
|
||||
kubectl cluster-info >/dev/null 2>&1 || error "Cannot reach Kubernetes cluster. Check kubeconfig."
|
||||
info "Prerequisites OK."
|
||||
|
||||
# ── Multi-node: hostPath nodeAffinity reminder ───────────────────────────────
|
||||
NODE_COUNT=$(kubectl get nodes --no-headers 2>/dev/null | wc -l)
|
||||
if [ "$NODE_COUNT" -gt 1 ]; then
|
||||
warn "Multi-node cluster detected ($NODE_COUNT nodes)."
|
||||
warn "MySQL data is stored at /mnt/data/mysql on ONE node only."
|
||||
warn "Open k8s/overlays/on-premise/mysql-pv.yaml and uncomment"
|
||||
warn "the nodeAffinity block, setting it to the correct node hostname."
|
||||
warn "Run: kubectl get nodes to list hostnames."
|
||||
read -rp "Press ENTER to continue anyway, or Ctrl+C to abort and fix first..."
|
||||
fi
|
||||
|
||||
# ── Build Docker images ──────────────────────────────────────────────────────
|
||||
info "Building Docker images..."
|
||||
|
||||
BACKEND_TAG="${REGISTRY:+${REGISTRY}/}scrum-backend:latest"
|
||||
FRONTEND_TAG="${REGISTRY:+${REGISTRY}/}scrum-frontend:latest"
|
||||
|
||||
docker build -t "$BACKEND_TAG" -f server/Dockerfile server/
|
||||
docker build -t "$FRONTEND_TAG" -f Dockerfile .
|
||||
|
||||
# ── Push or load images into cluster ────────────────────────────────────────
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
info "Pushing images to registry $REGISTRY..."
|
||||
docker push "$BACKEND_TAG"
|
||||
docker push "$FRONTEND_TAG"
|
||||
else
|
||||
warn "No REGISTRY set. Attempting to load images via 'docker save | ssh'..."
|
||||
warn "If you have a single-node cluster and Docker runs on the same host,"
|
||||
warn "set imagePullPolicy: Never in the deployments (already set)."
|
||||
warn "For multi-node, set REGISTRY=<your-registry> before running this script."
|
||||
warn ""
|
||||
warn " Alternatively, load images manually on each node with:"
|
||||
warn " docker save scrum-backend:latest | ssh NODE docker load"
|
||||
warn " docker save scrum-frontend:latest | ssh NODE docker load"
|
||||
fi
|
||||
|
||||
# ── Apply Kubernetes manifests ────────────────────────────────────────────────
|
||||
info "Applying manifests via kustomize..."
|
||||
kubectl apply -k "$OVERLAY"
|
||||
|
||||
# ── Wait for rollout ──────────────────────────────────────────────────────────
|
||||
info "Waiting for MySQL to become ready (this can take up to 90s on first run)..."
|
||||
kubectl rollout status deployment/mysql -n "$NAMESPACE" --timeout=120s || \
|
||||
warn "MySQL rollout timed out — check: kubectl describe pod -l app.kubernetes.io/name=mysql -n $NAMESPACE"
|
||||
|
||||
info "Waiting for backend..."
|
||||
kubectl rollout status deployment/backend -n "$NAMESPACE" --timeout=90s || \
|
||||
warn "Backend rollout timed out — check: kubectl logs -l app.kubernetes.io/name=backend -n $NAMESPACE"
|
||||
|
||||
info "Waiting for frontend..."
|
||||
kubectl rollout status deployment/frontend -n "$NAMESPACE" --timeout=60s || \
|
||||
warn "Frontend rollout timed out."
|
||||
|
||||
# ── Show access info ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
info "Deploy complete! Access the app:"
|
||||
|
||||
NODEPORT=$(kubectl get svc frontend -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].nodePort}' 2>/dev/null || echo "")
|
||||
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null || echo "<NODE-IP>")
|
||||
|
||||
if [ -n "$NODEPORT" ]; then
|
||||
echo ""
|
||||
echo -e " NodePort: ${GREEN}http://${NODE_IP}:${NODEPORT}${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " Ingress: ${GREEN}http://scrum.local${NC} (add '$NODE_IP scrum.local' to /etc/hosts)"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " kubectl get pods -n $NAMESPACE"
|
||||
echo " kubectl logs -f deployment/backend -n $NAMESPACE"
|
||||
echo " kubectl logs -f deployment/mysql -n $NAMESPACE"
|
||||
@@ -4,12 +4,25 @@ metadata:
|
||||
name: scrum-manager-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
# No rewrite-target here — the old global rewrite-target: / was
|
||||
# rewriting every path (including /api/tasks) to just /, breaking the API.
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
spec:
|
||||
rules:
|
||||
- host: scrum.local
|
||||
http:
|
||||
paths:
|
||||
# Socket.io long-polling and WebSocket connections go directly to backend.
|
||||
- path: /socket.io
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 3001
|
||||
# All other traffic (including /api/) goes to frontend nginx,
|
||||
# which proxies /api/ to backend internally. This avoids double-routing.
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -17,10 +30,3 @@ spec:
|
||||
name: frontend
|
||||
port:
|
||||
number: 80
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 3001
|
||||
|
||||
29
k8s/overlays/on-premise/mysql-pv.yaml
Normal file
29
k8s/overlays/on-premise/mysql-pv.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mysql-pv
|
||||
labels:
|
||||
type: local
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
hostPath:
|
||||
path: "/mnt/data/mysql"
|
||||
# DirectoryOrCreate: kubelet will create the dir if it doesn't exist,
|
||||
# preventing MySQL CrashLoopBackOff due to missing mount path.
|
||||
type: DirectoryOrCreate
|
||||
# IMPORTANT for multi-node clusters: uncomment nodeAffinity and set your
|
||||
# node's hostname so MySQL always schedules on the node that has the data.
|
||||
# Run: kubectl get nodes to find the hostname.
|
||||
# nodeAffinity:
|
||||
# required:
|
||||
# nodeSelectorTerms:
|
||||
# - matchExpressions:
|
||||
# - key: kubernetes.io/hostname
|
||||
# operator: In
|
||||
# values:
|
||||
# - YOUR-NODE-HOSTNAME
|
||||
@@ -1,15 +1,12 @@
|
||||
# apiVersion: v1
|
||||
# kind: PersistentVolumeClaim
|
||||
# metadata:
|
||||
# name: mysql-data-pvc
|
||||
# spec:
|
||||
# storageClassName: manual
|
||||
# volumeName: mysql-pv
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mysql-data-pvc
|
||||
spec:
|
||||
storageClassName: local-path
|
||||
storageClassName: manual
|
||||
volumeName: mysql-pv
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
Reference in New Issue
Block a user