带有RESTEasy Reactive、Undertow或Reactive Routes的Amazon Lambda
通过Quarkus,您可以使用 AWS Gateway HTTP API 或 AWS 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在容器中进行构建)
创建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.5.Final
如果你想使用AWS Gateway REST API,请用这个脚本生成你的项目:
mvn archetype:generate \
-DarchetypeGroupId=io.quarkus \
-DarchetypeArtifactId=quarkus-amazon-lambda-rest-archetype \
-DarchetypeVersion=2.16.5.Final
构建和部署
构建项目:
quarkus build
./mvnw install
这将编译代码并运行生成的项目中包含的单元测试。单元测试和其他的Java项目一样,不需要运行在Amazon上。Quarkus开发模式也可以通过这个扩展来提供。
如果你想构建一个本地可执行文件,请确保你已经正确安装了GraalVM,并且只需向构建添加一个 native
属性
quarkus build --native
./mvnw install -Dnative
如果你在非Linux系统上进行构建,你还需要传入一个属性,指示quarkus使用Docker构建,因为Amazon Lambda需要Linux二进制文件。你可以通过将 -Dquarkus.native.container-build=true 传给你的构建命令来实现。不过,这需要你在本地安装了Docker。
|
quarkus build --native --no-tests -Dquarkus.native.container-build=true
# The --no-tests flag is required only on Windows and macOS.
./mvnw install -Dnative -DskipTests -Dquarkus.native.container-build=true
构建额外生成的文件
运行构建后,你所使用的Quarkus lambda扩展会生成一些额外的文件。对于Maven,这些文件位于构建目录: target/
中,对于Gradle,则位于 build/
中。
-
function.zip
- lambda部署文件 -
sam.jvm.yaml
- sam cli部署脚本 -
sam.native.yaml
- sam cli的原生部署脚本
实时编码和本地模拟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.AwsProxyRequest
和 io.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
。
对二进制类型的响应将会使用base64自动编码。这与使用 quarkus:dev 的行为不同,后者将返回原始字节。亚马逊的API有额外的限制,要求base64编码。一般来说,客户端代码会自动处理这种编码,但在某些自定义情况下,你应该知道你可能需要手动管理这种编码。
|
部署一个本地可执行文件
要部署一个本地可执行文件,你必须用GraalVM构建它。
quarkus build --native --no-tests -Dquarkus.native.container-build=true
# The --no-tests flag is required only on Windows and macOS.
./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-reactive
、 quarkus-reactive-routes
和 quarkus-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.Context
和 com.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.Context
和 io.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
下面是它的映射方式:
认证类型 | 主体类 | 主体名的Json路径 |
---|---|---|
Cognito JWT |
|
|
IAM |
|
|
自定义Lambda |
|
|
认证类型 | 主体类 | 主体名的Json路径 |
---|---|---|
Cognito |
|
|
IAM |
|
|
自定义Lambda |
|
|
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安全集成提供的额外的属性。
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
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.