CDN Configuration Validation Before Deploy: Schema Checks, Syntax Tests, and Dry Runs

I've seen a single missing comma in a CDN config take down image delivery for 40 million users. It wasn't a complex edge logic bug or a failed failover — it was a JSON array that passed code review because everyone assumed the deploy pipeline would catch it. It didn't. If you're managing CDN infrastructure as code, configuration validation isn't optional housekeeping, it's the difference between a boring Tuesday and a 3 AM incident call.

Schema Checks: Catching Structural Errors Before They Hit the Edge

Most CDN misconfigurations I've debugged over the years weren't logic errors at all. They were structural — a required field missing, a string where an integer should be, or an origin hostname that slipped in without a protocol. Without automated schema checks, these mistakes sail right through pull requests. Your editor might highlight JSON syntax, but it won't tell you that your cache_ttl is set to "3600" as a string when the API expects an integer, or that you forgot the forward_host_header block entirely.

That's where JSON Schema or OpenAPI-based validation earns its keep. When we moved our edge configurations into version control back in 2019, the first thing we added wasn't fancy testing — it was a strict schema that described every allowed field, type, and enum value. It felt pedantic at the time, but it cut our pre-deploy failures by more than half in the first month. If you're working with infrastructure as code tools, you get a lot of this for free. For instance, CDN Infrastructure with Pulumi: Type-Safe CDN Configuration in Real Programming Languages gives you type safety at the language level, so your IDE screams at you before you even commit, while CDN Resources in CloudFormation: AWS-Native Infrastructure as Code for Content Delivery will fail a cfn-lint run if your CloudFront distribution properties don't match the resource specification.

Don't rely on manual review for this. Define the schema once, validate on every commit. Here's a simplified example we use to validate a custom edge routing config before any syntax tests even run:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "EdgeRoutingConfig",
  "type": "object",
  "required": ["version", "origins", "behaviors"],
  "properties": {
    "version": { "type": "string", "pattern": "^\\d+\\.\\d+$" },
    "origins": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["id", "hostname", "protocol"],
        "properties": {
          "id": { "type": "string", "minLength": 3 },
          "hostname": { "type": "string", "format": "hostname" },
          "protocol": { "type": "string", "enum": ["https", "http"] },
          "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
        },
        "additionalProperties": false
      }
    },
    "behaviors": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["path_pattern", "target_origin_id", "cache_ttl"],
        "properties": {
          "path_pattern": { "type": "string", "minLength": 1 },
          "target_origin_id": { "type": "string" },
          "cache_ttl": { "type": "integer", "minimum": 0, "maximum": 31536000 }
        }
      }
    }
  },
  "additionalProperties": false
}

# Validation in CI with ajv-cli
# ajv validate -s cdn-schema.json -d cdn-config.json --strict=true

Syntax Testing Beyond Linting: Making Pre-Deploy Checks Actually Useful

Schema validation tells you the shape is right, but syntax testing tells you the CDN will actually accept it. They're not the same thing. I learned this the hard way when a perfectly valid JSON file contained a regex for path matching that was technically correct JSON but invalid for our provider's VCL engine. The deploy API returned a 400 error with a cryptic message, and the release was blocked for two hours while we hunted down the unsupported lookahead.

Good pre-deploy checks run the provider's own syntax validator locally. Every major CDN gives you one — whether it's Fastly's fastly vcl custom create --dry-run, CloudFront's aws cloudfront validate-distribution-config, or Akamai's Property Manager validation API. The goal is to run the exact same parser the edge will use, but on your laptop or in CI. You don't want to discover an illegal header name or an invalid cache-key directive during a production push. Check the HTTP Working Group Specifications for what's actually allowed in header field names and cache directives, and cross-reference it with the web.dev Articles on caching best practices — then codify those rules into your tests so you don't have to remember them.

We've standardized our syntax testing stage to run in under 30 seconds, so developers don't skip it. If you're using configuration management at scale, this fits naturally into your pipeline. Our team that uses CDN Automation with Ansible Playbooks: Configuration Management at Edge Scale runs an Ansible lint and a provider-specific validation as two separate CI jobs — one catches YAML issues, the other catches CDN-specific syntax. Here's the bash pattern we use in GitHub Actions for a generic CDN provider with a validate endpoint:

#!/bin/bash
set -euo pipefail

CONFIG_FILE="edge/cdn-config.json"
API_TOKEN="${CDN_API_TOKEN}"

echo "Running configuration validation..."

# 1. Schema check (fast, no network)
ajv validate -s schemas/cdn-schema.json -d "$CONFIG_FILE" --strict=true

# 2. Syntax testing via provider API (dry validation, no deployment)
HTTP_STATUS=$(curl -s -o /tmp/validate.json -w "%{http_code}" \
  -X POST "https://api.cdn-provider.example/v1/config/validate" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d @"$CONFIG_FILE")

if [ "$HTTP_STATUS" -ne 200 ]; then
  echo "Syntax testing failed with status $HTTP_STATUS"
  cat /tmp/validate.json | jq '.errors'
  exit 1
fi

# 3. Check for warnings that don't fail but should be reviewed
WARNINGS=$(jq '.warnings | length' /tmp/validate.json)
if [ "$WARNINGS" -gt 0 ]; then
  echo "Validation passed with $WARNINGS warning(s):"
  jq '.warnings' /tmp/validate.json
fi

echo "Syntax tests passed."

CDN Dry Runs: The Only Way to Preview Real Edge Behavior

If schema checks and syntax testing are about "will this deploy?", a CDN dry run answers "what will this actually do?". I don't trust any CDN change that hasn't been through a dry run, even if it's just adding a single header. A dry run simulates request routing, cache key generation, and origin selection against the proposed config without serving a single byte to real users. It's the closest you'll get to seeing the future.

What makes dry runs so valuable is that they surface logical mistakes that earlier checks can't. For example, your config might be syntactically perfect but route all /api/* traffic to the image optimization origin, or set a cache TTL of zero on your most expensive product page. A proper dry run with a synthetic request bundle will flag that. We feed ours a list of 50 representative URLs — homepage, static assets, API routes, authenticated pages — and assert on expected cache behaviors, headers, and origin targets. It's not a full integration test, but it catches about 90% of the "oops, wrong behavior" errors.

Not all dry runs are equal, so you need to know what your provider simulates and what it doesn't. Some providers only validate syntax, others simulate edge logic, and a few will even run your edge functions in a sandbox. That distinction matters when you're deciding how much confidence to place in the result.

Validation Stage What It Catches Avg. Execution Time Prevented Incidents (per 100 deploys)
Schema Checks (ajv / cfn-lint) Missing fields, wrong types, enum violations, extra properties 1-3 seconds ~18
Syntax Testing (provider API) Invalid regex, illegal headers, unsupported directives, VCL errors 5-15 seconds ~11
CDN Dry Run (simulated requests) Wrong origin routing, cache-key collisions, 0-TTL on hot paths 20-45 seconds ~9
Header & Cache Policy Lint Duplicate Cache-Control, missing surrogate keys, CORS misconfigs 2-4 seconds ~6

Putting It All Together: A Validation Pipeline That Fails Fast

Knowing about schema checks, syntax tests, and dry runs is one thing — wiring them into a deployment pipeline that actually stops you from shooting yourself in the foot is another. Most teams I’ve audited have the checks, but they’re running them as warnings, not gates. If a broken edge config can still be merged because someone clicked “ignore,” you don’t have validation, you have theater.

Your pipeline should run in three strict, sequential stages and fail in under 90 seconds. We run ours on every pull request and we’ve tuned it to fail in about 42 seconds on average. Stage 1 is the JSON Schema / OpenAPI validation — it’s cheap, it’s sub-second, and it catches 70% of the dumb mistakes like a missing originShield region or an invalid cache-key directive. Stage 2 is the syntax and semantic linting with the vendor’s actual parser, not a generic YAML linter. And Stage 3 is the dry run against a staging PoP. That last one costs you 15-25 seconds but it’s the only place you’ll catch logic errors like a regex that matches `/api/*` but also accidentally matches `/api-docs`.

Don’t try to build this from scratch if you’re already using a configuration management tool. If you’re managing edge nodes with CDN Configuration with Chef: Recipe-Based Infrastructure for Content Delivery or CDN Puppet Management for Edge Nodes: Declarative Configuration at Scale, you can hook these validators right into your `knife` or `puppet parser` workflow. For templated configs, the same principle applies — validate the rendered output, not the template. I’ve seen teams validate their Jinja2 and go validation passes, but the rendered VCL is still broken because a variable was empty. Our rule: templating is step zero, validation always runs on the final artifact.

# .github/workflows/cdn-validate.yml - Fails fast, no dry-run if schema fails
name: cdn-config-validate
on: [pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Stage 1: Fast schema check (<1s)
      - name: Render and Schema Check
        run: |
          jinja2 edge.vcl.j2 vars/prod.json > rendered.vcl
          ajv validate -s schemas/cdn-config.v7.json -d rendered.vcl.json --strict

      # Stage 2: Vendor syntax test
      - name: Syntax & Semantic Tests
        run: |
          varnishd -C -f rendered.vcl  # or fastly compute validate
          cdn-lint --rule-set edge-best-practices rendered.vcl.json

      # Stage 3: Dry run - only if 1 & 2 pass (15-30s)
      - name: Edge Dry Run on Staging PoP
        run: |
          cdn-cli config:deploy --dry-run --pop stag-ord-01 \
            --config rendered.vcl.json --expect-status 200,304
          cdn-cli diff --staged --current | grep -v "x-cache-status"

Advanced Patterns: When Validation Passes But Production Still Breaks

Here’s the ugly truth I’ve learned after a dozen 2 AM rollbacks: a green pipeline doesn’t guarantee a safe deploy. Templating drift is the biggest culprit. You’ll pass validation on a config rendered with your CI’s variables, but production uses a different set of secrets or edge dictionaries. That’s why teams moving to CDN Configuration Templating: Jinja2, HCL, and Go Templates for Edge Configs need to implement variable freezing. We snapshot all variables — `{{ origin_timeout }}`, `{{ stale_while_revalidate }}` — into an artifact and validate that snapshot. If your dry run uses `origin_timeout: 3s` but prod is `5s`, you’re not testing reality.

The second pattern is canaried diffing. Don’t just dry-run; diff the behavior. After your dry-run deploys to a single staging PoP, fire 500-1,000 synthetic requests covering your critical paths: cached HITs, MISSes, 404s, range requests, and that one weird legacy client that still sends HTTP/1.0. We compare the `x-cache`, `age`, `cache-control`, and `ttl` headers between staging and production. If your new config drops your hit ratio from 94.2% to 88% on the canary, that’s a failure, even if syntax is perfect. We block deploys if hit ratio deviates by more than 1.5% or if origin requests spike by >10% in the canary window.

When a validation does fail, don’t just read the log — reproduce it locally. 90% of “flaky validator” tickets I get are actually stale local schemas. Pin your schema version, vendor CLI, and linter ruleset in your repo — we use `[email protected]` and `[email protected]` via `package.json` — so every engineer validates against the exact same edge logic. And if your dry run times out after 30 seconds, it’s not hanging; it’s telling you your origin health check or shielding config is unreachable from that PoP. Fix the network path before you force the deploy, or you’ll turn a validation timeout into a 5xx outage for 20% of your users.

Frequently Asked Questions

How long should a full CDN config validation pipeline take?

For most teams, 45 to 90 seconds is the sweet spot. Schema checks should be <1 second, syntax tests 2-5 seconds, and a single-PoP dry run 15-30 seconds. If you’re over 2 minutes, you’re probably dry-running against too many PoPs — one representative PoP with synthetic traffic is enough for pre-merge. Save multi-region canaries for post-merge staging.

Should I validate the template or the rendered config?

Always validate the rendered, final artifact — the exact JSON, VCL, or Terraform you’ll push to the edge. Validating a Jinja2 or HCL template only checks template syntax, not whether a missing variable will render into an invalid `proxy_pass` or an empty `surrogate-key`. Render first, then run schema and dry-run checks on that output.

What's the difference between a syntax test and a dry run?

A syntax test checks if the edge will accept your config — is the VCL parseable, is the JSON schema correct. A dry run checks if the edge will behave correctly with your config. It boots the config on a real (but isolated) edge node and tests request routing, caching logic, header manipulation, and origin shielding without serving real user traffic. You need both.

Our validation passes but hit ratio still drops after deploy. Why?

That’s usually a semantic or logic error, not a syntax error. Common causes: an overly broad `Vary` header, a regex that busts the cache key for 30% of URLs, or a TTL change from `1d` to `1h` that wasn’t flagged as breaking. Add behavioral validation: compare `x-cache: HIT/MISS` ratios and origin offload on your dry-run PoP against production using 500+ synthetic requests before promoting.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker