Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -413,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"
]
}
]
Expand Down
198 changes: 198 additions & 0 deletions magic-containers/deployment-strategies.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
---
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. That unlocks three well-known release strategies. Here's the short version of each, and how it maps to Magic Containers:

- **[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.

| | 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?

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.

<Info>
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.
</Info>

## 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.

<Steps>
<Step title="Deploy the new version as a separate app">
Create a second Magic Containers app (the "green" version) with the new image. Leave the current "blue" app running and serving production traffic.
</Step>

<Step title="Point a pull zone at the active version">
Use a pull zone with a `ComputeContainer` origin that targets the currently
active app's container and endpoint.
</Step>

<Step title="Cut over">
When the new version is ready, switch the origin to the green app's container
and endpoint, then apply. All traffic moves at once.
</Step>

<Step title="Roll back if needed">
To revert, point the origin back at the blue app. Because it's still running, rollback is immediate.
</Step>
</Steps>

### 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
}
```

<Tip>
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.
</Tip>

<Info>
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.
</Info>

## 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
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<Response> => {
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. For a standalone reference, see [Weighted traffic splitting](/scripting/standalone/examples/weighted-routing).

<Info>
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.
</Info>

## 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/cdn-acceleration#http-headers) 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<Response> => {
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).

<Tip>
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.
</Tip>
30 changes: 30 additions & 0 deletions scripting/standalone/examples/geo-routing.mdx
Original file line number Diff line number Diff line change
@@ -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<Response> => {
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.
40 changes: 40 additions & 0 deletions scripting/standalone/examples/weighted-routing.mdx
Original file line number Diff line number Diff line change
@@ -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<Response> => {
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.