# SAPL — Kubernetes Redis Manifests for the shared Redis instance used by all SAPL pods for cross-pod rate limiting (DB 1) and view/static-file caching (DB 0). --- ## Directory layout ``` docker/k8s/ ├── redis-configmap.yaml # redis.conf — no persistence, allkeys-lru, 5 GB ceiling ├── redis-deployment.yaml # Deployment (1 replica, redis:7-alpine) ├── redis-service.yaml # ClusterIP service on port 6379 └── README.md # this file ``` --- ## Prerequisites - `kubectl` configured to talk to the target cluster. - A `redis` namespace (created below if it doesn't exist). --- ## Deploy ```bash # 1. Create the namespace (idempotent) kubectl create namespace redis --dry-run=client -o yaml | kubectl apply -f - # 2. Apply all three manifests kubectl apply -f docker/k8s/redis-configmap.yaml kubectl apply -f docker/k8s/redis-deployment.yaml kubectl apply -f docker/k8s/redis-service.yaml # 3. Verify the pod is Running kubectl -n redis get pods -l app=sapl-redis ``` Expected output: ``` NAME READY STATUS RESTARTS AGE sapl-redis-6d9f8b7c4d-xk2lm 1/1 Running 0 30s ``` --- ## Wire a SAPL namespace to Redis ```bash # Create the per-namespace Secret (one-off per tenant) kubectl create secret generic sapl-redis \ --namespace= \ --from-literal=REDIS_URL="redis://sapl-redis.redis.svc.cluster.local:6379" \ --dry-run=client -o yaml | kubectl apply -f - # Ensure the waffle switch row exists (starts OFF) kubectl exec -n deploy/sapl -- \ python manage.py waffle_switch REDIS_CACHE off --create # Enable Redis for this namespace kubectl exec -n deploy/sapl -- \ python manage.py waffle_switch REDIS_CACHE on # Rolling restart so start.sh picks up the new switch value kubectl rollout restart deployment/sapl -n kubectl rollout status deployment/sapl -n ``` ### Fleet-wide rollout ```bash kubectl get namespaces -l app=sapl -o name | sed 's|namespace/||' | \ xargs -P 10 -I{} kubectl exec -n {} deploy/sapl -- \ python manage.py waffle_switch REDIS_CACHE on --create kubectl get namespaces -l app=sapl -o name | sed 's|namespace/||' | \ xargs -P 5 -I{} kubectl rollout restart deployment/sapl -n {} ``` ### Roll back (without removing the Secret) ```bash kubectl exec -n deploy/sapl -- \ python manage.py waffle_switch REDIS_CACHE off kubectl rollout restart deployment/sapl -n ``` --- ## Monitor ### Pod and events ```bash # Pod status kubectl -n redis get pods -l app=sapl-redis -o wide # Deployment events (useful right after apply) kubectl -n redis describe deployment sapl-redis # Pod events (OOMKill, restarts, etc.) kubectl -n redis describe pod -l app=sapl-redis ``` ### Logs ```bash # Tail live logs kubectl -n redis logs -f deploy/sapl-redis # Last 100 lines kubectl -n redis logs deploy/sapl-redis --tail=100 ``` ### Redis INFO ```bash # Memory usage kubectl exec -n redis deploy/sapl-redis -- \ redis-cli info memory \ | grep -E 'used_memory_human|maxmemory_human|mem_fragmentation_ratio' # Connection pressure kubectl exec -n redis deploy/sapl-redis -- \ redis-cli info stats \ | grep -E 'rejected_connections|instantaneous_ops_per_sec' # Key distribution per DB kubectl exec -n redis deploy/sapl-redis -- redis-cli info keyspace # Recent slow queries kubectl exec -n redis deploy/sapl-redis -- redis-cli slowlog get 10 # Live command sampling (1-second window) kubectl exec -n redis deploy/sapl-redis -- redis-cli --latency-history -i 1 ``` ### Rate-limiter keys (DB 1) ```bash kubectl exec -n redis deploy/sapl-redis -- \ redis-cli -n 1 dbsize kubectl exec -n redis deploy/sapl-redis -- \ redis-cli -n 1 --scan --pattern 'rl:ip:*' | head -20 ``` --- ## Seed the UA deny list (once after first deploy) ```bash kubectl exec -n redis deploy/sapl-redis -- redis-cli -n 1 \ SADD rl:bot:ua:blocked \ "$(echo -n 'GPTBot' | sha256sum | cut -d' ' -f1)" \ "$(echo -n 'ClaudeBot' | sha256sum | cut -d' ' -f1)" \ "$(echo -n 'PerplexityBot' | sha256sum | cut -d' ' -f1)" \ "$(echo -n 'Bytespider' | sha256sum | cut -d' ' -f1)" \ "$(echo -n 'AhrefsBot' | sha256sum | cut -d' ' -f1)" \ "$(echo -n 'meta-externalagent' | sha256sum | cut -d' ' -f1)" # Add a new offender at runtime (no restart required) kubectl exec -n redis deploy/sapl-redis -- redis-cli -n 1 \ SADD rl:bot:ua:blocked "$(echo -n 'NewBot/1.0' | sha256sum | cut -d' ' -f1)" ``` --- ## Local standalone Redis (development / testing) No Kubernetes? Run Redis directly with Docker: ```bash sudo docker run --rm -p 6379:6379 redis:7-alpine \ redis-server --save "" --appendonly no ``` Then point Django at it by exporting the env var before starting the dev server: ```bash export REDIS_URL="redis://localhost:6379" export CACHE_BACKEND="redis" python manage.py runserver ``` Or add them to your local `.env` file: ``` REDIS_URL=redis://localhost:6379 CACHE_BACKEND=redis ``` > **Note**: the waffle switch `REDIS_CACHE` must also be `on` in your local > database for `start.sh` to activate the Redis backend. Run: > ```bash > python manage.py waffle_switch REDIS_CACHE on --create > ``` --- ## Update `redis.conf` without redeploying ```bash # Edit the ConfigMap kubectl -n redis edit configmap redis-config # Restart the pod to pick up the new config kubectl -n redis rollout restart deployment/sapl-redis ``` --- ## Key schema reference | DB | Use case | Key pattern | TTL | |----|----------|-------------|-----| | 0 | Page / view cache | `sapl:cache:*` | 60 – 3 600 s | | 0 | Static file cache (logos) | `static:{ns}:{sha256}` | 3 – 24 h | | 0 | PDF cache (≤ 360 KB) | `file:{ns}:{sha256}` | 1 h | | 1 | IP rate-limit counter | `rl:ip:{ip}:reqs` | 60 s | | 1 | IP blocked marker | `rl:ip:{ip}:blocked` | 300 s | | 1 | User rate-limit counter | `rl:{ns}:user:{id}:reqs` | 60 s | | 1 | Path counter | `rl:{ns}:path:{sha256}:reqs` | 60 s | | 1 | UA deny list | `rl:bot:ua:blocked` | permanent SET | | 2 | Django Channels (future) | `channels:*` | session TTL |