From a72c7e48f4af88a25a67b6df07ea0cbb35e9d7de Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Mon, 8 Jun 2026 10:06:37 +0100 Subject: [PATCH 1/3] docs: add Blue/Green & A/B deployment strategies page --- docs.json | 1 + magic-containers/deployment-strategies.mdx | 155 +++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 magic-containers/deployment-strategies.mdx diff --git a/docs.json b/docs.json index 1ebebe3..176c53b 100644 --- a/docs.json +++ b/docs.json @@ -316,6 +316,7 @@ "pages": [ "magic-containers/autoscaling", "magic-containers/rolling-updates", + "magic-containers/deployment-strategies", "magic-containers/graceful-shutdown", "magic-containers/sandbox", "magic-containers/multi-container", diff --git a/magic-containers/deployment-strategies.mdx b/magic-containers/deployment-strategies.mdx new file mode 100644 index 0000000..848d9af --- /dev/null +++ b/magic-containers/deployment-strategies.mdx @@ -0,0 +1,155 @@ +--- +title: "Blue/Green & A/B Deployments" +description: "Run multiple versions of your app at the same time and control how traffic is routed between them." +--- + +Magic Containers lets you run more than one version of an app simultaneously. Two routing patterns build on this: + +- **Blue/green**: switch _all_ traffic from the current version to a new one in a single, deterministic cutover. +- **A/B**: split traffic between two versions by percentage, keeping each user pinned to the same version. + +Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. For everything else, a standard [rolling update](/magic-containers/rolling-updates) is enough. + +| | Blue/Green | A/B | +| ------------- | --------------------------------------------- | --------------------------------------- | +| Traffic | 100% to one version at a time | Split by percentage (e.g. 90/10) | +| Goal | Safe release + instant rollback | Test a version on a slice of users | +| Routing | Pull zone origin points at the active version | Edge Script picks a version per request | +| Configured in | Terraform / dashboard | Edge Scripting (+ two apps) | + +## Why not just use rolling updates? + +A [rolling update](/magic-containers/rolling-updates) gradually replaces instances of a single app with the new image. During the rollout, both the old and new versions serve traffic at the same time, and across many regions a partial or stalled rollout can leave versions co-existing longer than you'd like. + +Blue/green sidesteps this by keeping the new version completely separate until you flip every request to it at once. There's no in-between state, and rolling back is just pointing traffic at the previous version again. + + + Rolling updates are the right default for most apps. Reach for blue/green when + a release must be atomic, such as a schema change where both versions need to + serve the same traffic safely. + + +## Blue/green deployment + +The idea: deploy the new version as a **separate container app** while the current version keeps serving, then switch a pull zone's origin from the old endpoint to the new one. + + + + Create a second Magic Containers app (the "green" version) with the new image. Leave the current "blue" app running and serving production traffic. + + + + Use a pull zone with a `ComputeContainer` origin that targets the currently + active app's container and endpoint. + + + + When the new version is ready, switch the origin to the green app's container + and endpoint, then apply. All traffic moves at once. + + + + To revert, point the origin back at the blue app. Because it's still running, rollback is immediate. + + + +### Terraform example + +This pull zone routes to one container version at a time. To cut over, comment out the active block, uncomment the other, and apply: + +```hcl main.tf theme={null} +resource "bunnynet_dns_zone" "app" { + domain = "example.net" +} + +resource "bunnynet_dns_record" "app" { + zone = bunnynet_dns_zone.app.id + name = "*.app" + type = "CNAME" + value = "example.b-cdn.net" +} + +resource "bunnynet_pullzone" "prod" { + name = "my-app-prod" + + origin { + type = "ComputeContainer" + + # Blue (current version): inactive + # container_app_id = "BLUE_APP_ID" + # container_endpoint_id = "BLUE_APP_ID-http" + + # Green (new version): active + container_app_id = "GREEN_APP_ID" + container_endpoint_id = "GREEN_APP_ID-http" + } +} + +resource "bunnynet_pullzone_hostname" "prod" { + pullzone = bunnynet_pullzone.prod.id + name = "*.app.example.net" + tls_enabled = true + force_ssl = false +} +``` + + + Replace `BLUE_APP_ID` and `GREEN_APP_ID` with your container app IDs. The + cutover is a one-line change to which origin block is active. + + + + For **stateful** workloads such as databases, both versions read and write the + same data, so the two versions must be compatible with a shared schema. The + version-selection logic typically lives in your application or database layer. + + +## A/B deployment + +A/B routing sends a percentage of traffic to each version while keeping individual users pinned to one version (stickiness). [Edge Scripting](/scripting) handles the weighted split, running in front of two separate apps and choosing a version per request. + +The approach: + +1. Deploy **two Magic Containers apps**: version A and version B. +2. Add a [standalone Edge Script](/scripting/standalone/overview) that derives a stable key per visitor (client IP works well for stickiness). +3. Hash the key into a bucket and route to app A or app B based on your target percentage. + +```typescript ab-router.ts theme={null} +import * as BunnySDK from "@bunny.net/edgescript-sdk"; + +const VARIANTS = { + a: "https://app-a.bunny.run", + b: "https://app-b.bunny.run", +}; + +const B_PERCENTAGE = 10; // send 10% of visitors to version B + +// Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket +function bucket(key: string): "a" | "b" { + let h = 0; + for (let i = 0; i < key.length; i++) { + h = (h * 31 + key.charCodeAt(i)) >>> 0; + } + return h % 100 < B_PERCENTAGE ? "b" : "a"; +} + +BunnySDK.net.http.serve(async (request: Request): Promise => { + const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0"; + const variant = bucket(ip); + + const target = new URL(request.url); + const origin = new URL(VARIANTS[variant]); + target.protocol = origin.protocol; + target.host = origin.host; + + return fetch(new Request(target, request)); +}); +``` + +Because the same IP always hashes to the same bucket, a given visitor consistently sees the same version. Adjust `B_PERCENTAGE` to change the split, and update the variant URLs to your two apps' endpoints. + + + Use a key that's stable for the session you want to pin. Client IP is the + simplest. To weight by something else (a cookie, a header, a user ID), hash + that value instead. + From 2999b295d5d91a276a316988f656c8d2c0f74207 Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Mon, 8 Jun 2026 10:50:46 +0100 Subject: [PATCH 2/3] update script examples --- docs.json | 4 +- magic-containers/deployment-strategies.mdx | 63 ++++++++++++++++--- scripting/standalone/examples/geo-routing.mdx | 30 +++++++++ .../standalone/examples/weighted-routing.mdx | 40 ++++++++++++ 4 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 scripting/standalone/examples/geo-routing.mdx create mode 100644 scripting/standalone/examples/weighted-routing.mdx diff --git a/docs.json b/docs.json index 176c53b..88aceb1 100644 --- a/docs.json +++ b/docs.json @@ -414,7 +414,9 @@ "scripting/standalone/examples/return-json", "scripting/standalone/examples/return-html", "scripting/standalone/examples/send-email", - "scripting/standalone/examples/redirect-domain" + "scripting/standalone/examples/redirect-domain", + "scripting/standalone/examples/weighted-routing", + "scripting/standalone/examples/geo-routing" ] } ] diff --git a/magic-containers/deployment-strategies.mdx b/magic-containers/deployment-strategies.mdx index 848d9af..6b2905b 100644 --- a/magic-containers/deployment-strategies.mdx +++ b/magic-containers/deployment-strategies.mdx @@ -3,19 +3,20 @@ title: "Blue/Green & A/B Deployments" description: "Run multiple versions of your app at the same time and control how traffic is routed between them." --- -Magic Containers lets you run more than one version of an app simultaneously. Two routing patterns build on this: +Magic Containers lets you run more than one version of an app simultaneously. Three routing patterns build on this: - **Blue/green**: switch _all_ traffic from the current version to a new one in a single, deterministic cutover. - **A/B**: split traffic between two versions by percentage, keeping each user pinned to the same version. +- **Canary**: route a targeted slice, such as a single country, to the new version first, then widen as it proves healthy. -Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. For everything else, a standard [rolling update](/magic-containers/rolling-updates) is enough. +Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. Pick canary when you want to validate a new version on a targeted slice before rolling it out everywhere. For everything else, a standard [rolling update](/magic-containers/rolling-updates) is enough. -| | Blue/Green | A/B | -| ------------- | --------------------------------------------- | --------------------------------------- | -| Traffic | 100% to one version at a time | Split by percentage (e.g. 90/10) | -| Goal | Safe release + instant rollback | Test a version on a slice of users | -| Routing | Pull zone origin points at the active version | Edge Script picks a version per request | -| Configured in | Terraform / dashboard | Edge Scripting (+ two apps) | +| | Blue/Green | A/B | Canary | +| ------------- | --------------------------------------------- | --------------------------------------- | --------------------------------------- | +| Traffic | 100% to one version at a time | Split by percentage (e.g. 90/10) | Ramps from a targeted slice to 100% | +| Goal | Safe release + instant rollback | Test a version on a slice of users | Validate a version on a targeted slice | +| Routing | Pull zone origin points at the active version | Edge Script picks a version per request | Edge Script picks a version per request | +| Configured in | Terraform / dashboard | Edge Scripting (+ two apps) | Edge Scripting (+ two apps) | ## Why not just use rolling updates? @@ -114,7 +115,7 @@ The approach: 2. Add a [standalone Edge Script](/scripting/standalone/overview) that derives a stable key per visitor (client IP works well for stickiness). 3. Hash the key into a bucket and route to app A or app B based on your target percentage. -```typescript ab-router.ts theme={null} +```typescript import * as BunnySDK from "@bunny.net/edgescript-sdk"; const VARIANTS = { @@ -146,10 +147,52 @@ BunnySDK.net.http.serve(async (request: Request): Promise => { }); ``` -Because the same IP always hashes to the same bucket, a given visitor consistently sees the same version. Adjust `B_PERCENTAGE` to change the split, and update the variant URLs to your two apps' endpoints. +Because the same IP always hashes to the same bucket, a given visitor consistently sees the same version. Adjust `B_PERCENTAGE` to change the split, and update the variant URLs to your two apps' endpoints. For a standalone reference, see [Weighted traffic splitting](/scripting/standalone/examples/weighted-routing). Use a key that's stable for the session you want to pin. Client IP is the simplest. To weight by something else (a cookie, a header, a user ID), hash that value instead. + +## Canary deployment + +A canary release sends the new version to a narrow, targeted audience first, lets you watch it under real traffic, then widens the audience as your confidence grows. Country makes a natural targeting key: ship to one or two markets, confirm the metrics look healthy, then add more. + +This uses the same two-app, [standalone Edge Script](/scripting/standalone/overview) setup as A/B, routing on the visitor's country instead of a percentage. bunny.net adds the [`CDN-RequestCountryCode`](/cdn/vary-cache) header to every request, so the script can read it directly. + +The approach: + +1. Deploy **two Magic Containers apps**: the stable version and the canary version. +2. List the countries that should receive the canary. +3. Route those countries to the canary app and everything else to stable. + +```typescript +import * as BunnySDK from "@bunny.net/edgescript-sdk"; + +const STABLE = "https://app-stable.bunny.run"; +const CANARY = "https://app-canary.bunny.run"; + +// Countries that receive the canary version first +const CANARY_COUNTRIES = new Set(["NZ", "FI"]); + +BunnySDK.net.http.serve(async (request: Request): Promise => { + const country = request.headers.get("CDN-RequestCountryCode") ?? ""; + const variant = CANARY_COUNTRIES.has(country) ? CANARY : STABLE; + + const target = new URL(request.url); + const origin = new URL(variant); + target.protocol = origin.protocol; + target.host = origin.host; + + return fetch(new Request(target, request)); +}); +``` + +To widen the rollout, add countries to `CANARY_COUNTRIES` and redeploy the script. To roll back, empty the set so all traffic returns to the stable app. For a standalone reference, see [Route by country](/scripting/standalone/examples/geo-routing). + + + Combine this with the A/B bucket to ramp inside a country. Route a country to + the canary, then send a growing percentage of its visitors to the new version + as you gain confidence. + diff --git a/scripting/standalone/examples/geo-routing.mdx b/scripting/standalone/examples/geo-routing.mdx new file mode 100644 index 0000000..cc5e27f --- /dev/null +++ b/scripting/standalone/examples/geo-routing.mdx @@ -0,0 +1,30 @@ +--- +title: "Route by country" +description: "Send visitors from specific countries to a different origin using the CDN-RequestCountryCode header." +--- + +bunny.net adds the [`CDN-RequestCountryCode`](/cdn/vary-cache) header to every request. This script reads it and routes the listed countries to a second origin. + +```typescript +import * as BunnySDK from "@bunny.net/edgescript-sdk"; + +const PRIMARY = "https://primary.example.com"; +const ALTERNATE = "https://alternate.example.com"; + +// Countries routed to the alternate origin +const ALTERNATE_COUNTRIES = new Set(["NZ", "FI"]); + +BunnySDK.net.http.serve(async (request: Request): Promise => { + const country = request.headers.get("CDN-RequestCountryCode") ?? ""; + const variant = ALTERNATE_COUNTRIES.has(country) ? ALTERNATE : PRIMARY; + + const target = new URL(request.url); + const origin = new URL(variant); + target.protocol = origin.protocol; + target.host = origin.host; + + return fetch(new Request(target, request)); +}); +``` + +Add or remove entries in `ALTERNATE_COUNTRIES` to change which countries get the alternate origin. diff --git a/scripting/standalone/examples/weighted-routing.mdx b/scripting/standalone/examples/weighted-routing.mdx new file mode 100644 index 0000000..1ac176a --- /dev/null +++ b/scripting/standalone/examples/weighted-routing.mdx @@ -0,0 +1,40 @@ +--- +title: "Weighted traffic splitting" +description: "Send a set percentage of visitors to a second origin and keep each visitor on the same origin across requests." +--- + +This script hashes a stable key (the client IP) into a 0-99 bucket and routes by percentage. The same key always lands in the same bucket, so a visitor stays on one origin across requests. + +```typescript +import * as BunnySDK from "@bunny.net/edgescript-sdk"; + +const ORIGINS = { + a: "https://origin-a.example.com", + b: "https://origin-b.example.com", +}; + +const B_PERCENTAGE = 10; // send 10% of visitors to origin B + +// Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket +function bucket(key: string): "a" | "b" { + let h = 0; + for (let i = 0; i < key.length; i++) { + h = (h * 31 + key.charCodeAt(i)) >>> 0; + } + return h % 100 < B_PERCENTAGE ? "b" : "a"; +} + +BunnySDK.net.http.serve(async (request: Request): Promise => { + const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0"; + const variant = bucket(ip); + + const target = new URL(request.url); + const origin = new URL(ORIGINS[variant]); + target.protocol = origin.protocol; + target.host = origin.host; + + return fetch(new Request(target, request)); +}); +``` + +Adjust `B_PERCENTAGE` to change the split. To pin on something other than the client IP, such as a cookie or a header, hash that value instead. From 7b0f44407ea009d2df48a00111bcf910ee7bdbcb Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Wed, 17 Jun 2026 09:20:13 +0100 Subject: [PATCH 3/3] update copy --- magic-containers/deployment-strategies.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/magic-containers/deployment-strategies.mdx b/magic-containers/deployment-strategies.mdx index 6b2905b..838bea5 100644 --- a/magic-containers/deployment-strategies.mdx +++ b/magic-containers/deployment-strategies.mdx @@ -3,11 +3,11 @@ title: "Blue/Green & A/B Deployments" description: "Run multiple versions of your app at the same time and control how traffic is routed between them." --- -Magic Containers lets you run more than one version of an app simultaneously. Three routing patterns build on this: +Magic Containers lets you run more than one version of an app simultaneously. That unlocks three well-known release strategies. Here's the short version of each, and how it maps to Magic Containers: -- **Blue/green**: switch _all_ traffic from the current version to a new one in a single, deterministic cutover. -- **A/B**: split traffic between two versions by percentage, keeping each user pinned to the same version. -- **Canary**: route a targeted slice, such as a single country, to the new version first, then widen as it proves healthy. +- **[Blue/green](#bluegreen-deployment)** keeps two environments side by side, the current one (blue) and the new one (green), then switches _all_ traffic over in a single cutover. You get a clean release with no in-between state, and rollback is just pointing traffic back at blue. +- **[A/B](#ab-deployment)** runs both versions at once and splits traffic between them by percentage, keeping each user pinned to the same version. It's how you put a new version in front of a slice of real users and compare how it performs. +- **[Canary](#canary-deployment)** releases the new version to a narrow, targeted slice first, such as a single country, watches it under real traffic, then widens the audience as your confidence grows. Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. Pick canary when you want to validate a new version on a targeted slice before rolling it out everywhere. For everything else, a standard [rolling update](/magic-containers/rolling-updates) is enough. @@ -159,7 +159,7 @@ Because the same IP always hashes to the same bucket, a given visitor consistent A canary release sends the new version to a narrow, targeted audience first, lets you watch it under real traffic, then widens the audience as your confidence grows. Country makes a natural targeting key: ship to one or two markets, confirm the metrics look healthy, then add more. -This uses the same two-app, [standalone Edge Script](/scripting/standalone/overview) setup as A/B, routing on the visitor's country instead of a percentage. bunny.net adds the [`CDN-RequestCountryCode`](/cdn/vary-cache) header to every request, so the script can read it directly. +This uses the same two-app, [standalone Edge Script](/scripting/standalone/overview) setup as A/B, routing on the visitor's country instead of a percentage. bunny.net adds the [`CDN-RequestCountryCode`](//cdn/cdn-acceleration#http-headers) header to every request, so the script can read it directly. The approach: