Files

22 KiB

Deployment Overview

Purpose

The Deployment system executes the actual deployment of releases to target environments, managing deployment jobs, tasks, artifact generation, and rollback capabilities.

Deployment Architecture

                         DEPLOYMENT ARCHITECTURE

  ┌─────────────────────────────────────────────────────────────────────────────┐
  │                        DEPLOY ORCHESTRATOR                                   │
  │                                                                             │
  │  ┌─────────────────────────────────────────────────────────────────────┐   │
  │  │                      DEPLOYMENT JOB MANAGER                          │   │
  │  │                                                                      │   │
  │  │  Promotion ───► Create Job ───► Plan Tasks ───► Execute Tasks       │   │
  │  │                                                                      │   │
  │  └─────────────────────────────────────────────────────────────────────┘   │
  │                                    │                                        │
  │                    ┌───────────────┼───────────────┐                       │
  │                    │               │               │                       │
  │                    ▼               ▼               ▼                       │
  │  ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐      │
  │  │   TARGET EXECUTOR   │ │ RUNNER EXECUTOR │ │ ARTIFACT GENERATOR  │      │
  │  │                     │ │                 │ │                     │      │
  │  │  - Task dispatch    │ │ - Agent tasks   │ │ - Compose files     │      │
  │  │  - Status tracking  │ │ - SSH tasks     │ │ - Env configs       │      │
  │  │  - Log aggregation  │ │ - API tasks     │ │ - Manifests         │      │
  │  └─────────────────────┘ └─────────────────┘ └─────────────────────┘      │
  │                                    │                                        │
  └─────────────────────────────────────────────────────────────────────────────┘
                                       │
          ┌────────────────────────────┼────────────────────────────┐
          │                            │                            │
          ▼                            ▼                            ▼
   ┌─────────────┐             ┌─────────────┐             ┌─────────────┐
   │   Agent     │             │  Agentless  │             │    API      │
   │  Execution  │             │  Execution  │             │  Execution  │
   │             │             │             │             │             │
   │  Docker,    │             │  SSH,       │             │  ECS,       │
   │  Compose    │             │  WinRM      │             │  Nomad      │
   └─────────────┘             └─────────────┘             └─────────────┘

Deployment Flow

Standard Deployment Flow

                    DEPLOYMENT FLOW

  Promotion        Deployment        Task             Agent/Target
  Approved         Job               Execution
     │                │                 │                 │
     │  Create Job    │                 │                 │
     ├───────────────►│                 │                 │
     │                │                 │                 │
     │                │  Generate       │                 │
     │                │  Artifacts      │                 │
     │                ├────────────────►│                 │
     │                │                 │                 │
     │                │  Create Tasks   │                 │
     │                │  per Target     │                 │
     │                ├────────────────►│                 │
     │                │                 │                 │
     │                │                 │  Dispatch Task  │
     │                │                 ├────────────────►│
     │                │                 │                 │
     │                │                 │  Execute        │
     │                │                 │  (Pull, Deploy) │
     │                │                 │                 │
     │                │                 │  Report Status  │
     │                │                 │◄────────────────┤
     │                │                 │                 │
     │                │  Aggregate      │                 │
     │                │  Results        │                 │
     │                │◄────────────────┤                 │
     │                │                 │                 │
     │  Job Complete  │                 │                 │
     │◄───────────────┤                 │                 │
     │                │                 │                 │

Deployment Job

Job Entity

interface DeploymentJob {
  id: UUID;
  promotionId: UUID;
  releaseId: UUID;
  environmentId: UUID;

  // Execution configuration
  strategy: DeploymentStrategy;
  parallelism: number;

  // Status tracking
  status: JobStatus;
  startedAt?: DateTime;
  completedAt?: DateTime;

  // Artifacts
  artifacts: GeneratedArtifact[];

  // Rollback reference
  rollbackOf?: UUID;           // If this is a rollback job
  previousJobId?: UUID;        // Previous successful job

  // Tasks
  tasks: DeploymentTask[];
}

type JobStatus =
  | "pending"
  | "preparing"
  | "running"
  | "completing"
  | "completed"
  | "failed"
  | "rolling_back"
  | "rolled_back";

type DeploymentStrategy =
  | "all-at-once"
  | "rolling"
  | "canary"
  | "blue-green";

Job State Machine

                      JOB STATE MACHINE

                          ┌──────────┐
                          │ PENDING  │
                          └────┬─────┘
                               │ start()
                               ▼
                          ┌──────────┐
                          │PREPARING │
                          │          │
                          │ Generate │
                          │ artifacts│
                          └────┬─────┘
                               │
                               ▼
                          ┌──────────┐
                          │ RUNNING  │◄────────────────┐
                          │          │                 │
                          │ Execute  │                 │
                          │ tasks    │                 │
                          └────┬─────┘                 │
                               │                       │
               ┌───────────────┼───────────────┐       │
               │               │               │       │
               ▼               ▼               ▼       │
          ┌──────────┐   ┌──────────┐   ┌──────────┐  │
          │COMPLETING│   │ FAILED   │   │ ROLLING  │  │
          │          │   │          │   │  BACK    │──┘
          │ Verify   │   │          │   │          │
          │ health   │   │          │   │          │
          └────┬─────┘   └────┬─────┘   └────┬─────┘
               │              │              │
               ▼              │              ▼
          ┌──────────┐        │         ┌──────────┐
          │COMPLETED │        │         │ ROLLED   │
          └──────────┘        │         │  BACK    │
                              │         └──────────┘
                              │
                              ▼
                         [Failure
                          handling]

Deployment Task

Task Entity

interface DeploymentTask {
  id: UUID;
  jobId: UUID;
  targetId: UUID;

  // What to deploy
  componentId: UUID;
  digest: string;

  // Execution
  status: TaskStatus;
  agentId?: UUID;
  startedAt?: DateTime;
  completedAt?: DateTime;

  // Results
  logs: string;
  previousDigest?: string;    // For rollback
  error?: string;

  // Retry tracking
  attemptNumber: number;
  maxAttempts: number;
}

type TaskStatus =
  | "pending"
  | "queued"
  | "dispatched"
  | "running"
  | "verifying"
  | "succeeded"
  | "failed"
  | "retrying";

Task Dispatch

class TaskDispatcher {
  async dispatchTask(task: DeploymentTask): Promise<void> {
    const target = await this.targetRepository.get(task.targetId);

    switch (target.executionModel) {
      case "agent":
        await this.dispatchToAgent(task, target);
        break;

      case "ssh":
        await this.dispatchViaSsh(task, target);
        break;

      case "api":
        await this.dispatchViaApi(task, target);
        break;
    }
  }

  private async dispatchToAgent(
    task: DeploymentTask,
    target: Target
  ): Promise<void> {
    // Find available agent for target
    const agent = await this.agentManager.findAgentForTarget(target);

    if (!agent) {
      throw new NoAgentAvailableError(target.id);
    }

    // Create task payload
    const payload: AgentTaskPayload = {
      taskId: task.id,
      targetId: target.id,
      action: "deploy",
      digest: task.digest,
      config: target.connection,
      credentials: await this.fetchTaskCredentials(target)
    };

    // Dispatch to agent
    await this.agentClient.dispatchTask(agent.id, payload);

    // Update task status
    task.status = "dispatched";
    task.agentId = agent.id;
    await this.taskRepository.update(task);
  }
}

Generated Artifacts

Artifact Types

Type Description Format
compose-file Docker Compose file YAML
compose-lock Pinned compose file YAML
env-file Environment variables .env
systemd-unit Systemd service unit .service
nginx-config Nginx configuration .conf
manifest Deployment manifest JSON

Compose Lock Generation

interface ComposeLock {
  version: string;
  services: Record<string, LockedService>;
  generated: {
    releaseId: string;
    promotionId: string;
    timestamp: string;
    digest: string;         // Hash of this file
  };
}

interface LockedService {
  image: string;            // Full image reference with digest
  environment?: Record<string, string>;
  labels: Record<string, string>;
}

class ComposeArtifactGenerator {
  async generateLock(
    release: Release,
    target: Target,
    template: ComposeTemplate
  ): Promise<ComposeLock> {
    const services: Record<string, LockedService> = {};

    for (const [serviceName, serviceConfig] of Object.entries(template.services)) {
      // Find component for this service
      const componentDigest = release.components.find(
        c => c.name === serviceConfig.componentName
      );

      if (!componentDigest) {
        throw new Error(`No component found for service ${serviceName}`);
      }

      // Build locked image reference
      const imageRef = `${componentDigest.repository}@${componentDigest.digest}`;

      services[serviceName] = {
        image: imageRef,
        environment: {
          ...serviceConfig.environment,
          STELLA_RELEASE_ID: release.id,
          STELLA_DIGEST: componentDigest.digest
        },
        labels: {
          "stella.release.id": release.id,
          "stella.component.name": componentDigest.name,
          "stella.digest": componentDigest.digest,
          "stella.deployed.at": new Date().toISOString()
        }
      };
    }

    const lock: ComposeLock = {
      version: "3.8",
      services,
      generated: {
        releaseId: release.id,
        promotionId: target.promotionId,
        timestamp: new Date().toISOString(),
        digest: "" // Computed below
      }
    };

    // Compute content hash
    const content = yaml.stringify(lock);
    lock.generated.digest = crypto.createHash("sha256").update(content).digest("hex");

    return lock;
  }
}

Deployment Execution

Execution Models

Model Description Use Case
agent Stella agent on target Docker hosts, servers
ssh SSH-based agentless Unix servers
winrm WinRM-based agentless Windows servers
api API-based ECS, Nomad, K8s

Agent-Based Execution

class AgentExecutor {
  async execute(task: DeploymentTask): Promise<ExecutionResult> {
    const agent = await this.agentManager.get(task.agentId);
    const target = await this.targetRepository.get(task.targetId);

    // Prepare task payload with secrets
    const payload: TaskPayload = {
      taskId: task.id,
      targetId: target.id,
      action: "deploy",
      digest: task.digest,
      config: target.connection,
      artifacts: await this.getArtifacts(task.jobId),
      credentials: await this.secretsManager.fetchForTask(target)
    };

    // Dispatch to agent
    const taskRef = await this.agentClient.dispatchTask(agent.id, payload);

    // Wait for completion
    const result = await this.waitForTaskCompletion(taskRef, task.timeout);

    return result;
  }

  private async waitForTaskCompletion(
    taskRef: TaskReference,
    timeout: number
  ): Promise<ExecutionResult> {
    const deadline = Date.now() + timeout * 1000;

    while (Date.now() < deadline) {
      const status = await this.agentClient.getTaskStatus(taskRef);

      if (status.completed) {
        return {
          success: status.success,
          logs: status.logs,
          deployedDigest: status.deployedDigest,
          error: status.error
        };
      }

      await sleep(1000);
    }

    throw new TimeoutError(`Task did not complete within ${timeout} seconds`);
  }
}

SSH-Based Execution

class SshExecutor {
  async execute(task: DeploymentTask): Promise<ExecutionResult> {
    const target = await this.targetRepository.get(task.targetId);
    const sshConfig = target.connection as SshConnectionConfig;

    // Get SSH credentials from vault
    const creds = await this.secretsManager.fetchSshCredentials(
      sshConfig.credentialRef
    );

    // Connect via SSH
    const ssh = new NodeSSH();
    await ssh.connect({
      host: sshConfig.host,
      port: sshConfig.port || 22,
      username: creds.username,
      privateKey: creds.privateKey
    });

    try {
      // Upload artifacts
      const artifacts = await this.getArtifacts(task.jobId);
      for (const artifact of artifacts) {
        await ssh.putFile(artifact.localPath, artifact.remotePath);
      }

      // Execute deployment script
      const result = await ssh.execCommand(
        this.buildDeployCommand(task, target),
        { cwd: sshConfig.workDir }
      );

      return {
        success: result.code === 0,
        logs: `${result.stdout}\n${result.stderr}`,
        error: result.code !== 0 ? result.stderr : undefined
      };
    } finally {
      ssh.dispose();
    }
  }

  private buildDeployCommand(task: DeploymentTask, target: Target): string {
    // Build deployment command based on target type
    switch (target.targetType) {
      case "compose_host":
        return `cd ${target.connection.workDir} && docker-compose pull && docker-compose up -d`;

      case "docker_host":
        return `docker pull ${task.digest} && docker stop ${target.containerName} && docker run -d --name ${target.containerName} ${task.digest}`;

      default:
        throw new Error(`Unsupported target type: ${target.targetType}`);
    }
  }
}

Health Verification

interface HealthCheckConfig {
  type: "http" | "tcp" | "command";
  timeout: number;
  retries: number;
  interval: number;

  // HTTP-specific
  path?: string;
  expectedStatus?: number;
  expectedBody?: string;

  // TCP-specific
  port?: number;

  // Command-specific
  command?: string;
}

class HealthVerifier {
  async verify(
    target: Target,
    config: HealthCheckConfig
  ): Promise<HealthCheckResult> {
    let lastError: Error | undefined;

    for (let attempt = 0; attempt < config.retries; attempt++) {
      try {
        const result = await this.performCheck(target, config);

        if (result.healthy) {
          return result;
        }

        lastError = new Error(result.message);
      } catch (error) {
        lastError = error as Error;
      }

      if (attempt < config.retries - 1) {
        await sleep(config.interval * 1000);
      }
    }

    return {
      healthy: false,
      message: lastError?.message || "Health check failed",
      attempts: config.retries
    };
  }

  private async performCheck(
    target: Target,
    config: HealthCheckConfig
  ): Promise<HealthCheckResult> {
    switch (config.type) {
      case "http":
        return this.httpCheck(target, config);

      case "tcp":
        return this.tcpCheck(target, config);

      case "command":
        return this.commandCheck(target, config);
    }
  }

  private async httpCheck(
    target: Target,
    config: HealthCheckConfig
  ): Promise<HealthCheckResult> {
    const url = `${target.healthEndpoint}${config.path || "/health"}`;

    try {
      const response = await fetch(url, {
        signal: AbortSignal.timeout(config.timeout * 1000)
      });

      const healthy = response.status === (config.expectedStatus || 200);

      return {
        healthy,
        message: healthy ? "OK" : `Status ${response.status}`,
        statusCode: response.status
      };
    } catch (error) {
      return {
        healthy: false,
        message: (error as Error).message
      };
    }
  }
}

Rollback Management

class RollbackManager {
  async initiateRollback(
    jobId: UUID,
    reason: string
  ): Promise<DeploymentJob> {
    const failedJob = await this.jobRepository.get(jobId);
    const previousJob = await this.findPreviousSuccessfulJob(
      failedJob.environmentId,
      failedJob.releaseId
    );

    if (!previousJob) {
      throw new NoRollbackTargetError(jobId);
    }

    // Create rollback job
    const rollbackJob: DeploymentJob = {
      id: uuidv4(),
      promotionId: failedJob.promotionId,
      releaseId: previousJob.releaseId,  // Previous release
      environmentId: failedJob.environmentId,
      strategy: "all-at-once",           // Fast rollback
      parallelism: 10,
      status: "pending",
      rollbackOf: jobId,
      previousJobId: previousJob.id,
      artifacts: [],
      tasks: []
    };

    // Create tasks to restore previous state
    for (const task of failedJob.tasks) {
      const previousTask = previousJob.tasks.find(
        t => t.targetId === task.targetId
      );

      if (previousTask) {
        rollbackJob.tasks.push({
          id: uuidv4(),
          jobId: rollbackJob.id,
          targetId: task.targetId,
          componentId: previousTask.componentId,
          digest: previousTask.previousDigest || task.previousDigest!,
          status: "pending",
          logs: "",
          attemptNumber: 0,
          maxAttempts: 3
        });
      }
    }

    await this.jobRepository.save(rollbackJob);

    // Execute rollback
    await this.executeJob(rollbackJob);

    return rollbackJob;
  }

  private async findPreviousSuccessfulJob(
    environmentId: UUID,
    excludeReleaseId: UUID
  ): Promise<DeploymentJob | null> {
    return this.jobRepository.findOne({
      environmentId,
      status: "completed",
      releaseId: { $ne: excludeReleaseId }
    }, {
      orderBy: { completedAt: "desc" }
    });
  }
}

References