mirror of https://github.com/interlegis/sapl.git
Browse Source
django/settings.py: - REDIS_URL / CACHE_BACKEND env vars read at startup (written by start.sh) - CACHES['default'] (DB0, KEY_PREFIX='sapl') switches between django-redis and FileBasedCache transparently; IGNORE_EXCEPTIONS=True for graceful degradation on Redis failure - CACHES['ratelimit'] (DB1, no prefix) for cross-pod rate-limit counters - RATELIMIT_USE_CACHE = 'ratelimit' - Connection pool capped at 6/worker (1,200 pods × 2 workers × 6 = 14,400 peak connections; maxclients=20,000 gives 40% headroom) start.sh: - resolve_redis_url(): reads REDIS_URL from local namespace Secret (envFrom) or falls back to global cluster Secret via k8s API - configure_redis_cache(): ensures REDIS_CACHE waffle switch row exists (off) - resolve_cache_backend(): reads waffle switch; sets CACHE_BACKEND=redis|file - wait_for_redis(): blocks until Redis reachable; falls back gracefully - write_env_file() now persists REDIS_URL + CACHE_BACKEND into pod .env k8s manifests (docker/k8s/): - redis-configmap.yaml: no persistence, allkeys-lru, maxmemory=5gb, maxclients=20000, activedefrag, 4 databases - redis-deployment.yaml: redis:7-alpine, 1 replica, liveness/readiness probes, 1Gi request / 6Gi limit - redis-service.yaml: ClusterIP on port 6379 requirements: add django-redis==5.4.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>rate-limiter-2026
7 changed files with 479 additions and 5 deletions
@ -0,0 +1,228 @@ |
|||||
|
# 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=<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 <NAMESPACE> deploy/sapl -- \ |
||||
|
python manage.py waffle_switch REDIS_CACHE off --create |
||||
|
|
||||
|
# Enable Redis for this namespace |
||||
|
kubectl exec -n <NAMESPACE> 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 <NAMESPACE> |
||||
|
kubectl rollout status deployment/sapl -n <NAMESPACE> |
||||
|
``` |
||||
|
|
||||
|
### 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 <NAMESPACE> deploy/sapl -- \ |
||||
|
python manage.py waffle_switch REDIS_CACHE off |
||||
|
kubectl rollout restart deployment/sapl -n <NAMESPACE> |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 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 | |
||||
@ -0,0 +1,35 @@ |
|||||
|
apiVersion: v1 |
||||
|
kind: ConfigMap |
||||
|
metadata: |
||||
|
name: redis-config |
||||
|
namespace: redis |
||||
|
data: |
||||
|
redis.conf: | |
||||
|
save "" |
||||
|
appendonly no |
||||
|
|
||||
|
maxmemory 5gb |
||||
|
maxmemory-policy allkeys-lru |
||||
|
maxmemory-samples 10 |
||||
|
|
||||
|
maxclients 20000 |
||||
|
tcp-backlog 511 |
||||
|
timeout 300 |
||||
|
tcp-keepalive 60 |
||||
|
|
||||
|
hz 20 |
||||
|
lazyfree-lazy-eviction yes |
||||
|
lazyfree-lazy-expire yes |
||||
|
lazyfree-lazy-server-del yes |
||||
|
|
||||
|
slowlog-log-slower-than 10000 |
||||
|
slowlog-max-len 256 |
||||
|
latency-monitor-threshold 10 |
||||
|
|
||||
|
bind 0.0.0.0 |
||||
|
protected-mode no |
||||
|
databases 4 # DB0: cache, DB1: rate limiter, DB2: channels (future) |
||||
|
|
||||
|
activedefrag yes |
||||
|
active-defrag-ignore-bytes 100mb |
||||
|
active-defrag-threshold-lower 10 |
||||
@ -0,0 +1,48 @@ |
|||||
|
apiVersion: apps/v1 |
||||
|
kind: Deployment |
||||
|
metadata: |
||||
|
name: sapl-redis |
||||
|
namespace: redis |
||||
|
labels: |
||||
|
app: sapl-redis |
||||
|
spec: |
||||
|
replicas: 1 |
||||
|
selector: |
||||
|
matchLabels: |
||||
|
app: sapl-redis |
||||
|
template: |
||||
|
metadata: |
||||
|
labels: |
||||
|
app: sapl-redis |
||||
|
spec: |
||||
|
containers: |
||||
|
- name: redis |
||||
|
image: redis:7-alpine |
||||
|
command: ["redis-server", "/etc/redis/redis.conf"] |
||||
|
resources: |
||||
|
requests: |
||||
|
memory: "1Gi" |
||||
|
cpu: "250m" |
||||
|
limits: |
||||
|
memory: "6Gi" |
||||
|
cpu: "1000m" |
||||
|
ports: |
||||
|
- containerPort: 6379 |
||||
|
livenessProbe: |
||||
|
exec: |
||||
|
command: ["redis-cli", "ping"] |
||||
|
initialDelaySeconds: 10 |
||||
|
periodSeconds: 15 |
||||
|
failureThreshold: 3 |
||||
|
readinessProbe: |
||||
|
exec: |
||||
|
command: ["redis-cli", "ping"] |
||||
|
initialDelaySeconds: 5 |
||||
|
periodSeconds: 10 |
||||
|
volumeMounts: |
||||
|
- name: redis-config |
||||
|
mountPath: /etc/redis |
||||
|
volumes: |
||||
|
- name: redis-config |
||||
|
configMap: |
||||
|
name: redis-config |
||||
@ -0,0 +1,15 @@ |
|||||
|
apiVersion: v1 |
||||
|
kind: Service |
||||
|
metadata: |
||||
|
name: sapl-redis |
||||
|
namespace: redis |
||||
|
labels: |
||||
|
app: sapl-redis |
||||
|
spec: |
||||
|
selector: |
||||
|
app: sapl-redis |
||||
|
ports: |
||||
|
- name: redis |
||||
|
port: 6379 |
||||
|
targetPort: 6379 |
||||
|
type: ClusterIP |
||||
Loading…
Reference in new issue