これは、なにをしたくて書いたもの?
前にSpring Cloud Function AWS Adapterを使って、AWS Lambda関数をLocalStackにデプロイしてみました。
Spring Cloud Function AWS AdapterでAWS Lambda関数を作成して、LocalStackにデプロイしてみる - CLOVER🍀
Spring Cloud Function AWS Adapterには、Amazon API Gatewayと組み合わせるための機能もあるみたいなので、
こちらを試してみることにしました。
SpringBootApiGatewayRequestHandler
Spring Cloud Function AWS Adapterのドキュメントには、プラットフォーム固有の機能に関する記述があります。
Spring Cloud Function Reference Documentation / AWS Adapter / Platform Specific Features
Amazon API Gatewayとのプロキシ統合も、そのひとつです。
org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler
というクラスが
提供されています。
Spring Cloud Function Reference Documentation / AWS Adapter / HTTP and API Gateway
なんですけど、よく見るとdeprecatedなんですけどね。
まあ、今回はドキュメントに載っている情報をそのまま使うとしましょう。
環境
今回の環境は、こちら。
LocalStack。
$ localstack --version 0.12.19.1
起動。
$ LAMBDA_EXECUTOR=docker-reuse localstack start
アプリケーションを作成するJava環境。
$ 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"
Spring Cloud Functionを使ったプロジェクトそのものは、JARファイルを作ることしかしないので、LocalStack側に
Amazon API Gateway等の構築が必要です。
これには、Terraformを使うことにします。
$ terraform version Terraform v1.0.9 on linux_amd64
合わせて、AWS Lambda関数のデプロイもTerraformで行います。
プロジェクトを作成する
まずは、Spring Cloud Functionを使うプロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \ -d dependencies=cloud-function,webflux \ -d groupId=org.littlewings \ -d artifactId=hello-spring-cloud-function-api-gateway-with-lambda \ -d packageName=org.littlewings.spring.cloudfuntion.aws \ -d bootVersion=2.5.6 \ -d javaVersion=11 \ -d type=maven-project \ -d baseDir=hello-spring-cloud-function-api-gateway-with-lambda | tar zxvf - $ cd hello-spring-cloud-function-api-gateway-with-lambda
含まれるソースコードは、いったん削除。
$ rm src/main/java/org/littlewings/spring/cloudfuntion/aws/DemoApplication.java src/test/java/org/littlewings/spring/cloudfuntion/aws/DemoApplicationTests.java
依存関係とMavenプラグインの設定を、このあたりの情報を見て調整します。
Spring Cloud Function Reference Documentation / AWS Adapter / Build file setup
Spring Cloud Function Reference Documentation / AWS Adapter / Notes on JAR Layout
こんな感じになりました。
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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.6</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.littlewings</groupId> <artifactId>hello-spring-cloud-function-api-gateway-with-lambda</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> <spring-cloud.version>2020.0.4</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-adapter-aws</artifactId> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>3.10.0</version> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.2.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot.experimental</groupId> <artifactId>spring-boot-thin-layout</artifactId> <version>1.0.27.RELEASE</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <shadedArtifactAttached>true</shadedArtifactAttached> <shadedClassifierName>aws</shadedClassifierName> </configuration> </plugin> </plugins> </build> </project>
ポイントは、この依存関係角追加と
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-adapter-aws</artifactId> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>3.10.0</version> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.2.1</version> <scope>provided</scope> </dependency>
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot.experimental</groupId> <artifactId>spring-boot-thin-layout</artifactId> <version>1.0.27.RELEASE</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <shadedArtifactAttached>true</shadedArtifactAttached> <shadedClassifierName>aws</shadedClassifierName> </configuration> </plugin> </plugins>
AWS Lambda関数を作成して、LocalStackにデプロイする
次に、AWS Lambda関数に相当するクラスを作成します。
src/main/java/org/littlewings/spring/cloudfuntion/aws/App.java
package org.littlewings.spring.cloudfuntion.aws; import java.util.Map; import java.util.function.Function; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import reactor.core.publisher.Mono; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } // @Bean // public Function<Mono<APIGatewayProxyRequestEvent>, Mono<APIGatewayProxyResponseEvent> upper(ObjectMapper mapper) { @Bean public Function<Mono<Map<String, Object>>, Mono<Map<String, Object>>> upper() { return request -> request.map(r -> Map.of("result", r.get("message").toString().toUpperCase())); } }
関数は、AWS Lambdaとして単純にデプロイした時と同様、message
キーに対応するメッセージを大文字にして
result
として返すだけにしています。
あと、こちらを見てちょっと勘違いしていたのですが
Spring Cloud Function Reference Documentation / AWS Adapter / HTTP and API Gateway
コメントアウトしていますが、作成する関数自体のリクエスト、レスポンスはAPIGatewayProxyRequestEvent
や
APIGatewayProxyResponseEvent
である必要はないみたいです。
// @Bean // public Function<Mono<APIGatewayProxyRequestEvent>, Mono<APIGatewayProxyResponseEvent> upper(ObjectMapper mapper) {
むしろ、そう書いていたらうまくいかなかったです…。
パッケージング。
$ mvn package
-aws.jar
となっているJARファイルをデプロイします。
$ ll -h target 合計 21M drwxrwxr-x 8 xxxxx xxxxx 4.0K 10月 25 01:25 ./ drwxr-xr-x 7 xxxxx xxxxx 4.0K 10月 25 01:25 ../ drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 classes/ drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 generated-sources/ drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 generated-test-sources/ -rw-rw-r-- 1 xxxxx xxxxx 21M 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar -rw-rw-r-- 1 xxxxx xxxxx 12K 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT.jar -rw-rw-r-- 1 xxxxx xxxxx 4.2K 10月 25 01:25 hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT.jar.original drwxrwxr-x 2 xxxxx xxxxx 4.0K 10月 25 01:25 maven-archiver/ drwxrwxr-x 3 xxxxx xxxxx 4.0K 10月 25 01:25 maven-status/ drwxrwxr-x 2 xxxxx xxxxx 4.0K 10月 25 01:25 test-classes/
デプロイおよびAmazon API Gatewayの構築などは、Terraformで行います。
terraform/main.tf
というファイルに各種リソースの定義を行いますが、こちらは最後にまた載せます。
概要としては、Amazon API GatewayでREST APIとして作成し、AWS Lambda関数とプロキシ統合します。
path-part
は、リクエストされた内容をすべてAWS Lambda関数に転送するように設定します。
前に書いた、こちらの内容をほぼ使う感じですね。
Terraformで、LocalStackにAmazon API Gateway+AWS Lambdaの環境を構成してみる - CLOVER🍀
注意点としては、タイムアウト(timeout
)を伸ばしておかないと、起動しきれずにタイムアウトしてしまいます。
resource "aws_lambda_function" "lambda" { filename = "../target/hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar" function_name = "hello_spring_cloud_function_api_gateway_with_lambda" role = aws_iam_role.lambda_role.arn handler = "org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler" runtime = "java11" timeout = 30 memory_size = 1024 }
では、init
してapply
。
$ terraform init $ terraform apply -auto-approve
REST APIのIDを取得して
$ REST_API_ID=`awslocal apigateway get-rest-apis --query 'items[0].id' --output text`
動作確認。
$ curl -XPOST -H 'Content-Type: application/json' http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/upper -d '{"message": "Hello World"}' {"result":"HELLO WORLD"}
OKですね。パスについては、どのリクエストも同じAWS Lambda関数に転送するようになっているので、
なにを指定しても大丈夫です。
これで、確認したいことは動作確認できました、と。
まとめ
Spring Cloud Function AWS Adapterを使って、AWS Lambda関数とAmazon API Gatewayのプロキシ統合を構成して
みました。
今回は全リクエストを単一のAWS Lambda関数に転送しているのですが、これをSpring Web MVCのような
ルーティングをするのは、いろいろ調べましたがちょっとできなさそうでした。
※HTTPヘッダーなどでのルーティングはできそうですが
他の手段もありそうなので、そのうち試してみるとしましょう。
オマケ
省略しておいた、Terraformのリソース定義全体を載せておきます。
terraform/main.tf
terraform { required_version = "1.0.9" required_providers { aws = { source = "hashicorp/aws" version = "3.63.0" } } } provider "aws" { access_key = "mock_access_key" region = "us-east-1" s3_force_path_style = true secret_key = "mock_secret_key" skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true endpoints { apigateway = "http://localhost:4566" cloudformation = "http://localhost:4566" cloudwatch = "http://localhost:4566" dynamodb = "http://localhost:4566" es = "http://localhost:4566" firehose = "http://localhost:4566" iam = "http://localhost:4566" kinesis = "http://localhost:4566" lambda = "http://localhost:4566" route53 = "http://localhost:4566" redshift = "http://localhost:4566" s3 = "http://localhost:4566" secretsmanager = "http://localhost:4566" ses = "http://localhost:4566" sns = "http://localhost:4566" sqs = "http://localhost:4566" ssm = "http://localhost:4566" stepfunctions = "http://localhost:4566" sts = "http://localhost:4566" } } ## API Gateway resource "aws_api_gateway_rest_api" "rest_api" { name = "my-rest-api" } resource "aws_api_gateway_resource" "proxy_resource" { rest_api_id = aws_api_gateway_rest_api.rest_api.id parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id path_part = "{proxy+}" } resource "aws_api_gateway_method" "method" { rest_api_id = aws_api_gateway_rest_api.rest_api.id authorization = "NONE" http_method = "ANY" resource_id = aws_api_gateway_resource.proxy_resource.id } resource "aws_api_gateway_integration" "integration" { rest_api_id = aws_api_gateway_rest_api.rest_api.id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.method.http_method integration_http_method = "ANY" type = "AWS_PROXY" uri = aws_lambda_function.lambda.invoke_arn } resource "aws_api_gateway_deployment" "deployment" { rest_api_id = aws_api_gateway_rest_api.rest_api.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_rest_api.rest_api.body, aws_api_gateway_resource.proxy_resource.id, aws_api_gateway_method.method.id, aws_api_gateway_integration.integration.id, ])) } lifecycle { create_before_destroy = true } depends_on = [aws_api_gateway_integration.integration] } resource "aws_api_gateway_stage" "stage" { deployment_id = aws_api_gateway_deployment.deployment.id rest_api_id = aws_api_gateway_rest_api.rest_api.id stage_name = "Prod" } ## Lambda data "aws_caller_identity" "current" {} data "aws_region" "current" {} resource "aws_lambda_permission" "lambda" { statement_id = "AllowExecutionFromApiGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.rest_api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.proxy_resource.path}" } resource "aws_lambda_function" "lambda" { filename = "../target/hello-spring-cloud-function-api-gateway-with-lambda-0.0.1-SNAPSHOT-aws.jar" function_name = "hello_spring_cloud_function_api_gateway_with_lambda" role = aws_iam_role.lambda_role.arn handler = "org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler" runtime = "java11" timeout = 30 memory_size = 1024 } # IAM resource "aws_iam_role" "lambda_role" { name = "MyLambdaRole" assume_role_policy = data.aws_iam_policy_document.assume_role.json } data "aws_iam_policy_document" "assume_role" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } }