UNPKG

open-next-cdk

Version:

Deploy a NextJS app using OpenNext packaging to serverless AWS using CDK

619 lines (496 loc) 17.3 kB
"use strict"; var exec = require( "child_process" ).exec var fs = require( "fs" ) var os = require( "os" ) var path = require( "path" ) var yaml = require('js-yaml') var buildYamlSchema = require('./schema') const spawn = require('child_process').spawn var Watchpack = require("watchpack"); var Lambda = function( o ) { this.settings = o || {}; return this; } Lambda.prototype._resolve_refs = function( $config ) { this.envs = {} if ( (typeof $config.AWS_KEY === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_KEY , 'Ref') && (typeof $config.AWS_KEY.Ref === "string") && ($config.AWS_KEY.Ref.indexOf('env.') === 0) ) { this.envs[ $config.AWS_KEY.Ref.split('env.')[1] ] = process.env[ $config.AWS_KEY.Ref.split('env.')[1] ] $config.AWS_KEY = process.env[ $config.AWS_KEY.Ref.split('env.')[1] ] } if ( (typeof $config.AWS_SECRET === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_SECRET , 'Ref') && (typeof $config.AWS_SECRET.Ref === "string") && ($config.AWS_SECRET.Ref.indexOf('env.') === 0) ) { this.envs[ $config.AWS_SECRET.Ref.split('env.')[1] ] = process.env[ $config.AWS_SECRET.Ref.split('env.')[1] ] $config.AWS_SECRET = process.env[ $config.AWS_SECRET.Ref.split('env.')[1] ] } if ( (typeof $config.Role === "object") && Object.prototype.hasOwnProperty.call( $config.Role , 'Ref') && (typeof $config.Role.Ref === "string") && ($config.Role.Ref.indexOf('env.') === 0) ) { this.envs[ $config.Role.Ref.split('env.')[1] ] = process.env[ $config.Role.Ref.split('env.')[1] ] $config.Role = process.env[ $config.Role.Ref.split('env.')[1] ] } return $config; } Lambda.prototype._parse_config_file = function( program ) { if (program.substr(-7) === '.lambda') program = program.substr(0,program.length - 7) var config_file = program + '.lambda' if ( !fs.existsSync( config_file ) ) { console.log('Lambda config not found (' + program + '.lambda )') process.exit(-1) } var $config; try { //var $config = JSON.parse(fs.readFileSync( config_file, "utf8")) $config = yaml.safeLoad(fs.readFileSync( config_file, "utf8"), { schema: buildYamlSchema(), onWarning: function(warning) { console.error(warning); }, json: true, }) } catch (e) { console.log('Invalid config file (' + program + '.lambda )', e ) if ( this.settings.exitOnError !== false ) process.exit(-1) return; } // console.log( JSON.stringify($config, null, "\t")) $config = this._resolve_refs($config); if (!$config.FunctionName) $config.FunctionName = program.split('/').slice(-1)[0] var $configPath = program.split('/').slice(0,-1).join('/') //console.log( $configPath ) var fullFunctionPath if (($config.PATH.substr(0,1) === '/') || ($config.PATH.substr(0,2) == '~/')) { fullFunctionPath = ($config.PATH).replace(/\/\.\//g, '/') } else { fullFunctionPath = (process.cwd() + '/' + $configPath + '/' + $config.PATH).replace(/\/\.\//g, '/') } if ( fullFunctionPath.substr(-1) !== '/') fullFunctionPath+='/' //console.log($fullFunctionPath) $config.PATH = fullFunctionPath; return $config; } Lambda.prototype.deploy = function( program ) { var aws = require( "aws-sdk" ) if (process.env.AWSPILOT_DOCKER_WORKDIR) process.chdir(process.env.AWSPILOT_DOCKER_WORKDIR) if (program.substr(-7) === '.lambda') program = program.substr(0,program.length - 7) var config_file = program + '.lambda' if ( !fs.existsSync( config_file ) ) { console.log('Lambda config not found (' + program + '.lambda )') process.exit(-1) } try { //var $config = JSON.parse(fs.readFileSync( config_file, "utf8")) var $config = yaml.safeLoad(fs.readFileSync( config_file, "utf8"), { schema: buildYamlSchema(), onWarning: function(warning) { console.error(warning); }, json: true, }) } catch (e) { console.log('Invalid config file (' + program + '.lambda )', e ) if ( _this.settings.exitOnError !== false ) process.exit(-1) return; } // console.log( JSON.stringify($config, null, "\t")) if ( (typeof $config.AWS_KEY === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_KEY , 'Ref') && (typeof $config.AWS_KEY.Ref === "string") && ($config.AWS_KEY.Ref.indexOf('env.') === 0) ) $config.AWS_KEY = process.env[ $config.AWS_KEY.Ref.split('env.')[1] ] if ( (typeof $config.AWS_SECRET === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_SECRET , 'Ref') && (typeof $config.AWS_SECRET.Ref === "string") && ($config.AWS_SECRET.Ref.indexOf('env.') === 0) ) $config.AWS_SECRET = process.env[ $config.AWS_SECRET.Ref.split('env.')[1] ] if ( (typeof $config.Role === "object") && Object.prototype.hasOwnProperty.call( $config.Role , 'Ref') && (typeof $config.Role.Ref === "string") && ($config.Role.Ref.indexOf('env.') === 0) ) $config.Role = process.env[ $config.Role.Ref.split('env.')[1] ] if (!$config.FunctionName) $config.FunctionName = program.split('/').slice(-1)[0] if (!/^([a-zA-Z0-9-_.]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$/.test($config.FunctionName)) { console.log("Invalid function name", $config.FunctionName ); process.exit(-1); } aws.config.update({ accessKeyId: $config.AWS_KEY, secretAccessKey: $config.AWS_SECRET, region: $config.AWS_REGION }) var lambdaV2 = new aws.Lambda( { apiVersion: "2015-03-31" } ) var _this = this var tmpfile = path.join( os.tmpdir(), $config.FunctionName + "-" + new Date().getTime() + ".zip" ) var $configPath = program.split('/').slice(0,-1).join('/') var fullFunctionPath if (($config.PATH.substr(0,1) === '/') || ($config.PATH.substr(0,2) == '~/')) { fullFunctionPath = ($config.PATH).replace(/\/\.\//g, '/') } else { fullFunctionPath = (process.cwd() + '/' + $configPath + '/' + $config.PATH).replace(/\/\.\//g, '/') } if (fullFunctionPath.substr(-1) !== '/') fullFunctionPath+='/' if (!fs.existsSync( fullFunctionPath )) { console.log( "No such directory:" + fullFunctionPath ) process.exit(-1) } var $cwd = process.cwd() process.chdir( fullFunctionPath ) var zipfileList = '.' try { var packageJson = fs.readFileSync( 'package.json', 'utf8' ) if ( packageJson ) { var packageJsonFiles = JSON.parse( packageJson ).files if ( typeof packageJsonFiles === 'object' ) { zipfileList = packageJsonFiles.join( ' ' ) } } } catch (e) { } var $zipCmd = "zip -r -9 " + tmpfile + ' ' + zipfileList // maxBuffer specifies the largest amount of data allowed on stdout or stderr - if this value is exceeded then the child process is killed. exec( $zipCmd, { maxBuffer: 1024 * 1024 * 5 /* 5MB */} ,function( err /*, stdout, stderr */ ) { process.chdir($cwd) if ( err ) { console.log("Error generating zip file") throw err; } var buffer = fs.readFileSync( tmpfile ) // var params = { // FunctionName: $config.FunctionName, // FunctionZip: buffer, // Handler: $config.Handler, // Mode: 'event', // Role: $config.Role, // Runtime: $config.Runtime || 'nodejs10.x', // Description: $config.Description, // MemorySize: $config.MemorySize, // Timeout: $config.Timeout // } if (typeof $config.Environment !== "object") $config.Environment = {} if (typeof $config.Environment.Variables !== "object") $config.Environment.Variables = {} if (typeof $config.Tags !== "object") $config.Tags = {} var paramsV2 = { Code: { ZipFile: buffer, }, Description: $config.Description, FunctionName: $config.FunctionName, Handler: $config.Handler, MemorySize: $config.MemorySize, Publish: true, Role: $config.Role, Runtime: $config.Runtime || 'nodejs10.x', Timeout: $config.Timeout, Environment: $config.Environment, Layers: $config.Layers || [], Tags: $config.Tags, //VpcConfig: {} }; // remove temp file fs.unlinkSync( tmpfile ) lambdaV2.createFunction( paramsV2, function( err ) { if ( err && err.code !== 'ResourceConflictException') { console.log("ERROR:", err ) if ( _this.settings.exitOnError !== false ) process.exit(-1); return } if ( err && err.code === 'ResourceConflictException') { // console.log("function exists, should update config and code") var paramsV2 = { Description: $config.Description, FunctionName: $config.FunctionName, Handler: $config.Handler, MemorySize: $config.MemorySize, Role: $config.Role, Runtime: $config.Runtime || 'nodejs10.x', Timeout: $config.Timeout, Environment: $config.Environment, Layers: $config.Layers || [], //Tags: $config.Tags, //VpcConfig: {} }; // interesting: tags not supported by updateFunctionConfiguration but it will remve tags lambdaV2.updateFunctionConfiguration(paramsV2, function(err, data) { if ( err) { console.log("ERROR:", err ) if ( _this.settings.exitOnError !== false ) process.exit(-1) return; } var paramsV2 = { FunctionName: $config.FunctionName, //DryRun: true || false, Publish: true, // RevisionId: 'STRING_VALUE', ZipFile: buffer, }; lambdaV2.updateFunctionCode(paramsV2, function( err ) { if ( err) { console.log("ERROR:", err ) if ( _this.settings.exitOnError !== false ) process.exit(-1) return; } // lambdaV2.listTags({ Resource: data.FunctionArn }, function(err, data) { // console.log( err, data ) // }); // lambdaV2.listTags({ Resource: data.FunctionArn }, function(err, data) { // console.log( err, data ) // }); if (Object.keys($config.Tags).length) { lambdaV2.tagResource({ Resource: data.FunctionArn, Tags: $config.Tags }, function( err ) { if (err) console.log("WARNING: tagResource failed on function ", $config.FunctionName ) console.log( "Deployed!" ); }); return; } // @todo: listTags, diff with $config.Tags, untagResource not in $config.Tags, tagResource with $config.Tags console.log( "Deployed!" ); return; }); }) return; } console.log( "Deployed!" ); }); } ); } Lambda.prototype.delete = function( program ) { var aws = require( "aws-sdk" ) if (program.substr(-7) === '.lambda') program = program.substr(0,program.length - 7) var config_file = program + '.lambda' if ( !fs.existsSync( config_file ) ) { console.log('Lambda config not found (' + program + '.lambda )') process.exit(-1) } try { //var $config = JSON.parse(fs.readFileSync( config_file, "utf8")) var $config = yaml.safeLoad(fs.readFileSync( config_file, "utf8"), { schema: buildYamlSchema(), onWarning: function(warning) { console.error(warning); }, json: true, }) } catch (e) { console.log('Invalid config file (' + program + '.lambda )', e ) process.exit(-1) } if ( (typeof $config.AWS_KEY === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_KEY , 'Ref') && (typeof $config.AWS_KEY.Ref === "string") && ($config.AWS_KEY.Ref.indexOf('env.') === 0) ) $config.AWS_KEY = process.env[ $config.AWS_KEY.Ref.split('env.')[1] ] if ( (typeof $config.AWS_SECRET === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_SECRET , 'Ref') && (typeof $config.AWS_SECRET.Ref === "string") && ($config.AWS_SECRET.Ref.indexOf('env.') === 0) ) $config.AWS_SECRET = process.env[ $config.AWS_SECRET.Ref.split('env.')[1] ] if ( (typeof $config.Role === "object") && Object.prototype.hasOwnProperty.call( $config.Role , 'Ref') && (typeof $config.Role.Ref === "string") && ($config.Role.Ref.indexOf('env.') === 0) ) $config.Role = process.env[ $config.Role.Ref.split('env.')[1] ] aws.config.update({ accessKeyId: $config.AWS_KEY, secretAccessKey: $config.AWS_SECRET, region: $config.AWS_REGION }) if (!$config.FunctionName) $config.FunctionName = program.split('/').slice(-1)[0] var lambdaV2 = new aws.Lambda( { apiVersion: "2015-03-31" } ) lambdaV2.deleteFunction({ FunctionName: $config.FunctionName }, function( err ) { if ( err && err.code !== 'ResourceNotFoundException') { console.log("delete error:", err ) process.exit(-1) } if ( err && err.code === 'ResourceNotFoundException') console.log("WARNING: deleteFunction ",$config.FunctionName,": ResourceNotFoundException " ) else console.log( "Deleted!" ); }); } Lambda.prototype.invoke = function( program ) { var aws = require( "aws-sdk" ) if (program.substr(-7) === '.lambda') program = program.substr(0,program.length - 7) var config_file = program + '.lambda' if ( !fs.existsSync( config_file ) ) { console.log('Lambda config not found (' + program + '.lambda )') process.exit(-1) } try { //var $config = JSON.parse(fs.readFileSync( config_file, "utf8")) var $config = yaml.safeLoad(fs.readFileSync( config_file, "utf8"), { schema: buildYamlSchema(), onWarning: function(warning) { console.error(warning); }, json: true, }) } catch (e) { console.log('Invalid config file (' + program + '.lambda )', e ) process.exit(-1) } if ( (typeof $config.AWS_KEY === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_KEY , 'Ref') && (typeof $config.AWS_KEY.Ref === "string") && ($config.AWS_KEY.Ref.indexOf('env.') === 0) ) $config.AWS_KEY = process.env[ $config.AWS_KEY.Ref.split('env.')[1] ] if ( (typeof $config.AWS_SECRET === "object") && Object.prototype.hasOwnProperty.call( $config.AWS_SECRET , 'Ref') && (typeof $config.AWS_SECRET.Ref === "string") && ($config.AWS_SECRET.Ref.indexOf('env.') === 0) ) $config.AWS_SECRET = process.env[ $config.AWS_SECRET.Ref.split('env.')[1] ] if ( (typeof $config.Role === "object") && Object.prototype.hasOwnProperty.call( $config.Role , 'Ref') && (typeof $config.Role.Ref === "string") && ($config.Role.Ref.indexOf('env.') === 0) ) $config.Role = process.env[ $config.Role.Ref.split('env.')[1] ] aws.config.update({ accessKeyId: $config.AWS_KEY, secretAccessKey: $config.AWS_SECRET, region: $config.AWS_REGION }) if (!$config.FunctionName) $config.FunctionName = program.split('/').slice(-1)[0] var lambdaV2 = new aws.Lambda( { apiVersion: "2015-03-31" } ) var paramsV2 = { FunctionName: $config.FunctionName, //ClientContext: 'STRING_VALUE', InvocationType:'RequestResponse', LogType: 'Tail', Payload: JSON.stringify({}), }; lambdaV2.invoke(paramsV2, function(err, data) { if ( err ) { console.log("invoke error:", err ) process.exit(-1) } try { console.log(JSON.stringify(JSON.parse(data.Payload), null, "\t")) } catch(e) {} console.log("------------") console.log( Buffer.from( data.LogResult, 'base64').toString() ) }); } Lambda.prototype.start = function( program ) { var l = new Lambda( { exitOnError: false } ) var config = l._parse_config_file( program ) var config_path = path.resolve( program ); var cwd = process.cwd() var wp = new Watchpack({ aggregateTimeout: 3000, // wait 1 quiet sec then fire agregated //mfollowSymlinks: true, ignored: "**/.git", // poll: true, // poll: true, undefined=native, true=default, 3000=ms }); console.log("---|", ( process.env.AWSPILOT_DOCKER_WORKDIR ? 'DOCKER' : 'LOCAL' ) ,new Date().toISOString() ) try { l.deploy( program ) } catch (e) { console.log(e) } var files = [config_path] var folders = [config.PATH] if (process.env.AWSPILOT_DOCKER_WORKDIR) { files = [] folders = [config.PATH, path.dirname(config_path) ] } wp.watch({ files: files, directories: folders, //missing: listOfNotExistingItems, startTime: Date.now() - 10000 }); //wp.watch([program], [config.PATH], Date.now() - 10000); // wp.on("change", function(filePath, explanation) { // console.log("filePath change", filePath, explanation ) // }); // wp.on("remove", function(filePath, explanation) { // console.log("filePath remove", filePath, explanation ) // }); wp.on("aggregated", function( changes, removals ) { console.log("\n---|", ( process.env.AWSPILOT_DOCKER_WORKDIR ? 'DOCKER' : 'LOCAL' ), new Date().toISOString() ) //console.log("changes", typeof changes, changes) Array.from(changes).map(function(filePath) { console.log("---| * ", filePath ) }) Array.from(removals).map(function(filePath) { console.log("---| - ", filePath ) }) try { process.chdir(cwd) l.deploy( program ) } catch (e) { console.log(e) } }); } Lambda.prototype.start_docker = function( program ) { var l = new Lambda( { exitOnError: true } ) var config = l._parse_config_file( program ) //console.log( config ) var envs = [] Object.keys(l.envs).map(function(env_k) { envs.push('-e') envs.push(env_k + '=' + l.envs[env_k] ) }) //console.log( l.envs ) //console.log( envs ) var config_path = path.resolve( program ); spawn('docker', [ 'run', '-it', '--rm', '-v', config_path + ':' + config_path, '-v', config.PATH + ':' + config.PATH, '-e', 'AWSPILOT_DOCKER_WORKDIR='+path.dirname(config_path)+'/', '-e', 'AWSPILOT_LAMBDA_CONFIG=' + config_path.slice(path.dirname(config_path).length+1) , ] .concat( envs ) .concat([ 'awspilotdev/lambda_deploy_nodejs8.10', //'sh' ]) , {stdio: [process.stdin, process.stdout, process.stderr]}); } module.exports = new Lambda( );