Amazon Lambda with RESTEasy Reactive, Undertow, or Reactive Routes

preview

通过Quarkus,您可以使用 AWS Gateway HTTP APIAWS Gateway REST API 将您喜欢的Java HTTP框架部署为Amazon Lambda。这意味着你可以将用RESTEasy Reactive(JAX-RS)、Undertow(servlet)、Reactive Routes、 Funqy HTTP 或任何其他Quarkus HTTP框架编写的微服务部署为AWS Lambda。

你可以将你的Lambda部署为一个纯Java jar,或者你可以将你的项目编译为一个原生镜像,并以较少的内存占用和启动时间来部署。我们的集成还可以生成SAM部署文件,这些文件可以被 亚马逊的SAM框架 来使用。

Quarkus对每个网关API都有一个不同的扩展。HTTP网关API是在 quarkus-amazon-lambda-http 扩展中实现的。REST网关API是在 quarkus-amazon-lambda-rest 扩展中实现的。如果你对使用哪种网关产品感到困惑,亚马逊有一个 很好的指南 来帮助你做这个决定。

与大多数Quarkus扩展一样,Quarkus AWS Lambda HTTP/REST扩展支持实时编码。

这项技术被认为是preview。

preview(预览) 中,不保证向后兼容和在生态系统中的存在。具体的改进可能需要改变配置或API,并且正在计划变得 稳定 。欢迎在我们的 邮件列表 中提供反馈,或在我们的 GitHub问题列表 中提出问题。

For a full list of possible statuses, check our FAQ entry.

先决条件

要完成这个指南,你需要:

  • 大概30 minutes

  • 编辑器

  • 安装JDK 11以上版本并正确配置了 JAVA_HOME

  • Apache Maven 3.8.6

  • 如果你愿意的话,还可以选择使用Quarkus CLI

  • 如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)

  • An Amazon AWS account

  • AWS SAM CLI

入门指南

本指南将引导您通过Maven原型生成一个Java项目示例。随后,还将介绍项目的结构,以便您能够调整现有的项目以使用Amazon Lambda。

安装AWS位

安装所有的AWS位可能是本指南中最困难的事情。请确保你遵循安装AWS SAM CLI的所有步骤。

创建Maven部署项目

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

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

mvn archetype:generate \
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-http-archetype \
       -DarchetypeVersion=2.16.12.Final

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

mvn archetype:generate \
       -DarchetypeGroupId=io.quarkus \
       -DarchetypeArtifactId=quarkus-amazon-lambda-rest-archetype \
       -DarchetypeVersion=2.16.12.Final

构建和部署

构建项目。

CLI
quarkus build
Maven
./mvnw install

这将编译代码并运行生成的项目中包含的单元测试。单元测试和其他的Java项目一样,不需要运行在Amazon上。Quarkus开发模式也可以通过这个扩展来提供。

如果你想构建一个本地可执行文件,请确保你已经正确安装了GraalVM,并且只需向构建添加一个 native 属性

CLI
quarkus build --native
Maven
./mvnw install -Dnative
If you are building on a non-Linux system, you will need to also pass in a property instructing quarkus to use a Docker build as Amazon Lambda requires Linux binaries. You can do this by passing -Dquarkus.native.container-build=true to your build command. This requires you to have Docker installed locally, however.
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

构建额外生成的文件

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

  • function.zip - lambda deployment file

  • sam.jvm.yaml - sam cli deployment script

  • sam.native.yaml - sam cli deployment script for native

实时编码和本地模拟AWS Lambda环境

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

当使用Quarkus开发模式时,只需对 http://localhost:8080 调用HTTP请求,就像通常测试REST端点时所做的那样。这个请求将访问模拟事件服务器,并将被转换为API Gateway json消息,被Quarkus Lambda Poll循环所消费。

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

如果你想在你的测试中模拟更复杂的API Gateway事件,那么请使用API Gateway json事件手动对 http://localhost:8080/_lambda_ (测试模式下的8081端口)做一个HTTP POST。这些事件将被直接放在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 Lambda发送一个带有HTTP请求的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相对应。

也会为 @NativeImageTest@QuarkusIntegrationTest 测试启动模拟事件服务器,因此其也会与本地二进制文件一起工作。所有这些都为SAM CLI本地测试提供了类似的功能,而没有Docker的开销。

最后,如果端口8080或端口8081在你的电脑上不可用,你可以使用application.properties修改开发和测试模式的端口

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

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

用SAM CLI模拟Amazon Lambda部署

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

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

这将启动一个模仿亚马逊的Lambda部署环境的Docker容器。一旦环境启动了,你就可以在你的浏览器中调用lambda示例,方法是去:

在控制台,你会看到lambda的启动信息。这个特定的部署启动了一个JVM,并将你的lambda作为纯Java加载。

部署到AWS

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

回答所有的问题,你的lambda将被部署,必要的钩子将被设置到API网关。如果一切部署成功,你的微服务的根URL将被输出到控制台。类似这样:

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

Value 属性是你的lambda的根URL。把它复制到你的浏览器,并在末尾添加 hello

Responses for binary types will be automatically encoded with base64. This is different from the behavior using quarkus:dev which will return the raw bytes. Amazon’s API has additional restrictions requiring the base64 encoding. In general, client code will automatically handle this encoding but in certain custom situations, you should be aware you may need to manually manage that encoding.

部署一个本地可执行文件

要部署一个本地可执行文件,你必须用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

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

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

要部署到AWS Lambda:

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

检查POM

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

另外,至少在生成的Maven原型 pom.xml 中, quarkus-resteasy-reactivequarkus-reactive-routesquarkus-undertow 依赖项都是可选的。选择你想使用的HTTP框架(JAX-RS、Reactive Routes和/或Servlet),并删除其他依赖项以缩小部署规模。

检查sam.yaml

sam.yaml 语法已经超出了本文档的范围。有几件事情必须强调,以防你要制作你自己的自定义 sam.yaml 部署文件。

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

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

这个处理程序是lambda运行时和你正在使用的Quarkus HTTP框架(JAX-RS、Servlet等)之间的桥梁。

如果你想使用原生,那么必须为本地GraalVM部署设置一个环境变量。如果你看一下 sam.native.yaml ,你会看到这个变量:

        Environment:
          Variables:
            DISABLE_SIGNAL_HANDLERS: true

这个环境变量解决了Quarkus和Amazon Lambda Custom Runtime环境之间的一些不兼容问题。

最后,对于AWS Gateway REST API的部署,有一件特别的事情。该API假定HTTP响应体是文本,除非你通过配置明确告诉它哪些媒体类型是二进制。为了使事情更简单,Quarkus扩展强制对所有的HTTP响应信息进行二进制(base 64)编码, sam.yaml 文件必须配置API网关,以假设所有的媒体类型都是二进制:

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

可注入的AWS上下文变量

如果您使用RESTEasy Reactive和JAX-RS,您可以使用JAX-RS @Context 注解将各种AWS上下文变量注入到JAX-RS资源类中,或者注入到带有CDI @Inject 注解的任何其他地方。

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

import javax.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 javax.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) { }

}

使用AWS XRay和GraalVM进行追踪

如果你正在构建原生镜像,并希望在你的lambda中使用 AWS X-Ray 追踪 ,你将需要在你的pom中把 quarkus-amazon-lambda-xray 作为一个依赖项来包括。AWS X-Ray库与GraalVM并不完全兼容,因此我们不得不做一些整合工作来使其可以正常工作。

安全集成

当你在API网关上调用一个HTTP请求时,网关会把这个HTTP请求转换成一个JSON事件文档,并转发给Quarkus Lambda。Quarkus Lambda解析这个json,并将其转换为HTTP请求的内部表示,这样它就可以被Quarkus支持的任何HTTP框架(JAX-RS、servlet、Reactive Routes)所消费。

API Gateway支持许多不同的方式来对你的HTTP端点进行安全调用,这些端点是由Lambda和Quarkus支持的。如果你启用了它,Quarkus会自动解析 事件json文档 的相关部分,查找基于安全的元数据,并在内部注册一个 java.security.Principal ,通过注入 javax.ws.rs.core.SecurityContext ,通过Servlet中的 HttpServletRequest.getUserPrincipal() ,以及Reactive Routes中的 RouteContext.user() ,可以在JAX-RS中进行查找。如果你了解更多的安全信息, Principal 对象可以被类型转化为一个能给你更多信息的类。

要启用这一安全特性,请将此添加到你的 application.properties 文件中:

quarkus.lambda-http.enable-security=true

下面是它的映射方式:

Table 1. HTTP quarkus-amazon-lambda-http
认证类型 主体类 主体名的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

自定义Lambda

io.quarkus.amazon.lambda.http.CustomPrincipal

requestContext.authorizer.lambda.principalId

Table 2. REST quarkus-amazon-lambda-rest
认证类型 主体类 主体名的Json路径

Cognito

io.quarkus.amazon.lambda.http.CognitoPrincipal

requestContext.authorizer.claims.cognito:username

IAM

io.quarkus.amazon.lambda.http.IAMPrincipal

requestContext.identity.user

自定义Lambda

io.quarkus.amazon.lambda.http.CustomPrincipal

requestContext.authorizer.principalId

If the cognito:groups claim is present, then Quarkus will extract and map those groups to Quarkus roles which can then be used in authorization with annotations like @RolesAllowed. If you do not want to map cognito:groups to Quarkus roles, then you must explicitly disable it in configuration:

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

You can also specify a different Cognito claim to extract roles from:

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

By default, it expects roles in a space delimited list enclosed in brackets i.e. [ user admin ]. You can specify the regular expression to use to find individual roles in the claim string too:

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

自定义安全集成

对AWS安全的默认支持只是将主体名称映射到Quarkus安全API,并没有对声明、角色或权限进行映射。你可以使用 io.quarkus.amazon.lambda.http.LambdaIdentityProvider 接口的实现来完全控制lambda HTTP事件中的安全元数据是如何被映射到Quarkus安全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 javax.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 javax.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应该自动发现这个实现并使用它,而不是前面讨论的默认实现。

简单的SAM本地主体

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

SnapStart

To optimize your application for Lambda SnapStart, check the SnapStart Configuration Documentation.