これは、なにをしたくて書いたもの?
AWSにServerless Java Containerというものがあるのに気づいたので。
サーバレスで王道 Web フレームワークを使う方法 - Awsstatic
これで少し遊んでみようかなと思いまして。
いろいろ試した結果、単に生成したプロジェクトをデプロイしただけになりましたが…。
デプロイ先は、LocalStackとします。
AWS Serverless Java Container
AWS Serverless Java Containerは、Amazon API Gateway+AWS LambdaでJavaの各種フレームワークを使って
作ったアプリケーションを動作させるためのライブラリです。
Amazon API Gatewayとの連携は、プロキシ統合となっています。
ドキュメントは、Wikiにあります。
Home · awslabs/aws-serverless-java-container Wiki · GitHub
対応しているフレームワークとしては、Spring、Spring Boot、Apache Struts 2、Jersey、Sparkがあります。
このライブラリは、サーブレットコンテナとして機能します。
その他、このページ内には非同期初期化、ALBとの統合、セキュリティ、サーブレットフィルター、ロギング等の
テーマが書かれています。
今回は、Spring Bootを使って試してみることにします。
Quick start Spring Boot2 · awslabs/aws-serverless-java-container Wiki · GitHub
現時点で対応しているSpring Bootのバージョンは、2.2〜2.5です。
Spring Boot version 2.2.x, 2.3.x, 2.4.x and 2.5.x.
WebFluxにも対応しているそうです。
今回はこちらを使って作成したプロジェクトを、LocalStackにデプロイしてみます。
環境
今回の環境は、こちら。
LocalStack。
$ localstack --version 0.12.19.2
起動。
$ localstack start
$ samlocal --version SAM CLI, version 1.31.0
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-89-generic", arch: "amd64", family: "unix"
プロジェクトを作成する
AWS Serverless Java Containerを使ったプロジェクトは、Mavenアーキタイプとして作成します。
Quick start Spring Boot2 / Maven archetype
今回は、Spring Bootを使うのでaws-serverless-springboot2-archetype
を指定。
$ mvn archetype:generate \ -DinteractiveMode=false \ -DgroupId=org.littlewings \ -DartifactId=spring-boot2-serverless \ -Dpackage=org.littlewings.spring.serverless \ -Dversion=0.0.1-SNAPSHOT \ -DarchetypeGroupId=com.amazonaws.serverless.archetypes \ -DarchetypeArtifactId=aws-serverless-springboot2-archetype \ -DarchetypeVersion=1.6
手動での構築手順も、記載されています。
Quick start Spring Boot2 / Manual setup / Converting existing projects
生成されたディレクトリ内へ移動。
$ cd spring-boot2-serverless
ディレクトリ内に生成されているファイルは、こんな感じになります。
$ tree . ├── README.md ├── build.gradle ├── pom.xml ├── src │ ├── assembly │ │ └── bin.xml │ ├── main │ │ ├── java │ │ │ └── org │ │ │ └── littlewings │ │ │ └── spring │ │ │ └── serverless │ │ │ ├── Application.java │ │ │ ├── StreamLambdaHandler.java │ │ │ └── controller │ │ │ └── PingController.java │ │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── org │ └── littlewings │ └── spring │ └── serverless │ └── StreamLambdaHandlerTest.java └── template.yml 16 directories, 10 files
ちょっと見てみましょう。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>spring-boot2-serverless</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>Serverless Spring Boot 2 API</name> <url>https://github.com/awslabs/aws-serverless-java-container</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> </parent> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.amazonaws.serverless</groupId> <artifactId>aws-serverless-java-container-springboot2</artifactId> <version>1.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <profiles> <profile> <id>shaded-jar</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <excludes> <exclude>org.apache.tomcat.embed:*</exclude> </excludes> </artifactSet> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile> <profile> <id>assembly-zip</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <plugins> <!-- don't build a jar, we'll use the classes dir --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>default-jar</id> <phase>none</phase> </execution> </executions> </plugin> <!-- select and copy only runtime dependencies to a temporary lib folder --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}${file.separator}lib</outputDirectory> <includeScope>runtime</includeScope> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>zip-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> <configuration> <finalName>${project.artifactId}-${project.version}</finalName> <descriptors> <descriptor>src${file.separator}assembly${file.separator}bin.xml</descriptor> </descriptors> <attach>false</attach> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles> </project>
こちらの依存関係が、AWS Serverless Java ContainerのSpring Boot向けのライブラリですね。
<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-springboot2</artifactId>
<version>1.6</version>
</dependency>
mainクラス。
src/main/java/org/littlewings/spring/serverless/Application.java
package org.littlewings; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; import org.littlewings.controller.PingController; @SpringBootApplication // We use direct @Import instead of @ComponentScan to speed up cold starts // @ComponentScan(basePackages = "org.littlewings.controller") @Import({ PingController.class }) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
ハンドラークラス。
Quick start Spring Boot2 / Create the Lambda handler
src/main/java/org/littlewings/spring/serverless/StreamLambdaHandler.java
package org.littlewings; import com.amazonaws.serverless.exceptions.ContainerInitializationException; import com.amazonaws.serverless.proxy.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class StreamLambdaHandler implements RequestStreamHandler { private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler; static { try { handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); // For applications that take longer than 10 seconds to start, use the async builder: // long startTime = Instant.now().toEpochMilli(); // handler = new SpringBootProxyHandlerBuilder() // .defaultProxy() // .asyncInit(startTime) // .springBootApplication(Application.class) // .buildAndInitialize(); } catch (ContainerInitializationException e) { // if we fail here. We re-throw the exception to force another cold start e.printStackTrace(); throw new RuntimeException("Could not initialize Spring Boot application", e); } } @Override public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { handler.proxyStream(inputStream, outputStream, context); } }
RestControllerクラス。
src/main/java/org/littlewings/spring/serverless/controller/PingController.java
package org.littlewings.controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import java.util.HashMap; import java.util.Map; @RestController @EnableWebMvc public class PingController { @RequestMapping(path = "/ping", method = RequestMethod.GET) public Map<String, String> ping() { Map<String, String> pong = new HashMap<>(); pong.put("pong", "Hello, World!"); return pong; } }
ふつうのControllerですね。
application.properties
は、ログレベルが変更されているだけですね。
src/main/resources/application.properties
# Reduce logging level to make sure the application works with SAM local # https://github.com/awslabs/aws-serverless-java-container/issues/134 logging.level.root=WARN
テストコード。
src/test/java/org/littlewings/spring/serverless/StreamLambdaHandlerTest.java
package org.littlewings; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.services.lambda.runtime.Context; import org.junit.BeforeClass; import org.junit.Test; import javax.ws.rs.HttpMethod; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import static org.junit.Assert.*; public class StreamLambdaHandlerTest { private static StreamLambdaHandler handler; private static Context lambdaContext; @BeforeClass public static void setUp() { handler = new StreamLambdaHandler(); lambdaContext = new MockLambdaContext(); } @Test public void ping_streamRequest_respondsWithHello() { InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) .buildStream(); ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); handle(requestStream, responseStream); AwsProxyResponse response = readResponse(responseStream); assertNotNull(response); assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); assertFalse(response.isBase64Encoded()); assertTrue(response.getBody().contains("pong")); assertTrue(response.getBody().contains("Hello, World!")); assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)); } @Test public void invalidResource_streamRequest_responds404() { InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) .buildStream(); ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); handle(requestStream, responseStream); AwsProxyResponse response = readResponse(responseStream); assertNotNull(response); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode()); } private void handle(InputStream is, ByteArrayOutputStream os) { try { handler.handleRequest(is, os, lambdaContext); } catch (IOException e) { e.printStackTrace(); fail(e.getMessage()); } } private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { try { return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); } catch (IOException e) { e.printStackTrace(); fail("Error while parsing response: " + e.getMessage()); } return null; } }
AWS SAM用のテンプレート…のテンプレートみたいです。
template.yml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: AWS Serverless Spring Boot 2 API - org.littlewings::spring-boot2-serverless Globals: Api: EndpointConfiguration: REGIONAL Resources: SpringBoot2ServerlessFunction: Type: AWS::Serverless::Function Properties: Handler: org.littlewings.StreamLambdaHandler::handleRequest Runtime: java8 CodeUri: . MemorySize: 512 Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: ProxyResource: Type: Api Properties: Path: /{proxy+} Method: any Outputs: SpringBoot2ServerlessApi: Description: URL for application Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ping' Export: Name: SpringBoot2ServerlessApi
ちょっと、中を見てみましょう。
mainクラスの方は、@Import
でRestControllerをインポートしています。
@SpringBootApplication // We use direct @Import instead of @ComponentScan to speed up cold starts // @ComponentScan(basePackages = "org.littlewings.controller") @Import({ PingController.class }) public class Application {
これは、コールドスタート向けの最適化のひとつで、Component scanを避けよということみたいです。
Quick start Spring Boot2 / Optimizing cold start times / Avoid component scan
その他にも最適化についていくつか記載があるので、見ておくとよいでしょう。
Quick start Spring Boot2 / Optimizing cold start times
アプリケーションをビルドして、LocalStackにデプロイする
では、アプリケーションをビルドしてLocalStackにデプロイしましょう。
アプリケーションのビルド方法は、README.md
に書かれています。
README.md
$ mvn archetype:generate -DartifactId=spring-boot2-serverless -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=1.6 -DgroupId=org.littlewings -Dversion=0.0.1-SNAPSHOT -Dinteractive=false $ cd spring-boot2-serverless $ sam build Building resource 'SpringBoot2ServerlessFunction' Running JavaGradleWorkflow:GradleBuild Running JavaGradleWorkflow:CopyArtifacts Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Invoke Function: sam local invoke [*] Deploy: sam deploy --guided
まずはビルド。このコマンドは、samlocal
ではなくsam
で行わないとうまくいきませんでした(Gradleを起動しようと
します)。
$ sam build
ですが、内部で行われるmvn install
で失敗します。
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ spring-boot2-serverless --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.934 s [INFO] Finished at: 2021-10-27T23:10:18+09:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-install-plugin:2.5.2:install (default-install) on project spring-boot2-serverless: The packaging for this project did not assign a file to the build artifact -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
パッケージングの形式がJARなのに
<groupId>org.littlewings</groupId> <artifactId>spring-boot2-serverless</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging>
デフォルトのProfileでは、JARファイルは作成されない設定になっていることが原因みたいです。
<profile> <id>assembly-zip</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <plugins> <!-- don't build a jar, we'll use the classes dir --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>default-jar</id> <phase>none</phase> </execution> </executions> </plugin>
試しに、JARファイルを作成するようにコメントアウトして
<!-- don't build a jar, we'll use the classes dir --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>default-jar</id> <!-- <phase>none</phase> --> </execution> </executions> </plugin>
ビルド。
$ sam build
今度は成功します。
Running JavaMavenWorkflow:CopySource Running JavaMavenWorkflow:MavenBuild Running JavaMavenWorkflow:MavenCopyDependency Running JavaMavenWorkflow:MavenCopyArtifacts Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Invoke Function: sam local invoke [*] Deploy: sam deploy --guided
AWS SAMテンプレートは.aws-sam/build
ディレクトリ内にできあがるのですが、プロジェクトのルートディレクトリにある
テンプレートからはちょっと変更されて出力されるようですね。
$ diff template.yml .aws-sam/build/template.yaml 7d6 < 14c13 < CodeUri: . --- > CodeUri: SpringBoot2ServerlessFunction 24d22 < 28c26,27 < Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ping' --- > Value: > Fn::Sub: https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ping
CodeUri
がポイントな気がします。
ちなみに、LocalStackにAWS SAMを使ってデプロイする場合は、ちょっとAWS SAMテンプレートを変更する
必要があって。
Resources: SpringBoot2ServerlessFunction: Type: AWS::Serverless::Function Properties: Handler: org.littlewings.StreamLambdaHandler::handleRequest Runtime: java8 CodeUri: . MemorySize: 512 #Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: ProxyResource: Type: Api Properties: #Path: /{proxy+} #Method: any Path: /ping Method: get
Policies
があるとデプロイが止まるので外します…。Path
が/{proxy+}
およびMethod
がany
だと、デプロイは
できてもAmazon API Gateway経由で呼び出せなくなるので、微妙ですが単一のPath
とMethod
に
マッピングします。
Path
とMethod
に関しては、あとでオマケとして書きます。
テンプレートを変更したので、再度ビルド。
$ sam build
LocalStack上にAmazon S3バケットを作成して
$ awslocal s3 mb s3://my-bucket
デプロイ。
$ samlocal deploy --template-file .aws-sam/build/template.yaml --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
成功したら、REST APIのIDを取得して
$ REST_API_ID=`awslocal apigateway get-rest-apis --query 'items[0].id' --output text`
呼び出します。
$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/ping {"pong":"Hello, World!"}
OKですね。
Java 11に変更してみる
生成されたプロジェクトはJava 8だったので、Java 11に変更してみましょう。
まずはpom.xml
を変更。
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>
次に、AWS SAMテンプレートのRuntime
も変更。Runtime
をjava11
としておきます。
Resources: SpringBoot2ServerlessFunction: Type: AWS::Serverless::Function Properties: Handler: org.littlewings.StreamLambdaHandler::handleRequest Runtime: java11 CodeUri: . MemorySize: 512 #Policies: AWSLambdaBasicExecutionRole Timeout: 30 Events: ProxyResource: Type: Api Properties: #Path: /{proxy+} #Method: any Path: /ping Method: get
ビルドしてデプロイ。
$ sam build $ awslocal s3 mb s3://my-bucket $ samlocal deploy --template-file .aws-sam/build/template.yaml --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
動作確認。
$ REST_API_ID=`awslocal apigateway get-rest-apis --query 'items[0].id' --output text` $ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/ping {"pong":"Hello, World!"}
Java 11でもOKですね。
ビルド結果
AWS SAMでビルドした際にAWS SAMテンプレートが.aws-sam/build
ディレクトリに生成されていましたが、
このディレクトリってどうなっているんでしょう。
こうなっています。
$ tree .aws-sam/build/SpringBoot2ServerlessFunction .aws-sam/build/SpringBoot2ServerlessFunction ├── application.properties ├── lib │ ├── ch.qos.logback.logback-classic-1.2.4.jar │ ├── ch.qos.logback.logback-core-1.2.4.jar │ ├── com.amazonaws.aws-lambda-java-core-1.2.1.jar │ ├── com.amazonaws.serverless.aws-serverless-java-container-core-1.6.jar │ ├── com.amazonaws.serverless.aws-serverless-java-container-springboot2-1.6.jar │ ├── com.fasterxml.jackson.core.jackson-annotations-2.12.4.jar │ ├── com.fasterxml.jackson.core.jackson-core-2.12.4.jar │ ├── com.fasterxml.jackson.core.jackson-databind-2.12.4.jar │ ├── com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.12.4.jar │ ├── com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.12.4.jar │ ├── com.fasterxml.jackson.module.jackson-module-afterburner-2.12.4.jar │ ├── com.fasterxml.jackson.module.jackson-module-parameter-names-2.12.4.jar │ ├── com.sun.activation.jakarta.activation-1.2.2.jar │ ├── commons-codec.commons-codec-1.15.jar │ ├── commons-fileupload.commons-fileupload-1.4.jar │ ├── commons-io.commons-io-2.2.jar │ ├── jakarta.annotation.jakarta.annotation-api-1.3.5.jar │ ├── javax.servlet.javax.servlet-api-4.0.1.jar │ ├── javax.ws.rs.javax.ws.rs-api-2.1.jar │ ├── org.apache.httpcomponents.httpclient-4.5.13.jar │ ├── org.apache.httpcomponents.httpcore-4.4.14.jar │ ├── org.apache.httpcomponents.httpmime-4.5.13.jar │ ├── org.apache.logging.log4j.log4j-api-2.14.1.jar │ ├── org.apache.logging.log4j.log4j-to-slf4j-2.14.1.jar │ ├── org.jetbrains.annotations-17.0.0.jar │ ├── org.slf4j.jul-to-slf4j-1.7.32.jar │ ├── org.slf4j.slf4j-api-1.7.32.jar │ ├── org.springframework.boot.spring-boot-2.5.3.jar │ ├── org.springframework.boot.spring-boot-autoconfigure-2.5.3.jar │ ├── org.springframework.boot.spring-boot-starter-2.5.3.jar │ ├── org.springframework.boot.spring-boot-starter-json-2.5.3.jar │ ├── org.springframework.boot.spring-boot-starter-logging-2.5.3.jar │ ├── org.springframework.boot.spring-boot-starter-web-2.5.3.jar │ ├── org.springframework.spring-aop-5.3.9.jar │ ├── org.springframework.spring-beans-5.3.9.jar │ ├── org.springframework.spring-context-5.3.9.jar │ ├── org.springframework.spring-core-5.3.9.jar │ ├── org.springframework.spring-expression-5.3.9.jar │ ├── org.springframework.spring-jcl-5.3.9.jar │ ├── org.springframework.spring-web-5.3.9.jar │ ├── org.springframework.spring-webmvc-5.3.9.jar │ └── org.yaml.snakeyaml-1.28.jar └── org └── littlewings ├── Application.class ├── StreamLambdaHandler.class └── controller └── PingController.class 4 directories, 46 files
こう見ると、CodeUri:
で指定されていたSpringBoot2ServerlessFunction
というのは、
.aws-sam/build/SpringBoot2ServerlessFunction
ディレクトリを指していることがわかります。
あれ?デプロイしているのは、この中身なんですね?
Maven Assembly Pluginでzipファイルを作っていたはずですが、こちらは使わないのでしょうか…。
LocalStackでPathやMethodを/{proxy+}やanyにすると動かないという話
ちょっとオマケです。
今回、LocalStackにデプロイする際に、AWS SAMテンプレートを修正してPath
とMethod
で
/{proxy+}
やany
を使わないように修正しました。
AWS SAMテンプレートでデプロイすると、Amazon API Gatewayでのリソース定義が以下のようになるのですが
これだとAmazon API GatewayとAWS Lambdaをうまく統合できません。
{ "id": "xxxxx", "parentId": "xxxxx", "pathPart": "{proxy+}", "path": "/{proxy+}", "resourceMethods": { "X-AMAZON-APIGATEWAY-ANY-METHOD": { "httpMethod": "X-AMAZON-APIGATEWAY-ANY-METHOD", "apiKeyRequired": false, "methodIntegration": { "type": "aws_proxy", "httpMethod": "X-AMAZON-APIGATEWAY-ANY-METHOD",
AWS CLIやTerraformで作成すると以下のようになり、これだとAmazon API GatewayとAWS Lambdaを統合できます。
{ "id": "xxxxx", "parentId": "xxxxx", "pathPart": "{proxy+}", "path": "/{proxy+}", "resourceMethods": { "ANY": { "httpMethod": "ANY", "authorizationType": "NONE", "apiKeyRequired": false, "methodIntegration": { "type": "AWS_PROXY", "httpMethod": "POST",
どうなってるんでしょうね?このあたりが関係している気はしますが。
スタックトレースを見てみる
pom.xml
やsrc/assembly/bin.xml
を見ると、とにかくTomcatを除外しようとしているように見えます。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>
src/assembly/bin.xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd"> <id>lambda-package</id> <formats> <format>zip</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <!-- copy runtime dependencies with some exclusions --> <fileSet> <directory>${project.build.directory}${file.separator}lib</directory> <outputDirectory>lib</outputDirectory> <excludes> <exclude>tomcat-embed*</exclude> </excludes> </fileSet> <!-- copy all classes --> <fileSet> <directory>${project.build.directory}${file.separator}classes</directory> <includes> <include>**</include> </includes> <outputDirectory>${file.separator}</outputDirectory> </fileSet> </fileSets> </assembly>
では、このアプリケーションはどのようなWebコンテナ上で動作しているのでしょう?
スタックトレースを記録してみました。
@RequestMapping(path = "/ping", method = RequestMethod.GET) public Map<String, String> ping() { Thread.dumpStack(); Map<String, String> pong = new HashMap<>(); pong.put("pong", "Hello, World!"); return pong; }
これはAmazon CloudWatch Logsに残るので、確認してみるとこんな感じでした。
2021-10-27T14:59:17.970000+00:00 2021/10/27/[LATEST]36e27ed6 14:59:13.622 [main] INFO com.amazonaws.serverless.proxy.internal.LambdaContainerHandler - Starting Lambda Container Handler 2021-10-27T14:59:17.973000+00:00 2021/10/27/[LATEST]36e27ed6 . ____ _ __ _ _ 2021-10-27T14:59:17.974000+00:00 2021/10/27/[LATEST]36e27ed6 /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 2021-10-27T14:59:17.976000+00:00 2021/10/27/[LATEST]36e27ed6 ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 2021-10-27T14:59:17.977000+00:00 2021/10/27/[LATEST]36e27ed6 \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 2021-10-27T14:59:17.979000+00:00 2021/10/27/[LATEST]36e27ed6 ' |____| .__|_| |_|_| |_\__, | / / / / 2021-10-27T14:59:17.980000+00:00 2021/10/27/[LATEST]36e27ed6 =========|_|==============|___/=/_/_/_/ 2021-10-27T14:59:17.982000+00:00 2021/10/27/[LATEST]36e27ed6 :: Spring Boot :: (v2.5.3) 2021-10-27T14:59:17.986000+00:00 2021/10/27/[LATEST]36e27ed6 START RequestId: a99e9e98-9c7a-18c4-25ef-4cd534ee31b9 Version: $LATEST 2021-10-27T14:59:17.988000+00:00 2021/10/27/[LATEST]36e27ed6 2021-10-27T14:59:17.990000+00:00 2021/10/27/[LATEST]36e27ed6 java.lang.Exception: Stack trace 2021-10-27T14:59:17.991000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/java.lang.Thread.dumpStack(Unknown Source) 2021-10-27T14:59:17.993000+00:00 2021/10/27/[LATEST]36e27ed6 at org.littlewings.controller.PingController.ping(PingController.java:16) 2021-10-27T14:59:17.994000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 2021-10-27T14:59:17.996000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) 2021-10-27T14:59:17.997000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) 2021-10-27T14:59:17.999000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/java.lang.reflect.Method.invoke(Unknown Source) 2021-10-27T14:59:18+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) 2021-10-27T14:59:18.002000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) 2021-10-27T14:59:18.003000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) 2021-10-27T14:59:18.005000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) 2021-10-27T14:59:18.007000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) 2021-10-27T14:59:18.008000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) 2021-10-27T14:59:18.010000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1064) 2021-10-27T14:59:18.011000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) 2021-10-27T14:59:18.013000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) 2021-10-27T14:59:18.014000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) 2021-10-27T14:59:18.016000+00:00 2021/10/27/[LATEST]36e27ed6 at javax.servlet.http.HttpServlet.service(HttpServlet.java:645) 2021-10-27T14:59:18.017000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) 2021-10-27T14:59:18.019000+00:00 2021/10/27/[LATEST]36e27ed6 at javax.servlet.http.HttpServlet.service(HttpServlet.java:750) 2021-10-27T14:59:18.020000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.servlet.FilterChainManager$ServletExecutionFilter.doFilter(FilterChainManager.java:356) 2021-10-27T14:59:18.022000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.servlet.FilterChainHolder.doFilter(FilterChainHolder.java:90) 2021-10-27T14:59:18.023000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) 2021-10-27T14:59:18.025000+00:00 2021/10/27/[LATEST]36e27ed6 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) 2021-10-27T14:59:18.027000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.servlet.FilterChainHolder.doFilter(FilterChainHolder.java:90) 2021-10-27T14:59:18.028000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler.doFilter(AwsLambdaServletContainerHandler.java:156) 2021-10-27T14:59:18.030000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler.handleRequest(SpringBootLambdaContainerHandler.java:180) 2021-10-27T14:59:18.031000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler.handleRequest(SpringBootLambdaContainerHandler.java:53) 2021-10-27T14:59:18.033000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.LambdaContainerHandler.proxy(LambdaContainerHandler.java:214) 2021-10-27T14:59:18.034000+00:00 2021/10/27/[LATEST]36e27ed6 at com.amazonaws.serverless.proxy.internal.LambdaContainerHandler.proxyStream(LambdaContainerHandler.java:257) 2021-10-27T14:59:18.036000+00:00 2021/10/27/[LATEST]36e27ed6 at org.littlewings.StreamLambdaHandler.handleRequest(StreamLambdaHandler.java:38) 2021-10-27T14:59:18.037000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 2021-10-27T14:59:18.039000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) 2021-10-27T14:59:18.040000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) 2021-10-27T14:59:18.042000+00:00 2021/10/27/[LATEST]36e27ed6 at java.base/java.lang.reflect.Method.invoke(Unknown Source) 2021-10-27T14:59:18.044000+00:00 2021/10/27/[LATEST]36e27ed6 at lambdainternal.EventHandlerLoader$StreamMethodRequestHandler.handleRequest(EventHandlerLoader.java:375) 2021-10-27T14:59:18.045000+00:00 2021/10/27/[LATEST]36e27ed6 at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:899) 2021-10-27T14:59:18.047000+00:00 2021/10/27/[LATEST]36e27ed6 at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:258) 2021-10-27T14:59:18.048000+00:00 2021/10/27/[LATEST]36e27ed6 at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:192) 2021-10-27T14:59:18.050000+00:00 2021/10/27/[LATEST]36e27ed6 at lambdainternal.AWSLambda.main(AWSLambda.java:187)
独自のサーブレットフィルターのようなものがありますね。
このあたりのようです。
まとめ
AWS Serverless Java ContainerとSpring Bootを使い、LocalStackのAmazon API Gateway+AWS Lambdaに
アプリケーションをデプロイしてみました。
デプロイまわりはだいぶてこずったのですが、だいたい雰囲気はわかりました。