AWS Lambda with Quarkus REST, Undertow, or Reactive Routes

有了 Quarkus,您可以使用 AWS Gateway HTTP APIAWS Gateway REST API将您最喜欢的 Java HTTP 框架部署为 AWS Lambda。这意味着您可以将使用 Quarkus REST(我们的 Jakarta REST 实现)、Undertow(servlet)、Reactive Routes、Funqy HTTP或任何其他 Quarkus HTTP 框架编写的微服务部署为 AWS Lambda。

您应该仅将单个 HTTP 框架与 AWS Lambda 扩展一起使用,以避免意外的冲突和错误。

您可以将 Lambda 部署为纯 Java jar,也可以将项目编译为原生镜像并部署,以实现更小的内存占用和启动时间。我们的集成还会生成 SAM 部署文件, Amazon’s SAM framework可以使用这些文件。 Quarkus 为每个 Gateway API 提供不同的扩展。HTTP Gateway API 在 `quarkus-amazon-lambda-http`扩展中实现。REST Gateway API 在 `quarkus-amazon-lambda-rest`扩展中实现。如果您对使用哪个 Gateway 产品感到困惑,亚马逊有一个 great guide来帮助您进行权衡。 像大多数 Quarkus 扩展一样,Quarkus AWS Lambda HTTP/REST 扩展支持实时编码。 :iokays-category: quarkus :iokays-path: modules/ROOT/pages/_includes/extension-status.adoc :keywords: Quarkus, 中文文档, 编程技术

该技术被认为是 {extension-status}。 有关可能状态的完整列表,请查看我们的 FAQ entry.

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Getting Started

本指南指导你通过 Maven 原型生成示例 Java 项目。它随后介绍了项目的结构,以便你可以调整任何现有项目以使用 AWS Lambda。

Installing AWS bits

安装所有 AWS 位可能是本指南中最困难的事情。请务必按照所有步骤安装 AWS SAM CLI。

Creating the Maven Deployment Project

使用我们的 Maven 原型创建 Quarkus AWS Lambda Maven 项目。

如果你想使用 AWS Gateway HTTP API,请使用以下脚本生成你的项目:

mvn archetype:generate \
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-http-archetype \
       -DarchetypeVersion={quarkus-version}

如果你想使用 AWS Gateway REST API,请使用以下脚本生成你的项目:

mvn archetype:generate \
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-rest-archetype \
       -DarchetypeVersion={quarkus-version}

Build and Deploy

构建项目:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

这会编译代码并运行生成项目中包含的单元测试。单元测试与任何其他 Java 项目相同,不需要在 Amazon 上运行。Quarkus 开发模式也可用作此扩展。

如果你想构建一个本机可执行文件,请确保正确安装 GraalVM,并向构建添加 native 属性

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

如果你不是在 Linux 系统上构建,你还需要传递一个属性来指示 quarkus 使用 Docker 构建,因为 AmazonLambda 需要 Linux 二进制文件。你可以通过将 -Dquarkus.native.container-build=true 传递给构建命令来执行此操作。不过,这需要你在本地安装 Docker。

CLI
quarkus build --native --no-tests -Dquarkus.native.container-build=true
# The --no-tests flag is required only on Windows and macOS.
Maven
./mvnw install -Dnative -DskipTests -Dquarkus.native.container-build=true
Gradle
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true

Extra Build Generated Files

运行构建后,你使用的 Quarkus lambda 扩展会生成几个额外文件。这些文件位于构建目录中:Maven 的 target/ ,Gradle 的 build/

  • function.zip - lambda 部署文件

  • sam.jvm.yaml - sam cli 部署脚本

  • sam.native.yaml - 本机的 sam cli 部署脚本

Live Coding and Simulating AWS Lambda Environment Locally

在开发和测试模式下,Quarkus 将启动一个模拟 AWS Lambda 事件服务器,该服务器会将 HTTP 请求转换为相应的 API Gateway 事件类型,并将其发布到底层的 Quarkus HTTP lambda 环境进行处理。这尽可能地在本地模拟 AWS Lambda 环境,而无需 Docker 和 SAM CLI 等工具。

在使用 Quarkus 开发模式时,只要像通常测试 REST 端点时一样在 http://localhost:8080 上调用 HTTP 请求。此请求将命中模拟事件服务器,并将转换为 Quarkus Lambda 轮询循环使用的 API Gateway json 消息。

为了进行测试,Quarkus 在 8081 端口下启动一个单独的模拟事件服务器。Rest Assured 的默认端口由 Quarkus 自动设置为 8081,因此你不必担心设置此端口。

如果你想在测试中模拟更复杂的 API Gateway 事件,请手动使用原始 API Gateway json 事件以 HTTP POST 方式访问 http://localhost:8080/lambda (在测试模式下为端口 8081)。这些事件将直接放置在 Quarkus Lambda 轮询循环上进行处理。以下是一个示例:

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.equalTo;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class AmazonLambdaSimpleTestCase {
    @Test
    public void testJaxrsCognitoJWTSecurityContext() throws Exception {
        APIGatewayV2HTTPEvent request = request("/security/username");
        request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer());
        request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT());
        request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>());
        request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill");

        given()
                .contentType("application/json")
                .accept("application/json")
                .body(request)
                .when()
                .post("/_lambda_")
                .then()
                .statusCode(200)
                .body("body", equalTo("Bill"));
    }

以上示例模拟使用 HTTP 请求向你的 HTTP Lambda 发送 Cognito 主体。

如果你想为 AWS HTTP API 手动编写原始事件,AWS Lambda 库有请求事件类型为 com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent ,响应事件类型为 com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse 。这对应于 quarkus-amazon-lambda-http 扩展和 AWS HTTP API。

如果您想为 AWS REST API 手动编写原始事件,Quarkus 有自己的实现:io.quarkus.amazon.lambda.http.model.AwsProxyRequestio.quarkus.amazon.lambda.http.model.AwsProxyResponse。这对应于 quarkus-amazon-lambda-rest 扩展和 AWS REST API。

模拟事件服务器还为 @QuarkusIntegrationTest 测试启动,因此也将适用于原生二进制文件。所有这些都提供了类似于 SAM CLI 本地测试的功能,但无需 Docker 的开销。

最后,如果计算机上没有端口 8080 或端口 8081,您可以使用 application.properties 修改开发和测试模式端口

quarkus.lambda.mock-event-server.dev-port=8082
quarkus.lambda.mock-event-server.test-port=8083

端口值为零将导致随机分配端口。

关闭模拟事件服务器:

quarkus.lambda.mock-event-server.enabled=false

Simulate AWS Lambda Deployment with SAM CLI

AWS SAM CLI 允许您在模拟的 Lambda 环境中在笔记本电脑上本地运行 lambda。这需要安装 Docker。构建 Maven 项目后,执行此命令:

sam local start-api --template target/sam.jvm.yaml

这将启动一个模仿亚马逊 Lambda 部署环境的 Docker 容器。环境启动后,您可以通过转到以下位置在浏览器中调用示例 lambda:

[role="bare"][role="bare"]http://127.0.0.1:3000/hello

在控制台中,您将看到来自 lambda 的启动消息。此特定部署启动了 JVM 并将您的 lambda 加载为纯 Java。

Deploy to AWS

sam deploy -t target/sam.jvm.yaml -g

回答所有问题,您的 lambda 将被部署,并且将建立与 API Gateway 的必要挂钩。如果一切部署成功,您的微服务的根 URL 将输出到控制台。类似以下内容:

Key                 LambdaHttpApi
Description         URL for application
Value               https://234asdf234as.execute-api.us-east-1.amazonaws.com/

Value 属性是 lambda 的根 URL。将其复制到您的浏览器并在末尾添加 hello

二进制类型的响应将自动使用 base64 编码。这与使用 quarkus:dev 的行为不同,它将返回原始字节。亚马逊的 API 要求进行 base64 编码,还有其他限制。通常情况下,客户端代码将自动处理此编码,但在某些自定义情况下,您应该知道您可能需要手动管理该编码。

Deploying a native executable

要部署原生可执行文件,您必须使用 GraalVM 构建它。

CLI
quarkus build --native --no-tests -Dquarkus.native.container-build=true
# The --no-tests flag is required only on Windows and macOS.
Maven
./mvnw install -Dnative -DskipTests -Dquarkus.native.container-build=true
Gradle
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true

然后,您可以使用 sam local 在本地测试可执行文件

sam local start-api --template target/sam.native.yaml

要部署到 AWS Lambda:

sam deploy -t target/sam.native.yaml -g

Examine the POM

POM 没有什么特别的,除了包含 quarkus-amazon-lambda-http 扩展(如果您要部署 AWS Gateway HTTP API)或 quarkus-amazon-lambda-rest 扩展(如果您要部署 AWS Gateway REST API)。这些扩展会自动生成 lambda 部署所需的一切。

此外,至少在生成的 Maven 典型文件 pom.xml 中,quarkus-restquarkus-reactive-routesquarkus-undertow 依赖项都是可选的。选择您要使用的 HTTP 框架(Jakarta REST、Reactive Routes 和/或 Servlet)并删除其他依赖项以缩小部署大小。

Examine sam.yaml

sam.yaml 语法超出了本文档的范围。如果您打算制作自己的自定义 sam.yaml 部署文件,则必须重点强调几件事。

首先要注意的是,对于纯 Java lambda 部署,需要一个特定的处理程序类。不要更改 Lambda 处理程序的名称。

     Properties:
        Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
        Runtime: java17

该句柄是 Lambda 运行时和您正在使用的 Quarkus HTTP 框架(Jakarta REST、Servlet 等)之间的桥梁。

如果您想进行本机化部署,则需要为本机 GraalVM 部署设置一个环境变量。如果您查看 sam.native.yaml,您会看到以下内容:

        Environment:
          Variables:
            DISABLE_SIGNAL_HANDLERS: true

此环境变量解决了 Quarkus 和 AWS Lambda 自定义运行时环境之间的一些不兼容性。

最后,对于 AWS Gateway REST API 部署还有一项特殊设置。该 API 假定 HTTP 响应主体为文本,除非您通过配置明确告诉它哪些媒体类型是二进制的。为了简化操作,Quarkus 扩展会强制对所有 HTTP 响应消息进行二进制(base 64)编码,并且 sam.yaml 文件必须配置 API Gateway 以假定所有媒体类型都是二进制的:

  Globals:
    Api:
      EndpointConfiguration: REGIONAL
      BinaryMediaTypes:
        - "*/*"

Injectable AWS Context Variables

如果您正在使用 Quarkus REST 和 Jakarta REST,您可以使用 Jakarta REST @Context 注释或带有 CDI @Inject 注释的其他任何位置将各种 AWS 上下文变量注入到 Jakarta REST 资源类。

对于 AWS HTTP API,您可以注入 AWS 变量 com.amazonaws.services.lambda.runtime.Contextcom.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent。以下是一个示例:

import jakarta.ws.rs.core.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;


@Path("/myresource")
public class MyResource {
    @GET
    public String ctx(@Context com.amazonaws.services.lambda.runtime.Context ctx) { }

    @GET
    public String event(@Context APIGatewayV2HTTPEvent event) { }

    @GET
    public String requestContext(@Context APIGatewayV2HTTPEvent.RequestContext req) { }


}

对于 AWS REST API,您可以注入 AWS 变量 com.amazonaws.services.lambda.runtime.Contextio.quarkus.amazon.lambda.http.model.AwsProxyRequestContext。以下是一个示例:

import jakarta.ws.rs.core.Context;
import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext;
import io.quarkus.amazon.lambda.http.model.AwsProxyRequest;


@Path("/myresource")
public class MyResource {
    @GET
    public String ctx(@Context com.amazonaws.services.lambda.runtime.Context ctx) { }

    @GET
    public String reqContext(@Context AwsProxyRequestContext req) { }

    @GET
    public String req(@Context AwsProxyRequest req) { }

}

Tracing with AWS XRay and GraalVM

如果您正在构建本机镜像,并且希望在 lambda 中使用 AWS X-Ray Tracing,您需要将 quarkus-amazon-lambda-xray 包含到 pom 中作为依赖项。AWS X-Ray 库与 GraalVM 并不完全兼容,因此我们必须做一些集成工作才能使其正常运行。

Security Integration

当您在 API Gateway 上调用一个 HTTP 请求时,Gateway 会将该 HTTP 请求转换为 JSON 事件文档,并将其转发到 Quarkus Lambda。Quarkus Lambda 会解析此 json 并将其转换为内部 HTTP 请求表示,任何 Quarkus 支持的 HTTP 框架(Jakarta REST、servlet、Reactive Routes)均可使用该表示。

API Gateway 支持多种安全方式来调用由 Lambda 和 Quarkus 支持的 HTTP 端点。如果您启用它,Quarkus 将自动解析 event json document 的相关部分并查找基于安全性的元数据,并在内部注册一个 java.security.Principal,它可以通过在 Jakarta REST 中注入一个 jakarta.ws.rs.core.SecurityContext、在 servlet 中通过 HttpServletRequest.getUserPrincipal() 和在 Reactive Routes 中通过 RouteContext.user() 来查找。如果您想要更多安全信息,可以将 Principal 对象强制转换为一个类,该类将为您提供更多信息。

要启用此安全功能,请将以下内容添加到您的 application.properties 文件:

quarkus.lambda-http.enable-security=true

以下是它的映射方式:

Table 1. HTTP quarkus-amazon-lambda-http
Auth Type Principal Class 主体名称的 Json 路径

Cognito JWT

io.quarkus.amazon.lambda.http.CognitoPrincipal

requestContext.authorizer.jwt.claims.cognito:username

IAM

io.quarkus.amazon.lambda.http.IAMPrincipal

requestContext.authorizer.iam.userId

Custom Lambda

io.quarkus.amazon.lambda.http.CustomPrincipal

requestContext.authorizer.lambda.principalId

Table 2. REST quarkus-amazon-lambda-rest
Auth Type Principal Class 主体名称的 Json 路径

Cognito

io.quarkus.amazon.lambda.http.CognitoPrincipal

requestContext.authorizer.claims.cognito:username

IAM

io.quarkus.amazon.lambda.http.IAMPrincipal

requestContext.identity.user

Custom Lambda

io.quarkus.amazon.lambda.http.CustomPrincipal

requestContext.authorizer.principalId

如果 cognito:groups 声明存在,那么 Quarkus 将提取并映射这些组到 Quarkus 角色,然后可在授权中使用它们,如“@RolesAllowed”注释。如果您不希望将 cognito:groups 映射到 Quarkus 角色,那么您必须在配置中明确禁用它:

quarkus.lambda-http.map-cognito-to-roles=false

您还可以指定提取角色的不同 Cognito 声明:

quarkus.lambda-http.cognito-role-claim=cognito:roles

默认情况下,它会查找用括号括起来的空格分隔列表中的角色,即 [ user admin ]。您还可以指定正则表达式,以查找声明字符串中的各个角色:

quarkus.lambda-http.cognito-claim-matcher=[^\[\] \t]+

Custom Security Integration

对 AWS 安全的默认支持只会将主体名称映射到 Quarkus Security API,而不会对声明、角色或权限进行任何映射。您可以使用 io.quarkus.amazon.lambda.http.LambdaIdentityProvider 界面实现来完全控制 lambda HTTP 事件中的安全元数据如何映射到 Quarkus Security API。通过实现此界面,您可以执行操作,例如为您的主体定义角色映射,或发布由 IAM 或 Cognito 或您的自定义 Lambda 安全集成提供的其他属性。

HTTP quarkus-amazon-lambda-http
package io.quarkus.amazon.lambda.http;

/**
 * Helper interface that removes some boilerplate for creating
 * an IdentityProvider that processes APIGatewayV2HTTPEvent
 */
public interface LambdaIdentityProvider extends IdentityProvider<LambdaAuthenticationRequest> {
    @Override
    default public Class<LambdaAuthenticationRequest> getRequestType() {
        return LambdaAuthenticationRequest.class;
    }

    @Override
    default Uni<SecurityIdentity> authenticate(LambdaAuthenticationRequest request, AuthenticationRequestContext context) {
        APIGatewayV2HTTPEvent event = request.getEvent();
        SecurityIdentity identity = authenticate(event);
        if (identity == null) {
            return Uni.createFrom().optional(Optional.empty());
        }
        return Uni.createFrom().item(identity);
    }

    /**
     * You must override this method unless you directly override
     * IdentityProvider.authenticate
     *
     * @param event
     * @return
     */
    default SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) {
        throw new IllegalStateException("You must override this method or IdentityProvider.authenticate");
    }
}

对于 HTTP,要覆盖的重要方法是 LambdaIdentityProvider.authenticate(APIGatewayV2HTTPEvent event)。在此基础上,您将根据希望如何从 APIGatewayV2HTTPEvent 映射安全数据来分配一个 SecurityIdentity。

REST quarkus-amazon-lambda-rest
package io.quarkus.amazon.lambda.http;

import java.util.Optional;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;

import io.quarkus.amazon.lambda.http.model.AwsProxyRequest;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;

/**
 * Helper interface that removes some boilerplate for creating
 * an IdentityProvider that processes APIGatewayV2HTTPEvent
 */
public interface LambdaIdentityProvider extends IdentityProvider<LambdaAuthenticationRequest> {
...

    /**
     * You must override this method unless you directly override
     * IdentityProvider.authenticate
     *
     * @param event
     * @return
     */
    default SecurityIdentity authenticate(AwsProxyRequest event) {
        throw new IllegalStateException("You must override this method or IdentityProvider.authenticate");
    }
}

对于 REST,要覆盖的重要方法是 LambdaIdentityProvider.authenticate(AwsProxyRequest event)。在此基础上,您将根据希望如何从 AwsProxyRequest 映射安全数据来分配一个 SecurityIdentity。

您实现的提供程序必须是 CDI Bean。以下是一个示例:

package org.acme;

import java.security.Principal;

import jakarta.enterprise.context.ApplicationScoped;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;

import io.quarkus.amazon.lambda.http.LambdaIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;

@ApplicationScoped
public class CustomSecurityProvider implements LambdaIdentityProvider {
    @Override
    public SecurityIdentity authenticate(APIGatewayV2HTTPEvent event) {
        if (event.getHeaders() == null || !event.getHeaders().containsKey("x-user"))
            return null;
        Principal principal = new QuarkusPrincipal(event.getHeaders().get("x-user"));
        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
        builder.setPrincipal(principal);
        return builder.build();
    }
}

以下是相同的示例,但使用 AWS Gateway REST API:

package org.acme;

import java.security.Principal;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.amazon.lambda.http.model.AwsProxyRequest;

import io.quarkus.amazon.lambda.http.LambdaIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;

@ApplicationScoped
public class CustomSecurityProvider implements LambdaIdentityProvider {
    @Override
    public SecurityIdentity authenticate(AwsProxyRequest event) {
        if (event.getMultiValueHeaders() == null || !event.getMultiValueHeaders().containsKey("x-user"))
            return null;
        Principal principal = new QuarkusPrincipal(event.getMultiValueHeaders().getFirst("x-user"));
        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
        builder.setPrincipal(principal);
        return builder.build();
    }
}

Quarkus 应该自动发现此实现并使用它,而不是前面讨论的默认实现。

Simple SAM Local Principal

如果使用 sam local 测试应用程序,可以通过设置 QUARKUS_AWS_LAMBDA_FORCE_USER_NAME 环境变量来硬编码应用程序运行时要使用的主体名称。

SnapStart

要针对 Lambda SnapStart 优化您的应用程序,请查看 the SnapStart Configuration Documentation