CLOVER🍀

That was when it all began.

AWS Serverless Java Container × Spring Bootで、LocalStackのAmazon API Gateway+AWS Lambdaにデプロイする

これは、なにをしたくて書いたもの?

AWSにServerless Java Containerというものがあるのに気づいたので。

GitHub - awslabs/aws-serverless-java-container: A Java wrapper to run Spring, Jersey, Spark, and other apps inside AWS Lambda.

サーバレスで王道 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があります。

このライブラリは、サーブレットコンテナとして機能します。

How it works

その他、このページ内には非同期初期化、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

AWS SAM+LocalStackのプラグイン。

$ samlocal --version
SAM CLI, version 1.31.0

プロジェクトをビルドするのに使用するJava、Maven。

$ 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",

どうなってるんでしょうね?このあたりが関係している気はしますが。

Support resources with HTTP method 'ANY' in API Gateway by crypticmind · Pull Request #814 · localstack/localstack · GitHub

スタックトレースを見てみる

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)

独自のサーブレットフィルターのようなものがありますね。

このあたりのようです。

https://github.com/awslabs/aws-serverless-java-container/tree/aws-serverless-java-container-1.6/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet

まとめ

AWS Serverless Java ContainerとSpring Bootを使い、LocalStackのAmazon API Gateway+AWS Lambdaに
アプリケーションをデプロイしてみました。

デプロイまわりはだいぶてこずったのですが、だいたい雰囲気はわかりました。

これで少し、JavaとAWS Lambdaの組み合わせはひと段落にしようかなと思います。