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