如何获取相对 package.json 依赖项以在 Windows 上使用 AWS 的 sam build 命令?

Bra*_*don 7 amazon-web-services node.js npm aws-serverless

我的目标是使用层在我们的几个 lambda 函数之间共享库代码,并能够在本地调试和运行测试。

npm 能够从本地文件系统安装依赖项。当我们更改库代码时,我们希望该库的所有用户都能获取更新的代码,而无需设置专用的 npm 服务器。我可以使用相对路径在本地进行调试,但那是在我涉及sam build. sam build在存储库的根目录创建一个隐藏文件夹并构建该文件夹并最终运行npm install,但是这次文件夹结构不同。文件中使用的相对路径package.json现在已损坏。我们不能使用显式路径,因为我们的存储库位于用户文件夹下,这当然对于不同的开发人员来说是不同的。

这就是我所做的:

我使用创建了一个项目sam init,并采用了项目的默认值(除了 的名称sam-app-2nodejs 12.x(选项11)。

该命令创建了一个名为 的文件夹sam-app-2,它是以下所有文件名的引用。

我创建了一个dependencies/nodejs文件夹。

我添加dep.js到该文件夹​​:

exports.x = 'It works!!';
Run Code Online (Sandbox Code Playgroud)

我还添加package.json到同一文件夹:

{
  "name": "dep",
  "version": "1.0.0",
  "description": "",
  "main": "dep.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
Run Code Online (Sandbox Code Playgroud)

在 hello-world(包含 lambda 函数的文件夹)下,我将以下内容添加到了依赖项中package.json

"dep": "file:../dependencies/nodejs"
Run Code Online (Sandbox Code Playgroud)

我跑到npm installhello-world,它复制了下的依赖项node_modules/dep。通常情况下,你不会在这里这样做。这纯粹是为了让我能够在本地运行而不涉及 sam CLI。这只是纯粹的nodejs代码。我可以运行测试,可以调试,而不必在 sam 打包所有内容并调用我的函数时等待二十秒或更长时间。在这种状态下开发非常棒,因为速度非常快。然而,它最终需要在野外正确运行。

我编辑./hello-world/app.js

const dep = require('dep');
let response;

exports.lambdaHandler = async (event, context) => {
    try {
        // const ret = await axios(url);
        response = {
            'statusCode': 200,
            'dep.x': dep.x,
            'body': JSON.stringify({
                message: 'Hello, World!!',
                // location: ret.data.trim()
            })
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

if(require.main === module){
    (async () => {
        var result = await exports.lambdaHandler(process.argv[1]);
        console.log(result);
    })();
}
Run Code Online (Sandbox Code Playgroud)

我编辑了 template.yml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app-2

  Sample SAM Template for sam-app-2

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Layers:
        - !Ref DepLayer
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

  DepLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
        LayerName: sam-app-dependencies-2
        Description: Dependencies for sam app [temp-units-conv]
        ContentUri: ./dependencies/
        CompatibleRuntimes:
          - nodejs12.x
        LicenseInfo: 'MIT'
        RetentionPolicy: Retain
Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Run Code Online (Sandbox Code Playgroud)

直接从命令行运行它可以工作:

sam-app-2> node hello-world\app.js
{
  statusCode: 200,
  'dep.x': 'It works!!',
  body: '{"message":"Hello, World!!"}'
}
Run Code Online (Sandbox Code Playgroud)

甚至sam deploy有效!是的,它将代码部署到云端,当我在云端调用 lambda 函数时,它给出了与上面相同的结果。

但是,当我运行时sam build,它失败并显示:

Building resource 'HelloWorldFunction'
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrc
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall

Build Failed
Error: NodejsNpmBuilder:NpmInstall - NPM Failed: npm ERR! code ENOLOCAL
npm ERR! Could not install from "..\dependencies\nodejs" as it does not contain a package.json file.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Brandon\AppData\Roaming\npm-cache\_logs\2020-03-04T19_34_01_873Z-debug.log
Run Code Online (Sandbox Code Playgroud)

当我尝试在本地调用 lambda 时:

sam local invoke "HelloWorldFunction" -e events/event.json
Invoking app.lambdaHandler (nodejs12.x)
DepLayer is a local Layer in the template
Building image...
Requested to skip pulling images ...

Mounting C:\Users\Brandon\source\repos\sam-app-2\hello-world as /var/task:ro,delegated inside runtime container
2020-03-03T19:34:28.824Z        undefined       ERROR   Uncaught Exception      {"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dep'\nRequire stack:\n- /var/task/app.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js","stack":["Runtime.ImportModuleError: Error: Cannot find module 'dep'","Require stack:","- /var/task/app.js","- /var/runtime/UserFunction.js","- /var/runtime/index.js","    at _loadUserApp (/var/runtime/UserFunction.js:100:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:955:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:991:10)","    at Module.load (internal/modules/cjs/loader.js:811:32)","    at Function.Module._load (internal/modules/cjs/loader.js:723:14)","    at Function.Module.runMain (internal/modules/cjs/loader.js:1043:10)","    at internal/main/run_main_module.js:17:11"]}
?[32mSTART RequestId: b6f39717-746d-1597-9838-3b6472ec8843 Version: $LATEST?[0m
?[32mEND RequestId: b6f39717-746d-1597-9838-3b6472ec8843?[0m
?[32mREPORT RequestId: b6f39717-746d-1597-9838-3b6472ec8843     Init Duration: 237.77 ms        Duration: 3.67 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 38 MB  ?[0m

{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dep'\nRequire stack:\n- /var/task/app.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js"}
Run Code Online (Sandbox Code Playgroud)

当我尝试使用 本地启动 API 时sam local start-api,它失败并出现与上面相同的错误。

我在想,如果不是在构建阶段相对文件路径关闭,我就能够鱼与熊掌兼得(本地调试非常快)并吃掉它(运行sam buildsam local start-api)。

我应该怎么办?

Bra*_*don 8

经过多次挫折和焦虑后,这已经产生了: https ://github.com/blmille1/aws-sam-layers-template

享受!

  • 8 小时后我发现了这个……谢谢你,明天将会是美好的一天。谢谢。很烦人.... (2认同)

dan*_*gpg 6

我也遇到了同样的问题,并最终提出了替代解决方案。它提供了您正在寻找的开发人员体验,但避免了解决方案中的轻微不便和维护开销(也称为对访问共享层的每个导入使用三元运算符)。我提出的解决方案使用与您类似的方法,但只需要每个 lambda 函数进行一次初始化调用。在底层,它使用模块别名来解析运行时的依赖关系。

以下是包含示例模板的存储库的链接:https://github.com/dangpg/aws-sam-shared-layers-template

( tl;dr ) 使用链接模板时,您会得到:

  • 使用层在多个 lambda 函数之间共享公共代码或依赖关系
  • 不使用任何模块捆绑器(例如,Webpack)
  • 相反,在运行时使用模块别名来解决依赖关系
  • 支持通过AWS Toolkit在 VSCode 中进行本地调试
  • 代码可以在 AWS 沙箱之外执行 ( node app.js)
  • 支持使用 Jest 进行单元测试
  • VSCode 中的智能感知/自动完成
  • 与 SAM CLI 兼容(sam buildsam deploysam local invokesam local start-api等)。
  • 可以作为通用 lambda 在云中部署和运行

1. 文件夹结构

+ lambdas
|  + func1
|    - app.js
|    - package.json
|  + func2
|    - app.js
|    - package.json
+ layers
|  + common            // can be any name, I chose common
|    + nodejs          // needs to be nodejs due to how SAM handles layers
|      - index.js      // file for own custom code that you want to share
|      - package.json  // list any dependencies you want to share
- template.yaml
Run Code Online (Sandbox Code Playgroud)

这是我最终得到的文件夹结构。但请记住,它非常灵活,不需要硬规则来满足可能的相对文件路径(但是,如果您的结构不同,您将需要调整一些文件)。

如果你想在 lambda 函数之间共享 npm 包,只需将它们添加到package.json层文件夹内:

+ lambdas
|  + func1
|    - app.js
|    - package.json
|  + func2
|    - app.js
|    - package.json
+ layers
|  + common            // can be any name, I chose common
|    + nodejs          // needs to be nodejs due to how SAM handles layers
|      - index.js      // file for own custom code that you want to share
|      - package.json  // list any dependencies you want to share
- template.yaml
Run Code Online (Sandbox Code Playgroud)

为了完整起见,以下是以下内容index.js

{
  "name": "common",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "lorem-ipsum": "^2.0.4"
  }
}
Run Code Online (Sandbox Code Playgroud)

2.模板.yaml

exports.ping = () => {
  return "pong";
};
Run Code Online (Sandbox Code Playgroud)

非常简单,只需按照官方示例了解如何在 lambda 中包含层即可。就像在您的解决方案中一样,添加一个 gloval 环境变量,以便我们可以区分是否在 AWS 沙箱中运行代码。

3. Lambda package.json

module-alias将asdependency和您的本地公共文件夹添加devDependency到每个 lambda 函数:

[...]
Globals:
  Function:
    Timeout: 3
    Runtime: nodejs14.x
    Environment:
      Variables:
        AWS: true

Resources:
  Func1:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: lambdas/func1/
      Handler: app.lambdaHandler
      Layers:
        - !Ref CommonLayer
  [...]

  Func2:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: lambdas/func2/
      Handler: app.lambdaHandler
      Layers:
        - !Ref CommonLayer
  [...]

  CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      ContentUri: ./layers/common/
      CompatibleRuntimes:
        - nodejs14.x
      RetentionPolicy: Retain
Run Code Online (Sandbox Code Playgroud)

稍后我们将需要对公共文件夹的本地引用(例如用于测试)。我们将其添加为 devDependency,因为我们只需要它进行本地开发,因此在运行时不会遇到问题sam build(因为它忽略 devDependency)。我选择了common__internal包名称,但是您可以自由选择您喜欢的名称。确保npm install在进行任何本地开发之前运行。

4. Lambda 处理程序

在处理程序源代码中,在从共享层导入任何包之前,请初始化module-alias以执行以下操作:

...
  "dependencies": {
    "module-alias": "^2.2.2"
  },
  "devDependencies": {
    "common__internal": "file:../../layers/common/nodejs"  // adapt relative path according to your folder structure
  },
...
Run Code Online (Sandbox Code Playgroud)

您可以将module-alias代码部分移动到一个单独的文件中,然后只需在一开始就导入该文件(或者甚至发布您自己的自定义包,然后您可以正确导入它;这样您就可以在每个 lambda 函数中引用它,而不必必须重复代码)。再次根据您的文件夹结构调整相对文件路径。与您的方法类似,它会检查AWS环境变量并相应地调整导入路径。然而,这只需要进行一次。之后,所有连续导入都可以使用您定义的别名:const { ping } = require("@common");const { loremIpsum } = require("@common/lorem-ipsum");。同样在这里,您可以随意定义您自己的关于如何处理别名的自定义逻辑。这只是我想出的对我有用的解决方案。

从此时起,您应该能够通过本地node app.js或通过 SAM CLI(sam buildsam local invoke等)执行 lambda 代码。但是,如果您想要本地测试和智能感知,则还需要一些额外的工作。

5. 智能感知

对于 VSCode,您只需添加一个jsconfig.json文件,其中包含您定义的别名的相应路径映射。将其指向之前的内部开发依赖项:

const moduleAlias = require("module-alias");

moduleAlias.addAlias("@common", (fromPath, request) => {
  if (process.env.AWS && request.startsWith("@common/")) {
    // AWS sandbox and reference to dependency
    return "/opt/nodejs/node_modules";
  }

  if (process.env.AWS) {
    // AWS sandbox and reference to custom shared code
    return "/opt/nodejs";
  }

  if (request.startsWith("@common/")) {
    // Local development and reference to dependency
    return "../../layers/common/nodejs/node_modules";
  }

  // Local development and reference to custom shared code
  return "../../layers/common/nodejs";
});

const { ping } = require("@common");                     // your custom shared code
const { loremIpsum } = require("@common/lorem-ipsum");   // shared dependency

exports.lambdaHandler = async (event, context) => {
  try {
    const response = {
      statusCode: 200,
      body: JSON.stringify({
        message: "hello world",
        ping: ping(),
        lorem: loremIpsum(),
      }),
    };

    return response;
  } catch (err) {
    console.log(err);
    return err;
  }
};

if (require.main === module) {
  (async () => {
    var result = await exports.lambdaHandler(process.argv[1]);
    console.log(result);
  })();
}
Run Code Online (Sandbox Code Playgroud)

6. 测试

为了进行测试,我个人使用 Jest。幸运的是,Jest也提供了提供路径映射的选项:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@common": ["./node_modules/common__internal/index.js"],
      "@common/*": ["./node_modules/common__internal/node_modules/*"]
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

最终免责声明

该解决方案目前仅在使用 CommonJS 模块系统时有效。使用 ES 模块时我无法重现相同的结果(主要是由于缺乏的支持module-alias)。然而,如果有人能想出一个使用 ES 模块的解决方案,我很高兴听到!

就是这样!希望我没有遗漏任何东西。总的来说,我对该解决方案提供的开发人员体验非常满意。请随意查看链接的模板存储库以获取更多详细信息。我知道距离您最初的问题已经过去一段时间了,但我将其留在这里,希望它也能帮助其他开发人员。干杯