I've migrated CDN configurations more times than I can count over the last decade, and I can tell you the midnight cutover is never the hard part. The hard part is week three of rule translation, when you've got 400 edge rules in Akamai Property Manager and someone asks why the Cloudflare conversion is dropping authenticated API traffic. That's exactly what happened to us during a retail client's cross-provider migration last fall — 18,000 lines of edge logic, and no automated way to prove the new rules actually behaved the same.
Why Rule Translation Fails When You Treat It Like Syntax Conversion
Most teams approach config migration as a find-and-replace problem. You take an Akamai rule, you look up the Cloudflare equivalent, you map the fields. I've seen spreadsheets with 200 rows trying to do this mapping manually, and they always collapse under their own weight. The issue isn't that the syntax is different — it's that the execution models have fundamentally different assumptions about order, scope, and default behavior. A cache-key modification that runs early in Fastly's VCL might run after authentication in another provider's phased rule engine, and that timing difference will break your application in ways that don't show up until production traffic hits it.
This is where a lot of cross-provider migration projects stall. You can't just translate what a rule says, you have to translate when and how it executes. For example, Akamai's Property Manager uses a nested tree with inherited criteria, while Cloudflare Rules and Fastly VCL use a flat, sequentially evaluated list. If you don't account for that structural mismatch, your rule conversion will produce a technically valid config that makes completely different decisions. I learned this after a media customer spent six weeks manually rewriting rules only to discover their geo-blocking logic was evaluating after cache lookup instead of before, effectively exposing content they meant to restrict. If you're still planning your approach, the Comprehensive CDN Migration Checklist: Every Step from Planning to Cutover is a far better starting point than jumping straight into translation.
// Intermediate Representation (IR) for deterministic rule conversion
// Don't translate Akamai -> Cloudflare directly. Normalize first.
{
"id": "rule-042-cache-bypass-api",
"source_format": "akamai_pm_v1",
"normalized": {
"phase": "request", // request | cache_lookup | response
"priority": 100,
"criteria": {
"all": [
{ "field": "http.request.uri.path", "op": "matches", "value": "^/api/v2/.*" },
{ "field": "http.request.method", "op": "in", "value": ["POST", "PUT"] }
]
},
"actions": [
{ "type": "bypass_cache", "value": true },
{ "type": "set_header", "name": "x-migration-trace", "value": "bypass-apiv2" }
],
"execution_order": "before_auth"
},
"targets": {
"cloudflare_rules": { "phase": "http_request_cache_settings", "expression": "(http.request.uri.path matches \"^/api/v2/.*\")" },
"fastly_vcl": { "subroutine": "vcl_recv", "condition": "req.url ~ \"^/api/v2/\"" }
}
}
Building a Normalization Layer That Makes CDN Automation Possible
You don't automate provider-to-provider. You automate provider-to-IR and IR-to-provider, and that distinction is what makes CDN automation maintainable. My team now builds a canonical intermediate representation for every config migration, even if we're only moving between two vendors. The IR captures phase, priority, match criteria, and side effects in a provider-agnostic schema. That way, when you're moving from, say, CloudFront Functions to EdgeWorkers, you aren't rewriting your translator — you're just adding a new emitter. It feels like extra work up front, and it is, but it prevents the N-squared problem where every new provider requires you to write translators for every other provider you've ever supported.
Your translator also needs to handle what I call semantic defaults — the invisible behavior providers don't document well. Take header handling. One provider might preserve origin Cache-Control headers by default while another strips them unless you explicitly pass them through. Another might normalize header names to lowercase, breaking a downstream comparison that expects Title-Case. According to the HTTP Working Group Specifications, header field names are case-insensitive, but your application code may not be. We've had to code explicit lowercasing and passthrough directives into our IR just to keep behavior stable. The articles on web.dev Articles are surprisingly useful for sanity-checking how browsers will actually interpret the cached responses after these translations, especially around Cache-Control and Vary.
Once the IR is solid, the actual rule conversion becomes a code generation problem. We run everything through a validation pipeline before it ever touches a staging edge. That pipeline lints the IR for conflicts — like two rules with overlapping criteria but contradictory cache TTLs — and then emits provider-specific configs for testing. This is also where a proper CDN Vendor Evaluation Matrix: Scoring Providers on What Actually Matters pays off, because you've already documented which features have no equivalent on the target and need a workaround. Don't try to hand-migrate the exceptions; flag them in the IR as manual_review and fail the build until an engineer signs off.
#!/usr/bin/env python3
# cdn_translate.py - IR-based config migration emitter
import json
import glob
def load_ir_rules(path="ir_rules/*.json"):
rules = []
for f in glob.glob(path):
with open(f) as fh:
rules.append(json.load(f)["normalized"])
# Sort deterministically by phase + priority - execution order matters
return sorted(rules, key=lambda r: (r["phase"], r["priority"]))
def emit_cloudflare(rules):
out = []
for r in rules:
if r["phase"] != "request":
continue # skip non-request phases for this emitter
expr = " and ".join([f'{c["field"]} {c["op"]} "{c["value"]}"' for c in r["criteria"]["all"]])
actions = [a for a in r["actions"] if a["type"] in ("bypass_cache", "set_header")]
out.append({"expression": expr, "actions": actions, "priority": r["priority"]})
return out
def validate_no_shadow(rules):
# Simple shadowing detection: identical criteria, different actions
seen = {}
for r in rules:
key = json.dumps(r["criteria"], sort_keys=True)
if key in seen and seen[key] != r["actions"]:
raise ValueError(f"Conflicting actions for criteria {key}")
seen[key] = r["actions"]
if __name__ == "__main__":
rules = load_ir_rules()
validate_no_shadow(rules)
with open("dist/cloudflare_rules.json", "w") as out:
json.dump(emit_cloudflare(rules), out, indent=2)
print(f"Emitted {len(rules)} rules -> dist/cloudflare_rules.json")
Validating Behavioral Parity Instead of Configuration Parity
A successful config migration doesn't mean the configs look similar. It means the edge behaves identically under real traffic. I've stopped comparing line counts or rule counts — they're vanity metrics. What matters is whether a request with a specific set of headers, cookies, and path gets the same cache status, response headers, and origin fetch decision on both CDNs. That's why our final stage isn't a config diff; it's a replay harness that fires 50,000 recorded production requests at dual staging stacks and compares the outcomes. You're looking for behavioral parity, and you won't find it by eyeballing JSON.
We log every difference and categorize it: TTL mismatch, header mutation, cache-key variance, status code divergence. Most mismatches trace back to an implicit default we didn't normalize. One classic example is query string handling in cache keys — some providers include all query parameters by default, others ignore them unless you explicitly whitelist them. If your IR doesn't have an explicit cache_key.include_query field, your automatically generated rules will serve the same cached response for /product?id=123 and /product?id=456. That's a cache poisoning incident waiting to happen, and you won't catch it without traffic replay.
Before you automate, make sure your requirements actually capture these edge behaviors. A vague RFP that says "must support edge rules" tells you nothing about phase execution or cache-key control. We've started requiring vendors to fill out a rule semantics questionnaire as part of procurement, which is well described in the CDN RFP Template: Writing Requirements That Get Meaningful Vendor Responses. Once you have those answers, you can build your IR to cover the deltas. The table below shows the tolerances we enforce before we'll sign off on a cutover — if any of these drift beyond threshold, the migration doesn't ship.
| Validation Metric | Legacy CDN (Baseline) | Target CDN (Auto-Converted) | Allowed Variance |
|---|---|---|---|
| Cache Hit Ratio (24h replay) | 92.4% | 91.9% | ±1.5% |
| TTFB p95 (global) | 118 ms | 124 ms | ±10% |
| Rule Match Accuracy | 100% (baseline) | 99.97% | ≥99.9% |
| Origin Requests per Minute | 4,820 | 4,945 |