#aws-lambda #open-api #definition #strongly-typed #api-gateway #generation #opinionated

openapi-lambda

基于 OpenAPI 定义的 AWS Lambda 强类型代码生成

2 个版本

0.1.2 2024年1月18日
0.1.1 2024年1月18日

#2319 in 网页编程

MIT 许可证

98KB
1.5K SLoC

Rust OpenAPI Lambda 🦀

crates.io docs.rs

Rust OpenAPI Lambda 从 OpenAPI 定义生成用于在 Amazon API Gateway REST API 后的 AWS Lambda 上无服务器运行 API 的 Rust 模板代码。生成的代码自动路由请求、解析参数、序列化响应、调用中间件进行身份验证和处理相关错误。此项目的目标是让开发者专注于业务逻辑,而不是模板。

本项目与 OpenAPI 创新项目或亚马逊网络服务 (AWS) 无关。

用法

1. 添加依赖

openapi-lambda 添加为依赖项,并将 openapi-lambda-codegen 添加为构建依赖项到你的 Cargo.toml 文件中

[dependencies]
openapi-lambda = "0.1"

[build-dependencies]
openapi-lambda-codegen = "0.1"

这两个 crate 必须在 Cargo.lock 中具有相同的版本号。

2. 生成代码

将一个 build.rs Rust 构建脚本添加到你的 crate 根目录中(见以下注释)

use openapi_lambda_codegen::{ApiLambda, CodeGenerator, LambdaArn};

fn main() {
  CodeGenerator::new(
    // Path to OpenAPI definition (relative to build.rs).
    "openapi.yaml",
    // Output path to a directory for generating artifacts. This directory should be added to
    // `.gitignore`.
    ".openapi-lambda",
  )
  // Define one or more Lambda functions for implementing the API. A single "mono-Lambda" may
  // be used to handle all API endpoints, or endpoints may be grouped into multiple Lambda
  // functions using filters (see docs). Note that Lambda cold start time is roughly
  // proportional to the size of each Lambda binary, so consider splitting APIs into smaller
  // Lambda functions to reduce cold start times.
  .add_api_lambda(ApiLambda::new(
    // Name of the generated Rust module that will contain the API types.
    "backend",
    // AWS CloudFormation logical ID or Amazon Resource Name (ARN) that the Lambda function
    // will have when deployed to AWS. This value is used for adding
    // `x-amazon-apigateway-integration` extensions to the OpenAPI definition, which tells
    // API Gateway which Lambda function to use for handling each API request. If using
    // CloudFormation/SAM with a logical ID, the ARN will be populated automatically during
    // deployment.
    LambdaArn::cloud_formation("BackendApiFunction.Alias")
  ))
  .generate();
}

将生成的代码包含在你的 crate 的 src/lib.rs

include!(concat!(env!("OUT_DIR"), "/out.rs"));

生成的文件 out.rs 定义了一个名为 models 的模块,其中包含 OpenAPI 定义中定义的输入参数和请求/响应体所对应的 Rust 类型。它还定义了每个对 add_api_lambda() 的调用,它定义了一个 Api trait,该 trait 为 OpenAPI 定义中定义的每个操作(路径 + HTTP 方法)提供了一个方法。

生成文档

通常,参考 rustdoc 文档可以帮助理解生成的模型和 API 类型。要生成文档,请运行

cargo doc --open

3. 实现 API 处理程序

要实现API,请实现生成的 Api 特性。为了帮助您开始,代码生成器在配置的输出目录(例如,.openapi-lambda/backend_handler.rs)中创建了名为 <MODULE_NAME>_handler.rs 的文件,其中包含每个 Api 特性的占位符实现。将这些文件复制到 src/,在 src/lib.rs 中定义相应的模块(例如,mod backend_handler),并将每个 todo!() 替换为实现API。

每个 Api 特性声明了两个关联类型,您必须在实现中定义

  • AuthOk:您的中间件返回的成功请求认证的结果(见下文)。这可能代表用户、认证会话或其他与您的API相关的抽象。如果API端点中没有任何一个需要认证,只需使用单元类型(())。
  • HandlerError:每个API处理程序方法返回的错误类型。典型的API将为错误定义一个 enum 类型,并让 Api::respond_to_handler_error() 方法根据错误的性质返回适当的HTTP响应(例如,状态码403表示拒绝访问错误)。

4. 实现中间件

openapi_lambda::Middleware 特性定义了用于认证请求和可选地包装每个API处理程序以添加功能(如日志记录和遥测)的接口。为不需要认证端点的API提供了一个方便的 UnauthenticatedMiddleware 实现。

认证请求

Middleware::AuthOk 关联类型表示对 Middleware::authenticate() 特性方法的成功调用结果。这是一个您定义的类型,可能代表用户、认证会话或与您的API相关的其他抽象。如果API端点中没有任何一个需要认证,只需使用单元类型(())。Middleware::AuthOk 关联类型必须与您的 Api 特性实现中的 Api::AuthOk 关联类型匹配。

Middleware::authenticate() 方法提供了一个 headers 参数,可以访问所有请求头,允许您使用类似 AuthorizationCookie 的头进行请求认证。它还提供了一个 lambda_context 参数,如果使用API Gateway Cognito用户池授权,则可以访问Amazon Cognito身份信息。

如果请求认证失败,请务必返回一个包含适当HTTP状态码(即401)的 HttpResponse

5. 添加二进制目标

为每个Lambda函数(例如,bin/bootstrap_backend.rs)定义一个二进制目标以引导Lambda运行时。建议的入口点是openapi_lambda::run_lambda()函数,用于启动Lambda运行时并开始处理API请求。

// Replace `my_api` with the name of your crate and `backend` with the name of the module
// passed to `ApiLambda::new()`.
use my_api::backend::Api;
use my_api::backend_handler::BackendApiHandler;
use openapi_lambda::run_lambda;

#[tokio::main]
pub async fn main() {
  let api = BackendApiHandler::new(...);
  let middleware = ...; // Instantiate your middleware here.

  run_lambda(|event| api.dispatch_request(event, &middleware)).await
}

6. 编译二进制文件

Cargo Lambda

使用Cargo Lambda是编译用Rust编写的Lambda函数的最简单方法,它处理从您的开发环境到AWS Lambda(x86-64或基于ARM)的任何必要的交叉编译。

除了安装Cargo Lambda之外,请确保安装针对目标Lambda函数架构的相关目标(例如,通过rustup target add)的Rust工具链。

安装Cargo Lambda后,运行以下命令以在target/lambda/目录中构建Lambda bootstrap二进制文件。

cargo lambda build --release

如果针对基于ARM的Lambda函数,请务必添加--arm64标志。

musl-cross

另一个Cargo Lambda的替代方案是musl-cross,它在某些环境中编译(如macOS的Apple Silicon)时提供更好的回溯支持。macOS上有一个Homebrew软件包,可以轻松安装。

除了安装musl-cross之外,请确保安装针对目标Lambda函数架构的相关目标(例如,通过rustup target add)的Rust工具链。

要编译针对x86-64 Lambda函数的二进制文件,运行

CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
  cargo build --target x86_64-unknown-linux-musl --release

要编译针对ARM Lambda函数的二进制文件,运行

CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
  cargo build --target aarch64-unknown-linux-musl --release

最终二进制文件将写入target/x86_64-unknown-linux-musl/release/target/aarch64-unknown-linux-musl/release/目录,具体取决于目标架构。

7. 测试和部署

将应用程序部署到AWS涉及创建一个或多个Lambda函数和一个API Gateway REST API

用Rust编写的Lambda函数应使用提供的Lambda运行时之一。提供的运行时要求每个Lambda函数都包含一个名为bootstrap的二进制文件,该文件由上面的编译步骤生成。

API网关REST API使用带有关联x-amazon-apigateway-integration扩展的OpenAPI定义,这些扩展用于确定每个API端点使用的Lambda函数。名为openapi-apigw.yaml的文件,其中包含适用于此目的的注释OpenAPI定义,并写入在build.rs中指定的输出目录(例如,.openapi-lambda/openapi-apigw.yaml)。此OpenAPI定义经过修改以适应Amazon API网关支持的OpenAPI功能子集。特别是,所有引用都合并到一个文件中,并删除了discriminator属性。

作为最佳实践,请考虑使用基础设施即代码(IaC)解决方案,如AWS CloudFormationAWS Serverless Application Model (SAM)或Terraform

AWS Serverless Application Model (SAM)

“Petstore”示例提供了一个工作的AWS SAM模板(template.yaml)和相应的Makefile

AWS SAM提供了针对无服务器用例优化的CloudFormation版本和用于在AWS上部署以及本地测试API的命令行界面(CLI)。

当定义SAM CloudFormation堆栈模板时,为每个Lambda函数定义一个AWS::Serverless::Function资源。请确保在Rust构建脚本build.rs中使用LambdaArn::cloud_formation()函数指定相同的逻辑ID(即YAML键)。如果指定AutoPublishAlias属性(推荐),则将.Alias后缀附加到传递给LambdaArn::cloud_formation()的逻辑ID上。这确保API网关始终执行与指定别名关联的函数版本。别名有助于通过简单地更新别名以指向Lambda函数的先前版本来支持生产中的快速回滚,而无需等待完整堆栈部署。

每个 AWS::Serverless::Function

资源应在 Metadata 属性中指定 BuildMethod: makefile(见 构建自定义运行时)。该资源还应指定一个指向包含您的crate的目录的 CodeUri 属性。必须在指定目录中存在一个 Makefile。该 Makefile 必须定义一个名为 build-LOGICAL_ID 的目标,其中 LOGICAL_ID 是SAM模板中资源的逻辑ID(YAML键)。build-LOGICAL_ID 目标必须将名为 bootstrap 的二进制文件复制到由 ARTIFACTS_DIR 环境变量引用的目录(由AWS SAM CLI在构建时设置)。有关详细信息,请参阅 Petstore 示例。

SAM模板还必须包括一个定义API Gateway REST API的 AWS::Serverless::Api 资源。使用 AWS::Include 转换以及带注释的OpenAPI定义 openapi-apigw.yaml,该定义在部署期间自动解析每个Lambda函数的逻辑ID到相应的 Amazon资源名称 (ARN)

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: my-api
      StageName: prod
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: .openapi-lambda/openapi-apigw.yaml

在测试或部署AWS SAM模板之前,通过运行以下命令构建它:

sam build

要启动API以进行本地测试,请运行以下命令:

sam local start-api

要将模板部署到AWS,请运行以下命令:

sam deploy

示例

Petstore 示例说明了如何使用此crate与 AWS SAM 一起构建、测试和部署一个API到AWS Lambda后面Amazon API Gateway REST API。

最低支持的Rust版本(MSRV)

此crate最低支持的Rust版本(MSRV)为 1.70

此crate坚持至少支持过去6个月的Rust版本。不会考虑与6个月前的Rust版本不兼容的更改作为SemVer破坏性更改,并且不会为此crate产生新的主要版本号。MSRV更改将与次要版本更新同时发生,并且不会发生在补丁版本中。

日志记录

生成的代码使用 log crate记录请求。考虑使用 log4rsenv_logger crate来在每个Lambda函数的 main() 入口点中启用日志记录。

启用 TRACE 级别日志将记录每个请求和响应的原始内容。这可能在调试时很有用,但 在生产环境中绝对不应启用 TRACE 日志。除了占用Amazon CloudWatch 日志费用外,在生产环境中启用 TRACE 日志可能会记录敏感的秘密,如密码和API密钥。

OpenAPI支持

代码生成器支持大部分的OpenAPI 3.0 规范,但仍有不足。如果您在生成代码时遇到 未实现! 错误,请提交一个GitHub问题或发起一个pull request(见CONTRIBUTING.md)。

OpenAPI定义中找到的引用($ref)得到支持,包括对其他文件中对象的引用。但是,当前不支持解析为其他引用的引用。

每个端点必须有一个operationId属性,该属性在整个端点中必须是唯一的。该operationId属性用于路由请求以及在生成的代码中命名处理程序方法和相关类型。

认证与非认证API端点

默认情况下,所有API端点都假定需要认证。这意味着将调用Middleware::authenticate(),并将AuthOk结果传递给处理程序方法。

要将端点标记为非认证,请将一个空对象({})添加到端点的security属性中。例如

security:
  - {}

非认证端点将在不调用Middleware::authenticate()的情况下调用其处理程序,处理程序方法将不会接收到AuthOk参数。

请注意,此处的“非认证”仅意味着中间件将不会用于认证请求。您实现的处理程序方法可能仍然会执行自己的认证。这对于登录端点(尚未存在认证会话)或需要访问原始请求体以进行认证的webhook端点(例如,使用HMAC)非常有用。在后一种情况下,应使用具有type: string(可选地带有format: binary)的请求体模式。处理程序方法可以在验证HMAC后反序列化主体。

请求参数

请求参数必须定义一个单独的schema属性。目前不支持content属性。

目前不支持cookie参数(in: cookie)。标头参数(in: header)必须是纯字符串模式。

在支持的情况下,非字符串参数类型必须实现FromStr特质以进行解析。不支持在请求参数中使用对象类型。

请求/响应体

目前不支持定义多个媒体类型的请求和响应体。

代码生成器根据以下表格将请求和响应体表示为Rust类型。鼓励提交GitHub问题和pull request,以添加对其他广泛使用的数据格式的支持。

媒体类型 模式 type Rust类型 (反)序列化
应用/json 字符串 Vec<u8> 用于 format: binary 或默认情况下为 String (UTF-8)
应用/json 非字符串 见下文 serde_json
应用/字节- 任何 Vec<u8>
文本/* 任何 String (UTF-8)
其他(后备) 任何 Vec<u8>

字符串(type: string

指定至少一个 enum 变体的字符串模式将生成一个命名的Rust enum。请注意,当前不支持 null 变体。

enum 字符串类型由 format 属性确定,如下表所示

格式 Rust类型
未指定(默认) 字符串
日期 chrono::NaiveDate
日期时间 chrono::DateTime<Utc>
字节 String(不进行base64解码)
密码 字符串
二进制 Vec<u8>
其他 作为原始Rust类型处理

整数(type: integer

目前不支持整数 enum。非 enum 整数类型由 format 属性确定,如下表所示

格式 Rust类型
未指定(默认) i64
int32 i32
int64 i64
其他 作为原始Rust类型处理

浮点数(type: number

目前不支持数字 enum。非 enum 数字类型由 format 属性确定,如下表所示

格式 Rust类型
未指定(默认) f64
float f32
double f64
其他 作为原始Rust类型处理

布尔值(type: boolean

目前不支持布尔值 enum。布尔值始终表示为 bool

对象(type: object

下表指定了根据对象模式中的 propertiesadditionalProperties 字段生成的Rust类型。请注意,具有对象或 enum 模式的 properties 条目必须使用引用($ref)到命名模式。其他属性类型可以使用内联模式或引用。

属性 additionalProperties Rust类型
至少一个 false 或未指定 命名 struct
至少一个 true 命名 struct + HashMap<String, serde_json::Value>,带有 #[serde(flatten)]
至少一个 模式 命名 struct + HashMap<String, _> 并使用 #[serde(flatten)]
false 或未指定 openapi_lambda::models::EmptyModel
true HashMap<字符串, serde_json::Value>
模式 HashMap<字符串, _>

数组(type: array

具有 uniqueItems: true 的数组模式表示为 indexmap::IndexSet<_>。所有其他数组都表示为 Vec<_>

多态(oneOf

为利用 oneOf 的模式生成的名为 Rust enum,对于 oneOf 数组中的每个条目有一个变体。如果指定了 discriminator,则生成 Serde 内部标记的 enum,该字段作为标记。否则,生成 Serde 未标记的 enum。

请注意,每个 oneOf 变体都必须是一个命名引用($ref),这决定了 Rust enum 变体的名称。每个引用的模式必须是对象模式(type: object)或利用 allOf。不支持内联变体模式。

组合对象(allOf

利用 allOf 的模式在合并所有组件模式为一个 type: object 的单个模式后被视为对象(见上文)。allOf 模式的每个组件都必须是对象或嵌套的 allOf 模式。最多只能有一个组件定义 additionalProperties

其他模式类型

利用 anyOfnot 的模式目前不支持。

响应

响应必须指定单个 HTTP 状态码。目前不支持状态码范围。

赞助

此项目由 Unflakable 赞助。

依赖关系

~13–24MB
~342K SLoC