UNPKG

@sls-next/aws-lambda

Version:

Deploy Lambda functions to AWS in seconds with [Serverless Components](https://github.com/serverless/components). Utilizes layers for dependency management and S3 accelerated uploads for maximum upload speeds.

320 lines (280 loc) 7.23 kB
import { tmpdir } from "os"; import * as path from "path"; import * as archiver from "archiver"; import * as globby from "globby"; import { contains, isNil, last, split, equals, not, pick } from "ramda"; import { readFile, createReadStream, createWriteStream } from "fs-extra"; import { utils } from "@serverless/core"; import * as _ from "lodash"; const VALID_FORMATS = ["zip", "tar"]; const isValidFormat = (format) => contains(format, VALID_FORMATS); const packDir = async ( inputDirPath: string, outputFilePath: string, include: string[] = [], exclude: string[] = [], prefix?: string ) => { const format = last(split(".", outputFilePath)); if (!isValidFormat(format)) { throw new Error('Please provide a valid format. Either a "zip" or a "tar"'); } const patterns = ["**/*"]; if (!isNil(exclude)) { exclude.forEach((excludedItem) => patterns.push(`!${excludedItem}`)); } const files = ( await globby.default(patterns, { cwd: inputDirPath, dot: true }) ) .sort() // we must sort to ensure correct hash .map((file) => ({ input: path.join(inputDirPath, file), output: prefix ? path.join(prefix, file) : file })); return new Promise((resolve, reject) => { const output = createWriteStream(outputFilePath); const archive = archiver.create(format, { zlib: { level: 9 } }); output.on("open", () => { archive.pipe(output); // we must set the date to ensure correct hash files.forEach((file) => archive.append(createReadStream(file.input), { name: file.output, date: new Date(0) }) ); if (!isNil(include)) { include.forEach((file) => { const stream = createReadStream(file); archive.append(stream, { name: path.basename(file), date: new Date(0) }); }); } archive.finalize(); }); archive.on("error", (err) => reject(err)); output.on("close", () => resolve(outputFilePath)); }); }; const getAccountId = async (aws) => { const STS = new aws.STS(); const res = await STS.getCallerIdentity({}).promise(); return res.Account; }; const createLambda = async ({ lambda, name, handler, memory, timeout, runtime, env, description, zipPath, bucket, role, layer, tags }) => { const params: any = { FunctionName: name, Code: {}, Description: description, Handler: handler, MemorySize: memory, Publish: true, Role: role.arn, Runtime: runtime, Timeout: timeout, Environment: { Variables: env }, Tags: tags }; if (layer && layer.arn) { params.Layers = [layer.arn]; } if (bucket) { params.Code.S3Bucket = bucket; params.Code.S3Key = path.basename(zipPath); } else { params.Code.ZipFile = await readFile(zipPath); } const res = await lambda.createFunction(params).promise(); return { arn: res.FunctionArn, hash: res.CodeSha256 }; }; const updateLambdaConfig = async ({ lambda, name, handler, memory, timeout, runtime, env, description, role, layer, tags }) => { const functionConfigParams: any = { FunctionName: name, Description: description, Handler: handler, MemorySize: memory, Role: role.arn, Runtime: runtime, Timeout: timeout, Environment: { Variables: env } }; if (layer && layer.arn) { functionConfigParams.Layers = [layer.arn]; } const res = await lambda .updateFunctionConfiguration(functionConfigParams) .promise(); // Get and update Lambda tags only if tags are specified (for backwards compatibility and avoiding unneeded updates) if (tags) { const listTagsResponse = await lambda .listTags({ Resource: res.FunctionArn }) .promise(); const currentTags = listTagsResponse.Tags; // If tags are not the same then update them if (!_.isEqual(currentTags, tags)) { if (currentTags && Object.keys(currentTags).length > 0) await lambda .untagResource({ Resource: res.FunctionArn, TagKeys: Object.keys(currentTags) }) .promise(); if (Object.keys(tags).length > 0) await lambda .tagResource({ Resource: res.FunctionArn, Tags: tags }) .promise(); } } return { arn: res.FunctionArn, hash: res.CodeSha256 }; }; const updateLambdaCode = async ({ lambda, name, zipPath, bucket }) => { const functionCodeParams: any = { FunctionName: name, Publish: true }; if (bucket) { functionCodeParams.S3Bucket = bucket; functionCodeParams.S3Key = path.basename(zipPath); } else { functionCodeParams.ZipFile = await readFile(zipPath); } const res = await lambda.updateFunctionCode(functionCodeParams).promise(); return res.FunctionArn; }; const getLambda = async ({ lambda, name }) => { try { const res = await lambda .getFunctionConfiguration({ FunctionName: name }) .promise(); return { name: res.FunctionName, description: res.Description, timeout: res.Timeout, runtime: res.Runtime, role: { arn: res.Role }, handler: res.Handler, memory: res.MemorySize, hash: res.CodeSha256, env: res.Environment ? res.Environment.Variables : {}, arn: res.FunctionArn }; } catch (e) { if (e.code === "ResourceNotFoundException") { return null; } throw e; } }; const deleteLambda = async ({ lambda, name }) => { try { const params = { FunctionName: name }; await lambda.deleteFunction(params).promise(); } catch (error) { if (error.code !== "ResourceNotFoundException") { throw error; } } }; const getPolicy = ({ name, region, accountId }) => { return { Version: "2012-10-17", Statement: [ { Action: ["logs:CreateLogStream"], Resource: [ `arn:aws:logs:${region}:${accountId}:log-group:/aws/lambda/${name}:*` ], Effect: "Allow" }, { Action: ["logs:PutLogEvents"], Resource: [ `arn:aws:logs:${region}:${accountId}:log-group:/aws/lambda/${name}:*:*` ], Effect: "Allow" } ] }; }; const configChanged = (prevLambda, lambda) => { const keys = [ "description", "runtime", "role", "handler", "memory", "timeout", "env", "hash" ]; const inputs = pick(keys, lambda); inputs.role = { arn: inputs.role.arn }; // remove other inputs.role component outputs const prevInputs = pick(keys, prevLambda); return not(equals(inputs, prevInputs)); }; const pack = (code, shims = [], packDeps = true) => { if (utils.isArchivePath(code)) { return path.resolve(code); } let exclude = []; if (!packDeps) { exclude = ["node_modules/**"]; } const outputFilePath = path.join( tmpdir(), `${Math.random().toString(36).substring(6)}.zip` ); return packDir(code, outputFilePath, shims, exclude); }; export { createLambda, updateLambdaCode, updateLambdaConfig, getLambda, deleteLambda, getPolicy, getAccountId, configChanged, pack };