如何使用lerna将monorepo代码部署到AWS Lambda?

时间:2018-12-10 19:14:14

标签: amazon-web-services aws-lambda lerna

我正在尝试制作两个AWS Lambda函数(以打字稿编写)。这两个函数共享用于与API交互的相同代码。为了不必将相同的代码复制到两个不同的Lambda中,我想将共享的代码移至本地模块,并使两个Lambda都依赖于所述模块。

我最初尝试在两个lambda之间注视代码是使用monorepo和lerna。我当前的项目结构如下:

- lerna.json
- package.json
- packages
  - api
    - package.json
  - lambdas
    - funcA
      - package.json
    - func B
      - package.json

lerna.json:

{
  "packages": [
    "packages/api",
    "packages/lambdas/*"
  ],
  "version": "1.0.0"
}

在每个用于Lambda函数的package.json中,我都可以这样包含本地api模块:

"dependencies": {
    "@local/api": "*"
}

有了这个,我已经能够将通用代码移到其自己的模块中。但是,我现在不确定如何捆绑功能以部署到AWS Lambda。 lerna是否有办法创建可以部署的捆绑软件?

4 个答案:

答案 0 :(得分:1)

我已经为这个问题苦苦挣扎了一段时间,最后我不得不为此做点什么。

我使用了一个名为 slice-node-modules 的小包,如发现的 here in this similar question,它在一段时间内足以满足我的目的。随着我将更多项目整合到 monorepos 中,并开始使用作为兄弟姐妹而不是外部发布的共享依赖项,我遇到了这种方法的缺点。

我创建了一个名为 lerna-to-lambda 的新工具,专门针对我的用例量身定制。我用最少的文档公开发布了它,希望足以帮助处于类似情况的其他人。其要点是,在安装所有依赖项后,在捆绑步骤中运行 l2l,并将所需内容复制到输出目录中,然后准备好使用 SAM 或其他方式部署到 Lambda .

例如,在自述文件中,您的 Lambda 函数的 package.json 中可能有这样的内容:

"scripts": {
  ...
  "clean": "rimraf build lambda",
  "compile": "tsc -p tsconfig.build.json",
  "package": "l2l -i build -o lambda",
  "build": "yarn run clean && yarn run compile && yarn run package"
},

在本例中,compile 步骤是将源目录中的 TypeScript 文件编译为 build 目录中的 JavaScript 文件。然后 package 步骤将 build 中的所有代码以及 Lambda 的所有依赖项(aws-sdk 除外)打包到目录 lambda 中,这就是您要部署的到 AWS。如果有人使用纯 JavaScript 而不是 TypeScript,他们可以在打包前将必要的 .js 文件复制到 build 目录中。

我意识到这个问题已经存在 2 年多了,从那时起您可能已经找到了自己的解决方案和/或变通方法。但因为它仍然与我相关,我认为它仍然与外面的人相关,所以我正在分享。

答案 1 :(得分:0)

运行lerna bootstrap将在每个“包”中创建一个node_modules文件夹。这将包括您所有的lerna管理的依赖项以及该特定程序包的外部依赖项。

从那时起,每个lambda的部署将与您使用lerna无关。部署程序包将需要包含该特定lambda的代码和该lambda的node_modules文件夹-您可以将其压缩并手动上传,或使用SAM或CloudFormation之类的方法。

编辑:正如您正确指出的那样,您最终会在node_modules文件夹中使用符号链接,这使打包内容变得很尴尬。要解决此问题,您可以在打包进行部署之前运行以下操作:

cp -rL lambdas/funcA/node_modules lambdas/funcA/packaged/node_modules

-L将强制将符号链接的目录复制到该文件夹​​中,然后可以对其进行压缩。

答案 2 :(得分:0)

由于cp -rL在Mac上不起作用,因此我不得不提出类似的建议。

如果您的所有软件包都属于一个范围(@org),这是一个工作流程:

在您的lerna存储库的package.json中:

"scripts": {
    "deploy": "lerna exec \"rm -rf node_modules\" && lerna bootstrap -- --production && lerna run deploy && lerna bootstrap"
}

在包含lambda函数的包中:

"scripts":{
    "deploy": "npm pack && tar zxvf packagename-version.tgz && rm -rf node_modules/@org && cp -r node_modules/* package/node_modules && cd package && npm dedupe"
}

现在将“ packagename-version”和“ @org”替换为项目的相应值。还将所有依赖包添加到“ bundledDependencies”。

在lerna mono存储库的根目录中运行npm run deploy后,您最终会在包含lambda函数的包中找到一个文件夹“ package”。它具有运行功能所需的所有依赖关系。从那里拿走。

我曾希望使用npm pack可以使我利用.npmignore文件,但看来这是行不通的。如果有人知道如何使它工作,请告诉我。

答案 3 :(得分:0)

我在安装过程中使用了自定义脚本来复制依赖项。这将允许我使用相同的代码开发和部署应用程序。

Project structure 在 lambda_a 的 package.json 文件中,我有以下行:

  "scripts": {
    "install": "node ./install_libs.js @libs/library_a"
  },

@libs/library_a 可由 lambda 代码使用以下语句使用:

const library_a = require('@libs/library_a')

对于 SAM 构建,我使用来自 lmbdas folder 的以下命令:

export SAM_BUILD=true && sam build

install_libs.js

console.log("Starting module installation")
var fs = require('fs');
var path = require('path');
var {exec} = require("child_process");

if (!fs.existsSync("node_modules")) {
    fs.mkdirSync("node_modules");
}
if (!fs.existsSync("node_modules/@libs")) {
    fs.mkdirSync("node_modules/@libs");
}

const sam_build = process.env.SAM_BUILD || false
libs_path = "../../"
if (sam_build) {
    libs_path = "../../" + libs_path
}


process.argv.forEach(async function (val, index, array) {
    if (index > 1) {
        var currentLib = libs_path + val
        console.log(`Building lib ${currentLib}`)
        await exec(`cd ${currentLib} && npm install` , function (error, stdout, stderr){
            if (error) {
                console.log(`error: ${error.message}`);
                return;
            }
            console.log(`stdout: ${stdout}`);
            console.log('Importing module : ' + currentLib);
            copyFolderRecursiveSync(currentLib, "node_modules/@libs")
        });
    }
});


function copyFolderRecursiveSync(source, target) {
    var files = [];
    // Check if folder needs to be created or integrated
    var targetFolder = path.join(target, path.basename(source));
    if (!fs.existsSync(targetFolder)) {
        fs.mkdirSync(targetFolder);
    }

    // Copy
    if (fs.lstatSync(source).isDirectory()) {
        files = fs.readdirSync(source);
        files.forEach(function (file) {
            var curSource = path.join(source, file);
            if (fs.lstatSync(curSource).isDirectory()) {
                copyFolderRecursiveSync(curSource, targetFolder);
            } else {
                copyFileSync(curSource, targetFolder);
            }
        });
    }
}

function copyFileSync(source, target) {
    var targetFile = target;
    // If target is a directory, a new file with the same name will be created
    if (fs.existsSync(target)) {
        if (fs.lstatSync(target).isDirectory()) {
            targetFile = path.join(target, path.basename(source));
        }
    }
    fs.writeFileSync(targetFile, fs.readFileSync(source));
}