slush-aws-lambda
Version:
A slush generator to scaffold an AWS Lambda function package and upload it to AWS.
431 lines (396 loc) • 15 kB
JavaScript
/* eslint-disable no-console */
const { promisify } = require("util")
const clc = require("cli-color")
const zipdir = promisify(require("zip-dir"))
const gulp = require("gulp")
const usage = require("gulp-help-doc")
const install = require("gulp-install")
const { writeFileSync } = require("fs")
const { join } = require("path")
const inquirer = require("inquirer")
const AWS = require("aws-sdk")
const CwLogs = require("aws-cwlogs")
const { env = "staging", debug } = require("simple-argv")
const {
role: { create: createRole, delete: deleteRole, basicAssumeRolePolicy },
policy: { create: createPolicy, attachRolePolicy, basicLambdaPolicy, detachRolePolicy, delete: deletePolicy, getPolicyArn, updateDocument: updatePolicyDocument }
} = require("./utils/iam.js")
const {
create: createLambda,
delete: deleteLambda,
updateConfiguration: updateLambdaConfiguration
} = require("./utils/lambda.js")
const {
getServiceInstance
} = require("./utils/common.js")
const success = (...args) => console.log("[" + clc.green("SUCCESS") + "]", ...args)
const error = err => console.error("[" + clc.red("ERROR") + "]", err.message, debug ? err.stack : "")
let lambdaConfig
const getLambdaConfig = () => {
if (!lambdaConfig) {
try {
lambdaConfig = require(join(__dirname, "lambda-config.js"))(env)
console.log(`Using ${env === "production" ? clc.yellow(env) : clc.green(env)} lambda-config.js`)
} catch (err) {
if (err.message.indexOf("Cannot find module") !== -1) {
throw new Error(`WARNING! lambda config not found, run command ${clc.cyan("gulp configure")}`)
} else {
throw err
}
}
}
return lambdaConfig
}
let lambdaPolicy
const getLambdaPolicy = () => {
if (!lambdaPolicy) {
try {
lambdaPolicy = require(join(__dirname, "lambda-policy.js"))(env)
console.log(`Using ${env === "production" ? clc.yellow(env) : clc.green(env)} lambda-policy.js`)
} catch (err) {
if (err.message.indexOf("Cannot find module") !== -1) {
throw new Error(`WARNING! lambda policy not found, run command ${clc.cyan("gulp configure")}`)
} else {
throw err
}
}
}
return lambdaPolicy
}
let credentials
try {
credentials = require(join(__dirname, ".credentials.json"))
} catch(ignore) {
console.log("project specific AWS credentials not found, using global credentials; run \"gulp credentials\" to setup project specific credentials;")
}
/**
* List all gulp tasks and their descriptions;
* @task {help}
* @order {0}
*/
gulp.task("help", () => usage(gulp))
gulp.task("default", ["help"])
/**
* Set-up all settings of your AWS Lambda;
* @task {credentials}
* @order {1}
*/
gulp.task("credentials", () => {
if (!credentials) credentials = {}
const obfuscate = (str) => typeof str === "string" ? str.split("").map((char, i) => i < str.length - 4 ? "*" : char).join("") : ""
return inquirer.prompt([
{ type: "input", name: "accessKeyId", message: `AWS Access Key ID [${obfuscate(credentials.accessKeyId)}]:` },
{ type: "input", name: "secretAccessKey", message: `AWS Secret Access Key [${obfuscate(credentials.secretAccessKey)}]:` }
])
.then(({ accessKeyId, secretAccessKey }) => {
if (accessKeyId) credentials.accessKeyId = accessKeyId
if (secretAccessKey) credentials.secretAccessKey = secretAccessKey
writeFileSync(join(__dirname, ".credentials.json"), JSON.stringify(credentials, null, 2))
})
.catch(error)
})
/**
* Set-up all settings of your AWS Lambda;
* @task {configure}
* @order {2}
*/
gulp.task("configure", next => {
let lambdaConfig
let lambdaPolicy
try {
lambdaConfig = require(join(__dirname, "lambda-config.js"))("${env}")
lambdaPolicy = require(join(__dirname, "lambda-policy.js"))("${env}")
} catch(_) {}
inquirer.prompt([
{ type: "input", name: "FunctionName", message: "Function name:", default: lambdaConfig ? lambdaConfig.ConfigOptions.FunctionName : "my-lambda-${env}" },
{ type: "input", name: "Region", message: "Region:", default: lambdaConfig ? lambdaConfig.Region : "eu-west-1" },
{ type: "input", name: "Description", message: "Description:", default: lambdaConfig ? lambdaConfig.ConfigOptions.Description : null },
{ type: "input", name: "Handler", message: "Handler:", default: lambdaConfig ? lambdaConfig.ConfigOptions.Handler : "index.handler" },
{ type: "input", name: "RoleName", message: "RoleName:", default: lambdaConfig ? lambdaConfig.ConfigOptions.RoleName : "my-lambda-${env}" },
{ type: "input", name: "PolicyName", message: "PolicyName:", default: lambdaPolicy ? lambdaPolicy.PolicyName : "my-lambda-${env}-lambda" },
{ type: "input", name: "MemorySize", message: "MemorySize:", default: lambdaConfig ? lambdaConfig.ConfigOptions.MemorySize : "128" },
{ type: "input", name: "Timeout", message: "Timeout:", default: lambdaConfig ? lambdaConfig.ConfigOptions.Timeout : "3" },
{ type: "input", name: "Runtime", message: "Runtime:", default: lambdaConfig ? lambdaConfig.ConfigOptions.Runtime : "nodejs8.10" }
]).then(config_answers => {
const lambdaConfigFile =
`module.exports = env => ({
Region: "${config_answers.Region}",
ConfigOptions: {
FunctionName: \`${config_answers.FunctionName}\`,
Description: "${config_answers.Description}",
Handler: "${config_answers.Handler}",
RoleName: \`${config_answers.RoleName}\`,
MemorySize: ${config_answers.MemorySize},
Timeout: ${config_answers.Timeout},
Runtime: "${config_answers.Runtime}",
Environment: {
Variables: Object.assign({ NODE_ENV: env }, require("./variables.json").Variables)
}
}
})`
const lambdaPackage = require(join(__dirname, "src/package.json"))
lambdaPackage.name = config_answers.FunctionName
lambdaPackage.description = config_answers.Description
writeFileSync(join(__dirname, "/src/package.json"), JSON.stringify(lambdaPackage, null, 2))
writeFileSync(join(__dirname, "/variables.json"), JSON.stringify({
Variables: {}
}, null, 2))
writeFileSync(join(__dirname, "/lambda-config.js"), lambdaConfigFile)
const lambdaPolicyFile =
`module.exports = env => ({
PolicyName: \`${config_answers.PolicyName}\`,
Prefix: \`/\${env}/\`,
${JSON.stringify({
PolicyDocument: basicLambdaPolicy
}, null, 2).slice(2, -2)}
})`
writeFileSync(join(__dirname, "/lambda-policy.js"), lambdaPolicyFile)
success("Lambda configuration saved")
next()
})
.catch(error)
})
/**
* Install npm packages inside the src folder
* @task {install}
* @order {3}
*/
gulp.task("install", () => {
return gulp.src(join(__dirname, "src/package.json"))
.pipe(install())
})
/**
* Wrap everything inside the src folder in a zip file and upload
* it to AWS to create your new AWS Lambda using the configuration
* information you set in the lambda_config.json file;
* @task {create}
* @order {4}
*/
gulp.task("create", () => {
return zipdir(join(__dirname, "src"))
.then(ZipFile => {
const { ConfigOptions, Region: region } = getLambdaConfig()
const { PolicyName, PolicyDocument, Prefix: policyPrefix } = getLambdaPolicy()
const state = {}
return createPolicy(PolicyName, `Lambda "${ConfigOptions.FunctionName}" project policy attached to "${ConfigOptions.RoleName}"`, PolicyDocument, policyPrefix, credentials, region)
.then(({ Policy: { Arn: policyArn } }) => {
Object.assign(state, { policyArn })
success(`Policy ${PolicyName} created`)
return createRole(ConfigOptions.RoleName, `${ConfigOptions.FunctionName} lambda role`, basicAssumeRolePolicy(policyArn.split(":")[4]), `/${env}/`, credentials, region)
.then(({ Role: { RoleName: roleName, Arn: roleArn } }) => {
state.roleArn = roleArn
success(`Role ${roleName} created`)
})
})
.then(() => attachRolePolicy(state.policyArn, ConfigOptions.RoleName, credentials, region))
.then(() => {
success(`Policy ${PolicyName} attached to ${ConfigOptions.RoleName}`)
return createLambda(ConfigOptions.FunctionName, region, ConfigOptions.Description, ConfigOptions.Handler, ConfigOptions.MemorySize, ConfigOptions.Timeout, ConfigOptions.Runtime, state.roleArn, ZipFile, credentials)
})
.then(() => success(`lambda ${ConfigOptions.FunctionName} created`))
.catch(error)
})
.catch(error)
})
/**
* Update lambda policy, wrap everything inside the src folder in a zip file and upload
* it to AWS to update your existing AWS Lambda using the configuration
* information you set in the lambda-config.json file;
* @task {update}
* @order {5}
*/
gulp.task("update", ["update-policy", "update-config", "update-code"])
/**
* Wrap everything inside the src folder in a zip file and upload
* it to AWS to update the code of your existing AWS Lambda;
* @task {update-code}
* @order {6}
*/
gulp.task("update-code", next => {
return zipdir(join(__dirname, "src"))
.then(ZipFile => {
const { Region: region, ConfigOptions: { FunctionName } } = getLambdaConfig()
const lambda = new AWS.Lambda({ credentials, region })
lambda.updateFunctionCode({ FunctionName, ZipFile }).promise()
.then(data => {
success("lambda", clc.cyan(data.FunctionName), "code updated")
console.log(data)
// next()
})
.catch(err => {
error(err)
next(err)
})
})
.catch(error)
})
/**
* Change your AWS Lambda configuration using the information
* you set in the lambda-config.json file;
* @task {update-config}
* @order {7}
*/
gulp.task("update-config", () => {
const { Region: region, ConfigOptions } = getLambdaConfig()
const { FunctionName: functionName } = ConfigOptions
delete ConfigOptions.FunctionName
delete ConfigOptions.RoleName
return updateLambdaConfiguration(functionName, ConfigOptions, credentials, region).then(() => success("Lambda config updated"))
.catch(error)
})
/**
* Update Lambda policy document using the information
* you set in the lambda-policy.js file;
* @task {update-policy}
* @order {8}
*/
gulp.task("update-policy", () => {
const { Region: region } = getLambdaConfig()
const { PolicyName, Prefix, PolicyDocument } = getLambdaPolicy()
return updatePolicyDocument(PolicyName, Prefix, PolicyDocument, credentials, region).then(() => success("Lambda policy document updated"))
.catch(error)
})
/**
* Delete your AWS Lambda function;
* @task {delete}
* @order {9}
*/
gulp.task("delete", () => {
const { Region: region, ConfigOptions: { FunctionName, RoleName } } = getLambdaConfig()
const { PolicyName, Prefix } = getLambdaPolicy()
const state = {}
return getPolicyArn(PolicyName, Prefix, credentials, region)
.then(policyArn => {
state.policyArn = policyArn
const promises = [
detachRolePolicy(policyArn, RoleName, credentials, region).then(() => success(`Policy ${PolicyName} detached`)),
deleteLambda(FunctionName, region, credentials).then(() => success(`lambda ${FunctionName} deleted`))
]
return Promise.all(promises)
})
.then(() => {
const deletes = [
deleteRole(RoleName, credentials, region).then(() => success(`Role ${RoleName} deleted`)),
deletePolicy(state.policyArn, credentials, region).then(() => success(`Policy ${PolicyName} deleted`))
]
return Promise.all(deletes)
})
.catch(error)
})
/**
* Print in the console all logs generated by you Lambda
* function in Amazon CloudWatch;
* @task {logs}
* @order {10}
*/
gulp.task("logs", () => {
const { ConfigOptions: { FunctionName }, Region: region } = getLambdaConfig()
const cwlogs = new CwLogs({
logGroupName:`/aws/lambda/${FunctionName}`,
region,
momentTimeFormat: "hh:mm:ss:SSS",
logFormat: "lambda",
credentials
})
cwlogs.start()
})
/**
* Invoke the Lambda function passing test-payload.js as
* payload and printing the response to the console;
* @task {invoke}
* @order {11}
*/
gulp.task("invoke", next => {
const { Region: region, ConfigOptions: { FunctionName } } = getLambdaConfig()
const lambda = new AWS.Lambda({ credentials, region })
let Payload
try {
Payload = JSON.stringify(require("./test-payload.js"))
} catch(err) {
return next(err)
}
return lambda.invoke({
FunctionName,
InvocationType: "RequestResponse",
LogType: "None",
Payload
}).promise()
.then(data => {
success("lambda invocation done")
try {
console.log(JSON.parse(data.Payload))
} catch (_) {
console.log(data.Payload)
}
})
.catch(error)
})
/**
* Invoke the Lambda function LOCALLY passing test-payload.js
* as payload and printing the response to the console;
* @task {invoke-local}
* @order {12}
*/
gulp.task("invoke-local", next => {
require(join(__dirname, "utils", "test-local.js"))(next, credentials)
})
/**
* Add an env variable to lambda config
* @task {add-variable}
* @order {13}
*/
gulp.task("add-variable", () => {
const varsFile = require("./variables.json")
const { Variables } = varsFile
return inquirer.prompt([
{ type: "input", name: "name", message: "name:" },
{ type: "input", name: "value", message: "value:" }
])
.then(({ name, value }) => {
if (!name) {
throw new Error("Invalid variable name")
}
if (typeof Variables.name !== "undefined") {
throw new Error(`Variable ${name} already exists`)
}
Variables[name] = value
writeFileSync(join(__dirname, "variables.json"), JSON.stringify(varsFile, null, 2))
})
.catch(error)
})
/**
* Encrypt an env variable
* @task {encrypt}
* @order {14}
*/
gulp.task("encrypt", () => {
const varsFile = require("./variables.json")
const { Variables } = varsFile
const kms = getServiceInstance("KMS")(credentials, lambdaConfig.Region)
const state = {}
return kms.listAliases({}).promise()
.then(({ Aliases }) => {
return inquirer.prompt([
{ type: "list", name: "key", message: "Choose KMS Key:", choices: Aliases.map(({ AliasName, TargetKeyId }) => ({ name: AliasName, value: TargetKeyId })) }
])
})
.then(({ key }) => {
state.key = key
return inquirer.prompt([
{ type: "list", name: "toEncrypt", message: "Encrypt:", choices: Object.keys(Variables) }
])
})
.then(({ toEncrypt }) => {
state.toEncrypt = toEncrypt
return kms.encrypt({
KeyId: state.key,
Plaintext: Variables[toEncrypt]
}).promise()
})
.then(({ CiphertextBlob }) => {
Variables[state.toEncrypt] = new Buffer(CiphertextBlob, "base64").toString("base64")
writeFileSync(join(__dirname, "variables.json"), JSON.stringify(varsFile, null, 2))
})
.catch(error)
})
// const kms = getServiceInstance("KMS")(credentials, lambdaConfig.Region)