UNPKG

gcloud-deploy

Version:

Quickly deploy a Node.js project on Google Compute Engine

296 lines (239 loc) 8.7 kB
'use strict' var archiver = require('archiver') var assign = require('deep-assign') var async = require('async') var format = require('string-format-obj') var multiline = require('multiline') var outputStream = require('gce-output-stream') var path = require('path') var pumpify = require('pumpify') var slug = require('slug') var through = require('through2') var Compute = require('@google-cloud/compute') var Storage = require('@google-cloud/storage') var resolveConfig = function (pkg, explicitConfig) { var config = { root: process.cwd(), nodeVersion: 'stable', gcloud: { projectId: process.env.GCLOUD_PROJECT_ID, keyFile: process.env.GCLOUD_KEY_FILE }, vm: { zone: process.env.GCLOUD_ZONE || 'us-central1-a', os: 'centos', http: true, https: true } } assign(config, pkg.gcloudDeploy, explicitConfig) // gcloud wants `keyFilename` config.gcloud.keyFilename = config.gcloud.keyFile delete config.gcloud.keyFile if (!config.gcloud.projectId) { throw new Error('A projectId is required') } if (!config.gcloud.credentials && !config.gcloud.keyFilename) { throw new Error('Authentication with a credentials object or keyFile path is required') } return config } module.exports = function (config) { if (typeof config !== 'object') config = { root: config || process.cwd() } var pkg = require(path.join(config.root, 'package.json')) config = resolveConfig(pkg, config) var gcloudConfig = config.gcloud var pkgRoot = config.root var uniqueId = slug(pkg.name, { lower: true }) + '-' + Date.now() var gce = new Compute(gcloudConfig) var gcs = new Storage(gcloudConfig) var deployStream = pumpify() async.waterfall([ createTarStream, uploadTar, createVM, startVM ], function (err, vm) { if (err) return deployStream.destroy(err) var outputCfg = assign({}, gcloudConfig, { name: vm.name, zone: vm.zone.name }) outputCfg.authConfig = {} if (gcloudConfig.credentials) outputCfg.authConfig.credentials = gcloudConfig.credentials if (gcloudConfig.keyFilename) outputCfg.authConfig.keyFile = gcloudConfig.keyFilename deployStream.setPipeline(outputStream(outputCfg), through()) // sniff the output stream for when it's safe to delete the tar file deleteTarFile(outputStream(outputCfg)) }) function createTarStream (callback) { var tarStream = archiver.create('tar', { gzip: true }) tarStream.bulk([{ expand: true, cwd: pkgRoot, src: ['**', '!node_modules/**'] }]) tarStream.finalize() callback(null, tarStream) } function uploadTar (tarStream, callback) { var bucketName = gcloudConfig.projectId + '-gcloud-deploy-tars' var bucket = gcs.bucket(bucketName) bucket.get({ autoCreate: true }, function (err) { if (err) return callback(err) deployStream.emit('bucket', bucket) deployStream.bucket = bucket var tarFile = bucket.file(uniqueId + '.tar') var writeStream = tarFile.createWriteStream({ gzip: true, public: true }) tarStream.pipe(writeStream) .on('error', callback) .on('finish', function () { deployStream.emit('file', tarFile) deployStream.file = tarFile callback(null, tarFile) }) }) } function createVM (file, callback) { var vmCfg = config.vm // most node apps will have dependencies that requires compiling. without // these build tools, the libraries might not install var installBuildEssentialsCommands = { debian: multiline.stripIndent(function () {/* apt-get update apt-get install -yq build-essential git-core */}), fedora: multiline.stripIndent(function () {/* yum -y groupinstall "Development Tools" "Development Libraries" */}), suse: multiline.stripIndent(function () {/* sudo zypper --non-interactive addrepo http://download.opensuse.org/distribution/13.2/repo/oss/ repo sudo zypper --non-interactive --no-gpg-checks rm product:SLES-12-0.x86_64 cpp48-4.8.3+r212056-11.2.x86_64 suse-build-key-12.0-4.1.noarch sudo zypper --non-interactive --no-gpg-checks install --auto-agree-with-licenses --type pattern devel_basis */}) } var installBuildEssentialsCommand switch (vmCfg.os) { case 'centos': case 'centos-cloud': case 'redhat': case 'rhel': case 'rhel-cloud': installBuildEssentialsCommand = installBuildEssentialsCommands.fedora break case 'suse': case 'suse-cloud': case 'opensuse': case 'opensuse-cloud': installBuildEssentialsCommand = installBuildEssentialsCommands.suse break case 'debian': case 'debian-cloud': case 'ubuntu': case 'ubuntu-cloud': case 'ubuntu-os-cloud': default: installBuildEssentialsCommand = installBuildEssentialsCommands.debian break } var startupScript = format(multiline.stripIndent(function () {/* #! /bin/bash set -v {installBuildEssentialsCommand} {customStartupScript} export NVM_DIR=/usr/local/nvm export HOME=/root export GCLOUD_VM=true curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.0/install.sh | bash source /usr/local/nvm/nvm.sh nvm install {version} if [ ! -d /opt/app ]; then mkdir /opt/app fi cd /opt/app curl https://storage.googleapis.com/{bucketName}/{fileName} | tar -xz npm install --only-production npm start & */}), { installBuildEssentialsCommand: installBuildEssentialsCommand, customStartupScript: config.startupScript || '', bucketName: file.bucket.name, fileName: file.name, version: config.nodeVersion }) vmCfg.metadata = vmCfg.metadata || {} vmCfg.metadata.items = vmCfg.metadata.items || [] vmCfg.metadata.items.push({ key: 'startup-script', value: startupScript }) var zone = gce.zone(vmCfg.zone) var onVMReady = function (vm) { deployStream.emit('vm', vm) deployStream.vm = vm callback(null, vm) } var vm = zone.vm(vmCfg.name || uniqueId) if (vmCfg.name) { // re-use an existing VM // @tood implement `setMetadata` in gcloud-node#vm vm.setMetadata({ 'startup-script': startupScript }, _onOperationComplete(function (err) { if (err) return callback(err) onVMReady(vm) })) } else { // create a VM vm.create(vmCfg, _onOperationComplete(function (err) { if (err) return callback(err) onVMReady(vm) })) } } function startVM (vm, callback) { // if re-using a VM, we have to stop & start to apply the new startup script vm.stop(_onOperationComplete(function (err) { if (err) return callback(err) vm.start(_onOperationComplete(function (err) { if (err) return callback(err) vm.getMetadata(function (err, metadata) { if (err) return callback(err) var url = 'http://' + metadata.networkInterfaces[0].accessConfigs[0].natIP deployStream.emit('start', url) deployStream.url = url callback(null, vm) }) })) })) } function deleteTarFile (outputStream) { var tarFile = deployStream.file var startupScriptStarted = false outputStream.pipe(through(function (outputLine, enc, next) { outputLine = outputLine.toString('utf8') startupScriptStarted = startupScriptStarted || outputLine.indexOf('Starting Google Compute Engine user scripts') > -1 // if npm install is running, the file has already been downloaded if (startupScriptStarted && outputLine.indexOf('npm install') > -1) { outputStream.end() tarFile.delete(function (err, apiResponse) { if (err) { var error = new Error('The tar file (' + tarFile.name + ') could not be deleted') error.response = apiResponse deployStream.destroy(error) } }) } else { next() } })) } return deployStream } // helper to wait for an operation to complete before executing the callback // this also supports creation callbacks, specifically `createVM`, which has an // extra arg with the instance object of the created VM function _onOperationComplete (callback) { return function (err, operation, apiResponse) { if (err) return callback(err) if (arguments.length === 4) { operation = apiResponse } operation .on('error', callback) .on('complete', callback.bind(null, null)) } }