├── public ├── robots.txt ├── favicon.ico └── favicon.png ├── rebuild ├── src ├── main │ ├── resources │ │ ├── application-aws.properties │ │ ├── application.properties │ │ └── logback-aws.xml │ └── java │ │ └── my │ │ └── service │ │ ├── controller │ │ └── APIController.java │ │ ├── Application.java │ │ └── StreamLambdaHandler.java ├── test │ ├── resources │ │ ├── scheduled-event.json │ │ ├── sqs-event.json │ │ └── sns-event.json │ └── java │ │ └── my │ │ └── service │ │ └── StreamLambdaHandlerTest.java └── assembly │ └── bin.xml ├── sam-local-env.json ├── serverless-java-spring-boot.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ ├── gradle-wrapper.properties │ └── gradle-wrapper-wrapper ├── sam-local ├── gradle.properties ├── .gitignore ├── deploy-to-aws ├── mk-git-json ├── .circleci └── config.yml ├── sam-template.yaml ├── README.md ├── gradlew.bat ├── layers-support.patch ├── gradlew └── events.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /* 3 | 4 | 5 | -------------------------------------------------------------------------------- /rebuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./gradlew -x test build --continuous 3 | -------------------------------------------------------------------------------- /src/main/resources/application-aws.properties: -------------------------------------------------------------------------------- 1 | logging.config = classpath:logback-aws.xml 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/public/favicon.png -------------------------------------------------------------------------------- /sam-local-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "MyServiceFunction":{ 3 | "SPRING_PROFILES_ACTIVE": "aws,local" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /serverless-java-spring-boot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/serverless-java-spring-boot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huksley/serverless-java-spring-boot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sam-local: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # use gradle daemon by default 2 | org.gradle.daemon = true 3 | 4 | systemProp.org.gradle.internal.http.connectionTimeout=120000 5 | systemProp.org.gradle.internal.http.socketTimeout=120000 -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode=off 2 | 3 | # Reduce logging level to make sure the application works with SAM local 4 | # https://github.com/awslabs/aws-serverless-java-container/issues/134 5 | logging.level.root=INFO 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .attach* 2 | .classpath 3 | .project 4 | .gradle 5 | .settings 6 | # VSCOde editor generates this ffs 7 | bin 8 | # Gradle 9 | build 10 | # Maven 11 | target 12 | output-sam.yaml 13 | .vscode 14 | sam-package.yaml 15 | src/main/resources/git.json 16 | -------------------------------------------------------------------------------- /src/test/resources/scheduled-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "2528c5e5-7feb-9a8d-9329-b445a222897f", 4 | "detail-type": "Scheduled Event", 5 | "source": "aws.events", 6 | "account": "849707200000", 7 | "time": "2019-02-19T08:59:04Z", 8 | "region": "eu-west-1", 9 | "resources": [ 10 | "arn:aws:events:eu-west-1:849707200000:rule/ServerlessSpringApi-MyServiceFunctionCheckWebsiteS-17EIBMQX0K6PN" 11 | ], 12 | "detail": {} 13 | } -------------------------------------------------------------------------------- /src/main/resources/logback-aws.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [%d{yyyy-MM-dd HH:mm:ss.SSS}] %X{AWSRequestId:-} %.-6level %logger{5} - %msg \r%replace(%ex){'\n','\r'}%nopex%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper-wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Matthias Gr?ter 4 | #set -x 5 | 6 | function find_wcroot() 7 | { 8 | # Return 0 & output working copy root path if the current directory 9 | # is a subversion or git working area. Otherwise return 1. 10 | 11 | SVNROOT="$( svn info --xml --incremental 2> /dev/null | xmllint --xpath '//wcroot-abspath/text()' - 2> /dev/null )" 12 | [[ ! -z $SVNROOT ]] && { echo "$SVNROOT"; return 0; } 13 | 14 | GITROOT="$( git rev-parse --show-toplevel 2> /dev/null )" 15 | [[ ! -z $GITROOT ]] && { echo "$GITROOT"; return 0; } 16 | 17 | return 1 18 | } 19 | 20 | WCROOT=$( find_wcroot ) 21 | # if gradle wrapper exists use it, otherwise use gradle from system path 22 | [[ -f "$WCROOT/gradlew" ]] && CMD="$WCROOT/gradlew" || CMD="gradle" 23 | 24 | $CMD "$@" 25 | -------------------------------------------------------------------------------- /src/assembly/bin.xml: -------------------------------------------------------------------------------- 1 | 4 | lambda-package 5 | 6 | zip 7 | 8 | false 9 | 10 | 11 | 12 | ${project.build.directory}${file.separator}lib 13 | lib 14 | 15 | tomcat-embed* 16 | 17 | 18 | 19 | 20 | ${project.build.directory}${file.separator}classes 21 | 22 | ** 23 | 24 | ${file.separator} 25 | 26 | 27 | -------------------------------------------------------------------------------- /deploy-to-aws: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | ./gradlew compileJava compileTestJava assemble build --scan 5 | 6 | function parse_yaml { 7 | local prefix=$2 8 | local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034') 9 | sed -ne "s|^\($s\):|\1|" \ 10 | -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \ 11 | -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 | 12 | awk -F$fs '{ 13 | indent = length($1)/2; 14 | vname[indent] = $2; 15 | for (i in vname) {if (i > indent) {delete vname[i]}} 16 | if (length($3) > 0) { 17 | vn=""; for (i=0; i ping() { 21 | log.info("Received ping, sending pong"); 22 | Map pong = new HashMap<>(); 23 | pong.put("pong", "Hello, World!"); 24 | return pong; 25 | } 26 | 27 | /** 28 | * Handles AWS event. Delivered from {@link StreamLambdaHandler#handleRequest} by constructing special proxy request. 29 | */ 30 | @RequestMapping(path = "/event", method = RequestMethod.POST) 31 | public Map event(@RequestParam("type") String type, @RequestBody String post) { 32 | log.info("Received event {} body {}", type, post); 33 | Map pong = new HashMap<>(); 34 | pong.put("pong", "Hello, World!"); 35 | return pong; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/resources/sqs-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId": "72b03944-1af7-492b-aac9-412698960531", 5 | "receiptHandle": "AQEBicU8TU0NRCELI9WEuVNN/2EU6G1KUJTSVO9BoDPAUvZk4yLsyMQiiKLf0LNmx7ETrdQ1CrwlwdE7JaBVGAcLWPrMCx8wZLxu6x8QtlITVIIAEIwjt+5/rnbmUMY0ajOMok80gO9SceLa+Zr1g3yZnjUMf3xNSGqynvooQkHFRAtSTKf0bkkagkKIHCDt5RghXUeIYruEZTStOm7RTVQwWxaCTQMvbBgNAZFG6j54qg1hCI9Cv9P95FH6Tt8yRDjf9Ad3s7Jykm0yy+IWfvpvHBoSelM03LrUUTX5E6lFpmTByIIE+fmamzeTZOevyNviiiMRbvqvDkDlrVs3M00zjiocX5jz+GVUmE5q0tOzwGYC4yWbcPKDSOc1hiRYUhbo", 6 | "body": "Howdy?", 7 | "attributes": { 8 | "ApproximateReceiveCount": "1", 9 | "SentTimestamp": "1550567133640", 10 | "SenderId": "AIDAIEHDD23VAQL6ZVIAC", 11 | "ApproximateFirstReceiveTimestamp": "1550567133641" 12 | }, 13 | "messageAttributes": { 14 | "someattr": { 15 | "stringValue": "someval", 16 | "stringListValues": [], 17 | "binaryListValues": [], 18 | "dataType": "String" 19 | } 20 | }, 21 | "md5OfBody": "47b68d3722ece13893e9c5fcb3cac906", 22 | "md5OfMessageAttributes": "1e4b169c724503f3886d943e5bbe88e5", 23 | "eventSource": "aws:sqs", 24 | "eventSourceARN": "arn:aws:sqs:eu-west-1:849707200000:test-queue", 25 | "awsRegion": "eu-west-1" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /mk-git-json: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Creates git.json with repository and deployment information 4 | # NOTE: CodePipeline strips .git directory, so only git information available is commit.id from CODEBUILD_RESOLVED_SOURCE_VERSION env var 5 | # 6 | DST=${1:-src/main/resources} 7 | if [ -d .git ]; then 8 | git log -1 --pretty=format:'{%n"commit.id": "%H",%n"commit.id.abbrev": "%h",%n"commit.user.name": "%an",%n"commit.user.email": "%ae",%n"commit.info":"%D",%n"commit.message": "%s",%n"commit.date": "%ai"%n}%n' | sed -E -e "s/^\* //g;s/^\| //g" > $DST/git.json 9 | else 10 | if [ "$CODEBUILD_RESOLVED_SOURCE_VERSION" != "" ]; then 11 | echo "{ \"commit.id\": \"$CODEBUILD_RESOLVED_SOURCE_VERSION\" }" > $DST/git.json 12 | else 13 | echo "{ \"empty\": true }" > $DST/git.json 14 | fi 15 | fi 16 | 17 | NOW=`date +%Y-%m-%dT%H:%M:%S%z` 18 | HST=`hostname -f` 19 | 20 | 21 | platform='unknown' 22 | unamestr=`uname` 23 | if [[ "$unamestr" == 'Linux' ]]; then 24 | platform='linux' 25 | elif [[ "$unamestr" == 'FreeBSD' ]]; then 26 | platform='freebsd' 27 | fi 28 | 29 | if [[ "$platform" == "unknown" ]]; then 30 | # Probably MacOS X 31 | sed -i bak -E -e "s/^{/{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json 32 | elif [[ "$platform" == "freebsd" ]]; then 33 | # Validate proper -i call 34 | sed -i -E -e "s/^{/{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json 35 | else 36 | sed -i -E -e "s/^\{/\{ \"build.time\": \"$NOW\", \"build.user\": \"$USER\", \"build.host\": \"$HST\", /g" $DST/git.json 37 | fi 38 | -------------------------------------------------------------------------------- /src/test/resources/sns-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "EventVersion": "1.0", 6 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 7 | "Sns": { 8 | "Type": "Notification", 9 | "MessageId": "15d202b9-ecca-5bf1-86aa-565c2460e18e", 10 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic", 11 | "Subject": null, 12 | "Message": "hello world!", 13 | "Timestamp": "2019-02-19T09:03:38.472Z", 14 | "SignatureVersion": "1", 15 | "Signature": "Gtt+vpFy8xd5jEBqChL3GU2DFS2WKFzMzmuUCcndy2y+UH4XJZ9TaU082rXtKZdNnhPuGrRygoOJkemLhh6q2kHu25FkvYBfkddBVCgr/wpgxnukqwlu3xYnmrtNqDJM8louZ38ZGk99miMf6NO1CloleIRc+EA1Vgfq7B/fE8lX1863VlLFViS36G86yUeFagNmPMnnPu7VFOi4RQRnzWa8/fKTGgvLMyGFesv6eWPFgDLEL6wGOpd/0gfxh6vq7sU9yuqQFkLvHTX5j6gf1+9Cfh1yxoJkz36NG3Oo17bIBiSsTqMl5A4Wc/3EKkGaPyjxPRPvxVof4ES0a6pPIA==", 16 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", 17 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 18 | "MessageAttributes": { 19 | "somekey": { 20 | "Type": "String", 21 | "Value": "dsdasdas" 22 | } 23 | } 24 | } 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/main/java/my/service/Application.java: -------------------------------------------------------------------------------- 1 | package my.service; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.web.servlet.HandlerAdapter; 9 | import org.springframework.web.servlet.HandlerMapping; 10 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 11 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 12 | 13 | import my.service.controller.APIController; 14 | 15 | @SpringBootApplication 16 | // We use direct @Import instead of @ComponentScan to speed up cold starts 17 | // @ComponentScan(basePackages = "my.service.controller") 18 | @Import({ APIController.class }) 19 | public class Application extends SpringBootServletInitializer { 20 | 21 | /* 22 | * Create required HandlerMapping, to avoid several default HandlerMapping 23 | * instances being created 24 | */ 25 | @Bean 26 | public HandlerMapping handlerMapping() { 27 | return new RequestMappingHandlerMapping(); 28 | } 29 | 30 | /* 31 | * Create required HandlerAdapter, to avoid several default HandlerAdapter 32 | * instances being created 33 | */ 34 | @Bean 35 | public HandlerAdapter handlerAdapter() { 36 | return new RequestMappingHandlerAdapter(); 37 | } 38 | 39 | public static void main(String[] args) { 40 | SpringApplication.run(Application.class, args); 41 | } 42 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | # Build and run tests 5 | build: 6 | working_directory: ~/serverless 7 | docker: 8 | # https://discuss.circleci.com/t/spring-boot-application-build-failed-due-to-test-failure/27687 9 | - image: circleci/openjdk:8u171 10 | steps: 11 | - checkout 12 | - run: 13 | name: Install AWS cli 14 | command: | 15 | python --version 16 | cat /etc/issue 17 | sudo apt-get update 18 | sudo apt-get install -y python-pip 19 | sudo pip install awscli 20 | sudo pip install aws-sam-cli 21 | - run: 22 | name: Configure AWS cli 23 | command: | 24 | aws configure set aws_access_key_id ${AWS_ACCESS_KEY_ID} 25 | aws configure set aws_secret_access_key ${AWS_SECRET_ACCESS_KEY} 26 | aws configure set default.region ${AWS_DEFAULT_REGION} 27 | aws configure list 28 | - run: 29 | name: Build and deploy to AWS 30 | command: ./deploy-to-aws 31 | - save_cache: 32 | key: docs-{{ checksum "build.gradle" }} 33 | paths: 34 | - ~/.m2 35 | - store_artifacts: 36 | path: ~/serverless/build/reports 37 | - store_artifacts: 38 | path: ~/serverless/build/test-results 39 | - run: 40 | when: on_fail 41 | command: find > ~/serverless/all-files.txt 42 | - store_artifacts: 43 | path: ~/serverless/all-files.txt 44 | workflows: 45 | version: 2 46 | build: 47 | jobs: 48 | - build: 49 | filters: 50 | branches: 51 | only: master 52 | ignore: gh-pages 53 | -------------------------------------------------------------------------------- /sam-template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | # https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#sqs 3 | Transform: AWS::Serverless-2016-10-31 4 | Description: AWS Serverless Spring Boot 2 API - my.service::my-service 5 | Globals: 6 | Api: 7 | EndpointConfiguration: REGIONAL 8 | 9 | Parameters: 10 | Stage: 11 | Type: String 12 | Default: dev 13 | SNSTopic: 14 | Type: String 15 | SQSQueue: 16 | Type: String 17 | 18 | Resources: 19 | ApiGatewayApi: 20 | Type: AWS::Serverless::Api 21 | Properties: 22 | StageName: 23 | Ref: Stage 24 | Variables: 25 | Stage: 26 | Ref: Stage 27 | MyServiceFunction: 28 | Type: AWS::Serverless::Function 29 | FunctionName: "ServerlessSpringApi-MyServiceFunction" 30 | Properties: 31 | Handler: my.service.StreamLambdaHandler::handleRequest 32 | Runtime: java8 33 | CodeUri: "build/distributions/serverless-java-spring-boot.zip" 34 | MemorySize: 2048 35 | Policies: AWSLambdaBasicExecutionRole 36 | Timeout: 30 37 | Environment: 38 | Variables: 39 | # Specify here variables for deployment, 40 | # override local using sam-local-env.json 41 | SPRING_PROFILES_ACTIVE: aws 42 | Events: 43 | GetResource: 44 | Type: Api 45 | Properties: 46 | RestApiId: 47 | Ref: ApiGatewayApi 48 | Path: /ping 49 | Method: GET 50 | CheckWebsiteScheduledEvent: 51 | Type: Schedule 52 | Properties: 53 | Schedule: rate(5 minutes) 54 | ReceiveFromSNS: 55 | Type: SNS 56 | Properties: 57 | Topic: 58 | Ref: SNSTopic 59 | ReceiveFromQueue: 60 | Type: SQS 61 | Properties: 62 | Queue: 63 | Ref: SQSQueue 64 | Outputs: 65 | MyServiceApi: 66 | Description: URL for application 67 | Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/ping' 68 | Export: 69 | Name: MyServiceApi 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Spring Boot Serverless application 2 | 3 | ![Process](serverless-java-spring-boot.png) 4 | 5 | Using [AWS Proxy wrappers](https://github.com/awslabs/aws-serverless-java-container/wiki/Quick-start---Spring-Boot) and [`aws-serverless-java-container`](https://github.com/awslabs/aws-serverless-java-container) this application shows how you can easily build Java application and deploy it into AWS Lambda. 6 | 7 | Also this application able to handle following AWS Events 8 | 9 | * CloudWatch scheduled event 10 | * SNS Topic message 11 | * SQS queue message 12 | 13 | 14 | This project uses Java8, AWS CLI, AWS SAM CLI and Gradle to build, test and deploy code. 15 | Aslo consult [CircleCI](.circleci/config.yml) for instructions how to setup automated CI/CD build and deployment for this. 16 | 17 | ## Running locally 18 | 19 | To run function locally use `.\sam-local` 20 | 21 | Try API endpoint in terminal or browser 22 | 23 | ``` 24 | curl -s http://127.0.0.1:3000/ping | json_pp 25 | ``` 26 | 27 | ## Running in AWS 28 | 29 | Deploy to AWS using 30 | ```bash 31 | ./deploy-to-aws 32 | ``` 33 | 34 | Open URL provided in output, for example: https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping 35 | 36 | ```bash 37 | curl -s https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping | json_pp 38 | { 39 | "pong" : "Hello, World!" 40 | } 41 | ``` 42 | 43 | Bench time to run, notice that the first run with the same or more concurrent number of requests takes 6 seconds (i.e. Spring boot launch time) 44 | ```bash 45 | ab -n 100 -c 20 https://deadbeef.execute-api.eu-west-1.amazonaws.com/dev/ping 46 | ``` 47 | 48 | ## Requirements for deploying 49 | 50 | To successfully deploy to AWS you need to define following environment variables: 51 | 52 | * AWS_ACCESS_KEY_ID - AWS access key to use (IAM) 53 | * AWS_SECRET_ACCESS_KEY - AWS access key password to use (IAM) 54 | * AWS_DEFAULT_REGION - AWS Region to use, for example eu-west-1 55 | * S3_BUCKET - Existing S3 bucket to use for deploying artifacts 56 | * SNS_TOPIC - Full ARN for SNS topic to subscribe to 57 | * SQS_QUEUE - Full ARN for SQ queue to subcribe to 58 | 59 | ## Links 60 | 61 | * https://github.com/awslabs/aws-serverless-java-container/ 62 | * https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html 63 | * https://github.com/aws/aws-lambda-java-libs/tree/master/aws-lambda-java-events 64 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /layers-support.patch: -------------------------------------------------------------------------------- 1 | This patch introduces support for layers, 2 | however sam cli does not support it well, so it is not available here. 3 | 4 | Probably can be turned on later when using serverless framework for running locally. 5 | 6 | diff --git b/build.gradle a/build.gradle 7 | index 0dcc715..a50b21b 100644 8 | --- b/build.gradle 9 | +++ a/build.gradle 10 | @@ -41,5 +41,22 @@ task buildZipFull(type: Zip) { 11 | } 12 | } 13 | } 14 | + 15 | +task buildZipCode(type: Zip) { 16 | + baseName = "serverless-java-spring-boot-code" 17 | + from compileJava 18 | + from processResources 19 | +} 20 | + 21 | +task buildLayerZip(type: Zip) { 22 | + baseName = "serverless-java-spring-boot-layer1" 23 | + into("java/lib") { 24 | + from(configurations.compileClasspath) { 25 | + exclude 'tomcat-embed-*' 26 | + } 27 | + } 28 | +} 29 | 30 | build.dependsOn buildZipFull 31 | +build.dependsOn buildZipCode 32 | +build.dependsOn buildLayerZip 33 | diff --git b/deploy-to-aws a/deploy-to-aws 34 | index 492b7c3..3f36579 100755 35 | --- b/deploy-to-aws 36 | +++ a/deploy-to-aws 37 | @@ -22,9 +22,20 @@ function parse_yaml { 38 | 39 | eval $(parse_yaml sam-template.yaml) 40 | 41 | +#LAYER_PACKAGE=build/distributions/serverless-java-spring-boot-layer1.zip 42 | +#LAYER_NAME=`basename $LAYER_PACKAGE` 43 | +# 44 | +#aws s3 cp $LAYER_PACKAGE s3://${S3_BUCKET}/$LAYER_NAME 45 | +#aws lambda publish-layer-version --layer-name serverless-java-spring-boot-layer1 \ 46 | +# --content S3Bucket=${S3_BUCKET},S3Key=${LAYER_NAME} \ 47 | +# --compatible-runtimes java8 48 | +#LAYER_ARN=`aws lambda list-layer-versions --layer-name serverless-java-spring-boot-layer1 --max-items 1 | jq ".LayerVersions[0].LayerVersionArn" -r` 49 | +#echo "JAR layer ARN: ${LAYER_ARN}" 50 | + 51 | aws cloudformation package --template-file sam-template.yaml --s3-bucket ${S3_BUCKET} --s3-prefix java --output-template-file sam-package.yaml 52 | 53 | PARAMS="SNSTopic=${SNS_TOPIC} SQSQueue=${SQS_QUEUE}" 54 | +# CodeLayers=${LAYER_ARN} 55 | 56 | aws cloudformation deploy --template-file sam-package.yaml --stack-name ServerlessSpringApi --parameter-overrides ${PARAMS} --capabilities CAPABILITY_IAM 57 | 58 | diff --git b/sam-local a/sam-local 59 | index 047b969..98d8748 100755 60 | --- b/sam-local 61 | +++ a/sam-local 62 | @@ -1,4 +1,9 @@ 63 | #!/bin/bash 64 | 65 | -sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json 66 | +LAYER_ARN=`aws lambda list-layer-versions --layer-name serverless-java-spring-boot-layer1 --max-items 1 | jq ".LayerVersions[0].LayerVersionArn" -r` 67 | +echo "JAR layer ARN: ${LAYER_ARN}" 68 | + 69 | +PARAMS=ParameterKey=CodeLayers,ParameterValue=${LAYER_ARN} 70 | + 71 | +sam local start-api --debug --skip-pull-image -s public -t sam-template.yaml -p 3000 -n sam-local-env.json --parameter-overrides ${PARAMS} 72 | 73 | diff --git b/sam-template.yaml a/sam-template.yaml 74 | index 17a9b5f..6481466 100644 75 | --- b/sam-template.yaml 76 | +++ a/sam-template.yaml 77 | @@ -14,6 +14,8 @@ Parameters: 78 | Type: String 79 | SQSQueue: 80 | Type: String 81 | +# CodeLayers: 82 | +# Type: CommaDelimitedList 83 | 84 | Resources: 85 | ApiGatewayApi: 86 | @@ -24,16 +26,27 @@ Resources: 87 | Variables: 88 | Stage: 89 | Ref: Stage 90 | + MyServiceLayer: 91 | + Type: AWS::Serverless::LayerVersion 92 | + Properties: 93 | + LayerName: serverless-java-spring-boot-layer1 94 | + ContentUri: "build/distributions/serverless-java-spring-boot-layer1.zip" 95 | + CompatibleRuntimes: 96 | + - java8 97 | + RetentionPolicy: Retain 98 | MyServiceFunction: 99 | Type: AWS::Serverless::Function 100 | FunctionName: "ServerlessSpringApi-MyServiceFunction" 101 | Properties: 102 | Handler: my.service.StreamLambdaHandler::handleRequest 103 | Runtime: java8 104 | - CodeUri: "build/distributions/serverless-java-spring-boot.zip" 105 | + CodeUri: "build/distributions/serverless-java-spring-boot-code.zip" 106 | MemorySize: 2048 107 | Policies: AWSLambdaBasicExecutionRole 108 | Timeout: 30 109 | + Layers: 110 | +# Ref: CodeLayers 111 | + - !Ref MyServiceLayer 112 | Environment: 113 | Variables: 114 | # Specify here variables for deployment, 115 | -------------------------------------------------------------------------------- /src/test/java/my/service/StreamLambdaHandlerTest.java: -------------------------------------------------------------------------------- 1 | package my.service; 2 | 3 | 4 | import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; 5 | import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; 6 | import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; 7 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse; 8 | import com.amazonaws.services.lambda.runtime.Context; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | 11 | import org.junit.BeforeClass; 12 | import org.junit.Test; 13 | 14 | import javax.ws.rs.HttpMethod; 15 | import javax.ws.rs.core.HttpHeaders; 16 | import javax.ws.rs.core.MediaType; 17 | import javax.ws.rs.core.Response; 18 | 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.util.Map; 23 | 24 | import static org.junit.Assert.*; 25 | 26 | 27 | public class StreamLambdaHandlerTest { 28 | 29 | private static StreamLambdaHandler handler; 30 | private static Context lambdaContext; 31 | 32 | @BeforeClass 33 | public static void setUp() { 34 | handler = new StreamLambdaHandler(); 35 | lambdaContext = new MockLambdaContext(); 36 | } 37 | 38 | @Test 39 | public void ping_streamRequest_respondsWithHello() throws IOException { 40 | InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET) 41 | .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) 42 | .buildStream(); 43 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 44 | 45 | handle(requestStream, responseStream); 46 | 47 | AwsProxyResponse response = readResponse(responseStream); 48 | assertNotNull(response); 49 | assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); 50 | 51 | assertFalse(response.isBase64Encoded()); 52 | 53 | // Check it is parseable 54 | String json = response.getBody(); 55 | ObjectMapper om = new ObjectMapper(); 56 | om.readValue(json, Map.class); 57 | 58 | assertTrue(json.contains("pong")); 59 | assertTrue(json.contains("Hello, World!")); 60 | 61 | assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); 62 | assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)); 63 | } 64 | 65 | @Test 66 | public void cloudWatch_scheduled_event_processed() throws IOException { 67 | InputStream requestStream = getClass().getResourceAsStream("/scheduled-event.json"); 68 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 69 | handle(requestStream, responseStream); 70 | } 71 | 72 | @Test 73 | public void cloudWatch_sns_event_processed() throws IOException { 74 | InputStream requestStream = getClass().getResourceAsStream("/sns-event.json"); 75 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 76 | handle(requestStream, responseStream); 77 | } 78 | 79 | @Test 80 | public void cloudWatch_sqs_event_processed() throws IOException { 81 | InputStream requestStream = getClass().getResourceAsStream("/sqs-event.json"); 82 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 83 | handle(requestStream, responseStream); 84 | } 85 | 86 | @Test 87 | public void invalidResource_streamRequest_responds404() { 88 | InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET) 89 | .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) 90 | .buildStream(); 91 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 92 | 93 | handle(requestStream, responseStream); 94 | 95 | AwsProxyResponse response = readResponse(responseStream); 96 | assertNotNull(response); 97 | assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode()); 98 | } 99 | 100 | private void handle(InputStream is, ByteArrayOutputStream os) { 101 | try { 102 | handler.handleRequest(is, os, lambdaContext); 103 | } catch (IOException e) { 104 | e.printStackTrace(); 105 | fail(e.getMessage()); 106 | } 107 | } 108 | 109 | private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { 110 | try { 111 | return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); 112 | } catch (IOException e) { 113 | e.printStackTrace(); 114 | fail("Error while parsing response: " + e.getMessage()); 115 | } 116 | return null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /events.md: -------------------------------------------------------------------------------- 1 | # Different types of events handled 2 | 3 | ## API HTTP(s) event: 4 | 5 | { 6 | "resource": "/ping", 7 | "path": "/ping", 8 | "httpMethod": "GET", 9 | "headers": { 10 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 11 | "accept-encoding": "gzip, deflate, br", 12 | "accept-language": "en-US,en-FI;q=0.9,en;q=0.8,ru-RU;q=0.7,ru;q=0.6,fi;q=0.5", 13 | "dnt": "1", 14 | "Host": "deadbeef.execute-api.eu-west-1.amazonaws.com", 15 | "upgrade-insecure-requests": "1", 16 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36", 17 | "X-Amzn-Trace-Id": "Root=1-5c6bc702-3898654289f358229a7a44a1", 18 | "X-Forwarded-For": "8.8.8.8", 19 | "X-Forwarded-Port": "443", 20 | "X-Forwarded-Proto": "https" 21 | }, 22 | "multiValueHeaders": { 23 | "accept": [ 24 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" 25 | ], 26 | "accept-encoding": [ 27 | "gzip, deflate, br" 28 | ], 29 | "accept-language": [ 30 | "en-US,en-FI;q=0.9,en;q=0.8,ru-RU;q=0.7,ru;q=0.6,fi;q=0.5" 31 | ], 32 | "dnt": [ 33 | "1" 34 | ], 35 | "Host": [ 36 | "deadbeef.execute-api.eu-west-1.amazonaws.com" 37 | ], 38 | "upgrade-insecure-requests": [ 39 | "1" 40 | ], 41 | "User-Agent": [ 42 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36" 43 | ], 44 | "X-Amzn-Trace-Id": [ 45 | "Root=1-5c6bc702-3898654289f358229a7a44a1" 46 | ], 47 | "X-Forwarded-For": [ 48 | "8.8.8.8" 49 | ], 50 | "X-Forwarded-Port": [ 51 | "443" 52 | ], 53 | "X-Forwarded-Proto": [ 54 | "https" 55 | ] 56 | }, 57 | "queryStringParameters": null, 58 | "multiValueQueryStringParameters": null, 59 | "pathParameters": null, 60 | "stageVariables": { 61 | "Stage": "dev" 62 | }, 63 | "requestContext": { 64 | "resourceId": "6asu8s", 65 | "resourcePath": "/ping", 66 | "httpMethod": "GET", 67 | "extendedRequestId": "VVwIYF7nDoEFexw=", 68 | "requestTime": "19/Feb/2019:09:06:10 +0000", 69 | "path": "/dev/ping", 70 | "accountId": "849707200000", 71 | "protocol": "HTTP/1.1", 72 | "stage": "dev", 73 | "domainPrefix": "deadbeef", 74 | "requestTimeEpoch": 1550567170476, 75 | "requestId": "99788f80-3425-11e9-b73f-cd1fe08a2b6e", 76 | "identity": { 77 | "cognitoIdentityPoolId": null, 78 | "accountId": null, 79 | "cognitoIdentityId": null, 80 | "caller": null, 81 | "sourceIp": "8.8.8.8", 82 | "accessKey": null, 83 | "cognitoAuthenticationType": null, 84 | "cognitoAuthenticationProvider": null, 85 | "userArn": null, 86 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Safari/537.36", 87 | "user": null 88 | }, 89 | "domainName": "deadbeef.execute-api.eu-west-1.amazonaws.com", 90 | "apiId": "deadbeef" 91 | }, 92 | "body": null, 93 | "isBase64Encoded": false 94 | } 95 | 96 | ## CloudWatch scheduled event: 97 | 98 | { 99 | "version": "0", 100 | "id": "2528c5e5-7feb-9a8d-9329-b445a222897f", 101 | "detail-type": "Scheduled Event", 102 | "source": "aws.events", 103 | "account": "849707200000", 104 | "time": "2019-02-19T08:59:04Z", 105 | "region": "eu-west-1", 106 | "resources": [ 107 | "arn:aws:events:eu-west-1:849707200000:rule/ServerlessSpringApi-MyServiceFunctionCheckWebsiteS-17EIBMQX0K6PN" 108 | ], 109 | "detail": {} 110 | } 111 | 112 | ## SNS Topic message 113 | 114 | { 115 | "Records": [ 116 | { 117 | "EventSource": "aws:sns", 118 | "EventVersion": "1.0", 119 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 120 | "Sns": { 121 | "Type": "Notification", 122 | "MessageId": "bcdfbdd1-eb20-58a7-bf97-e934236795c2", 123 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic", 124 | "Subject": null, 125 | "Message": "dasdsadsa", 126 | "Timestamp": "2019-02-19T09:01:59.174Z", 127 | "SignatureVersion": "1", 128 | "Signature": "Bk2H8nqd9WHlxnBaLjIymS7WK8TJ9baFiSNUn6dcVqRaGqjI6yq66/Qsh2lc09tBTV4eeDonRQWN1nNJfOeBinRunnsoP2DTIfypORo+UsRXGXEUeQJ7tnQUQe52rkVU5G+hX+hafU0hko6NhtCYgxd2ANtCm4klF3oEDybaRFZN1D0W0WZ3u+5jQNQU01rBca8VZ+hCLcQzJtH51p+VKrLYMpVz5h1jFxEB3XTkUtG9Q04bPypuY4o/Qh4mCx1pgghhpeZp6hOvoCWYQrj8EVyJo/SXynXGtK/RPsugRgwCMHRm/TsU1HHJKEVSe8ATMQX1oA6cXfFRzctm9tKnVQ==", 129 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", 130 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 131 | "MessageAttributes": {} 132 | } 133 | } 134 | ] 135 | } 136 | 137 | { 138 | "Records": [ 139 | { 140 | "EventSource": "aws:sns", 141 | "EventVersion": "1.0", 142 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 143 | "Sns": { 144 | "Type": "Notification", 145 | "MessageId": "15d202b9-ecca-5bf1-86aa-565c2460e18e", 146 | "TopicArn": "arn:aws:sns:eu-west-1:849707200000:test-topic", 147 | "Subject": null, 148 | "Message": "hello world!", 149 | "Timestamp": "2019-02-19T09:03:38.472Z", 150 | "SignatureVersion": "1", 151 | "Signature": "Gtt+vpFy8xd5jEBqChL3GU2DFS2WKFzMzmuUCcndy2y+UH4XJZ9TaU082rXtKZdNnhPuGrRygoOJkemLhh6q2kHu25FkvYBfkddBVCgr/wpgxnukqwlu3xYnmrtNqDJM8louZ38ZGk99miMf6NO1CloleIRc+EA1Vgfq7B/fE8lX1863VlLFViS36G86yUeFagNmPMnnPu7VFOi4RQRnzWa8/fKTGgvLMyGFesv6eWPFgDLEL6wGOpd/0gfxh6vq7sU9yuqQFkLvHTX5j6gf1+9Cfh1yxoJkz36NG3Oo17bIBiSsTqMl5A4Wc/3EKkGaPyjxPRPvxVof4ES0a6pPIA==", 152 | "SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", 153 | "UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:849707200000:test-topic:50c099cd-3d0d-4696-903d-cf808b3ce306", 154 | "MessageAttributes": { 155 | "somekey": { 156 | "Type": "String", 157 | "Value": "dsdasdas" 158 | } 159 | } 160 | } 161 | } 162 | ] 163 | } 164 | 165 | ## SQS 166 | 167 | { 168 | "Records": [ 169 | { 170 | "messageId": "72b03944-1af7-492b-aac9-412698960531", 171 | "receiptHandle": "AQEBicU8TU0NRCELI9WEuVNN/2EU6G1KUJTSVO9BoDPAUvZk4yLsyMQiiKLf0LNmx7ETrdQ1CrwlwdE7JaBVGAcLWPrMCx8wZLxu6x8QtlITVIIAEIwjt+5/rnbmUMY0ajOMok80gO9SceLa+Zr1g3yZnjUMf3xNSGqynvooQkHFRAtSTKf0bkkagkKIHCDt5RghXUeIYruEZTStOm7RTVQwWxaCTQMvbBgNAZFG6j54qg1hCI9Cv9P95FH6Tt8yRDjf9Ad3s7Jykm0yy+IWfvpvHBoSelM03LrUUTX5E6lFpmTByIIE+fmamzeTZOevyNviiiMRbvqvDkDlrVs3M00zjiocX5jz+GVUmE5q0tOzwGYC4yWbcPKDSOc1hiRYUhbo", 172 | "body": "Howdy?", 173 | "attributes": { 174 | "ApproximateReceiveCount": "1", 175 | "SentTimestamp": "1550567133640", 176 | "SenderId": "AIDAIEHDD23VAQL6ZVIAC", 177 | "ApproximateFirstReceiveTimestamp": "1550567133641" 178 | }, 179 | "messageAttributes": { 180 | "someattr": { 181 | "stringValue": "someval", 182 | "stringListValues": [], 183 | "binaryListValues": [], 184 | "dataType": "String" 185 | } 186 | }, 187 | "md5OfBody": "47b68d3722ece13893e9c5fcb3cac906", 188 | "md5OfMessageAttributes": "1e4b169c724503f3886d943e5bbe88e5", 189 | "eventSource": "aws:sqs", 190 | "eventSourceARN": "arn:aws:sqs:eu-west-1:849707200000:test-queue", 191 | "awsRegion": "eu-west-1" 192 | } 193 | ] 194 | } 195 | 196 | -------------------------------------------------------------------------------- /src/main/java/my/service/StreamLambdaHandler.java: -------------------------------------------------------------------------------- 1 | package my.service; 2 | 3 | import com.amazonaws.serverless.exceptions.ContainerInitializationException; 4 | import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; 5 | import com.amazonaws.serverless.proxy.internal.jaxrs.AwsProxySecurityContext; 6 | import com.amazonaws.serverless.proxy.model.ApiGatewayRequestIdentity; 7 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest; 8 | import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; 9 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse; 10 | import com.amazonaws.serverless.proxy.model.Headers; 11 | import com.amazonaws.serverless.proxy.model.MultiValuedTreeMap; 12 | import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; 13 | import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; 14 | import com.amazonaws.services.lambda.runtime.Context; 15 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; 16 | import com.amazonaws.services.lambda.runtime.events.SNSEvent; 17 | import com.amazonaws.services.lambda.runtime.events.SQSEvent; 18 | import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; 19 | import com.amazonaws.services.lambda.runtime.events.SNSEvent.SNSRecord; 20 | import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; 21 | import com.fasterxml.jackson.annotation.JsonIgnore; 22 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 23 | import com.fasterxml.jackson.annotation.JsonProperty; 24 | import com.fasterxml.jackson.core.JsonParser; 25 | import com.fasterxml.jackson.core.JsonProcessingException; 26 | import com.fasterxml.jackson.databind.DeserializationContext; 27 | import com.fasterxml.jackson.databind.ObjectMapper; 28 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 29 | import com.fasterxml.jackson.databind.module.SimpleModule; 30 | import com.fasterxml.jackson.datatype.joda.JodaModule; 31 | 32 | import org.apache.commons.io.IOUtils; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | import org.springframework.http.MediaType; 36 | 37 | import java.io.ByteArrayOutputStream; 38 | import java.io.IOException; 39 | import java.io.InputStream; 40 | import java.io.OutputStream; 41 | import java.util.List; 42 | import java.util.Map; 43 | 44 | public class StreamLambdaHandler implements RequestStreamHandler { 45 | private static Logger log = LoggerFactory.getLogger(StreamLambdaHandler.class); 46 | 47 | private static SpringBootLambdaContainerHandler handler; 48 | 49 | static { 50 | try { 51 | log.info("Starting AWS Lambda"); 52 | handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); 53 | log.info("Created AWS Handler, {}", handler); 54 | } catch (ContainerInitializationException e) { 55 | // if we fail here. We re-throw the exception to force another cold start 56 | e.printStackTrace(); 57 | throw new RuntimeException("Could not initialize Spring Boot application", e); 58 | } 59 | } 60 | 61 | @Override 62 | @SuppressWarnings("unchecked") 63 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { 64 | log.info("Handle request {}, {}, {}, {}", context, context.getFunctionName(), context.getInvokedFunctionArn(), 65 | context.getLogStreamName()); 66 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 67 | IOUtils.copy(inputStream, bos); 68 | String event = new String(bos.toByteArray(), 69 | LambdaContainerHandler.getContainerConfig().getDefaultContentCharset()); 70 | log.info("Got event {} context {}", event); 71 | 72 | ObjectMapper mapper = LambdaContainerHandler.getObjectMapper(); 73 | mapper.registerModule(new JodaModule()); 74 | 75 | SimpleModule module = new SimpleModule(); 76 | module.addDeserializer(SNSRecord.class, new SNSRecordDeserializer()); 77 | module.addDeserializer(SQSMessage.class, new SQSMessageDeserializer()); 78 | mapper.registerModule(module); 79 | 80 | AwsProxyRequest request = mapper.readValue(event, AwsProxyRequest.class); 81 | if (request.getHttpMethod() == null || "".equals(request.getHttpMethod())) { 82 | log.info("Parsing AWS event {}", event); 83 | Map raw = (Map) mapper.readValue(event, Map.class); 84 | 85 | // Peek inside one event 86 | if (raw.get("Records") instanceof List) { 87 | List> events = (List>) raw.get("Records"); 88 | if (events.size() > 0) { 89 | raw = events.get(0); 90 | } else { 91 | log.warn("Empty, dummy event records {}", events); 92 | } 93 | } 94 | 95 | String eventSource = (String) raw.get("source"); // CloudWatch event 96 | if (eventSource == null) { 97 | eventSource = (String) raw.get("EventSource"); // SNS 98 | } 99 | if (eventSource == null) { 100 | eventSource = (String) raw.get("eventSource"); // SQS!?! 101 | } 102 | 103 | if (eventSource == null) { 104 | log.warn("Can`t get event type: {}", raw); 105 | } else if ("aws.events".equals(eventSource)) { 106 | ScheduledEvent ev = mapper.readValue(event, AnnotatedScheduledEvent.class); 107 | request = convertToRequest(ev, event, AnnotatedScheduledEvent.class.getName(), context); 108 | log.info("Converted to {}", request); 109 | AwsProxyResponse response = handler.proxy(request, context); 110 | mapper.writeValue(outputStream, response); 111 | } else if ("aws:sns".equals(eventSource)) { 112 | SNSEvent ev = mapper.readValue(event, AnnotatedSNSEvent.class); 113 | request = convertToRequest(ev, event, AnnotatedSNSEvent.class.getName(), context); 114 | log.info("Converted to {}", request); 115 | AwsProxyResponse response = handler.proxy(request, context); 116 | mapper.writeValue(outputStream, response); 117 | } else if ("aws:sqs".equals(eventSource)) { 118 | SQSEvent ev = mapper.readValue(event, AnnotatedSQSEvent.class); 119 | request = convertToRequest(ev, event, AnnotatedSQSEvent.class.getName(), context); 120 | log.info("Converted to {}", request); 121 | AwsProxyResponse response = handler.proxy(request, context); 122 | mapper.writeValue(outputStream, response); 123 | } else { 124 | log.warn("Unhandled event type {}", eventSource); 125 | } 126 | } else { 127 | log.info("Handling via Spring Boot rest APIs {}", request); 128 | AwsProxyResponse response = handler.proxy(request, context); 129 | mapper.writeValue(outputStream, response); 130 | } 131 | } 132 | 133 | @JsonIgnoreProperties(ignoreUnknown = true) 134 | public static class AnnotatedScheduledEvent extends ScheduledEvent { 135 | 136 | } 137 | 138 | @JsonIgnoreProperties(ignoreUnknown = true) 139 | public static class AnnotatedSNSRecord extends SNSRecord { 140 | } 141 | 142 | @JsonIgnoreProperties(ignoreUnknown = true) 143 | public static class AnnotatedSNSEvent extends SNSEvent { 144 | @Override 145 | @JsonProperty("Records") 146 | public List getRecords() { 147 | return super.getRecords(); 148 | } 149 | } 150 | 151 | public class SNSRecordDeserializer extends StdDeserializer { 152 | public SNSRecordDeserializer() { 153 | this(null); 154 | } 155 | 156 | public SNSRecordDeserializer(Class vc) { 157 | super(vc); 158 | } 159 | 160 | @Override 161 | public SNSRecord deserialize(JsonParser jp, DeserializationContext ctxt) 162 | throws IOException, JsonProcessingException { 163 | return ctxt.readValue(jp, AnnotatedSNSRecord.class); 164 | } 165 | } 166 | 167 | @JsonIgnoreProperties(ignoreUnknown = true) 168 | public static class AnnotatedSQSMessage extends SQSMessage { 169 | } 170 | 171 | public class SQSMessageDeserializer extends StdDeserializer { 172 | public SQSMessageDeserializer() { 173 | this(null); 174 | } 175 | 176 | public SQSMessageDeserializer(Class vc) { 177 | super(vc); 178 | } 179 | 180 | @Override 181 | public SQSMessage deserialize(JsonParser jp, DeserializationContext ctxt) 182 | throws IOException, JsonProcessingException { 183 | return ctxt.readValue(jp, AnnotatedSQSMessage.class); 184 | } 185 | } 186 | 187 | @JsonIgnoreProperties(ignoreUnknown = true) 188 | public static class AnnotatedSQSEvent extends SQSEvent { 189 | @Override 190 | @JsonProperty("Records") 191 | @JsonIgnoreProperties(ignoreUnknown = true) 192 | public List getRecords() { 193 | return super.getRecords(); 194 | } 195 | } 196 | 197 | /** 198 | * Delivers all event to POST /event?type=className 199 | */ 200 | public static AwsProxyRequest convertToRequest(Object ev, String json, String className, Context context) { 201 | AwsProxyRequest r = new AwsProxyRequest(); 202 | r.setPath("/event"); 203 | r.setResource("/event"); 204 | r.setHttpMethod("POST"); 205 | MultiValuedTreeMap q = new MultiValuedTreeMap(); 206 | q.putSingle("type", className); 207 | r.setBody(json); 208 | r.setMultiValueQueryStringParameters(q); 209 | Headers h = new Headers(); 210 | h.putSingle("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); 211 | r.setMultiValueHeaders(h); 212 | AwsProxyRequestContext rc = new AwsProxyRequestContext(); 213 | rc.setIdentity(new ApiGatewayRequestIdentity()); 214 | r.setRequestContext(rc); 215 | return r; 216 | } 217 | } --------------------------------------------------------------------------------