Skip to content

Instantly share code, notes, and snippets.

@brandonros
Created December 17, 2018 01:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save brandonros/5dddef2200cbd00f4459272795593bad to your computer and use it in GitHub Desktop.
Save brandonros/5dddef2200cbd00f4459272795593bad to your computer and use it in GitHub Desktop.
node.js + nginx + PM2 rolling release/blue green deployments (zero downtime)
const Promise = require('bluebird');
const fs = require('fs');
const execa = require('execa');
class BlueGreenDeployment {
constructor({appName, blueProxyPassPattern, greenProxyPassPattern, nginxConfigFile}) {
this.appName = appName;
this.blueProxyPassPattern = blueProxyPassPattern;
this.greenProxyPassPattern = greenProxyPassPattern;
this.nginxConfigFile = nginxConfigFile;
}
async getCurrentlyDeployedVersion() {
const nginxFileContents = fs.readFileSync(this.nginxConfigFile).toString();
if (nginxFileContents.indexOf(this.blueProxyPassPattern) !== -1) {
return 'blue';
} else if (nginxFileContents.indexOf(this.greenProxyPassPattern) !== -1) {
return 'green';
}
throw new Error(`${this.appName}: Failed to determine currently deployed version`);
}
async bringNewInstancesOnline(version) {
await execa('pm2', ['start', 'ecosystem.config.js', '--only', `${this.appName}-${version}`]);
await Promise.delay(1000 * 3);
}
async switchLoadBalancer(from, to) {
const nginxFileContents = fs.readFileSync(this.nginxConfigFile).toString();
const newNginxFileContents = (from === 'blue' && to === 'green') ?
nginxFileContents.replace(new RegExp(this.blueProxyPassPattern, 'g'), this.greenProxyPassPattern) :
nginxFileContents.replace(new RegExp(this.greenProxyPassPattern, 'g'), this.blueProxyPassPattern);
fs.writeFileSync(this.nginxConfigFile, newNginxFileContents);
await execa('sudo', ['service', 'nginx', 'reload']);
}
async waitForTrafficToStop(version) {
await Promise.delay(1000 * 15);
}
async bringOldInstancesOffline(version) {
try {
await execa('pm2', ['stop', `${this.appName}-${version}`]);
} catch (err) {
console.error(`${this.appName}: failed to stop ${this.appName}-${version}`);
}
try {
await execa('pm2', ['delete', `${this.appName}-${version}`]);
} catch (err) {
console.error(`${this.appName}: failed to delete ${this.appName}-${version}`);
}
}
async perform() {
var currentDeploymentVersion = await this.getCurrentlyDeployedVersion();
console.log(`${this.appName}: Currently deployed version: ${currentDeploymentVersion}`);
var newDeploymentVersion = currentDeploymentVersion === 'blue' ? 'green' : 'blue';
console.log(`${this.appName}: Bringing ${newDeploymentVersion} online`);
await this.bringNewInstancesOnline(newDeploymentVersion);
console.log(`${this.appName}: Switching load balancer upstream from ${currentDeploymentVersion} to ${newDeploymentVersion}`);
await this.switchLoadBalancer(currentDeploymentVersion, newDeploymentVersion);
console.log(`${this.appName}: Waiting for traffic to stop on ${currentDeploymentVersion}`);
await this.waitForTrafficToStop(currentDeploymentVersion);
console.log(`${this.appName}: Bringing old instances offline...`);
await this.bringOldInstancesOffline(currentDeploymentVersion);
}
}
(async () => {
const apiDeployment = new BlueGreenDeployment({
appName: 'project-api',
blueProxyPassPattern: 'proxy_pass http://project_api_blue;',
greenProxyPassPattern: 'proxy_pass http://project_api_green;',
nginxConfigFile: '/home/brandon/project/nginx/servers/project-api.conf'
});
const servicesDeployment = new BlueGreenDeployment({
appName: 'project-services',
blueProxyPassPattern: 'proxy_pass services_blue;',
greenProxyPassPattern: 'proxy_pass services_green;',
nginxConfigFile: '/home/brandon/project/nginx/servers/project-services.conf'
});
await Promise.all([
apiDeployment.perform(),
servicesDeployment.perform()
]);
process.exit(0);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment