cube-ms
Version:
Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support
859 lines (735 loc) • 22.8 kB
JavaScript
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;