diff --git a/.gitignore b/.gitignore index 5a05d27..69d5431 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ testProjects/*/package-lock.json testProjects/*/yarn.lock .serverlessUnzipped node_modules +node_modules.nosync .vscode/ .eslintcache dist diff --git a/README.md b/README.md index c2a78db..adf86f2 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ inputs: - xxx - xxx region: us-east-2 # (optional) aws region to deploy to. default is us-east-1. + provisionedConcurrency: 1 # (optional) provisioned concurrency, default is 0. + alias: + name: provisioned # (optional) alias for provisioned concurrency config, default is "provisioned". ``` **Note:** Unlike the `src` input, the `handler` input is relative to the `src` input, not to the current working directory. @@ -118,7 +121,7 @@ $ serverless dev ### 6. Monitor -Anytime you need to know more about your running `aws-lambda` instance, you can run the following command to view the most critical info. +Anytime you need to know more about your running `aws-lambda` instance, you can run the following command to view the most critical info. ``` $ serverless info @@ -126,14 +129,14 @@ $ serverless info This is especially helpful when you want to know the outputs of your instances so that you can reference them in another instance. It also shows you the status of your instance, when it was last deployed, and how many times it was deployed. You will also see a url where you'll be able to view more info about your instance on the Serverless Dashboard. -To digg even deeper, you can pass the `--debug` flag to view the state of your component instance in case the deployment failed for any reason. +To digg even deeper, you can pass the `--debug` flag to view the state of your component instance in case the deployment failed for any reason. ``` $ serverless info --debug ``` ### 7. Remove -If you want to tear down your entire `aws-lambda` infrastructure that was created during deployment, just run the following command in the directory containing the `serverless.yml` file. +If you want to tear down your entire `aws-lambda` infrastructure that was created during deployment, just run the following command in the directory containing the `serverless.yml` file. ``` $ serverless remove ``` diff --git a/package.json b/package.json index 0568fcc..a7777ea 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "license": "Apache", "dependencies": {}, "devDependencies": { - "@serverless/platform-client": "^0.24.0", + "@serverless/platform-client": "^4.2.2", "aws-sdk": "^2.640.0", - "dotenv": "^8.2.0", "babel-eslint": "9.0.0", + "dotenv": "^8.2.0", "eslint": "5.6.0", "eslint-config-prettier": "^3.6.0", "eslint-plugin-import": "^2.18.0", diff --git a/src/_src/handler.js b/src/_src/handler.js index f7f884f..68c2f82 100644 --- a/src/_src/handler.js +++ b/src/_src/handler.js @@ -1,3 +1 @@ -module.exports.handler = async () => { - return 'Hello Serverless' -} +module.exports.handler = () => 'Hello Serverless' diff --git a/src/serverless.js b/src/serverless.js index 769f211..63d31f1 100644 --- a/src/serverless.js +++ b/src/serverless.js @@ -8,6 +8,11 @@ const { updateLambdaFunctionCode, updateLambdaFunctionConfig, getLambdaFunction, + getLambdaAlias, + createLambdaAlias, + updateLambdaAlias, + deleteLambdaAlias, + updateProvisionedConcurrencyConfig, createOrUpdateFunctionRole, createOrUpdateMetaRole, deleteLambdaFunction, @@ -80,16 +85,42 @@ class AwsLambda extends Component { const createResult = await createLambdaFunction(this, clients.lambda, inputs) inputs.arn = createResult.arn inputs.hash = createResult.hash + inputs.version = createResult.version console.log(`Successfully created an AWS Lambda function`) } else { // Update a Lambda function inputs.arn = prevLambda.arn console.log(`Updating ${inputs.name} AWS lambda function.`) - await updateLambdaFunctionCode(clients.lambda, inputs) + const updateResult = await updateLambdaFunctionCode(clients.lambda, inputs) + inputs.version = updateResult.version await updateLambdaFunctionConfig(this, clients.lambda, inputs) console.log(`Successfully updated AWS Lambda function`) } + const prevAlias = await getLambdaAlias(clients.lambda, inputs) + + // Maintain Alias and its provisionedConcurrency + if (inputs.provisionedConcurrency) { + if (!prevAlias) { + // Create alias and provisioned concurrency settings + console.log(`Creating alias "${inputs.aliasName}" for version ${inputs.version}`) + await createLambdaAlias(clients.lambda, inputs) + await updateProvisionedConcurrencyConfig(clients.lambda, inputs) + console.log(`Successfully created alias`) + } else { + // Update alias to point to the correct version + console.log(`Updating alias "${inputs.aliasName}" for version ${inputs.version}`) + // Must go before alias, otherwise AWS will block until existing instances are shutdown + await updateProvisionedConcurrencyConfig(clients.lambda, inputs) + await updateLambdaAlias(clients.lambda, inputs) + console.log(`Successfully updated alias`) + } + } else if (prevAlias) { + console.log(`Deleting provisioned concurrency configuration`) + await deleteLambdaAlias(clients.lambda, inputs) + console.log(`Successfully deleted provisioned concurrency configuration`) + } + // Update state this.state.name = inputs.name this.state.arn = inputs.arn @@ -98,6 +129,8 @@ class AwsLambda extends Component { return { name: inputs.name, arn: inputs.arn, + version: inputs.version, + provisionedConcurrency: inputs.provisionedConcurrency, securityGroupIds: inputs.securityGroupIds, subnetIds: inputs.subnetIds } diff --git a/src/utils.js b/src/utils.js index 4f34f67..8e5b79c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -61,7 +61,9 @@ const prepareInputs = (inputs, instance) => { layers: inputs.layers || [], securityGroupIds: inputs.vpcConfig ? inputs.vpcConfig.securityGroupIds : false, subnetIds: inputs.vpcConfig ? inputs.vpcConfig.subnetIds : false, - retry: inputs.retry || 0 + retry: inputs.retry || 0, + provisionedConcurrency: inputs.provisionedConcurrency || 0, + aliasName: (inputs.alias && inputs.alias.name) || 'provisioned' } } @@ -226,7 +228,7 @@ const createLambdaFunction = async (instance, lambda, inputs) => { try { const res = await lambda.createFunction(params).promise() - return { arn: res.FunctionArn, hash: res.CodeSha256 } + return { arn: res.FunctionArn, hash: res.CodeSha256, version: res.Version } } catch (e) { if (e.message.includes(`The role defined for the function cannot be assumed by Lambda`)) { // we need to wait after the role is created before it can be assumed @@ -297,7 +299,101 @@ const updateLambdaFunctionCode = async (lambda, inputs) => { functionCodeParams.ZipFile = await readFile(inputs.src) const res = await lambda.updateFunctionCode(functionCodeParams).promise() - return res.FunctionArn + return { arn: res.FunctionArn, hash: res.CodeSha256, version: res.Version } +} + +/** + * Get Lambda Alias + * @param {*} lambda + * @param {*} inputs + */ +const getLambdaAlias = async (lambda, inputs) => { + try { + const res = await lambda + .getAlias({ + FunctionName: inputs.name, + Name: inputs.aliasName + }) + .promise() + + return { + name: res.Name, + description: res.Description, + arn: res.AliasArn, + resourceId: res.ResourceId, + routingConfig: res.RoutingConfig + } + } catch (e) { + if (e.code === 'ResourceNotFoundException') { + return null + } + throw e + } +} + +/** + * Create a Lambda Alias + * @param {*} lambda + * @param {*} inputs + */ +const createLambdaAlias = async (lambda, inputs) => { + const params = { + FunctionName: inputs.name, + FunctionVersion: inputs.version, + Name: inputs.aliasName + } + + const res = await lambda.createAlias(params).promise() + return { name: res.Name, arn: res.AliasArn } +} + +/** + * Update a Lambda Alias + * @param {*} lambda + * @param {*} inputs + */ +const updateLambdaAlias = async (lambda, inputs) => { + const params = { + FunctionName: inputs.name, + FunctionVersion: inputs.version, + Name: inputs.aliasName + } + + const res = await lambda.updateAlias(params).promise() + return { name: res.Name, arn: res.AliasArn } +} + +/** + * Delete Lambda Alias, provisioned concurrency settings will be deleted together + * @param {*} lambda + * @param {*} inputs + */ +const deleteLambdaAlias = async (lambda, inputs) => { + const params = { + FunctionName: inputs.name, + Name: inputs.aliasName + } + + const res = await lambda.deleteAlias(params).promise() +} + +/** + * Update provisioned concurrency configurations + * @param {*} lambda + * @param {*} inputs + */ +const updateProvisionedConcurrencyConfig = async (lambda, inputs) => { + const params = { + FunctionName: inputs.name, + ProvisionedConcurrentExecutions: inputs.provisionedConcurrency, + Qualifier: inputs.aliasName + } + + const res = await lambda.putProvisionedConcurrencyConfig(params).promise() + return { + allocated: res.AllocatedProvisionedConcurrentExecutions, + requested: res.RequestedProvisionedConcurrentExecutions + } } /** @@ -391,7 +487,8 @@ const inputsChanged = (prevLambda, lambda) => { 'env', 'hash', 'securityGroupIds', - 'subnetIds' + 'subnetIds', + 'provisionedConcurrency' ] const inputs = pick(keys, lambda) const prevInputs = pick(keys, prevLambda) @@ -482,6 +579,11 @@ module.exports = { updateLambdaFunctionCode, updateLambdaFunctionConfig, getLambdaFunction, + getLambdaAlias, + createLambdaAlias, + updateLambdaAlias, + deleteLambdaAlias, + updateProvisionedConcurrencyConfig, inputsChanged, deleteLambdaFunction, removeAllRoles,