Amazon Web ServicesCloud

Lambda Layer Tips

1.利用 esbuild 编译 typeScript 到javascript(有依赖)


我们可以使用typeScript来开发Lambda函数。但是lambda本身运行不了typeScript,需要我们通过esbulid,先build成Javascript,我们下面的例子会把依赖的代码,都打入到javascript中。

下面是typeScript的例子:

lambda.ts

package.json

{
  "name": "aws-lambda-layer",
  "version": "1.0.0",
  "description": "aws lambda layer demo",
  "main": "index.js",
  "scripts": {
    "build": "esbuild lambda.ts --bundle --minify --sourcemap --platform=node --target=es2020 --format=esm --outfile=dist/index.mjs",
    "postbuild": "cd dist && zip -r index.zip index.mjs*"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/aws-lambda": "^8.10.109",
    "esbuild": "^0.15.16",
    "lambda-local": "^2.0.3",
    "aws-sdk": "^2.1279.0",
    "class-transformer": "^0.5.1",
    "mongodb": "^4.12.1",
    "axios": "^1.2.0"
  }

}

注意 “scripts” 里面的参数

–bundle:会把 dependencies相关的依赖包都打进build好的javascript里面。

在 esbuild 中,bundle 是一个构建选项,用于指示 esbuild 打包所有代码并输出一个捆绑后的文件。具体来说,它将编译器的工作和模块加载器的工作结合起来,使得在浏览器或 Node.js 中直接加载生成的文件时,可以无需再去加载其他模块或脚本文件,这个打包后的 bundle.js 文件包含了所有相关的代码。

–sourcemap:会生成map映射文件

在终端执行:npm run build

可以看到在dist目录下,生成了mjs 和 map 以及zip包。可以看到mjs size有9.2MB,这是因为把依赖包都打进去了。

另外 –minify 这个参数会压缩代码,把代码变成一行。

去掉 –minify 参数以后的build出的 index.mjs

有 –minify 参数 build出的 index.mjs

重要

加了 –bundle参数以后,lambda执行总会出这个错误。

lambda执行结果
build出的index.mjs文件

加上 –format=esm 能解决这个问题,不知道原因

esbuild src/service/test.ts --bundle --external:axios --external:aws-sdk --external:mongodb --external:class-transformer --external:@aws-sdk/client-secrets-manager --external:zip --platform=node --target=es2020 --format=esm --outfile=dist/index.mjs",

一个完整的例子

esbuild src/service/test.ts --bundle --external:axios --external:aws-sdk --external:mongodb --external:class-transformer --external:@aws-sdk/client-secrets-manager --external:zip --platform=node --target=es2020 --format=esm --outfile=dist/index.mjs

2.利用 esbuild 编译 typeScript 到javascript(无依赖)


方法一:去掉–bundle

上面那种情况,可以看到把依赖都打到javascript中了,所以size很大。我们有时候只想编译文件,不想把依赖打进去。然后我们的依赖是通过layer层做出来。我们的javascript 直接关联lambda layer层,引用lambda layer层的依赖。

可以修改下package.json的build的命令

修改后的package.json

build 里面 去掉了 –bundle 和 –sourcemap

{
  "name": "aws-lambda-layer",
  "version": "1.0.0",
  "description": "aws lambda layer demo",
  "main": "index.js",
  "scripts": {
    "build": "esbuild lambda.ts --minify --platform=node --target=es2020  --format=esm --outfile=dist/index.mjs",
    "postbuild": "cd dist && zip -r index.zip index.mjs*"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/aws-lambda": "^8.10.109",
    "esbuild": "^0.15.16",
    "lambda-local": "^2.0.3",
    "aws-sdk": "^2.1279.0",
    "class-transformer": "^0.5.1",
    "mongodb": "^4.12.1",
    "axios": "^1.2.0"
  }

}

再次npm run build,可以看到index.mjs 变成313kb了。

但是方法一有个问题,假如要build的ts文件,里面引用了别的moudle,CommonUtil,如果去掉了 –bundle,则CommonUtil也不会打进编译好的index.mjs代码里面了。这个时候我们可以用下面的方法二,通过external参数具体指定除外的模块,可以精确的除外某个moudle,保留不想除外的moudle。

方法二:

在使用 esbuild 构建 Node.js 应用时,可以通过设置 external 选项来告诉 esbuild 哪些模块是外部依赖,不应该被打包进最终的输出文件中。这样可以减小输出文件的大小,并且可以避免一些不必要的重复代码。

你可以将以下的 build 命令添加到你的 package.json 文件中的 scripts 字段中:

{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "build": "esbuild index.js --bundle --external:fs --external:path --external:moduleA --platform=node --format=esm --outfile=dist/bundle.js"
  },
  "dependencies": {
    // ...
  }
}

在上面的 build 命令中,我们使用了 --external 选项来将 fspathmoduleA 模块设置为外部依赖。如果你的应用程序使用了其他模块,可以根据需要添加到 --external 选项中。

3.制作lambda layer


先在本地做好layer层用到的nodejs modules。

npm install mongodb
npm install aws-sdk
npm install axios

把node_modules, package.json,package-lock.json一起打成压缩包,并且命名成nodejs.zip

通过lambda控制台创建layer,并且上传nodejs.zip

4.lambda函数引用该layer层


5.文件制作layer层


有时候,我们希望把一些证明文件,普通文件等等,做到layer层里面,以供lamda函数引用,可以这样做。

比如下面的例子,我们希望连接把mongodb用到的pem文件打到laryer层里面。

pem
  |---lib
       |---rds-combined-ca-bundle.pem
      

然后打成pem.zip压缩包,在layer做成的控制台正常上传zip文件,做成就可以了

在lambda函数里面,正常引用这个layer层。

这样这个文件会放在lamda函数,下面的路径下面。

/opt/lib/rds-combined-ca-bundle.pem

lamda函数的例子:

import { MongoClient } from 'mongodb';
import { Axios } from 'axios';
import AWS from 'aws-sdk';
import * as fs from 'fs';

import {
    SecretsManagerClient,
    GetSecretValueCommand,
  } from "@aws-sdk/client-secrets-manager";

const secret_name = "secrets-docdb-01";

// This will persist DB connectionn if Lambda container still "warm" on invocation, see: https://docs.aws.amazon.com/lambda/latest
const cachedDb: Map<string, MongoClient> = new Map();

const client = new SecretsManagerClient({
  region: "ap-northeast-1",
});

export const handler = async (event) => {

  // 这块通过fs打印一下/opt/lib目录下面的文件
  let filenames = fs.readdirSync("/opt/lib");
  console.log("\nFilenames in directory:");
  filenames.forEach((file) => {
    console.log("File:", file);
  });
    
    let response;
    // 从AWS Secret里面取得documentdb的用户名和密码
    try {
        response = await client.send(
          new GetSecretValueCommand({
            SecretId: secret_name,
            VersionStage: "AWSCURRENT", // VersionStage defaults to AWSCURRENT if unspecified
          })
        );
      } catch (error) {
        throw error;
      }
      
    const secret = response.SecretString;
    const parsedResult = JSON.parse(secret);
    console.log(parsedResult.username);

    const uri= 'mongodb://'+ parsedResult.username + ':' + parsedResult.password+ '@xxxxx.ap-northeast-1.docdb.amazonaws.com:27017';

    // DB接続取得
    let conn = await connectToDatabase(uri);
    // Config Mongo Client'
    console.log("conn");
    const db = conn.db('accomDocDb02'); // which DB
    console.log(db);

    const collection = db.collection('lambdatest'); // Which "collection"/table     
    // Write to DB
    const dbWrite = await collection.insertOne({ 'lambdatestMsg': 'test' });

    console.log(dbWrite);

    console.log('******lambda tets start******');
    console.log(event);
    console.log('Axios:' + Axios.toString);
    console.log('MongoClient:' + MongoClient.name);
    console.log('S3:' + AWS.S3.toString);
    console.log('******lambda test end******');
};

async function connectToDatabase(uri) {

    console.log("=> connect to database");
    if (!cachedDb.has(uri)) {
        console.log("=> creating connection...");
        // 注意这里面pem的证书,是从layer层路径来的
        const connection = await MongoClient.connect(uri, { ssl: true, sslCA: '/opt/lib/rds-combined-ca-bundle.pem', retrywrites: false }).then(c => c);
        cachedDb.set(uri, connection);
        console.log("=> connection created...");
    }
    return cachedDb.get(uri) ?? null;
}