UNPKG

cube-ms

Version:

Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support

859 lines (735 loc) 22.8 kB
import fs from 'fs-extra'; import path from 'path'; import { spawn } from 'child_process'; import yaml from 'js-yaml'; import { logRequest, logError } from '../ccn-logger.js'; /** * Advanced Deployment Automation System * Comprehensive deployment tools with Docker, Kubernetes, and CI/CD integration */ export class DeploymentAutomation { constructor(options = {}) { this.config = { projectRoot: options.projectRoot || process.cwd(), deploymentTargets: options.deploymentTargets || ['docker', 'kubernetes'], registry: options.registry || 'docker.io', namespace: options.namespace || 'cube-ms', environment: options.environment || 'development', // Build configuration buildTimeout: options.buildTimeout || 300000, // 5 minutes pushTimeout: options.pushTimeout || 600000, // 10 minutes deployTimeout: options.deployTimeout || 900000, // 15 minutes // Feature flags enableHealthChecks: options.enableHealthChecks !== false, enableRollback: options.enableRollback !== false, enableCanaryDeployment: options.enableCanaryDeployment || false, enableBlueGreenDeployment: options.enableBlueGreenDeployment || false, ...options }; this.deploymentHistory = []; this.currentDeployment = null; } /** * Deploy application to specified targets */ async deploy(version, targets = null) { const deploymentTargets = targets || this.config.deploymentTargets; const deploymentId = `deploy-${Date.now()}`; this.currentDeployment = { id: deploymentId, version, targets: deploymentTargets, status: 'starting', startTime: Date.now(), steps: [] }; try { logRequest('deployment_start', 'Starting deployment', { deploymentId, version, targets: deploymentTargets, environment: this.config.environment }); // Pre-deployment checks await this.preDeploymentChecks(); // Build application const buildResult = await this.buildApplication(version); this.addDeploymentStep('build', 'completed', buildResult); // Deploy to each target for (const target of deploymentTargets) { const deployResult = await this.deployToTarget(target, version); this.addDeploymentStep(`deploy-${target}`, 'completed', deployResult); } // Post-deployment verification await this.postDeploymentVerification(); this.addDeploymentStep('verification', 'completed'); // Update deployment status this.currentDeployment.status = 'completed'; this.currentDeployment.endTime = Date.now(); this.currentDeployment.duration = this.currentDeployment.endTime - this.currentDeployment.startTime; this.deploymentHistory.push({ ...this.currentDeployment }); logRequest('deployment_complete', 'Deployment completed successfully', { deploymentId, duration: `${this.currentDeployment.duration}ms`, targets: deploymentTargets }); return this.currentDeployment; } catch (error) { logError('deployment_failed', 'Deployment failed', error, { deploymentId, version, targets: deploymentTargets }); this.currentDeployment.status = 'failed'; this.currentDeployment.error = error.message; this.currentDeployment.endTime = Date.now(); // Attempt rollback if enabled if (this.config.enableRollback) { await this.rollback(); } throw error; } } /** * Pre-deployment checks */ async preDeploymentChecks() { logRequest('deployment_pre_checks', 'Running pre-deployment checks'); // Check if all required files exist const requiredFiles = ['package.json', 'Dockerfile', 'src/index.js']; for (const file of requiredFiles) { const filePath = path.join(this.config.projectRoot, file); if (!(await fs.pathExists(filePath))) { throw new Error(`Required file not found: ${file}`); } } // Check Docker availability if (this.config.deploymentTargets.includes('docker')) { await this.checkDockerAvailability(); } // Check Kubernetes availability if (this.config.deploymentTargets.includes('kubernetes')) { await this.checkKubernetesAvailability(); } // Run tests await this.runPreDeploymentTests(); logRequest('deployment_pre_checks_complete', 'Pre-deployment checks completed'); } /** * Build application */ async buildApplication(version) { logRequest('deployment_build', 'Building application', { version }); const buildSteps = [ () => this.installDependencies(), () => this.runBuild(), () => this.buildDockerImage(version), () => this.pushDockerImage(version) ]; const results = {}; for (const [index, step] of buildSteps.entries()) { const stepName = step.name || `step-${index + 1}`; logRequest('deployment_build_step', `Running build step: ${stepName}`); const result = await step(); results[stepName] = result; } return results; } /** * Install dependencies */ async installDependencies() { logRequest('deployment_install_deps', 'Installing dependencies'); const result = await this.runCommand('npm', ['ci'], { cwd: this.config.projectRoot, timeout: this.config.buildTimeout }); return { success: true, output: result }; } /** * Run build process */ async runBuild() { logRequest('deployment_run_build', 'Running build process'); try { const result = await this.runCommand('npm', ['run', 'build'], { cwd: this.config.projectRoot, timeout: this.config.buildTimeout }); return { success: true, output: result }; } catch (error) { // Build step might not exist, which is okay if (error.message.includes('script not found')) { return { success: true, skipped: true }; } throw error; } } /** * Build Docker image */ async buildDockerImage(version) { const imageName = `${this.config.registry}/${this.config.namespace}/app:${version}`; logRequest('deployment_docker_build', 'Building Docker image', { imageName, version }); const result = await this.runCommand('docker', [ 'build', '-t', imageName, '-t', `${this.config.registry}/${this.config.namespace}/app:latest`, '.' ], { cwd: this.config.projectRoot, timeout: this.config.buildTimeout }); return { success: true, imageName, output: result }; } /** * Push Docker image to registry */ async pushDockerImage(version) { const imageName = `${this.config.registry}/${this.config.namespace}/app:${version}`; logRequest('deployment_docker_push', 'Pushing Docker image', { imageName, registry: this.config.registry }); // Push versioned image await this.runCommand('docker', ['push', imageName], { timeout: this.config.pushTimeout }); // Push latest tag const latestImage = `${this.config.registry}/${this.config.namespace}/app:latest`; await this.runCommand('docker', ['push', latestImage], { timeout: this.config.pushTimeout }); return { success: true, imageName, latestImage }; } /** * Deploy to specific target */ async deployToTarget(target, version) { logRequest('deployment_target', `Deploying to target: ${target}`, { target, version }); switch (target) { case 'docker': return await this.deployToDocker(version); case 'kubernetes': return await this.deployToKubernetes(version); case 'docker-swarm': return await this.deployToDockerSwarm(version); case 'docker-compose': return await this.deployToDockerCompose(version); default: throw new Error(`Unknown deployment target: ${target}`); } } /** * Deploy to Docker (standalone container) */ async deployToDocker(version) { const containerName = `${this.config.namespace}-app-${this.config.environment}`; const imageName = `${this.config.registry}/${this.config.namespace}/app:${version}`; logRequest('deployment_docker', 'Deploying to Docker', { containerName, imageName }); try { // Stop existing container await this.runCommand('docker', ['stop', containerName], { ignoreError: true }); await this.runCommand('docker', ['rm', containerName], { ignoreError: true }); } catch (error) { // Container might not exist, which is okay } // Start new container const dockerArgs = [ 'run', '-d', '--name', containerName, '--restart', 'unless-stopped', '-p', '3000:3000', '--env', `NODE_ENV=${this.config.environment}`, imageName ]; await this.runCommand('docker', dockerArgs, { timeout: this.config.deployTimeout }); return { success: true, containerName, imageName }; } /** * Deploy to Kubernetes */ async deployToKubernetes(version) { logRequest('deployment_kubernetes', 'Deploying to Kubernetes', { version }); // Generate Kubernetes manifests await this.generateKubernetesManifests(version); // Apply manifests const manifestsPath = path.join(this.config.projectRoot, 'k8s'); await this.runCommand('kubectl', [ 'apply', '-f', manifestsPath, '--namespace', this.config.namespace ], { timeout: this.config.deployTimeout }); // Wait for rollout to complete await this.runCommand('kubectl', [ 'rollout', 'status', 'deployment/cube-ms-app', '--namespace', this.config.namespace, '--timeout=300s' ]); return { success: true, namespace: this.config.namespace }; } /** * Deploy to Docker Swarm */ async deployToDockerSwarm(version) { logRequest('deployment_swarm', 'Deploying to Docker Swarm', { version }); // Generate Docker Compose file for Swarm await this.generateSwarmComposeFile(version); const stackName = `${this.config.namespace}-${this.config.environment}`; const composeFile = path.join(this.config.projectRoot, 'docker-compose.swarm.yml'); await this.runCommand('docker', [ 'stack', 'deploy', '-c', composeFile, stackName ], { timeout: this.config.deployTimeout }); return { success: true, stackName }; } /** * Deploy with Docker Compose */ async deployToDockerCompose(version) { logRequest('deployment_compose', 'Deploying with Docker Compose', { version }); // Generate Docker Compose file await this.generateComposeFile(version); const composeFile = path.join(this.config.projectRoot, 'docker-compose.yml'); // Stop existing services await this.runCommand('docker-compose', [ '-f', composeFile, 'down' ], { ignoreError: true }); // Start services await this.runCommand('docker-compose', [ '-f', composeFile, 'up', '-d' ], { timeout: this.config.deployTimeout }); return { success: true, composeFile }; } /** * Generate Kubernetes manifests */ async generateKubernetesManifests(version) { const k8sPath = path.join(this.config.projectRoot, 'k8s'); await fs.ensureDir(k8sPath); // Deployment manifest const deployment = { apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: 'cube-ms-app', namespace: this.config.namespace, labels: { app: 'cube-ms-app', version: version } }, spec: { replicas: 3, selector: { matchLabels: { app: 'cube-ms-app' } }, template: { metadata: { labels: { app: 'cube-ms-app', version: version } }, spec: { containers: [{ name: 'app', image: `${this.config.registry}/${this.config.namespace}/app:${version}`, ports: [{ containerPort: 3000 }], env: [{ name: 'NODE_ENV', value: this.config.environment }], livenessProbe: { httpGet: { path: '/health', port: 3000 }, initialDelaySeconds: 30, periodSeconds: 10 }, readinessProbe: { httpGet: { path: '/health/ready', port: 3000 }, initialDelaySeconds: 5, periodSeconds: 5 }, resources: { requests: { memory: '256Mi', cpu: '250m' }, limits: { memory: '512Mi', cpu: '500m' } } }] } } } }; // Service manifest const service = { apiVersion: 'v1', kind: 'Service', metadata: { name: 'cube-ms-app-service', namespace: this.config.namespace }, spec: { selector: { app: 'cube-ms-app' }, ports: [{ protocol: 'TCP', port: 80, targetPort: 3000 }], type: 'LoadBalancer' } }; // Write manifests await fs.writeFile( path.join(k8sPath, 'deployment.yaml'), yaml.dump(deployment) ); await fs.writeFile( path.join(k8sPath, 'service.yaml'), yaml.dump(service) ); logRequest('deployment_k8s_manifests', 'Generated Kubernetes manifests', { path: k8sPath }); } /** * Generate Docker Compose file */ async generateComposeFile(version) { const compose = { version: '3.8', services: { app: { image: `${this.config.registry}/${this.config.namespace}/app:${version}`, ports: ['3000:3000'], environment: { NODE_ENV: this.config.environment }, restart: 'unless-stopped', healthcheck: { test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'], interval: '30s', timeout: '10s', retries: 3 } } } }; const composeFile = path.join(this.config.projectRoot, 'docker-compose.yml'); await fs.writeFile(composeFile, yaml.dump(compose)); logRequest('deployment_compose_file', 'Generated Docker Compose file', { file: composeFile }); } /** * Generate Docker Swarm compose file */ async generateSwarmComposeFile(version) { const compose = { version: '3.8', services: { app: { image: `${this.config.registry}/${this.config.namespace}/app:${version}`, ports: ['3000:3000'], environment: { NODE_ENV: this.config.environment }, deploy: { replicas: 3, restart_policy: { condition: 'on-failure' }, update_config: { parallelism: 1, delay: '10s' }, rollback_config: { parallelism: 1, delay: '10s' } }, healthcheck: { test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'], interval: '30s', timeout: '10s', retries: 3 } } } }; const composeFile = path.join(this.config.projectRoot, 'docker-compose.swarm.yml'); await fs.writeFile(composeFile, yaml.dump(compose)); logRequest('deployment_swarm_compose', 'Generated Docker Swarm compose file', { file: composeFile }); } /** * Post-deployment verification */ async postDeploymentVerification() { if (!this.config.enableHealthChecks) { return; } logRequest('deployment_verification', 'Running post-deployment verification'); // Wait for services to be ready await this.waitForHealthy(); // Run smoke tests await this.runSmokeTests(); logRequest('deployment_verification_complete', 'Post-deployment verification completed'); } /** * Wait for application to be healthy */ async waitForHealthy(maxWaitTime = 120000) { // 2 minutes const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { try { // Try to reach health endpoint const response = await fetch('http://localhost:3000/health'); if (response.ok) { logRequest('deployment_health_check', 'Application is healthy'); return; } } catch (error) { // Service not ready yet } await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error('Application failed to become healthy within timeout'); } /** * Run smoke tests */ async runSmokeTests() { logRequest('deployment_smoke_tests', 'Running smoke tests'); try { const result = await this.runCommand('npm', ['run', 'test:smoke'], { cwd: this.config.projectRoot, timeout: 60000, ignoreError: true }); return { success: true, output: result }; } catch (error) { logError('deployment_smoke_tests_failed', 'Smoke tests failed', error); throw error; } } /** * Rollback to previous version */ async rollback() { if (!this.config.enableRollback) { return; } logRequest('deployment_rollback', 'Starting deployment rollback'); // Find last successful deployment const lastSuccessful = this.deploymentHistory .filter(d => d.status === 'completed') .sort((a, b) => b.endTime - a.endTime)[0]; if (!lastSuccessful) { throw new Error('No previous successful deployment found for rollback'); } try { // Rollback each target for (const target of lastSuccessful.targets) { await this.rollbackTarget(target, lastSuccessful.version); } logRequest('deployment_rollback_complete', 'Rollback completed successfully', { rolledBackToVersion: lastSuccessful.version }); } catch (error) { logError('deployment_rollback_failed', 'Rollback failed', error); throw error; } } /** * Rollback specific target */ async rollbackTarget(target, version) { switch (target) { case 'kubernetes': await this.runCommand('kubectl', [ 'rollout', 'undo', 'deployment/cube-ms-app', '--namespace', this.config.namespace ]); break; case 'docker-swarm': const stackName = `${this.config.namespace}-${this.config.environment}`; await this.generateSwarmComposeFile(version); await this.runCommand('docker', [ 'stack', 'deploy', '-c', path.join(this.config.projectRoot, 'docker-compose.swarm.yml'), stackName ]); break; default: // For other targets, redeploy previous version await this.deployToTarget(target, version); break; } } /** * Check Docker availability */ async checkDockerAvailability() { try { await this.runCommand('docker', ['--version']); logRequest('deployment_docker_check', 'Docker is available'); } catch (error) { throw new Error('Docker is not available or not installed'); } } /** * Check Kubernetes availability */ async checkKubernetesAvailability() { try { await this.runCommand('kubectl', ['version', '--client']); logRequest('deployment_k8s_check', 'Kubernetes CLI is available'); } catch (error) { throw new Error('kubectl is not available or not installed'); } } /** * Run pre-deployment tests */ async runPreDeploymentTests() { try { const result = await this.runCommand('npm', ['test'], { cwd: this.config.projectRoot, timeout: this.config.buildTimeout, ignoreError: true }); logRequest('deployment_tests', 'Pre-deployment tests completed', { success: true }); return result; } catch (error) { logError('deployment_tests_failed', 'Pre-deployment tests failed', error); throw error; } } /** * Add deployment step to current deployment */ addDeploymentStep(step, status, result = null) { if (this.currentDeployment) { this.currentDeployment.steps.push({ step, status, timestamp: Date.now(), result }); } } /** * Run shell command */ async runCommand(command, args, options = {}) { const { cwd = this.config.projectRoot, timeout = 60000, ignoreError = false } = options; logRequest('deployment_command', `Running command: ${command} ${args.join(' ')}`, { command, args, cwd }); return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, stdio: 'pipe' }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); const timeoutHandle = setTimeout(() => { child.kill('SIGKILL'); reject(new Error(`Command timeout: ${command} ${args.join(' ')}`)); }, timeout); child.on('close', (code) => { clearTimeout(timeoutHandle); if (code === 0 || ignoreError) { resolve(stdout.trim()); } else { reject(new Error(`Command failed with code ${code}: ${stderr.trim()}`)); } }); child.on('error', (error) => { clearTimeout(timeoutHandle); reject(error); }); }); } /** * Get deployment status and history */ getDeploymentStatus() { return { currentDeployment: this.currentDeployment, deploymentHistory: this.deploymentHistory, config: { targets: this.config.deploymentTargets, environment: this.config.environment, namespace: this.config.namespace, registry: this.config.registry } }; } /** * Get deployment logs */ async getDeploymentLogs(deploymentId) { const deployment = this.deploymentHistory.find(d => d.id === deploymentId); if (!deployment) { throw new Error(`Deployment not found: ${deploymentId}`); } return deployment; } } export default DeploymentAutomation;