使用 AWS Lambda 扩展为您的无服务器应用程序提供支持 Lambda 扩展示例 Lambda 扩展服务安装 Lambda 扩展示例

2025-06-07

使用 AWS Lambda 扩展为您的无服务器应用程序提供支持

Lambda 扩展示例

Lambda 扩展服务

安装

Lambda 扩展示例

AWS Lambda 服务包含许多令人印象深刻的功能,这些功能对您来说可能还很陌生。其中之一就是能够与处理程序代码并行运行其他代码。此功能称为扩展

本文将教您如何使用扩展程序创建自己的 Lambda 监控工具,且不会影响其性能。该实现使用通过 AWS CDK 部署的 NodeJS Lambda。

⚡ Lambda 扩展功能

我们先来快速回顾一下 Lambda 扩展。如果您已经熟悉它,可以直接跳到创建部分。

🤔 什么是 Lambda 扩展?

Lambda 扩展是一段在与 lambda 处理程序相同的执行环境中运行的代码。

它可以是:

  • internal:它与处理程序代码在同一个进程中运行。
  • external:它在单独的进程中运行。

Lambda 扩展架构

在两种情况下,它都与处理程序共享执行环境的内存、超时和临时存储。

它可以与 Lambda 服务交互,以便在处理程序收到每个事件时收到通知,并且可以从 Lambda 服务接收遥测事件。

内部扩展用于直接向处理程序代码添加功能,因为它在同一个进程中运行。

例如,内部监控扩展可以覆盖 http 库来拦截和记录所有 http 调用。

因为它在单独的进程中运行,所以外部扩展可以为处理程序添加功能而不会影响其性能。

例如,外部监控扩展可以在处理程序返回其响应后将指标发送到外部监控服务,以避免降低 API 性能。

🧩 如何使用 Lambda 扩展

Lambda 扩展以Lambda 层的形式出现。

要添加外部扩展,你只需将层添加到你的 lambda 表达式中即可。扩展进程将自动启动。

要添加内部扩展,您需要将层添加到 Lambda 表达式中,但您还必须配置运行时,以便与处理程序一起执行扩展的入口点。此配置可以使用运行时特定的环境变量或包装器脚本完成。

例如,对于 NodeJs,您可以将NODE_OPTIONS环境变量设置为--require /opt/my-extension.js或将AWS_LAMBDA_EXEC_WRAPPER环境变量设置为包装器脚本的路径。

⚙️ 如何创建扩展

让我们创建一个监控扩展,记录 lambda 发出的所有 http 调用,并将它们发送到外部服务进行存储和可视化。

我们将使用内部扩展来拦截所有 http 调用并将它们转发到外部扩展,该扩展在每次 lambda 调用后发送监控回顾。

监控扩展架构

以下所有代码均可在 github 上找到:

GitHub 徽标 CorentinDoue / lambda扩展示例

使用 NodeJs lambda 创建和使用内部和外部 Lambda 扩展的最小存储库

Lambda 扩展示例

使用 AWS Lambda 扩展为您的无服务器应用程序提供支持

此示例显示如何使用监控工具创建一个简单的 Lambda,该工具由以下内容组成:

  • 记录 Lambda 发出的所有 http 调用的内部扩展
  • 一个外部扩展,用于聚合这些日志并将其发送到假设的监控工具

监控扩展架构

安装

https://webhook.site/*将URL更改为src/urls.ts您自己的 webhook URL。

 pnpm install
 pnpm cdk bootstrap
 pnpm run deploy
Enter fullscreen mode Exit fullscreen mode

测试

pnpm integration-test
Enter fullscreen mode Exit fullscreen mode



🔍 拦截器内部扩展

拦截器必须与处理程序在同一个进程中执行,才能拦截所有 http 调用。

📝 拦截器代码

为了简单起见,我们使用msw作为开箱即用的节点拦截器。

// src/layers/monitorExtension/partial-interceptor.ts

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.all('*', async req => {
    const url = req.url.toString();
    const method = req.method;
    console.log(`request intercepted in interceptor: ${method} ${url}`);

    return req.passthrough();
  }),
);

server.listen({ onUnhandledRequest: 'bypass' });
Enter fullscreen mode Exit fullscreen mode

然后我们通过本地 http 调用将日志转发到外部扩展。

💡我们必须使用沙盒URL,因为它是 Lambda 执行环境中唯一授权的 URL。端口是空闲的,所以我随机选择了一个。

// src/layers/monitorExtension/interceptor.ts

import { rest } from 'msw';
import { setupServer } from 'msw/node';
import fetch from 'node-fetch';

console.log('Executing interceptor extension code...');

const LOG_EXTENSION_SERVER_URL = 'http://sandbox:4243';
const server = setupServer(
  rest.all('*', async (req, res, ctx) => {
    const url = req.url.toString();

    // Bypass the calls made by this code to the local server to avoid infinite loop
    if (url.includes(LOG_EXTENSION_SERVER_URL)) {
      return req.passthrough();
    }

    const method = req.method;
    const headers = req.headers;
    const body = await req.text();

    console.log(`request intercepted in interceptor: ${method} ${url}`);
    fetch(LOG_EXTENSION_SERVER_URL, {
      method: 'POST',
      body: JSON.stringify({
        url,
        method,
        headers,
        body,
        date: new Date().toISOString(),
      }),
    }).catch(error => console.error('error sending logs in interceptor extension', error));

    return req.passthrough();
  }),
);

server.listen({ onUnhandledRequest: 'bypass' });
Enter fullscreen mode Exit fullscreen mode

🚀 部署内部扩展

首先,我们需要使扩展代码可由 lambda 执行。最简单的方法是将esbuild其捆绑到单个 cjs 文件中。

💡 这基本上就是 CDK 使用 NodejsFunction 构造时对 wood 下处理程序代码所做的工作。但在这里,我们需要手动完成。

{
  "scripts": {
    "build:interceptor": "./node_modules/.bin/esbuild  ./src/layers/monitorExtension/interceptor.ts --bundle --outfile='./dist/layers/monitorExtension/interceptor.js' --platform=node --main-fields=module,main"
  }
}
Enter fullscreen mode Exit fullscreen mode

然后我们需要通过 lambda 层将代码发送到 lambda。

使用 CDK,我们可以使用该LambdaLayerVersion构造创建一个层,将目录的所有内容运送到/optlambda 的文件夹中。

// lib/partial-stack.ts

export class AppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const layer = new LayerVersion(scope, 'MonitorLayer', {
      code: Code.fromAsset('dist/layers/monitorExtension'),
    });

    const helloFunction = new NodejsFunction(this, 'Hello', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'handler',
      entry: path.join(__dirname, `/../src/functions/hello/handler.ts`),
      layers: [layer],
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,我们需要配置 lambda 来与处理程序代码一起执行拦截器代码。

为此,我们将使用特定于运行时的环境变量。对于 NodeJs,我们需要将NODE_OPTIONS环境变量设置为--require /opt/interceptor.js

// lib/stack.ts

export class AppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const layer = new LayerVersion(scope, 'MonitorLayer', {
      code: Code.fromAsset('dist/layers/monitorExtension'),
    });

    const helloFunction = new NodejsFunction(this, 'Hello', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'handler',
      entry: path.join(__dirname, `/../src/functions/hello/handler.ts`),
      layers: [layer],
      environment: {
        NODE_OPTIONS: '--require /opt/interceptor.js',
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

就这样!我们已经创建了内部扩展。

👀 结果

让我们将它部署在一个简单的 lambda 上,进行 http 调用:

// src/functions/hello/handler.ts

import fetch from 'node-fetch';

export const hello = async () => {
  await fetch('https://webhook.site/87c3df17-c965-40d9-a616-790c4002a162');

  await fetch('https://webhook.site/87c3df17-c965-40d9-a616-790c4002a162', {
    method: 'POST',
    body: JSON.stringify({
      message: 'hello world',
    }),
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: 'OK',
    }),
  };
};

export const handler = hello;
Enter fullscreen mode Exit fullscreen mode

我们可以看到那些日志:

拦截 lambda 执行日志

拦截器代码与处理程序代码一起运行并拦截 http 调用🚀。

但是我们可以看到,它无法将请求转发到本地 http 服务器,因为该服务器不存在。让我们创建外部扩展来解决这个问题。

📊 监控外部扩展

我们希望在不影响 Lambda 性能的情况下,聚合日志并将其发送到外部服务。因此,我们将使用一个外部进程(也称为外部扩展)来处理这些日志。

📝 外部扩展代码

首先,我们希望外部进程能够感知到 Lambda 表达式中发生的情况。为此,我们可以使用Lambda 扩展 API来实现:

  1. 将流程注册为扩展
  2. 订阅各种 lambda 事件或遥测事件

如果您不熟悉 lambda 扩展 API,我建议您阅读Wakeem's World 的精彩文章《简化内部 AWS Lambda API》 。

我发布了一个受他的文章启发的节点包,以抽象与 lambda 扩展 API 的通信。

GitHub 徽标 CorentinDoue / lambda 扩展服务

SDK轻松实现nodejs lambda扩展

Lambda 扩展服务

SDK 可在 NodeJs 和 typescript 中轻松构建 Lambda 扩展。

灵感来自简化内部 AWS Lambda API

安装

pnpm add lambda-extension-service
Enter fullscreen mode Exit fullscreen mode

或者如果使用纱线

yarn add lambda-extension-service
Enter fullscreen mode Exit fullscreen mode

或者使用 npm

npm install lambda-extension-service
Enter fullscreen mode Exit fullscreen mode

用法

import { EventTypes, ExtensionAPIService, TelemetryEventTypes } from "lambda-extension-service";
(async () => {
  const extensionApiService = new ExtensionAPIService({ extensionName: "my-extension" });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);
  extensionApiService.onTelemetryEvent((event) => 
      console.log("Telemetry event received: ", JSON.stringify(event))
  );
  await extensionApiService.registerTelemetry([
      TelemetryEventTypes.Function,
      TelemetryEventTypes.Platform,
      TelemetryEventTypes.Extension,
  ]);

  while (true) {
      const event = await extensionApiService.next();
      console.log
Enter fullscreen mode Exit fullscreen mode

有了它,我们可以轻松注册我们的外部扩展并订阅所有调用事件,并在 lambda 即将关闭时收到通知。

// src/layers/monitorExtension/partial-monitor.ts

import { EventTypes, ExtensionAPIService } from 'lambda-extension-service';

console.log('Executing monitor extension code...');

const main = async () => {
  const extensionApiService = new ExtensionAPIService({
    extensionName: 'monitor',
  });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);

  while (true) {
    const event = await extensionApiService.next();
    console.log('Received event', event);
  }
};

main().catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

现在我们可以启动一个 http 服务器来接收来自内部扩展的日志。

// src/layers/monitorExtension/logServer.ts

import { createServer } from 'http';
import { Log } from './types';

type LogServerOptions = {
  port: number;
};
export const listenForLog = (onLogReceived: (log: Log) => void, { port }: LogServerOptions = { port: 4243 }) => {
  const server = createServer(function (request, response) {
    if (request.method == 'POST') {
      let body = '';
      request.on('data', function (data) {
        body += data;
      });
      request.on('end', function () {
        try {
          onLogReceived(JSON.parse(body));
        } catch (e) {
          console.error('failed to parse logs', e);
        }
        response.writeHead(200, {});
        response.end('OK');
      });
    } else {
      console.error('unexpected request', request.method, request.url);
      response.writeHead(404, {});
      response.end();
    }
  });

  server.listen(port, 'sandbox');
  console.info(`Listening for logs at http://sandbox:${port}`);
};
Enter fullscreen mode Exit fullscreen mode

然后在每次新调用或关闭时将它们聚合并发送到外部服务。

// src/layers/monitorExtension//monitor.ts

import { EventTypes, ExtensionAPIService } from 'lambda-extension-service';
import { Log } from './types';
import { LogAggregator } from './logAggregator';
import { listenForLog } from './logServer';
import { LambdaContext } from './lambdaContext';
import { forwardLogs } from './forwardLogs';

console.log('Executing monitor extension code...');

const main = async () => {
  const logAggregator = new LogAggregator();
  const lambdaContext = new LambdaContext();
  const onLogReceived = (log: Log) => {
    logAggregator.addLog(log, lambdaContext.getRequestId());
  };
  listenForLog(onLogReceived);

  const extensionApiService = new ExtensionAPIService({
    extensionName: 'monitor',
  });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);

  while (true) {
    const event = await extensionApiService.next();
    const lastContext = lambdaContext.getContext();
    lambdaContext.updateContext(event);

    if (lastContext !== undefined) {
      await forwardLogs({
        context: lastContext,
        logs: logAggregator.getLogs(lastContext.requestId),
      });
    }
  }
};

main().catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

🚀 部署外部扩展

首先,我们还需要使扩展代码可由 lambda 执行。但这次它应该是一个独立的可执行文件。

💡 我们重复使用esbuild与拦截器扩展相同的命令,但我们需要添加一个 shebang 以使文件可以使用该--banner:js='#!/usr/bin/env node'选项执行。

{
  "scripts": {
    "build:monitor": "./node_modules/.bin/esbuild  src/layers/monitorExtension/monitor/index.ts --bundle --outfile='./dist/layers/monitorExtension/monitor.js' --platform=node --main-fields=module,main --banner:js='#!/usr/bin/env node'"
  }
}
Enter fullscreen mode Exit fullscreen mode

然后我们需要通过 lambda 层将代码发送到 lambda。

💡 通过将监视器代码的构建输出到与拦截器代码相同的文件夹中(./dist/layers/interceptorExtension),该层将发送这两个文件,而无需更改我们的 CDK 配置。

最后,我们需要让lambda执行监控进程。

/opt/extensions默认情况下,lambda服务在启动lambda环境时会尝试执行文件夹中的所有文件。

因此,让我们添加一个 bash 脚本来./dist/layers/monitorExtension/extensions启动监视代码。

# dist/layers/monitorExtension/extensions/monitor

#!/bin/bash
set -euo pipefail

OWN_FILENAME="$(basename $0)"
LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename
NODE_OPTIONS="" # Needed to reset NODE_OPTIONS set by Lambda runtime. Otherwise, the internal interceptor extension will be loaded in the external process too.

exec "/opt/${LAMBDA_EXTENSION_NAME}.js"
Enter fullscreen mode Exit fullscreen mode

该脚本将被发送到/opt/extensions/monitorlambda 服务并由其自动执行。

💡请注意,lambda 表达式的环境变量在进程之间共享。因此,外部进程将需要设置NODE_OPTIONS拦截器代码。我们需要重置NODE_OPTIONS以避免这种情况,并使拦截器代码加载两次并拦截监视器调用。

就这样!我们完成了监控扩展。

👀 结果

让我们在之前的 lambda 上再次部署它。以下是最终日志:

监控 lambda 执行日志

  1. 🟠 Lambda 服务初始化 Lambda 执行环境
  2. 🟢 监控外部扩展初始化
  3. 🟣 拦截器内部扩展初始化
  4. 🟠 Lambda 服务确认监控扩展的初始化
  5. 🟠 lambda 服务启动第一个事件处理
  6. 🟣 拦截器内部扩展拦截了两个 http 调用
  7. 🟠 lambda 服务结束第一个事件处理
  8. 🟠 lambda 服务启动第二个事件处理
  9. 🟢 监控外部扩展将第一个事件的日志发送到外部监控服务
  10. 🟣 拦截器内部扩展拦截了两个 http 调用
  11. 🟠 lambda 服务结束第一个事件处理
  12. 🟢 在 lambda 关闭时,监控外部扩展将第二个事件的日志发送到外部监控服务

在虚假的外部监控服务上,我们收到了按事件汇总的日志🥳:

转发日志

⏭️ 使用专用存储库在您自己的 AWS 帐户上进行测试:

GitHub 徽标 CorentinDoue / lambda扩展示例

使用 NodeJs lambda 创建和使用内部和外部 Lambda 扩展的最小存储库

Lambda 扩展示例

使用 AWS Lambda 扩展为您的无服务器应用程序提供支持

此示例显示如何使用监控工具创建一个简单的 Lambda,该工具由以下内容组成:

  • 记录 Lambda 发出的所有 http 调用的内部扩展
  • 一个外部扩展,用于聚合这些日志并将其发送到假设的监控工具

监控扩展架构

安装

https://webhook.site/*将URL更改为src/urls.ts您自己的 webhook URL。

 pnpm install
 pnpm cdk bootstrap
 pnpm run deploy
Enter fullscreen mode Exit fullscreen mode

测试

pnpm integration-test
Enter fullscreen mode Exit fullscreen mode





结论

现在了解如何为 Lambda 创建内部和外部扩展。这是一种强大的方法,可以为您的 Lambda 实现和共享支持工具,而不会影响其性能,也无需直接修改其他开发人员编写的处理程序代码。

欢迎在下面的评论区分享你的想法和问题。我很乐意解答。

文章来源:https://dev.to/slsbytheodo/power-up-your-serverless-application-with-aws-lambda-extensions-3a31
PREV
在 JavaScript 中编写异步构造函数的正确方法 异步构造函数???构造函数速成课程 解决方法 #1:延迟初始化 解决方法 #2:防御性编程 解决方案:静态异步工厂函数!实践总结
NEXT
SOLID 原则:编写 SOLID 程序;避免 STUPID 程序