Edited at

Serverless FrameworkをつかってSpring Cloud Function !!

More than 1 year has passed since last update.


  • Serverless FrameworkでLambda(java)を試したい

  • Spring Cloud Functionを試したい

の両方の欲求を一度に満たす方法を思いついたので試してみました:heart_eyes:


Serverless Frameworkのインストール

割愛:smile:


Serverless Frameworkでサービスをつくる

E:\workspaces\qiita> sls create --template aws-java-maven --path aws-spring-cloud-function-maven

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "E:\workspaces\qiita\aws-spring-cloud-function-maven"
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v1.24.1
-------'

Serverless: Successfully generated boilerplate for template: "aws-java-maven"

E:\workspaces\qiita>

一旦Serverless FrameworkはここまででSpring Cloud Functionに移ります。


Spring Cloud Functionまわり


pom.xmlを修正します

spring-cloud-functionのサンプルを見ながら

slsでできたpomを下記のファイルのように変更しました。

ちょっと長いので直接見てください。

pom.xml


thin.propertiesをコピーします

light weight packaging してくれます。(のはず:sweat_smile:

詳細はこちらでhttps://github.com/dsyer/spring-boot-thin-launcher


hello spring-cloud-function!!!!

Application.javaFunction.javaを作りました。

あと中身空っぽですがProperties.javaも。。。

@FunctionScanを使ってみたかったのでjava.util.function.Functionを継承しています。

実際にスキャンするベースパッケージのデフォルトが

@ComponentScan(basePackages = "${spring.cloud.function.scan.packages:functions}",

includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Function.class))

なのでfunctionsへ。

Function.javaにした理由はSpringFunctionInitializerがデフォルトでは「function」という名前のbeanを引っ張ってくるのでそうしてあります。

環境変数を設定することによって、そっちが優先されるので任意の名前もOKです。

これでやっとhelloできます:sunglasses:

  .   ____          _            __ _ _

/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.3.RELEASE)

2017-11-28 15:43:40.523 INFO 9740 --- [ main] Application : Starting Application on STYLEZPC210 with PID 9740 (E:\workspaces\e4.7\aws-spring-cloud-function-maven\target\classes started by hasegawa in E:\workspaces\e4.7\aws-spring-cloud-function-maven)
2017-11-28 15:43:40.525 INFO 9740 --- [ main] Application : No active profile set, falling back to default profiles: default
2017-11-28 15:43:40.575 INFO 9740 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3bd36c8e: startup date [Tue Nov 28 15:43:40 JST 2017]; root of context hierarchy
2017-11-28 15:43:41.460 INFO 9740 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.function.web.flux.ReactorAutoConfiguration' of type [org.springframework.cloud.function.web.flux.ReactorAutoConfiguration$$EnhancerBySpringCGLIB$$771a8d51] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2017-11-28 15:43:41.741 INFO 9740 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-11-28 15:43:41.751 INFO 9740 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2017-11-28 15:43:41.752 INFO 9740 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.14
2017-11-28 15:43:41.826 INFO 9740 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2017-11-28 15:43:41.826 INFO 9740 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1254 ms
2017-11-28 15:43:41.949 INFO 9740 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2017-11-28 15:43:41.954 INFO 9740 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-11-28 15:43:41.955 INFO 9740 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-11-28 15:43:41.955 INFO 9740 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-11-28 15:43:41.956 INFO 9740 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2017-11-28 15:43:42.223 INFO 9740 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@3bd36c8e: startup date [Tue Nov 28 15:43:40 JST 2017]; root of context hierarchy
2017-11-28 15:43:42.314 INFO 9740 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-11-28 15:43:42.315 INFO 9740 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-11-28 15:43:42.340 INFO 9740 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-28 15:43:42.341 INFO 9740 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-28 15:43:42.374 INFO 9740 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-28 15:43:42.464 INFO 9740 --- [ main] o.s.c.f.web.flux.FunctionHandlerMapping : FunctionCatalog: org.springframework.cloud.function.context.InMemoryFunctionCatalog@468e1b6, FunctionInspector: org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration$BeanFactoryFunctionInspector@360efe6c
2017-11-28 15:43:42.474 INFO 9740 --- [ main] o.s.c.f.web.flux.FunctionHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-11-28 15:43:42.474 INFO 9740 --- [ main] o.s.c.f.web.flux.FunctionHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-11-28 15:43:42.484 INFO 9740 --- [ main] o.s.c.f.web.flux.FunctionHandlerMapping : Mapped "{[/**],methods=[GET]}" onto public java.lang.Object org.springframework.cloud.function.web.flux.FunctionController.get(java.util.function.Function<reactor.core.publisher.Flux<?>, reactor.core.publisher.Flux<?>>,java.util.function.Supplier<reactor.core.publisher.Flux<?>>,java.lang.String)
2017-11-28 15:43:42.485 INFO 9740 --- [ main] o.s.c.f.web.flux.FunctionHandlerMapping : Mapped "{[/**],methods=[POST]}" onto public org.springframework.http.ResponseEntity<reactor.core.publisher.Flux<?>> org.springframework.cloud.function.web.flux.FunctionController.post(java.util.function.Function<reactor.core.publisher.Flux<?>, reactor.core.publisher.Flux<?>>,java.util.function.Consumer<reactor.core.publisher.Flux<?>>,java.lang.Boolean,org.springframework.cloud.function.web.flux.re
quest.FluxRequest<?>)
2017-11-28 15:43:42.525 INFO 9740 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-11-28 15:43:42.583 INFO 9740 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-11-28 15:43:42.587 INFO 9740 --- [ main] Application : Started Application in 2.497 seconds (JVM running for 5.276)

からの

POSTするデータを作成します。


event.json

{"value":"hello spring-cloud-function"}


E:\workspaces\e4.7\aws-spring-cloud-function-maven>curl -X POST "http://localhost:8080/function" -d @event.json -H "Content-Type: application/json"

[{"value":"hello spring-cloud-function:0"},{"value":"hello spring-cloud-function:1"},{"value":"hello spring-cloud-function:2"},{"value":"hello spring-cloud-function:3"},{"value":"hello spring-cloud-function:4"}]
E:\workspaces\e4.7\aws-spring-cloud-function-maven>

hello x 5でました。:)

Functionの返却値はFluxなのでServer-Sent Eventsに対応しています。

※返却値はFluxじゃなくても良いです

"Accept: text/event-stream"を追加すると

E:\workspaces\e4.7\aws-spring-cloud-function-maven>curl -X POST "http://localhost:8080/function" -d @event.json -H "Accept: text/event-stream" -H "Content-Type: application/json"

data:{"value":"hello spring-cloud-function:0"}

data:{"value":"hello spring-cloud-function:1"}

data:{"value":"hello spring-cloud-function:2"}

data:{"value":"hello spring-cloud-function:3"}

data:{"value":"hello spring-cloud-function:4"}

E:\workspaces\e4.7\aws-spring-cloud-function-maven>

レスポンスの形式が変わります。

Reactorについてはこの辺とかを参照してください。

Spring WebFluxとかSpring 5に追加されたし次回はこの辺を攻めようと思います。

と、ここまででSpring Cloud Function に満足したので:sweat_smile:


Lambdaにデプロイする


serverless.ymlを修正します。


serverless.yml

# Welcome to Serverless!

#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
# docs.serverless.com
#
# Happy Coding!
# https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/

service: aws-spring-cloud-function-maven

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
name: aws
runtime: java8
timeout: 30

# you can overwrite defaults here
# stage: dev
region: ap-northeast-1

# you can add statements to the Lambda function's IAM Role here
# iamRoleStatements:
# - Effect: "Allow"
# Action:
# - "s3:ListBucket"
# Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] }
# - Effect: "Allow"
# Action:
# - "s3:PutObject"
# Resource:
# Fn::Join:
# - ""
# - - "arn:aws:s3:::"
# - "Ref" : "ServerlessDeploymentBucket"
# - "/*"

# you can define service wide environment variables here
# environment:
# variable1: value1

# you can add packaging information here
package:
artifact: target/aws-spring-cloud-function-maven-dev-aws.jar

functions:
hello:
handler: org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler

# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details
events:
- http:
path: hello-spring-cloud-function
method: post
integration: lambda
request:
template:
application/json: '$input.json("$")'
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# - alexaSkill
# - iot:
# sql: "SELECT * FROM 'some_topic'"
# - cloudwatchEvent:
# event:
# source:
# - "aws.ec2"
# detail-type:
# - "EC2 Instance State-change Notification"
# detail:
# state:
# - pending
# - cloudwatchLog: '/aws/lambda/hello'

# Define function environment variables here
# environment:
# variable2: value2

# you can add CloudFormation resource templates here
#resources:
# Resources:
# NewResource:
# Type: AWS::S3::Bucket
# Properties:
# BucketName: my-new-bucket
# Outputs:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"


デプロイするまえに一回ローカルで実行してみます。

E:\workspaces\e4.7\aws-spring-cloud-function-maven>serverless invoke local --function hello -p event.json -v

Serverless: Building Java bridge, first invocation might take a bit longer.
events.js:161
throw er; // Unhandled 'error' event
^

Error: spawn mvn ENOENT
at exports._errnoException (util.js:1023:11)
at Process.ChildProcess._handle.onexit (internal/child_process.js:193:32)
at onErrorNT (internal/child_process.js:359:16)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)

動かない。。。試しに何も手を加えずクリエイトして実行してみます。。。

E:\workspaces\qiita>sls create --template aws-java-maven --path xxx

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "E:\workspaces\qiita\xxx"
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v1.24.1
-------'

Serverless: Successfully generated boilerplate for template: "aws-java-maven"

E:\workspaces\qiita>cd xxx

E:\workspaces\qiita\xxx>mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building hello dev
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ hello ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to E:\workspaces\qiita\xxx\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory E:\workspaces\qiita\xxx\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ hello ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ hello ---
[INFO] Building jar: E:\workspaces\qiita\xxx\target\hello-dev.jar
[INFO]
[INFO] --- maven-shade-plugin:2.3:shade (default) @ hello ---
[INFO] Including com.amazonaws:aws-lambda-java-core:jar:1.1.0 in the shaded jar.
[INFO] Including com.amazonaws:aws-lambda-java-log4j:jar:1.0.0 in the shaded jar.
[INFO] Including log4j:log4j:jar:1.2.17 in the shaded jar.
[INFO] Including com.fasterxml.jackson.core:jackson-core:jar:2.8.5 in the shaded jar.
[INFO] Including com.fasterxml.jackson.core:jackson-databind:jar:2.8.5 in the shaded jar.
[INFO] Including com.fasterxml.jackson.core:jackson-annotations:jar:2.8.5 in the shaded jar.
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing E:\workspaces\qiita\xxx\target\hello-dev.jar with E:\workspaces\qiita\xxx\target\hello-dev-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.127 s
[INFO] Finished at: 2017-11-28T18:50:00+09:00
[INFO] Final Memory: 24M/311M
[INFO] ------------------------------------------------------------------------

E:\workspaces\qiita\xxx>serverless invoke local --function hello
Serverless: Building Java bridge, first invocation might take a bit longer.
events.js:161
throw er; // Unhandled 'error' event
^

Error: spawn mvn ENOENT
at exports._errnoException (util.js:1023:11)
at Process.ChildProcess._handle.onexit (internal/child_process.js:193:32)
at onErrorNT (internal/child_process.js:359:16)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickDomainCallback (internal/process/next_tick.js:122:9)

E:\workspaces\qiita\xxx>

どうやら僕のせいじゃないようです。。。


デプロイ

mvn clean packageしてからの

E:\workspaces\e4.7\aws-spring-cloud-function-maven>sls deploy

Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............................
Serverless: Stack update finished...
Service Information
service: aws-spring-cloud-function-maven
stage: dev
region: ap-northeast-1
stack: aws-spring-cloud-function-maven-dev
api keys:
None
endpoints:
POST - https://ZZZzzzZZZzzzZZZ.execute-api.ap-northeast-1.amazonaws.com/dev/hello-spring-cloud-function
functions:
hello: aws-spring-cloud-function-maven-dev-hello


確認


Lambda

E:\workspaces\e4.7\aws-spring-cloud-function-maven>serverless invoke --function hello -p event.json

[
{
"value": "hello spring-cloud-function:0"
},
{
"value": "hello spring-cloud-function:1"
},
{
"value": "hello spring-cloud-function:2"
},
{
"value": "hello spring-cloud-function:3"
},
{
"value": "hello spring-cloud-function:4"
}
]

:smile:


API Gateway

E:\workspaces\e4.7\aws-spring-cloud-function-maven>curl -X POST "https://ZZZzzzZZZzzzZZZ.execute-api.ap-northeast-1.amazonaws.com/dev/hello-spring-cloud-function" -d @event.json -H "Content-Type: application/json"

[{"value":"hello spring-cloud-function:0"},{"value":"hello spring-cloud-function:1"},{"value":"hello spring-cloud-function:2"},{"value":"hello spring-cloud-function:3"},{"value":"hello spring-cloud-function:4"}]

:grin:


最後に

今回作ったものをgithubに公開したのでテンプレートとして使えるか確認します。

https://github.com/YusukeHasegawa/aws-spring-cloud-function-maven

E:\workspaces\qiita>sls create --template-url https://github.com/YusukeHasegawa/aws-spring-cloud-function-maven --path myService

Serverless: Generating boilerplate...
Serverless: Downloading and installing "aws-spring-cloud-function-maven"...
Serverless: Successfully installed "myService"

E:\workspaces\qiita>cd myService

E:\workspaces\qiita\myService>mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building aws-spring-cloud-function-maven dev
[INFO] ------------------------------------------------------------------------
.
.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 11.863 s
[INFO] Finished at: 2017-11-29T11:35:08+09:00
[INFO] Final Memory: 30M/292M
[INFO] ------------------------------------------------------------------------

E:\workspaces\qiita\myService>sls deploy
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............................
Serverless: Stack update finished...
Service Information
service: myService
stage: dev
region: ap-northeast-1
stack: myService-dev
api keys:
None
endpoints:
POST - https://ZZZzzzZZZzzzZZZ.execute-api.ap-northeast-1.amazonaws.com/dev/hello-spring-cloud-function
functions:
hello: myService-dev-hello

E:\workspaces\qiita\myService>curl -X POST "https://ZZZzzzZZZzzzZZZ.execute-api.ap-northeast-1.amazonaws.com/dev/hello-spring-cloud-function" -d @event.json -H "Content-Type: application/json"
[{"value":"hello spring-cloud-function:0"},{"value":"hello spring-cloud-function:1"},{"value":"hello spring-cloud-function:2"},{"value":"hello spring-cloud-function:3"},{"value":"hello spring-cloud-function:4"}]
E:\workspaces\qiita\myService>sls remove
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
....................
Serverless: Stack removal finished...

E:\workspaces\qiita\myService>

:ok_hand: