ت تیزرس · دمو

The config plane

Three files, kept apart on purpose:

fileholdswhy apart
config.jsonzones, pools, rulesthe part a panel edits
certs.jsonPEM cert/key (+ OCSP staple) per zonea settings change is not a key reship
state.jsonpurge generationsa 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.