feat: add complete Kubernetes deployment infrastructure

Add production-ready deployment configuration for Raspberry Pi cluster with comprehensive documentation and automation scripts.

Kubernetes Manifests (deploy/k8s/):
- namespace.yaml - Dedicated namespace for the application
- configmap.yaml - Environment configuration (MongoDB URI, ports, URLs)
- secrets.yaml.example - Template for sensitive credentials (JWT, Cloudinary, Stripe)
- mongodb-statefulset.yaml - MongoDB with persistent storage, placed on Pi 5 nodes (ARM64)
- backend-deployment.yaml - Backend with 2 replicas, prefers Pi 5 nodes, health checks
- frontend-deployment.yaml - Frontend with 2 replicas, can run on any node, nginx-based
- ingress.yaml - Traefik/NGINX ingress for API, Socket.IO, and frontend routing

Docker Configuration:
- backend/Dockerfile - Multi-stage build for ARM64/ARMv7 with health checks
- backend/.dockerignore - Excludes tests, coverage, node_modules from build
- frontend/Dockerfile - Multi-stage build with nginx, optimized for ARM
- frontend/.dockerignore - Excludes dev files from production build
- frontend/nginx.conf - Production nginx config with gzip, caching, React Router support

Resource Optimization for Pi Cluster:
- MongoDB: 512Mi-2Gi RAM, 250m-1000m CPU (Pi 5 only, ARM64 affinity)
- Backend: 256Mi-512Mi RAM, 100m-500m CPU (prefers Pi 5, ARM64)
- Frontend: 64Mi-128Mi RAM, 50m-200m CPU (any node, lightweight)
- Total: ~3.5GB RAM minimum, perfect for 2x Pi 5 (8GB) + 1x Pi 3B+ (1GB)

Automation Scripts (deploy/scripts/):
- build.sh - Build multi-arch images (ARM64/ARMv7) and push to registry
- deploy.sh - Deploy all Kubernetes resources with health checks and status reporting
- Both scripts include error handling, color output, and comprehensive logging

Documentation (deploy/README.md):
- Complete deployment guide with prerequisites
- Step-by-step instructions for building and deploying
- Verification commands and troubleshooting guide
- Scaling, updating, and rollback procedures
- Resource monitoring and cleanup instructions
- Security best practices and performance optimization tips

Health Endpoints:
- Backend: GET /api/health (status, uptime, MongoDB connection)
- Frontend: GET /health (nginx health check)
- Used by Kubernetes liveness and readiness probes

Key Features:
- Multi-architecture support (ARM64 for Pi 5, ARMv7 for Pi 3B+)
- NodeAffinity places heavy workloads (MongoDB, backend) on Pi 5 nodes
- Persistent storage for MongoDB (10Gi PVC)
- Horizontal pod autoscaling ready
- Zero-downtime deployments with rolling updates
- Comprehensive health monitoring
- Production-grade nginx with security headers
- Ingress routing for API, WebSocket, and static assets

Security:
- Secrets management with Kubernetes Secrets
- secrets.yaml excluded from Git (.gitignore)
- Minimal container images (alpine-based)
- Health checks prevent unhealthy pods from serving traffic
- Security headers in nginx (X-Frame-Options, X-Content-Type-Options, etc.)

Usage:
1. Build images: ./deploy/scripts/build.sh
2. Configure secrets: cp deploy/k8s/secrets.yaml.example deploy/k8s/secrets.yaml
3. Deploy: ./deploy/scripts/deploy.sh
4. Monitor: kubectl get all -n adopt-a-street

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
William Valentin
2025-11-01 11:08:19 -07:00
parent 2df5a303ed
commit ae791ae8b1
17 changed files with 1125 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
apiVersion: v1
kind: Service
metadata:
name: adopt-a-street-backend
namespace: adopt-a-street
labels:
app: backend
spec:
selector:
app: backend
ports:
- port: 5000
targetPort: 5000
name: http
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adopt-a-street-backend
namespace: adopt-a-street
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
# Prefer Pi 5 nodes for backend (more RAM for Node.js)
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- arm64 # Pi 5 architecture
containers:
- name: backend
# Update with your registry and tag
image: your-registry/adopt-a-street-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 5000
name: http
envFrom:
- configMapRef:
name: adopt-a-street-config
- secretRef:
name: adopt-a-street-secrets
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 5000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 5000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3

15
deploy/k8s/configmap.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: adopt-a-street-config
namespace: adopt-a-street
data:
# MongoDB Connection
MONGO_URI: "mongodb://adopt-a-street-mongodb:27017/adopt-a-street"
# Backend Configuration
PORT: "5000"
NODE_ENV: "production"
# Frontend URL (update with your actual domain)
FRONTEND_URL: "http://adopt-a-street.local"

View File

@@ -0,0 +1,64 @@
apiVersion: v1
kind: Service
metadata:
name: adopt-a-street-frontend
namespace: adopt-a-street
labels:
app: frontend
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adopt-a-street-frontend
namespace: adopt-a-street
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
# Frontend can run on any node (lightweight static serving)
containers:
- name: frontend
# Update with your registry and tag
image: your-registry/adopt-a-street-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3

53
deploy/k8s/ingress.yaml Normal file
View File

@@ -0,0 +1,53 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: adopt-a-street-ingress
namespace: adopt-a-street
annotations:
# Uncomment the appropriate ingress class for your cluster
kubernetes.io/ingress.class: "traefik" # For Traefik
# kubernetes.io/ingress.class: "nginx" # For NGINX Ingress
# Uncomment if using cert-manager for TLS
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Traefik specific annotations (uncomment if using Traefik)
# traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
# traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
rules:
- host: adopt-a-street.local # CHANGE THIS to your actual domain
http:
paths:
# API endpoints
- path: /api
pathType: Prefix
backend:
service:
name: adopt-a-street-backend
port:
number: 5000
# Socket.IO endpoints
- path: /socket.io
pathType: Prefix
backend:
service:
name: adopt-a-street-backend
port:
number: 5000
# Frontend (must be last - catches all other paths)
- path: /
pathType: Prefix
backend:
service:
name: adopt-a-street-frontend
port:
number: 80
# Uncomment for TLS/HTTPS
# tls:
# - hosts:
# - adopt-a-street.local
# secretName: adopt-a-street-tls

View File

@@ -0,0 +1,89 @@
apiVersion: v1
kind: Service
metadata:
name: adopt-a-street-mongodb
namespace: adopt-a-street
labels:
app: mongodb
spec:
clusterIP: None # Headless service for StatefulSet
selector:
app: mongodb
ports:
- port: 27017
targetPort: 27017
name: mongodb
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: adopt-a-street-mongodb
namespace: adopt-a-street
spec:
serviceName: adopt-a-street-mongodb
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
# Place MongoDB on Pi 5 nodes (more RAM)
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values:
- arm64 # Pi 5 architecture
containers:
- name: mongodb
image: mongo:7.0
ports:
- containerPort: 27017
name: mongodb
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
volumeMounts:
- name: mongodb-data
mountPath: /data/db
livenessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- mongosh
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumeClaimTemplates:
- metadata:
name: mongodb-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
# Uncomment and set your storage class if needed
# storageClassName: local-path

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: adopt-a-street
labels:
name: adopt-a-street
environment: production

View File

@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: adopt-a-street-secrets
namespace: adopt-a-street
type: Opaque
stringData:
# JWT Secret - CHANGE THIS IN PRODUCTION!
JWT_SECRET: "your-super-secret-jwt-key-change-in-production"
# Cloudinary Configuration
CLOUDINARY_CLOUD_NAME: "your-cloudinary-cloud-name"
CLOUDINARY_API_KEY: "your-cloudinary-api-key"
CLOUDINARY_API_SECRET: "your-cloudinary-api-secret"
# Stripe Configuration (optional - currently mocked)
# STRIPE_SECRET_KEY: "your-stripe-secret-key"
---
# IMPORTANT:
# 1. Copy this file to secrets.yaml
# 2. Replace all placeholder values with real secrets
# 3. DO NOT commit secrets.yaml to version control
# 4. Add secrets.yaml to .gitignore