The config plane
Three files, kept apart on purpose:
| file | holds | why apart |
|---|---|---|
config.json | zones, pools, rules | the part a panel edits |
certs.json | PEM cert/key (+ OCSP staple) per zone | a settings change is not a key reship |
state.json | purge generations | a purge is not a settings edit racing another |
config.json is strict: an unknown key, a bad enum or a
malformed value rejects that zone (or, for a fault that
spans zones, the whole file) with a message naming the zone and the
key. The alternative — silently ignoring a typo — would mean a
misspelled min_version quietly allowing every TLS
version.
This demo's own zone, in the same shape every zone on this fleet takes:
"cdn-demo": {
"hostnames": ["demo.zag.taxi-plus.ir"],
"origins": {
"web": {
"servers": [{ "address": "10.64.30.203:8080" }],
"health": { "type": "http", "path": "/healthz",
"interval": "5s", "timeout": "2s" }
}
},
"default_origin": "web",
"tls": { "min_version": "1.2", "hsts": "1y", "http3": true, "force_https": true },
"cache": { "default_ttl": "5m" },
"access": { "waf": "detect" },
"rules": [
{ "match": { "path": "/api" }, "cache": { "enabled": false } }
]
}
Origins are named pools
Defined once, referenced by name — from default_origin
and from any rule. balance picks between healthy
servers (round_robin by default, weighted;
least_conn; ip_hash).
backup: true marks a server used only when every
regular one is down — and even then, still used, because a stale
cache entry beats nothing while the backup catches up.
Health checks run in the background, always
One worker per edge probes origins on a clock; the request path only ever reads the verdict, so a slow probe never delays a request.
"health": {
"type": "http", "path": "/healthz", "expect": [200, 204],
"interval": "5s", "timeout": "2s", "fall": 3, "rise": 2
}
fall/rise are consecutive results, so one
slow probe does not take an origin out and one lucky probe does not
put a flapping one back in. GET /status on the admin
port reports how every origin looks from that edge
specifically — a fleet-wide average would hide exactly the case
that matters: one edge that cannot reach an origin the others can.
Cache lifetime rounds down, on purpose
cache.default_ttl rounds down to one of 30, 300, 3600
or 86400 seconds, because proxy_cache_valid takes a
literal time and a Lua header filter cannot change what nginx
already decided to store — measured: an entry given a 2-second
X-Accel-Expires from a Lua hook still lived out the 30
seconds the config actually said. This demo's zone uses
5m, which rounds to the 300-second bucket — the number
the measurements section of the platform-level write-up
(docs/17-cdn-demo.md) times a purge against.
Path rules — how /api stays out of cache
"rules": [
{ "match": { "path": "/app" }, "origin": "api" },
{ "match": { "path": "/old/*" }, "redirect": { "to": "/new/$1", "status": 301 } },
{ "match": { "regex": "(?i)\\.(jpe?g|png|webp)$" },
"origin": "static", "cache": { "ttl": "7d", "browser_ttl": "30d" } },
{ "match": { "path": "/api/export" }, "cache": { "enabled": false } }
]
/app matches /app and /app/...
but not /application. A rule may set origin
(a pool name), cache, redirect and
headers. This demo's own rule is the third row's twin:
one line, {"match": {"path": "/api"}, "cache": {"enabled": false}},
which is the entire reason /api/now can say something
true on every single load instead of repeating a cached answer.
Purge is a generation number, not a fan-out
{ "demo.zag.taxi-plus.ir": { "generation": 2 } }
Bump the number in state.json, ship it, and every edge
stops finding the old entries at once — no per-file deletion, no
per-edge crawl. Old files are never actively deleted; nothing asks
for them again and inactive= reclaims the space. The
measurements page shows this done for real against this exact zone:
a purge, then MISS, then HIT again.
One tenant's typo does not block the others
A zone that will not compile used to refuse the whole file. Now a
zone-scoped error sets that zone aside — its last good
version keeps serving if it had one, and the rest of the file
publishes regardless. Only three things still refuse the whole
file: a fault that spans zones, too many broken zones at once (five,
or a twentieth of the file, capped at fifty), or nothing left to
serve. cdn_zones_set_aside_total{outcome="carried"|"dropped"}
counts both cases and nothing about it is silent.