#!/usr/bin/env bash # Configure a Bunny.net pull zone for the APIX registry. # # What this script does: # 1. Creates a pull zone pointing at the origin VPS # 2. Enables query-string vary cache (so /services?capability=nlp and # /services?capability=translation are cached as separate entries) # 3. Sets the edge TTL to follow the origin Cache-Control headers # 4. Adds the custom hostname (api-index.org) # 5. Prints the CDN hostname to use in your DNS CNAME record # # Prerequisites: # - A Bunny.net account with an API key (bunny.net → Account → API) # - The origin VPS is reachable at ORIGIN_URL before running this # - DNS for api-index.org is under your control # # Usage: # BUNNYNET_API_KEY=your-key \ # ORIGIN_URL=https://your-vps-ip-or-hostname \ # CUSTOM_HOSTNAME=api-index.org \ # ./scripts/setup-bunnynet.sh # # Optional — enable real-time Loki log forwarding for live telemetry: # SYSLOG_HOST=your-vps-ip \ # SYSLOG_PORT=5514 \ # ./scripts/setup-bunnynet.sh # # Optional — install weekly cron job (Mondays 06:00) for CDN analytics report: # INSTALL_CRON=true \ # ./scripts/setup-bunnynet.sh # # Deploy scripts/promtail-cdn-logs.yaml on the VPS before setting SYSLOG_HOST. # Bunny.net will start streaming access logs (one syslog line per request, # including CDN cache hits) to Promtail, which pushes to your Loki instance. # # CDN selection rationale: # Bunny.net is European (Slovenia), GDPR-compliant, has PoPs in Shanghai, # Singapore, Tokyo, Hong Kong, and Frankfurt. No founding-member conflict # (Cloudflare and AWS are both founding member candidates and must not # operate infrastructure over the registry). set -euo pipefail BUNNYNET_API_KEY="${BUNNYNET_API_KEY:?BUNNYNET_API_KEY is required}" ORIGIN_URL="${ORIGIN_URL:?ORIGIN_URL is required (e.g. https://1.2.3.4)}" CUSTOM_HOSTNAME="${CUSTOM_HOSTNAME:-api-index.org}" PULL_ZONE_NAME="${PULL_ZONE_NAME:-apix-registry}" BUNNYNET_API="https://api.bunny.net" # Optional — set SYSLOG_HOST to enable real-time log forwarding to Promtail/Loki. # Bunny.net streams one syslog line per request (all requests, not just misses). # Start Promtail with scripts/promtail-cdn-logs.yaml on the VPS first. SYSLOG_HOST="${SYSLOG_HOST:-}" SYSLOG_PORT="${SYSLOG_PORT:-5514}" INSTALL_CRON="${INSTALL_CRON:-false}" echo "==> Creating Bunny.net pull zone '${PULL_ZONE_NAME}' → ${ORIGIN_URL} ..." CREATE_RESPONSE=$(curl -sf -X POST "${BUNNYNET_API}/pullzone" \ -H "AccessKey: ${BUNNYNET_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "Name": "'"${PULL_ZONE_NAME}"'", "OriginUrl": "'"${ORIGIN_URL}"'", "Type": 0, "EnableQueryStringVaryCache": true, "UseBackgroundUpdate": false, "EnableWebPVary": false, "EnableCountryCodeVary": false, "EnableMobileVary": false, "EnableHostnameVary": false, "IgnoreQueryStrings": false, "CacheControlMaxAgeOverride": -1, "CacheControlBrowserMaxAgeOverride": -1, "EnableLogging": true, "LoggingIPAnonymizationEnabled": true }') # -1 on both MaxAgeOverride fields means "respect the origin Cache-Control header" # The application sets: GET / → max-age=60, GET /services|/devices → max-age=30 PULL_ZONE_ID=$(echo "${CREATE_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])") CDN_HOSTNAME=$(echo "${CREATE_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin)['Hostnames'][0]['Value'])") echo " Pull zone ID : ${PULL_ZONE_ID}" echo " CDN hostname : ${CDN_HOSTNAME}" echo "" echo "==> Adding custom hostname '${CUSTOM_HOSTNAME}' to pull zone ..." curl -sf -X POST "${BUNNYNET_API}/pullzone/${PULL_ZONE_ID}/addHostname" \ -H "AccessKey: ${BUNNYNET_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"Hostname": "'"${CUSTOM_HOSTNAME}"'"}' > /dev/null echo " Done." echo "" echo "==> Adding edge rule: bypass cache for internal paths (/q/*) ..." curl -sf -X POST "${BUNNYNET_API}/pullzone/${PULL_ZONE_ID}/edgerules/addOrUpdate" \ -H "AccessKey: ${BUNNYNET_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "Enabled": true, "Description": "Bypass cache for Quarkus health and metrics endpoints", "ActionType": 15, "Triggers": [{ "Type": 0, "PatternMatches": ["/q/"], "PatternMatchingType": 12 }], "ActionParameter1": "", "ActionParameter2": "" }' > /dev/null echo " Done." if [ -n "${SYSLOG_HOST}" ]; then echo "" echo "==> Enabling real-time log forwarding → ${SYSLOG_HOST}:${SYSLOG_PORT} (TCP Syslog) ..." curl -sf -X POST "${BUNNYNET_API}/pullzone/${PULL_ZONE_ID}" \ -H "AccessKey: ${BUNNYNET_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "LogForwardingEnabled": true, "LogForwardingHostname": "'"${SYSLOG_HOST}"'", "LogForwardingPort": '"${SYSLOG_PORT}"', "LogForwardingProtocol": 1, "LogForwardingToken": "" }' > /dev/null echo " Done." echo " Bunny.net will forward every request (hits + misses) as a syslog line." echo " Promtail → Loki → Grafana will show live capability traffic within seconds." fi echo "" echo "==> Purging initial cache to ensure clean state ..." curl -sf -X POST "${BUNNYNET_API}/pullzone/${PULL_ZONE_ID}/purgeCache" \ -H "AccessKey: ${BUNNYNET_API_KEY}" > /dev/null echo " Done." echo "" echo "======================================================================" echo " Setup complete. Next steps:" echo "" echo " 1. Add a DNS CNAME record:" echo " ${CUSTOM_HOSTNAME} CNAME ${CDN_HOSTNAME}" echo "" echo " 2. Wait for DNS propagation (~5 minutes for most registrars)" echo "" echo " 3. Bunny.net will provision a free TLS certificate automatically" echo " once the CNAME is live. No manual cert management needed." echo "" echo " 4. Verify the CDN is caching correctly:" echo " curl -sI https://${CUSTOM_HOSTNAME}/ | grep -i cache-control" echo " Expected: Cache-Control: public, max-age=60" echo "" echo " 5. Test from Asia (requires a remote machine or VPN):" echo " curl -w '%{time_total}s\n' -o /dev/null -s https://${CUSTOM_HOSTNAME}/" echo " First request: ~200ms (cache miss → origin). Subsequent: <20ms (edge)." echo "" echo " CDN hostname : ${CDN_HOSTNAME}" echo " Pull zone ID : ${PULL_ZONE_ID}" echo "" echo " Log access (Bunny.net access logs, gzip, one file per day):" echo " curl -H 'AccessKey: \$BUNNYNET_API_KEY' \\" echo " https://logging.bunnycdn.com/YYYY/MM/DD/${PULL_ZONE_ID}/ -o logs.gz" echo "" echo " Query frequency report (last 7 days):" echo " BUNNYNET_API_KEY=\$BUNNYNET_API_KEY PULL_ZONE_ID=${PULL_ZONE_ID} \\" echo " ./scripts/query-report.sh" echo "" echo " Logs are IP-anonymised (last octet zeroed) for GDPR compliance." echo "" if [ -n "${SYSLOG_HOST}" ]; then echo " Live Loki forwarding : ENABLED → ${SYSLOG_HOST}:${SYSLOG_PORT}" echo " Every request (CDN hit and miss) streams as a syslog line." echo " Grafana dashboard shows capability traffic in real-time." echo " Promtail config: scripts/promtail-cdn-logs.yaml" else echo " Live Loki forwarding : DISABLED (re-run with SYSLOG_HOST= to enable)" echo " Without this, only cache misses appear in origin Prometheus metrics." echo " For the OpenClaw demo: enable this before the session starts." fi echo "======================================================================" # Persist pull zone ID for query-report.sh convenience echo "${PULL_ZONE_ID}" > .bunnynet-pull-zone-id echo "" echo "Pull zone ID saved to .bunnynet-pull-zone-id (add to .gitignore)." if [ "${INSTALL_CRON}" = "true" ]; then echo "" echo "==> Installing weekly cron job for CDN analytics report ..." SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPORT_SCRIPT="${SCRIPT_DIR}/query-report.sh" LOG_DIR="${SCRIPT_DIR}/../logs" mkdir -p "${LOG_DIR}" CRON_LINE="0 6 * * 1 BUNNYNET_API_KEY=${BUNNYNET_API_KEY} PULL_ZONE_ID=${PULL_ZONE_ID} ${REPORT_SCRIPT} >> ${LOG_DIR}/cdn-report.log 2>&1" # Install without duplicating — removes any existing query-report.sh entry first ( crontab -l 2>/dev/null | grep -v "query-report\.sh"; echo "${CRON_LINE}" ) | crontab - echo " Done." echo " Schedule : every Monday at 06:00" echo " Log : ${LOG_DIR}/cdn-report.log" echo "" echo " Verify with: crontab -l | grep query-report" fi