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.

AdapterConfig sectionPurpose
webhookReleaseOrchestrator:TrafficAdapters:WebhookThe 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-fileReleaseOrchestrator:TrafficAdapters:LocalFileLab/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:

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 TrafficRouterPlugin interface, Nginx config generation, HAProxy/Traefik/ALB plugin, or plugin manifest exists in src/. 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

PluginStatusDescription
Webhook adapterShipped (default-disabled)The supported contract — see “As-built” section above
Local-file adapterShipped, lab onlyRoute state written to a local JSON file for e2e proof
NginxOrphaned library codeNginxRouter exists but is never DI-registered; use the webhook adapter
HAProxyDesign onlyRuntime API for traffic management
TraefikDesign onlyDynamic configuration via API
AWS ALBDesign onlyWeighted target groups
EnvoyDesign onlyxDS API integration

Creating Custom Router Plugins

To create a custom router plugin:

  1. Implement Interface: Create a class implementing TrafficRouterPlugin
  2. Register Plugin: Add to plugin registry with capabilities
  3. Configuration Schema: Define JSON Schema for plugin config
  4. Health Checks: Implement connection testing
  5. 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

See Also