CDN Environment Promotion: Moving Configurations from Dev to Staging to Production

About three years ago I watched a junior engineer copy a CDN caching rule straight from dev to production to "save time" — and take down checkout for 22 minutes because the production origin path was different. We've all felt that pressure to move fast. But after a decade of managing edge configs at scale, I've learned that a boring, predictable promotion pipeline beats heroic firefighting every single time.

The Promotion Pipeline That Doesn't Rely on Copy-Paste

When I started, environment promotion meant someone would export JSON from the dev CDN, manually find-and-replace hostnames, and import it into staging. It worked until it didn't. You'd get subtle drift — a cache TTL that was 60 seconds in dev but meant to be 3600 in production, or a WAF rule that never got promoted because it was added directly in the console on a Friday afternoon. If you can't tell me exactly what changed between dev and prod, you don't have a pipeline, you have a hope.

These days I won't run a CDN setup without Git as the source of truth. Every environment's configuration lives in code, and promotion is a pull request, not a manual task. Dev automatically deploys on merge to develop, staging on merge to staging, and production requires a tagged release and an approval. That gives you an audit trail, a rollback point, and — crucially — a place for automated checks to run. It pairs well with approaches like CDN Infrastructure with Pulumi: Type-Safe CDN Configuration in Real Programming Languages if you're managing edge logic in TypeScript or Python, but the principle works whether you're using Terraform, Pulumi, or plain API calls.

The trick is to never let humans edit staging or production directly. If someone hotfixes production — and they will — that change has to be back-ported to code immediately, or your next promotion will wipe it out. I've seen teams enforce this with a simple drift check that runs every hour and posts to Slack when the live config doesn't match Git.

# promote.sh - Promotes validated CDN config from staging to production
#!/bin/bash
set -euo pipefail

ENV_FROM="staging"
ENV_TO="production"
CONFIG_VERSION=$(git rev-parse --short HEAD)

echo "Promoting $CONFIG_VERSION from $ENV_FROM to $ENV_TO..."

# 1. Diff configs - fail if unexpected changes exist
cdn diff --from $ENV_FROM --to $ENV_TO --fail-on-drift

# 2. Run validation suite against staging
npm run test:cdn -- --env=staging --strict

# 3. Create versioned artifact and tag
cdn publish --env $ENV_TO --version $CONFIG_VERSION \
  --config ./cdn/config.$ENV_TO.json \
  --message "Promote $CONFIG_VERSION via pipeline"

echo "Promotion complete. Monitoring rollout..."
cdn rollout-status --env $ENV_TO --watch

Keeping Environment Configs Consistent Without Hardcoding Everything

The biggest pain in promotion isn't moving files, it's handling the differences that *should* exist. Dev might point at origin-dev.example.internal, staging at origin-staging.example.com, and production at origin.example.com. Cache settings, log levels, and feature flags will vary too. If you hardcode those, you'll end up maintaining three completely separate configs that inevitably diverge.

What works better is a single base configuration with environment-specific overlays. I typically keep a cdn.base.json that holds 90% of the rules — cache behaviors, header manipulations, error pages — and then tiny cdn.dev.json, cdn.staging.json, and cdn.prod.json files that only override what's necessary. Tools like Ansible make this pattern natural with variable files and Jinja templates, as covered in CDN Automation with Ansible Playbooks: Configuration Management at Edge Scale, and the same idea applies if you're on CDN Resources in CloudFormation: AWS-Native Infrastructure as Code for Content Delivery. You're not duplicating logic, you're parameterizing it.

Don't forget to validate the merged result before it ever hits an edge node. The HTTP Working Group Specifications (https://httpwg.org/specs/) are ruthless about how Cache-Control, Vary, and Surrogate-Control interact, and a typo in a header name won't fail your deployment but will silently break caching. I lint every merged config against a JSON schema and run a quick check for common mistakes like forgetting stale-while-revalidate on static assets, which the guides on web.dev Articles (https://web.dev/articles/) explain well for optimal performance.

// cdn.base.json - Shared across all environments
{
  "origin": "{{ origin_hostname }}",
  "behaviors": [
    {
      "path": "/static/*",
      "cache": {
        "ttl": 86400,
        "staleWhileRevalidate": 3600,
        "key": ["uri", "accept-encoding"]
      },
      "headers": {
        "Cache-Control": "public, max-age=86400, stale-while-revalidate=3600"
      }
    },
    {
      "path": "/api/*",
      "cache": { "ttl": 0, "bypass": true },
      "headers": {
        "Cache-Control": "private, no-store"
      }
    }
  ]
}

// environments/production.vars.json
{
  "origin_hostname": "origin.example.com",
  "log_level": "error",
  "waf_mode": "block"
}

What to Validate in Staging Before You Even Think About Production

Staging isn't just a second dev environment — it's your last chance to catch problems with real-world traffic patterns. I treat staging promotion as a gate, not a formality. After deploying to staging, I replay a sample of production access logs, run synthetic checks from multiple regions, and compare cache performance against production baselines. If staging's hit ratio is 15 points lower than production's, something in your cache key or TTL logic is wrong, and you don't want to discover that when customers are waiting.

I've gotten into the habit of automating three checks every time: functional correctness (do the right assets return HIT vs MISS?), security headers (are HSTS and CSP present on the right routes?), and origin protection (is the origin actually shielded, or did a rule change accidentally bypass the cache?). An automated test that requests /static/app.js twice and expects a x-cache: HIT on the second try will catch more misconfigurations than you'd expect. Don't underestimate how often a regex in a path pattern is off by one character.

You should define hard gates for promotion. If the checks don't pass, the pipeline stops. No exceptions for "we'll fix it in prod." Below are the thresholds I use as a starting point — adjust them for your traffic, but make them explicit and make the pipeline enforce them.

Gate Metric Dev Target Staging Target Production Gate (Block Promotion If Fails)
Cache Hit Ratio (static assets) > 75% > 88% Must be within 3% of current production
Origin 5xx Error Rate (5 min window) < 1.0% < 0.2% < 0.1% and no increase > 0.05% vs baseline
p95 Time to First Byte (TTFB) < 250 ms < 180 ms < 150 ms, no regression > 20 ms vs prod
Config Validation & Security Tests 100% schema pass 100% pass + no critical WAF warnings 100% pass, 0 failed header/security checks
Stale-While-Revalidate Coverage Best effort > 95% of cacheable routes 100% of static / image routes

Automating Promotion: Building a GitOps Pipeline That Actually Scales

You've got the strategy, now you need the plumbing. The teams I've seen nail this don't rely on a senior engineer manually running a promote script at 2 AM. They wire it into Git. Every CDN config lives in a repo, templated once, with environment-specific values injected at deploy time. Your dev branch deploys to the dev edge network on every merge, staging deploys on tagged releases, and prod only moves when a promotion is explicitly approved. No SSH-ing into edge nodes, no hand-edited JSON blobs.

The trick is separating your base config from your environment overlays. We use a single source template — whether it's Jinja2, HCL, or Go templates doesn't matter as much as the discipline — and render it with different variable files for dev, staging, and prod. That way your cache-key logic, stale-while-revalidate rules, and origin shielding config are identical everywhere. What changes are the values: origin hostnames, TLS cert ARNs, rate limits, and feature flags. If you're still hardcoding origin-prod.example.com in your VCL or edge worker, you're asking for a promotion failure. For a deeper dive on rendering, check out our breakdown of CDN Configuration Templating: Jinja2, HCL, and Go Templates for Edge Configs — we've found Go templates cut our render errors by roughly 60% compared to raw string replacement.

Here's what a minimal, working promotion workflow looks like in practice. We run this via GitHub Actions, but the same logic works in GitLab CI or Jenkins. It validates the rendered config, diffs it against what's live in staging, and only promotes if hit-ratio and error-rate checks pass for 10 minutes post-deploy:

# .github/workflows/cdn-promote.yml
name: cdn-promote
on:
  push:
    tags: ['v*.*.*']

jobs:
  render-and-validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Render config for target env
        run: |
          python render.py --template cdn/base.vcl.j2 \
            --vars envs/${{ inputs.target_env }}.yaml \
            --out dist/edge.vcl
          cdn-cli validate --config dist/edge.vcl --strict

  promote-to-staging:
    needs: render-and-validate
    if: inputs.target_env == 'staging'
    steps:
      - name: Deploy to staging PoPs
        run: cdn-cli deploy --env staging --config dist/edge.vcl --auto-rollback
      - name: Wait and verify
        run: |
          sleep 600
          cdn-cli check --env staging --metric error_rate --lt 0.5 \
            --metric p95_ttfb --lt 180ms --metric hit_ratio --gt 92%

  promote-to-prod:
    needs: promote-to-staging
    environment: production # requires manual approval
    steps:
      - run: cdn-cli promote --from staging --to prod --diff --require-approval
      - run: cdn-cli deploy --env prod --canary 5% --duration 15m

Advanced Patterns: Canaries, Blue-Green, and Fixing Drift When It Bites You

Once your basic pipeline's humming, you'll hit the harder problems. The biggest one isn't a bad deploy — it's drift. Someone hotfixes prod directly to mitigate a 3 AM origin outage, and now staging is 12 commits behind reality. We've seen prod configs drift by as much as 18% from staging in under two weeks when teams allow emergency bypasses without automation. You don't want to discover that during your next big promotion. We run a drift detection cron every 30 minutes that pulls the live config from all 280+ PoPs via API, hashes it, and compares it to the Git HEAD for that environment. If there's a mismatch, it opens a blocking PR and pings Slack. No drift goes unnoticed for more than an hour, and you can't promote to prod if staging and prod have diverged from Git.

For prod itself, don't do big-bang deploys. Even with perfect staging validation, prod traffic is a different beast — you'll see cache key collisions or origin shielding issues at 40 Gbps that never showed up at staging's 200 Mbps. Our default is a 5% canary for 15 minutes, then 25%, then 50%, then 100%. We watch four signals religiously: 5xx rate (must stay under 0.1%), p95 TTFB (we alert if it regresses more than 20ms), cache hit ratio (shouldn't drop more than 1.5 points), and origin offload. If any signal trips, the canary auto-rolls back in under 45 seconds. Blue-green is another solid option if your CDN provider supports parallel configs — you keep the old config warm for 30 minutes, which has saved us twice when a regex change caused 2.4% of image requests to bypass cache entirely.

When things do break — and they will — don't try to fix forward on prod. Roll back first, debug in staging. Your pipeline should make rollback a one-click operation: cdn-cli rollback --env prod --to v1.42.3 should restore the previous known-good config globally in under 90 seconds. After that, figure out why. Most promotion failures we troubleshoot come down to three things: inconsistent variable naming across env files, missing purge logic for new cache keys, or declarative tool conflicts. If your team is split between Chef and Puppet for edge node management, standardize the interface. We've written extensively on both approaches — see CDN Configuration with Chef: Recipe-Based Infrastructure for Content Delivery and CDN Puppet Management for Edge Nodes: Declarative Configuration at Scale — but mixing them without a clear ownership boundary is how you get a node that thinks its TTL is 1 day and 1 hour at the same time.

Frequently Asked Questions

How long should we soak a config in staging before promoting to prod?

Don't go by time alone, go by traffic coverage. We recommend at least 24 hours in staging that includes one full daily traffic peak, or 15 minutes of mirrored prod traffic at 10% volume if your CDN supports traffic shadowing. For a low-risk header change, 2-4 hours with synthetic checks might be fine. For cache-key or origin logic changes, give it 48 hours — we've caught hit-ratio regressions of 3-4% on day two that looked perfect on day one.

Should we use the same TLS certificates and WAF rules in staging as in prod?

Almost, but not exactly. Use the same WAF rule set in log-only mode in staging to catch false positives — blocking mode in staging will hide production issues. For TLS, use a staging cert from the same CA with identical cipher suites and HSTS settings, but don't reuse the prod private key. Your staging TLS handshake should be byte-for-byte identical except for the SAN. We've seen a 12ms TTFB difference just from a mismatched OCSP stapling config.

What's the safest way to handle emergency hotfixes that bypass the pipeline?

Allow it, but make it painful and temporary. Require a break-glass approval, auto-create a backport PR to Git within 5 minutes, and set a TTL on the hotfix — our hotfixes auto-revert after 6 hours unless explicitly merged through the normal promotion path. If you don't force the Git reconciliation, that hotfix becomes permanent drift, and your next full promotion will silently overwrite it and reintroduce the outage.

Can we promote configs between CDN vendors or just between environments?

Between environments on the same vendor, promotion can be near-automatic with templating. Cross-vendor promotion is a different project — you're translating VCL to Cloudflare Workers or Akamai EdgeWorkers, and there's no 1:1 mapping. Treat it as a port, not a promotion. You'll need an abstraction layer and separate validation. Don't try to force your Fastly VCL through a CloudFront Function pipeline; you'll spend more time debugging translation than you save.

Related Articles

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