亚马逊AWS官方博客

现在开放 AWS Lambda Rust

Rust + Lambda 徽标

AWS Lambda 让开发人员可以轻松为几乎任何类型的应用程序或后端服务运行代码,而且全部无需管理。它刚刚推出了 Runtime API。Runtime API 定义了基于 HTTP 的 Lambda 编程模型规范,可通过任何编程语言实现。为了启动该 API,我们开放了 Rust 语言运行时源代码。Rust 是一种用于编写和维护快速、可靠且高效代码的编程语言。

新推出的 Rust 运行时让您可以轻松启动实现我们以下 Handler 类型的 Rust 函数:pub type Handler<E, O> = fn(E, Context) -> Result<O, HandlerError>。运行时依靠 Serde 来对事件与响应进行序列化和反序列化。如下示例:

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

use lambda::error::HandlerError;

use std::error::Error;

#[derive(Deserialize, Clone)]
struct CustomEvent {
    #[serde(rename = "firstName")]
    first_name: String,
}

#[derive(Serialize, Clone)]
struct CustomOutput {
    message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info)?;
    lambda!(my_handler);

    Ok(())
}

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty first name"));
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

创建、构建和部署 Rust 函数

首先我们建议使用 Rust 的构建工具和包管理器 Cargo 来创建和构建新项目:

$ cd MY_WORKSPACE
$ cargo new my_lambda_function --bin

Cargo 会自动为新项目创建文件夹,以及在项目根目录下创建 Cargo.toml 文件。打开 Cargo.toml 文件,并在 [dependencies] 部分添加一个 lambda_runtime 包:

[dependencies]
lambda_runtime = "0.1"

此外,我们还将需要一些依赖项:Serde 用于对事件进行序列化(反序列化),logsimple_logger 用于输出日志。

serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"

还需在 Cargo.toml 文件中再进行一项设置。当配置为使用 Runtime API 的自定义运行时时,AWS Lambda 希望在部署包中包含名为 bootstrap 的可执行文件。我们可以配置 Cargo 来生成一个名为 bootstrap 的文件,不考虑包的名称。首先,在文件的 [package] 部分添加 autobins = false 设置。然后,在 Cargo.toml 的底部添加新的 [[bin]] 部分:

[[bin]]
name = "bootstrap"

我们已完成的 Cargo.toml 文件应如下所示:

[package]
name = "my_lambda_function"
version = "0.1.0"
authors = ["me <my_email@my_server.com>"]
autobins = false

[dependencies]
lambda_runtime = "^0.1"
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"

[[bin]]
name = "bootstrap"
path = "src/main.rs"

接下来,打开 Cargo 在您项目的 src 文件夹中创建的文件 main.rs,将上述基本示例的内容复制并粘贴到该文件中。它应该会替换 Cargo 创建的存根 main 方法。设置好新的来源后,我们差不多准备好构建和部署 Lambda 函数了

在开始构建之前,我们需要确保 Rust 编译器的目标平台无误。AWS Lambda 在 Amazon Linux 环境中执行函数。除非您已在 x86 64bit Linux 环境中运行此教程,否则我们需要为 Rust 编译器添加新的目标环境,可以通过 Rustup 工具来更轻松地实现这一点。然后按照以下说明,在 Mac OS X 上编译基本示例。

在 Mac OS X 上编译

如果您尚未安装 rustup,则首先需要安装它。然后,添加目标 x86_64-unknown-linux-musl

$ rustup target add x86_64-unknown-linux-musl

在构建应用程序之前,我们还需要安装目标平台链接器。幸好,Homebrewmusl-cross tap 为 Mac OS 提供了完整的交叉编译工具链。

$ brew install filosottile/musl-cross/musl-cross

现在,我们需要告诉 Cargo,我们的项目使用新安装的链接器来构建 x86_64-unknown-linux-musl 平台。在您的项目文件夹中创建一个名为 .cargo 的新目录,并在这个新文件夹中创建一个名为 config 的新文件。

$ mkdir .cargo
$ echo '[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"' > .cargo/config

在我的系统中,部分依赖项没有自动选择已配置好的链接器,而是尝试使用了 musl-gcc。为了尽快解决这个问题,我创建了一个链接到新链接器的符号链接:

$ ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc

安装并配置完编译器的新目标平台后,现在我们就可以进行 Cargo 交叉编译了!

构建函数

使用以下命令构建 AWS Lambda 应用程序。如果您在 Amazon Linux 上运行这些命令,则不需要添加 --target 选项。

$ cargo build --release --target x86_64-unknown-linux-musl

我们创建的是发布版本,而不是调试版本。调试版本非常大,而且虽然基本示例没有超出,但其他应用程序很可能会超出 AWS Lambda 函数的最大部署包大小

在构建过程中会在 ./target/x86_64-unknown-linux-musl/release/bootstrap 目录中创建一个可执行文件。Lambda 需要部署包为 Zip 文件。运行以下命令为 AWS Lambda 创建 Zip 部署包文件:

$ zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

为了简化部署和构建过程,我们将向 SAM CLI(无服务器应用程序模型)添加 Cargo 构建器。当该版本的 SAM CLI 发布后,您将能够使用 SAM 模板轻松运行 sam build

在 AWS Lambda 中部署函数

现在,我们可以在 AWS Lambda 中部署该文件。导航至 AWS Lambda 控制台,然后创建新函数

选中从头开始创作选项,为您的函数命名,我的函数名叫 test-rust。然后,从运行时下拉菜单中选择 Provided。我们的示例函数不需要任何特殊权限。如果您已有基本执行角色,则可以选择现有角色。否则可以让 Lambda 控制台创建具有基本权限的新角色(您无需选择模板)。最后,单击创建函数

使用已提供的运行时 (rust) 创建 AWS Lambda 函数

在函数屏幕中,使用 Function code(函数代码)部分的上传按钮上传我们在此教程的构建步骤中创建的 rust.zip 文件。选择新文件后,保存函数更改。我们不需要更改任何其他配置。

因为我们的代码完全包含在 Lambda 将启动的 bootstrap 可执行文件中,因此不需要处理程序信息。128MB 的内存和 3 秒执行超时为“Hello, world”提供了充足的空余空间。

抛出错误时,运行时可以选择在函数的输出中包含完整的堆栈跟踪。要实现这一点,只需将 RUST_BACKTRACE 环境变量设为 1 即可。

现在,我们可以测试下函数。在 Lambda 控制台中,单击右上角的测试按钮。由于这是我们第一次测试该函数,因此 Lambda 控制台会要求我们定义测试事件。您可能注意到了,在上述示例代码中,我们希望传入事件中包含 firstName 属性。使用以下 JSON 作为测试事件,并为您的测试对象命名。

{
  "firstName": "Rustacean"
}

为 AWS Lambda 函数配置测试事件

最后,单击测试事件模式窗口中的创建。保存新的测试事件后,再次单击控制台右上角的测试,实际启动该函数。展开“执行结果”部分,查看函数的输出和日志。

在 Rust 中测试 AWS Lambda 函数

祝贺您! 现在已成功构建并部署了使用 Rust 编写的第一个 AWS Lambda 函数。接下来,尝试使用无服务器应用程序模型 (SAM) 模板部署此函数。

深入了解代码

既然我们已成功运行了 Rust Lambda 函数,那么接下来我们来拆分看下示例代码中最重要的组成部分。从最上方开始,我们导入了包:

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

我们导入的第一个包是 lambda_runtime。这是我们的新运行时,简洁起见,将其命名为 lambda。您可能还注意到了 #[macro_use],这是我们导入宏时对 Rust 编译器的声明。过不了多久,您就不需要这样做了,因为 Rust 2018 版让我们能够像导入普通函数或值一样导入宏。该运行时定义了一个 lambda! 宏,能让它轻松引导运行时。

serde_derive还使用宏来为给定结构体生成 marshaller。您会看到,示例代码中的结构体包含注释 #[derive(Serialize, Deserialize)],,分别处理序列化和反序列化。

库使用通过 log定义的宏来生成日志消息。示例代码包含 simple_logger,用于将消息打印到 stdout。有许多包实现了 log 门面,而且运行时本身并不会限制您选择哪一个。

externuse 语句之后,我们的示例代码声明了 main() 方法,即 bootstrap 可执行文件的入口点。该代码将在 Lambda 启动我们的函数时运行。

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info).unwrap();
    lambda!(my_handler);

    Ok(())
}

我们要做的第一件事是初始化 simple_logger,并将日志记录级别设置为 Info。您也可以将其更改为 DebugTrace,以便接收有关库及其依赖项在后台执行操作的更多信息。注意,simple_logger 包会锁定 stdout,因此在调试或跟踪模式下记录日志将大大降低您函数的性能。

接下来,我们使用在 lambda_runtime 包中定义的 lambda!() 宏来引导我们的自定义运行时。在其最基本的形式下,宏会将指针指向您在代码中定义的处理程序函数。自定义运行时使用 hyper向 Lambda Runtime API 发出 HTTP 请求。您可以选择将自己的 tokio 运行时传递给 lambda!() 宏:

let rt = tokio::runtime::Runtime::new()?;
lambda!(my_handler, rt);

在没有提示且现有库将创建其自己的运行时的情况下,您可能想创建一个自定义 Tokio 运行时。您可以单击此处,查看一个完整的运行示例

之后,自定义运行时会启动并开始轮询 Lambda Runtime API 来获取新事件。
代码的下一部分定义了处理程序函数。处理程序函数必须遵守在 lambda_runtime 包中创建的 Handler 类型。

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty first name"));
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

处理程序函数会接收实现 serde::Deserialize 特征的事件对象。自定义运行时还会为每个事件生成 Context 对象,并将其传递给处理程序。Context 对象包含您会在正式运行时中找到的相同属性

处理程序返回的值必须是具有实现 serde::Serialize 的自定义输出类型的 Result。此外,自定义运行时库会指定可用于包装自定义错误的 HandlerError 类型。您可以在 Context 对象中使用 new_error(msg: &str) 方法来实例化具有 backtrace 的新 HandlerError 对象。自定义运行时知道如何将 HandlerError 序列化为 JSON,并且会在 RUST_BACKTRACE 环境变量告知它应该包含 backtrace 时包含 backtrace。

小结

该运行时仍然处于早期阶段,非常欢迎您提供相关反馈,促进其发展。此外,我们知道目前还有其他 Rust for Lambda 库,如 landorust-aws-lambdarust-crowbar。非常感谢从事这些项目的相应作者所付出的努力和提供的灵感。

该运行时让使用 Rust 编写高性能 Lambda 函数变得轻而易举。欢迎访问我们的 aws-lambda-rust-runtime GitHub 存储库,参与该项目,并提供您的反馈和问题!


Rust 徽标由 Mozilla 提供,根据知识共享署名协议 (CC-BY) 条款使用。