Router Plugins
Traffic router plugins for progressive delivery (Nginx, AWS ALB, and custom implementations).
Status: Mixed — the webhook traffic adapter is the shipped, supported production contract (see “As-built” below); everything from “Router Plugin Interface” down is illustrative design pseudocode that is NOT implemented. Source: Architecture Advisory Section 11.4 Related Modules: Progressive Delivery Module, Plugin System Sprint: 110_004 Router Plugins
As-built: traffic adapters (verify against src/)
The shipped traffic-shifting mechanism is the deployment traffic adapter in the ReleaseOrchestrator WebApi (src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Services/DeploymentTrafficAdapters.cs, registered in Program.cs next to DeploymentEnginePipeline). Two adapters exist; both are DISABLED by default — an adapter is only added to DI when its Enabled config flag is true.
| Adapter | Config section | Purpose |
|---|---|---|
webhook | ReleaseOrchestrator:TrafficAdapters:Webhook | The supported production contract. Delegates traffic changes to an operator-owned HTTP endpoint (your Nginx/HAProxy/ALB/custom router automation) without giving the orchestrator router credentials. |
local-file | ReleaseOrchestrator:TrafficAdapters:LocalFile | Lab/e2e only. Writes the requested route state to a local JSON file to prove adapter/evidence plumbing. Never a production router. |
Webhook adapter configuration keys
From WebhookDeploymentTrafficAdapterOptions (DeploymentTrafficAdapters.cs):
ReleaseOrchestrator:
TrafficAdapters:
Webhook:
Enabled: true # default false — adapter is not registered without this
Name: webhook # adapter name referenced by strategyConfig.trafficAdapter
ShiftUrl: https://router-ops.example/hooks/traffic-shift
RollbackUrl: https://router-ops.example/hooks/traffic-rollback
TimeoutSeconds: 15 # clamped 1..300
LocalFile: # lab only
Enabled: false # default false
StateDirectory: /tmp/stellaops-traffic-adapter
Webhook request payload (POST, JSON)
Shift (operation: "shift"):
{
"schemaVersion": "1.0",
"operation": "shift",
"jobId": "<deployment job guid>",
"releaseName": "checkout-api",
"strategy": "BlueGreen",
"batchIndex": 0,
"batchCount": 2,
"controlWeight": 90,
"treatmentWeight": 10,
"trafficRole": "canary",
"slot": "green",
"variantId": null,
"trafficSplit": "90/10",
"endpoints": [
{
"taskId": "…", "targetId": "…", "targetName": "prod-web-01",
"slot": "green", "trafficRole": "canary", "projectName": "…",
"endpoint": "…", "versionStickerPath": "…"
}
]
}
Rollback (operation: "rollback") carries rollbackSlot, reason, and endpoints instead of the batch/weight fields. Expected response (all fields optional; non-2xx or invalid JSON = failure):
{ "shifted": true, "mode": "traffic_shift", "status": "shifted", "reason": "…", "adapterKind": "nginx", "evidence": { } }
Failure semantics (fail-closed)
The deployment engine (DeploymentEnginePipeline.cs) records per-batch traffic evidence (strategy.traffic.* keys, surfaced on the deployment detail as strategyEvidence) and fails the deployment when:
- any adapter call returns
status: "failed"(reasons includetraffic_adapter_disabled,traffic_adapter_webhook_url_missing,traffic_adapter_webhook_timeout,traffic_adapter_webhook_unreachable,traffic_adapter_webhook_http_error,traffic_adapter_webhook_invalid_response,traffic_adapter_failed), or - the deployment requested
strategyConfig.requireTrafficAdapter: trueand the shift did not complete — includingtraffic_adapter_missing(no adapter name configured on the deployment) andtraffic_adapter_executor_missing(adapter named but not registered/enabled).
Without requireTrafficAdapter, a canary/blue-green deployment with no adapter proceeds without any traffic shifting (status: not_shifted, reason traffic_adapter_missing recorded in evidence) — batches still deploy, but no traffic ever moves.
The deployment request opts into an adapter via strategyConfig keys parsed in LibraryDeploymentRequestStarter.CreateOptions: trafficAdapter (alias router/routerAdapter), requireTrafficAdapter, trafficSplit (alias weights), slot, variantId, canaryThreshold/canarySuccessThreshold, canaryBakeTime/bakeSeconds.
NginxRouter: orphaned library code (honest note)
src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Progressive/Routers/Nginx/NginxRouter.cs implements a real ITrafficRouter (with TrafficRouterRegistry and CanaryController in the same library), but nothing registers it in DI — it is referenced only by its own unit tests and is NOT reachable from any deployment path. Until it is wired, integrate Nginx (or any router) through the webhook adapter above.
Design (below): illustrative pseudocode — NOT implemented
Everything below this line is the original aspirational plugin design (TypeScript pseudocode). No
TrafficRouterPlugininterface, Nginx config generation, HAProxy/Traefik/ALB plugin, or plugin manifest exists insrc/. Kept for design reference only.
Overview
Router plugins enable traffic shifting for progressive delivery. The design proposes an Nginx router plugin for v1, with HAProxy, Traefik, and AWS ALB available as additional plugins.
Router Plugin Interface
All router plugins implement the TrafficRouterPlugin interface:
interface TrafficRouterPlugin {
// Configuration
configureRoute(config: RouteConfig): Promise<void>;
// Traffic operations
shiftTraffic(from: string, to: string, percentage: number): Promise<void>;
getTrafficDistribution(): Promise<TrafficDistribution>;
// Health
validateConfig(): Promise<ValidationResult>;
reload(): Promise<void>;
}
interface RouteConfig {
upstream: string;
serverName: string;
variations: Variation[];
splitType: "weight" | "header" | "cookie";
headerName?: string;
headerValueB?: string;
stickySession?: boolean;
stickyDuration?: number;
}
interface Variation {
name: string;
targets: string[];
weight: number;
}
interface TrafficDistribution {
variations: {
name: string;
percentage: number;
targets: string[];
}[];
}
Nginx Router Plugin (design — not implemented)
The proposed Nginx plugin would generate and manage Nginx configuration for traffic splitting.
Implementation
class NginxRouterPlugin implements TrafficRouterPlugin {
async configureRoute(config: RouteConfig): Promise<void> {
const upstreamConfig = this.generateUpstreamConfig(config);
const serverConfig = this.generateServerConfig(config);
// Write configuration files
await this.writeConfig(
`/etc/nginx/conf.d/upstream-${config.upstream}.conf`,
upstreamConfig
);
await this.writeConfig(
`/etc/nginx/conf.d/server-${config.upstream}.conf`,
serverConfig
);
// Validate configuration
const validation = await this.validateConfig();
if (!validation.valid) {
throw new Error(`Nginx config validation failed: ${validation.error}`);
}
// Reload nginx
await this.reload();
}
}
Upstream Configuration
private generateUpstreamConfig(config: RouteConfig): string {
const lines: string[] = [];
for (const variation of config.variations) {
lines.push(`upstream ${config.upstream}_${variation.name} {`);
for (const target of variation.targets) {
lines.push(` server ${target};`);
}
lines.push(`}`);
lines.push(``);
}
// Combined upstream with weights (for percentage-based routing)
if (config.splitType === "weight") {
lines.push(`upstream ${config.upstream} {`);
for (const variation of config.variations) {
const weight = variation.weight;
for (const target of variation.targets) {
lines.push(` server ${target} weight=${weight};`);
}
}
lines.push(`}`);
}
return lines.join("\n");
}
Server Configuration
private generateServerConfig(config: RouteConfig): string {
if (config.splitType === "header" || config.splitType === "cookie") {
// Split block based on header/cookie
return `
map $http_${config.headerName || "x-variation"} $${config.upstream}_backend {
default ${config.upstream}_A;
"${config.headerValueB || "B"}" ${config.upstream}_B;
}
server {
listen 80;
server_name ${config.serverName};
location / {
proxy_pass http://$${config.upstream}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
`;
} else {
// Weight-based (default)
return `
server {
listen 80;
server_name ${config.serverName};
location / {
proxy_pass http://${config.upstream};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
`;
}
}
Traffic Shifting
async shiftTraffic(from: string, to: string, percentage: number): Promise<void> {
const config = await this.getCurrentConfig();
// Update weights
for (const variation of config.variations) {
if (variation.name === to) {
variation.weight = percentage;
} else {
variation.weight = 100 - percentage;
}
}
await this.configureRoute(config);
}
async getTrafficDistribution(): Promise<TrafficDistribution> {
// Parse current nginx config to get weights
const config = await this.parseCurrentConfig();
return {
variations: config.variations.map(v => ({
name: v.name,
percentage: v.weight,
targets: v.targets,
})),
};
}
AWS ALB Router Plugin
The AWS ALB plugin manages weighted target groups for traffic splitting.
Implementation
class AWSALBRouterPlugin implements TrafficRouterPlugin {
private alb: AWS.ELBv2;
async configureRoute(config: RouteConfig): Promise<void> {
const listenerArn = config.listenerArn;
// Create/update target groups for each variation
const targetGroupArns: Record<string, string> = {};
for (const variation of config.variations) {
const tgArn = await this.ensureTargetGroup(
`${config.upstream}-${variation.name}`,
variation.targets
);
targetGroupArns[variation.name] = tgArn;
}
// Update listener rule with weighted target groups
await this.alb.modifyRule({
RuleArn: config.ruleArn,
Actions: [{
Type: "forward",
ForwardConfig: {
TargetGroups: config.variations.map(v => ({
TargetGroupArn: targetGroupArns[v.name],
Weight: v.weight,
})),
TargetGroupStickinessConfig: {
Enabled: config.stickySession || false,
DurationSeconds: config.stickyDuration || 3600,
},
},
}],
}).promise();
}
async shiftTraffic(from: string, to: string, percentage: number): Promise<void> {
const rule = await this.getRule();
const forwardConfig = rule.Actions[0].ForwardConfig;
// Update weights
for (const tg of forwardConfig.TargetGroups) {
if (tg.TargetGroupArn.includes(`-${to}`)) {
tg.Weight = percentage;
} else {
tg.Weight = 100 - percentage;
}
}
await this.alb.modifyRule({
RuleArn: rule.RuleArn,
Actions: rule.Actions,
}).promise();
}
async getTrafficDistribution(): Promise<TrafficDistribution> {
const rule = await this.getRule();
const forwardConfig = rule.Actions[0].ForwardConfig;
const variations = [];
for (const tg of forwardConfig.TargetGroups) {
const targets = await this.getTargetGroupTargets(tg.TargetGroupArn);
const name = tg.TargetGroupArn.split("-").pop();
variations.push({
name,
percentage: tg.Weight,
targets: targets.map(t => t.Id),
});
}
return { variations };
}
}
Router Plugin Catalog
| Plugin | Status | Description |
|---|---|---|
| Webhook adapter | Shipped (default-disabled) | The supported contract — see “As-built” section above |
| Local-file adapter | Shipped, lab only | Route state written to a local JSON file for e2e proof |
| Nginx | Orphaned library code | NginxRouter exists but is never DI-registered; use the webhook adapter |
| HAProxy | Design only | Runtime API for traffic management |
| Traefik | Design only | Dynamic configuration via API |
| AWS ALB | Design only | Weighted target groups |
| Envoy | Design only | xDS API integration |
Creating Custom Router Plugins
To create a custom router plugin:
- Implement Interface: Create a class implementing
TrafficRouterPlugin - Register Plugin: Add to plugin registry with capabilities
- Configuration Schema: Define JSON Schema for plugin config
- Health Checks: Implement connection testing
- Rollback Support: Handle traffic reversion on failures
Example Plugin Manifest
plugin:
name: my-router
version: 1.0.0
type: router
capabilities:
- traffic-routing
- weight-based
- header-based
config:
type: object
properties:
endpoint:
type: string
description: Router API endpoint
auth:
type: object
properties:
type:
enum: [basic, token]
credentialRef:
type: string
