Files
git.stella-ops.org/docs/modules/release-orchestrator/progressive-delivery/routers.md

8.5 KiB

Router Plugins

Traffic router plugins for progressive delivery (Nginx, AWS ALB, and custom implementations).

Status: Planned (not yet implemented) Source: Architecture Advisory Section 11.4 Related Modules: Progressive Delivery Module, Plugin System Sprint: 110_004 Router Plugins

Overview

Router plugins enable traffic shifting for progressive delivery. The orchestrator ships with 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 (v1 Built-in)

The Nginx plugin generates and manages 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
Nginx v1 Built-in Configuration-based weight/header routing
HAProxy Plugin Runtime API for traffic management
Traefik Plugin Dynamic configuration via API
AWS ALB Plugin Weighted target groups
Envoy Planned xDS 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