Configuration: canary deployments
This page documents the canary directive. It assigns each request a variant from a weighted list and keeps the assignment stable for a given client, so you can roll out new content gradually or run A/B tests without a reverse proxy.
Directives#
canary#
canary <name: string> { ... }(http-canary)- Assigns a variant to each request based on a sticky key and the configured variant weights. Ferron evaluates the block in declaration order and selects the first block that matches the current host. Default: none
Block sub-directives#
| Sub-directive | Arguments | Description | Default |
|---|---|---|---|
affinity | ip, cookie <name>, header <name>, or hash <variable> | The sticky key source. With cookie or header, Ferron uses the value of the named cookie or header. With hash, Ferron uses the value of the named variable. | ip |
set_cookie | [bool] | When true, Ferron sets the affinity cookie itself when the request has none. Valid only with cookie affinity. | false |
variant | <value: string> <weight: number> | Declares one variant with its weight. Repeat the directive to declare more variants. Weights must be at least 1. | none |
cookie | { ... } | Configures the affinity cookie attributes used with set_cookie. See the cookie block section. | none |
Configuration example:
example.com {
canary rollout {
variant stable 90
variant next 10
}
}Ferron assigns about 90% of requests to stable and 10% to next. Each client IP stays on the same variant across requests, as long as the weights do not change.
cookie block#
The cookie block sets the attributes of the affinity cookie that Ferron writes when set_cookie is enabled. Without this block, Ferron writes a persistent cookie that survives browser restarts, with HttpOnly and SameSite=Lax.
The cookie block takes effect only with set_cookie and cookie affinity. It does not change how Ferron reads an existing cookie.
| Sub-directive | Arguments | Description | Default |
|---|---|---|---|
ttl | <duration> | How long the cookie lasts. Ferron writes it as Max-Age. Omit it to keep the browser-session default. | 7d (persistent) |
path | <value: string> | The cookie path. | / |
domain | <value: string> | The cookie domain. Omit it to scope the cookie to the current host. | none (current host) |
secure | [bool] | Sets the Secure flag so the cookie is sent only over HTTPS. | false |
httponly | [bool] | Sets the HttpOnly flag so client scripts cannot read the cookie. | true |
samesite | <policy> | The SameSite policy: strict, lax, or none. | lax |
Example:
example.com {
canary rollout {
affinity cookie ab_variant
set_cookie
variant stable 90
variant next 10
cookie {
ttl "30d"
domain example.com
secure
samesite lax
}
}
}Ferron writes the ab_variant cookie for 30 days, scoped to example.com and sent only over HTTPS.
Pipeline position#
The canary stage runs after client IP resolution and before the set_var and map stages. This means variants are available for variable interpolation, map evaluation, and all downstream stages.
Using the variant in configuration#
Ferron sets the following variables during the canary stage:
| Variable | Description |
|---|---|
canary.variant | The selected variant name. |
canary.weight | The weight of the selected variant. |
canary.key | The sticky key value Ferron used for the selection. |
Serving different content per variant:
example.com {
canary rollout {
variant stable 90
variant next 10
}
root "/srv/www/{{canary.variant}}"
index index.html
}Ferron interpolates the canary.variant variable per request, so each variant serves its own document root without a reload.
Branching with set_var:
example.com {
canary rollout {
variant stable 90
variant next 10
}
set_var canary.variant "^next$" is_next {
value "true"
}
#...
}Client affinity#
By default, Ferron hashes the client IP address. A client keeps its variant as long as the weights do not change. Change this with the affinity sub-directive.
Keeping each visitor on one variant with a cookie:
example.com {
canary ab_test {
affinity cookie ab_variant
variant control 50
variant experiment 50
}
}Ferron hashes the ab_variant cookie value. If the cookie is missing, Ferron falls back to the client IP. This is useful for A/B tests where the client application sets the cookie.
Ferron does not set the cookie itself. The client application sets it before the first request. To make Ferron set the cookie, add set_cookie (see below).
Making Ferron set the cookie:
example.com {
canary rollout {
affinity cookie ab_variant
set_cookie
variant stable 90
variant next 10
}
}When the request has no ab_variant cookie, Ferron generates a random sticky key, assigns the variant from the ring, and writes the cookie to the response. By default the cookie is persistent (ab_variant=<key>; Path=/; Max-Age=604800; HttpOnly; SameSite=Lax), so the assignment survives browser restarts and IP changes. Use the cookie block to change the lifetime and other attribute. Use set_cookie false to disable, and note that set_cookie works only with cookie affinity.
Using a request header:
example.com {
canary rollout {
affinity header x_canary_group
variant stable 80
variant eastus 20
}
}Ferron hashes the value of the X-Canary-Group header. Header names are case-insensitive. If the header is missing, Ferron falls back to the client IP.
Hashing a variable:
example.com {
canary rollout {
affinity hash request.cookie.user_id
variant stable 80
variant premium 20
}
}The hash affinity runs before the set_var and map stages, so the variable must be a built-in request variable such as request.cookie.<name>, request.header.<name>, or request.uri.query.<param>. Variables produced by set_var or map are not available yet at this point.
Promotion and rollback#
The variant weights and the variant list can change on reload. Ferron keeps the assignment for clients whose keys map to a part of the hash ring that did not change. Clients near a variant boundary may move to another variant, which is expected with consistent hashing.
To promote a variant, increase its weight over several reloads. To roll back, decrease the weight or change it to variant stable 100.
For weighted load balancing across multiple backend servers, see HTTP reverse proxy.
Observability#
Trace spans#
The canary stage sets the following attributes on its ferron.stage.canary span:
| Attribute | Type | Description |
|---|---|---|
ferron.canary.variant | string | The selected variant name. |
ferron.canary.name | string | The canary block name. |
ferron.canary.key_source | string | Where the key came from: ip, cookie, header, hash, or generated (a random key persisted in the affinity cookie). |
ferron.canary.weight | int | The weight of the selected variant. |
Metrics#
| Metric | Type | Description |
|---|---|---|
ferron.canary.requests | Counter | Total requests processed by canary blocks. The attributes ferron.canary.name and ferron.canary.variant distinguish series per variant. |
Access log fields#
Ferron writes the selected variant to the custom access log field ferron.canary.variant. See Logging for how to enable custom fields.
For variable mapping and conditional logic, see HTTP map and Conditionals and variables. For serving each variant from its own directory, see Static file serving.