-
-
Save designeng/02048edad5eba3e5de3698bf00154bfc to your computer and use it in GitHub Desktop.
node.js + nginx + PM2 rolling release/blue green deployments (zero downtime)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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