Spring Cloud Function

Use Spring Cloud Function framework with LocalStack

Spring logo

Overview

In this guide, you will learn how to use LocalStack to test your serverless applications powered by Spring Cloud Function framework.

Complexity★★★☆☆
Time to read30 minutes
Editioncommunity [pro]
Platformx64_86 (-aarch64)

aarch64 warning

Some features and services described in this document may not work properly on aarch64, including Apple’s M1 silicon

Covered Topics

We will create a new Rest API application that will route requests to a Cloud Function using functionRouter and routing expressions.

The primary language for the application is Kotlin powered by Gradle build tool, but the described concepts would work for any other JVM setup.

Limitations

This document demonstrates the usage of the Spring Cloud Function framework together with LocalStack. It does not cover some of the application-specific topics, like 404 error handling, or parametrized routing, that you need to consider when building production-ready applications.

Setting up an Application

We recommend using jenv to manage multiple Java runtimes.

Starting a new Project

Please follow the instructions from the official website to install the Gradle build tool on your machine.

Then run the following command to initialize a new Gradle project

  1. $ gradle init

Running the command below will run the gradle wrapper task

  1. $ gradle wrapper

After running the wrapper task, you will find the Gradle wrapper script gradlew. From now on, we will use the wrapper instead of the globally installed Gradle binary:

  1. $ ./gradlew <command>

Project Settings

Let’s give our project a name: open settings.gradle, and adjust the autogenerated name to something meaningful.

  1. rootProject.name = 'localstack-sampleproject'

Now we need to define our dependencies. Here’s a list of what we will be using in our project.

Gradle plugins:

Dependencies:

In order to deploy our application to AWS, we need to build so-called “fat jar” which contains all application dependencies. To that end, we use the “Shadow Jar” plugin.

Here’s our final build.gradle:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  1. plugins {
  2. id java
  3. id org.jetbrains.kotlin.jvm version 1.5.31
  4. id org.jetbrains.kotlin.plugin.spring version 1.5.31
  5. id org.springframework.boot version 2.5.5
  6. id io.spring.dependency-management version 1.0.11.RELEASE
  7. id com.github.johnrengelman.shadow version 7.0.0
  8. }
  9. group = org.localstack.sampleproject
  10. sourceCompatibility = 11
  11. tasks.withType(JavaCompile) {
  12. options.encoding = UTF-8
  13. }
  14. repositories {
  15. mavenCentral()
  16. maven { url https://plugins.gradle.org/m2/“ }
  17. }
  18. ext {
  19. springCloudVersion = 3.1.4
  20. awsLambdaLog4jVersion = 1.2.0
  21. awsLambdaJavaEventsVersion = 3.10.0
  22. jacksonVersion = 2.12.5
  23. }
  24. dependencies {
  25. implementation org.jetbrains.kotlin:kotlin-stdlib
  26. implementation org.springframework.cloud:spring-cloud-starter-function-web:$springCloudVersion
  27. implementation org.springframework.cloud:spring-cloud-function-adapter-aws:$springCloudVersion
  28. implementation com.amazonaws:aws-lambda-java-log4j2:$awsLambdaLog4jVersion
  29. implementation com.amazonaws:aws-lambda-java-events:$awsLambdaJavaEventsVersion
  30. implementation com.fasterxml.jackson.core:jackson-core:$jacksonVersion
  31. implementation com.fasterxml.jackson.core:jackson-databind:$jacksonVersion
  32. implementation com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion
  33. implementation com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion
  34. }
  35. import com.github.jengelman.gradle.plugins.shadow.transformers.*
  36. // Configure the main class
  37. jar {
  38. manifest {
  39. attributes Start-Class’: org.localstack.sampleproject.Application
  40. }
  41. }
  42. // Build a fatjar (with dependencies) for aws lambda
  43. shadowJar {
  44. transform(Log4j2PluginsCacheFileTransformer)
  45. dependencies {
  46. exclude(
  47. dependency(“org.springframework.cloud:spring-cloud-function-web:${springCloudVersion}”)
  48. )
  49. }
  50. // Required for Spring
  51. mergeServiceFiles()
  52. append META-INF/spring.handlers
  53. append META-INF/spring.schemas
  54. append META-INF/spring.tooling
  55. transform(PropertiesFileTransformer) {
  56. paths = [‘META-INF/spring.factories’]
  57. mergeStrategy = append
  58. }
  59. }
  60. assemble.dependsOn shadowJar

Please note that we will be using org.localstack.sampleproject as a working namespace, and org.localstack.sampleproject.Application as an entry class for our application. You can adjust it for your needs, but don’t forget to change your package names accordingly.

Configure Log4J2 for AWS Lambda

Spring framework comes with Log4J logger, so all we need to do is to configure it for AWS Lambda. In this project, we are following official documentation to setup up src/main/resources/log4j2.xml content.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <Configuration packages="com.amazonaws.services.lambda.runtime.log4j2.LambdaAppender">
  3. <Appenders>
  4. <Lambda name="Lambda">
  5. <PatternLayout>
  6. <pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n</pattern>
  7. </PatternLayout>
  8. </Lambda>
  9. </Appenders>
  10. <Loggers>
  11. <Root level="debug">
  12. <AppenderRef ref="Lambda" />
  13. </Root>
  14. </Loggers>
  15. </Configuration>

Configure Spring Cloud Function for Rest API

Spring Function comes with functionRouter that can route requests to different Beans based on predefined routing expressions. Let’s configure it to lookup our function Beans by HTTP method and path, create a new application.properties file under src/main/resources/application.properties with the following content:

  1. spring.main.banner-mode=off
  2. spring.cloud.function.definition=functionRouter
  3. spring.cloud.function.routing-expression=headers['httpMethod'].concat(' ').concat(headers['path'])
  4. spring.cloud.function.scan.packages=org.localstack.sampleproject.api

Once configured, you can use FunctionInvoker as a handler for your Rest API lambda function. It will automatically pick up the configuration we have just set.

  1. org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest

Define an Application class

Now our application needs an entry-class, the one we referenced earlier. Let’s add it under src/main/kotlin/org/localstack/sampleproject/Application.kt.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  1. package org.localstack.sampleproject
  2. import org.springframework.boot.autoconfigure.SpringBootApplication
  3. @SpringBootApplication
  4. class Application
  5. fun main(args: Array<String>) {
  6. // Do nothing unless you use a custom runtime
  7. }

Configure Jackson

In our sample project we are using a JSON format for requests and responses. The easiest way to get started with JSON is to use the Jackson library. Let’s configure it by creating a new configuration class JacksonConfiguration.kt under src/main/kotlin/org/localstack/sampleproject/config:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  1. package org.localstack.sampleproject.config
  2. import com.fasterxml.jackson.annotation.JsonInclude
  3. import com.fasterxml.jackson.databind.*
  4. import org.springframework.context.annotation.Bean
  5. import org.springframework.context.annotation.Configuration
  6. import org.springframework.context.annotation.Primary
  7. import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
  8. import java.text.DateFormat
  9. @Configuration
  10. class JacksonConfiguration {
  11. @Bean
  12. fun jacksonBuilder() = Jackson2ObjectMapperBuilder()
  13. .dateFormat(DateFormat.getDateInstance(DateFormat.FULL))
  14. @Bean
  15. @Primary
  16. fun objectMapper(): ObjectMapper = ObjectMapper().apply {
  17. configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
  18. configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
  19. configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true)
  20. configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
  21. configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
  22. configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
  23. setSerializationInclusion(JsonInclude.Include.NON_NULL)
  24. findAndRegisterModules()
  25. }
  26. }

In applications where you need support for multiple formats or a format different from JSON (for example, SOAP/XML applications) simply use multiple beans with corresponding ObjectMapper implementations.

Define Logging Utility

Let’s create a small logging utility to simplify interactions with the logger

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  1. package org.localstack.sampleproject.util
  2. import org.apache.logging.log4j.LogManager
  3. import org.apache.logging.log4j.Logger
  4. open class Logger {
  5. val LOGGER: Logger = LogManager.getLogger(javaClass.enclosingClass)
  6. }

Add Request/Response utilities

To reduce the amount of boilerplate code, we are going to introduce three utility functions for our Rest API communications:

  • to build regular json response
  • to build error json response
  • to parse request payload using ObjectMapper. Note that ObjectMapper does not necessarily need to be a JSON only. It could also be XML or any other Mapper extended from standard ObjectMapper. Your application may even support multiple protocols with different request/response formats at once.

Let’s define utility functions to to build API gateway responses:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  1. package org.localstack.sampleproject.util
  2. import org.springframework.messaging.Message
  3. import org.springframework.messaging.support.MessageBuilder
  4. data class ResponseError(
  5. val message: String,
  6. )
  7. fun <T>buildJsonResponse(data: T, code: Int = 200): Message<T> {
  8. return MessageBuilder
  9. .withPayload(data)
  10. .setHeader(“Content-Type”, application/json”)
  11. .setHeader(“Access-Control-Allow-Origin”, “*”)
  12. .setHeader(“Access-Control-Allow-Methods”, OPTIONS,POST,GET”)
  13. .setHeader(“statusCode”, code)
  14. .build()
  15. }
  16. fun buildJsonErrorResponse(message: String, code: Int = 500) =
  17. buildJsonResponse(ResponseError(message), code)

And now a utility function to process API Gateway requests:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  1. package org.localstack.sampleproject.util
  2. import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
  3. import com.fasterxml.jackson.databind.ObjectMapper
  4. import org.springframework.messaging.Message
  5. import java.util.function.Function
  6. fun <T>apiGatewayFunction(
  7. objectMapper: ObjectMapper,
  8. callable: (message: Message<T>, context: APIGatewayProxyRequestEvent) -> Message<>
  9. ): Function<Message<T>, Message<>> = Function { input ->
  10. try {
  11. val context = objectMapper.readValue(
  12. objectMapper.writeValueAsString(input.headers),
  13. APIGatewayProxyRequestEvent::class.java
  14. )
  15. return@Function callable(input, context)
  16. } catch (e: Throwable) {
  17. val message = e.message?.replace(“\n”, “”)?.replace(“\””, “‘“)
  18. return@Function buildJsonErrorResponse(message ?: “”, 500)
  19. }
  20. }

Creating a sample Model / DTO

To transfer data from requests into something more meaningful than JSON strings (and back) you will be using a lot of Models and Data Transfer Objects (DTOs). It’s time to define our first one.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  1. package org.localstack.sampleproject.model
  2. import com.fasterxml.jackson.annotation.JsonIgnore
  3. data class SampleModel(
  4. val id: Int,
  5. val name: String,
  6. @JsonIgnore
  7. val jsonIgnoredProperty: String? = null,
  8. )

Creating Rest API endpoints

Let’s add our first endpoints to simulate CRUD operations on previously defined SampleModel:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  1. package org.localstack.sampleproject.api
  2. import com.fasterxml.jackson.databind.ObjectMapper
  3. import org.localstack.sampleproject.model.SampleModel
  4. import org.localstack.sampleproject.util.Logger
  5. import org.localstack.sampleproject.util.apiGatewayFunction
  6. import org.localstack.sampleproject.util.buildJsonResponse
  7. import org.springframework.context.annotation.Bean
  8. import org.springframework.stereotype.Component
  9. private val SAMPLE_RESPONSE = mutableListOf(
  10. SampleModel(id = 1, name = Sample #1”),
  11. SampleModel(id = 2, name = Sample #2”),
  12. )
  13. @Component
  14. class SampleApi(private val objectMapper: ObjectMapper) {
  15. companion object : Logger()
  16. @Bean(“POST /v1/entities”)
  17. fun createSampleEntity() = apiGatewayFunction<SampleModel>(objectMapper) { input, context ->
  18. LOGGER.info(“calling POST /v1/entities”)
  19. SAMPLE_RESPONSE.add(input.payload)
  20. buildJsonResponse(input.payload, code = 201)
  21. }
  22. @Bean(“GET /v1/entities”)
  23. fun listSampleEntities() = apiGatewayFunction<ByteArray>(objectMapper) { input, context ->
  24. LOGGER.info(“calling GET /v1/entities”)
  25. buildJsonResponse(“hello world”)
  26. }
  27. @Bean(“GET /v1/entities/get”)
  28. fun getSampleEntity() = apiGatewayFunction<ByteArray>(objectMapper) { input, context ->
  29. LOGGER.info(“calling GET /v1/entities/get”)
  30. val desiredId = context.queryStringParameters[“id”]!!.toInt()
  31. buildJsonResponse(SAMPLE_RESPONSE.find { it.id == desiredId })
  32. }
  33. }

Note how we used Spring’s dependency injection to inject ObjectMapper Bean we configured earlier.

Cold Start and Warmup (PRO)

Pro Features

Please note that EVENTS is a LocalStack PRO feature and is not supported in Community version

We know Java’s cold start is always a pain. To minimize this pain, we will try to define a pre-warming endpoint within the Rest API. By invoking this function every 5-10 mins we can make sure Rest API lambda is always kept in a pre-warmed state.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  1. package org.localstack.sampleproject.api
  2. import com.fasterxml.jackson.databind.ObjectMapper
  3. import org.localstack.sampleproject.util.apiGatewayFunction
  4. import org.localstack.sampleproject.util.buildJsonResponse
  5. import org.springframework.context.annotation.Bean
  6. import org.springframework.stereotype.Component
  7. @Component
  8. class ScheduleApi(private val objectMapper: ObjectMapper) {
  9. @Bean(“SCHEDULE warmup”)
  10. fun warmup() = apiGatewayFunction<ByteArray>(objectMapper) { input, context ->
  11. // execute scheduled events
  12. buildJsonResponse(“OK”)
  13. }
  14. }

Now you can add a scheduled event to the Rest API lambda function with the following synthetic payload (to simulate API gateway request). This way, you can define any other scheduled events, but we recommend using pure lambda functions.

  1. {
  2. "httpMethod": "SCHEDULE",
  3. "path": "warmup"
  4. }

As you may have guessed, this input will get mapped to the SCHEDULE warmup Bean.

For more information, please read the “Setting up Deployment” section.

Creating other lambda Handlers

HTTP requests are not the only thing our Spring Function-powered lambdas can do. We can still define pure lambda functions, DynamoDB stream handlers, and so on.

Below you can find a little example of few lambda functions grouped in LambdaApi class.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  1. package org.localstack.sampleproject.api
  2. import com.amazonaws.services.lambda.runtime.events.DynamodbEvent
  3. import org.localstack.sampleproject.model.SampleModel
  4. import org.localstack.sampleproject.util.Logger
  5. import org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler
  6. import org.springframework.context.annotation.Bean
  7. import org.springframework.stereotype.Component
  8. import java.util.function.Function
  9. @Component
  10. class LambdaApi : SpringBootStreamHandler() {
  11. companion object : Logger()
  12. @Bean
  13. fun functionOne(): Function<Any, String> {
  14. return Function {
  15. LOGGER.info(“calling function one”)
  16. return@Function ONE”;
  17. }
  18. }
  19. @Bean
  20. fun functionTwo(): Function<SampleModel, SampleModel> {
  21. return Function {
  22. LOGGER.info(“calling function two”)
  23. return@Function it;
  24. }
  25. }
  26. @Bean
  27. fun dynamoDbStreamHandlerExample(): Function<DynamodbEvent, Unit> {
  28. return Function {
  29. LOGGER.info(“handling DynamoDB stream event”)
  30. }
  31. }
  32. }

As you can see from the example above, we are using SpringBootStreamHandler class as a base that takes care of the application bootstrapping process and AWS requests transformation.

Now org.localstack.sampleproject.api.LambdaApi can be used as a handler for your lambda function along with FUNCTION_NAME environmental variable with the function bean name.

You may have noticed we used DynamodbEvent in the last example. The Lambda-Events package comes with a set of predefined wrappers that you can use to handle different lifecycle events from AWS.

Setting up Deployment

Check our sample project for usage examples.

  1. service: localstack-sampleproject-serverless
  2. provider:
  3. name: aws
  4. runtime: java11
  5. stage: ${opt:stage}
  6. region: us-west-1
  7. lambdaHashingVersion: 20201221
  8. deploymentBucket:
  9. name: deployment-bucket
  10. package:
  11. artifact: build/libs/localstack-sampleproject-all.jar
  12. plugins:
  13. - serverless-localstack
  14. - serverless-deployment-bucket
  15. custom:
  16. localstack:
  17. stages:
  18. - local
  19. functions:
  20. http_proxy:
  21. timeout: 30
  22. handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
  23. events:
  24. - http:
  25. path: /{proxy+}
  26. method: ANY
  27. cors: true
  28. # Please, note that events are a LocalStack PRO feature
  29. - schedule:
  30. rate: rate(10 minutes)
  31. enabled: true
  32. input:
  33. httpMethod: SCHEDULE
  34. path: warmup
  35. lambda_helloOne:
  36. timeout: 30
  37. handler: org.localstack.sampleproject.api.LambdaApi
  38. environment:
  39. FUNCTION_NAME: functionOne
  40. lambda_helloTwo:
  41. timeout: 30
  42. handler: org.localstack.sampleproject.api.LambdaApi
  43. environment:
  44. FUNCTION_NAME: functionTwo
  1. package org.localstack.cdkstack
  2. import java.util.UUID
  3. import software.amazon.awscdk.core.Construct
  4. import software.amazon.awscdk.core.Duration
  5. import software.amazon.awscdk.core.Stack
  6. import software.amazon.awscdk.services.apigateway.CorsOptions
  7. import software.amazon.awscdk.services.apigateway.LambdaRestApi
  8. import software.amazon.awscdk.services.apigateway.StageOptions
  9. import software.amazon.awscdk.services.events.Rule
  10. import software.amazon.awscdk.services.events.RuleTargetInput
  11. import software.amazon.awscdk.services.events.Schedule
  12. import software.amazon.awscdk.services.events.targets.LambdaFunction
  13. import software.amazon.awscdk.services.lambda.*
  14. import software.amazon.awscdk.services.lambda.Function
  15. import software.amazon.awscdk.services.s3.Bucket
  16. private val STAGE = System.getenv("STAGE") ?: "local"
  17. private const val JAR_PATH = "../../build/libs/localstack-sampleproject-all.jar"
  18. class ApplicationStack(parent: Construct, name: String) : Stack(parent, name) {
  19. init {
  20. val restApiLambda = Function.Builder.create(this, "RestApiFunction")
  21. .code(Code.fromAsset(JAR_PATH))
  22. .handler("org.springframework.cloud.function.adapter.aws.FunctionInvoker")
  23. .timeout(Duration.seconds(30))
  24. .runtime(Runtime.JAVA_11)
  25. .tracing(Tracing.ACTIVE)
  26. .build()
  27. val corsOptions = CorsOptions.builder().allowOrigins(listOf("*")).allowMethods(listOf("*")).build()
  28. LambdaRestApi.Builder.create(this, "ExampleRestApi")
  29. .proxy(true)
  30. .restApiName("ExampleRestApi")
  31. .defaultCorsPreflightOptions(corsOptions)
  32. .deployOptions(StageOptions.Builder().stageName(STAGE).build())
  33. .handler(restApiLambda)
  34. .build()
  35. val warmupRule = Rule.Builder.create(this, "WarmupRule")
  36. .schedule(Schedule.rate(Duration.minutes(10)))
  37. .build()
  38. val warmupTarget = LambdaFunction.Builder.create(restApiLambda)
  39. .event(RuleTargetInput.fromObject(mapOf("httpMethod" to "SCHEDULE", "path" to "warmup")))
  40. .build()
  41. // Please note that events is a LocalStack PRO feature
  42. warmupRule.addTarget(warmupTarget)
  43. SingletonFunction.Builder.create(this, "ExampleFunctionOne")
  44. .code(Code.fromAsset(JAR_PATH))
  45. .handler("org.localstack.sampleproject.api.LambdaApi")
  46. .environment(mapOf("FUNCTION_NAME" to "functionOne"))
  47. .timeout(Duration.seconds(30))
  48. .runtime(Runtime.JAVA_11)
  49. .uuid(UUID.randomUUID().toString())
  50. .build()
  51. SingletonFunction.Builder.create(this, "ExampleFunctionTwo")
  52. .code(Code.fromAsset(JAR_PATH))
  53. .handler("org.localstack.sampleproject.api.LambdaApi")
  54. .environment(mapOf("FUNCTION_NAME" to "functionTwo"))
  55. .timeout(Duration.seconds(30))
  56. .runtime(Runtime.JAVA_11)
  57. .uuid(UUID.randomUUID().toString())
  58. .build()
  59. }
  60. }
  1. variable "STAGE" {
  2. type = string
  3. default = "local"
  4. }
  5. variable "AWS_REGION" {
  6. type = string
  7. default = "us-east-1"
  8. }
  9. variable "JAR_PATH" {
  10. type = string
  11. default = "build/libs/localstack-sampleproject-all.jar"
  12. }
  13. provider "aws" {
  14. access_key = "test_access_key"
  15. secret_key = "test_secret_key"
  16. region = var.AWS_REGION
  17. s3_force_path_style = true
  18. skip_credentials_validation = true
  19. skip_metadata_api_check = true
  20. skip_requesting_account_id = true
  21. endpoints {
  22. apigateway = var.STAGE == "local" ? "http://localhost:4566" : null
  23. cloudformation = var.STAGE == "local" ? "http://localhost:4566" : null
  24. cloudwatch = var.STAGE == "local" ? "http://localhost:4566" : null
  25. cloudwatchevents = var.STAGE == "local" ? "http://localhost:4566" : null
  26. iam = var.STAGE == "local" ? "http://localhost:4566" : null
  27. lambda = var.STAGE == "local" ? "http://localhost:4566" : null
  28. s3 = var.STAGE == "local" ? "http://localhost:4566" : null
  29. }
  30. }
  31. resource "aws_iam_role" "lambda-execution-role" {
  32. name = "lambda-execution-role"
  33. assume_role_policy = <<EOF
  34. {
  35. "Version": "2012-10-17",
  36. "Statement": [
  37. {
  38. "Action": "sts:AssumeRole",
  39. "Principal": {
  40. "Service": "lambda.amazonaws.com"
  41. },
  42. "Effect": "Allow",
  43. "Sid": ""
  44. }
  45. ]
  46. }
  47. EOF
  48. }
  49. resource "aws_lambda_function" "restApiLambdaFunction" {
  50. filename = var.JAR_PATH
  51. function_name = "RestApiFunction"
  52. role = aws_iam_role.lambda-execution-role.arn
  53. handler = "org.springframework.cloud.function.adapter.aws.FunctionInvoker"
  54. runtime = "java11"
  55. timeout = 30
  56. source_code_hash = filebase64sha256(var.JAR_PATH)
  57. }
  58. resource "aws_api_gateway_rest_api" "rest-api" {
  59. name = "ExampleRestApi"
  60. }
  61. resource "aws_api_gateway_resource" "proxy" {
  62. rest_api_id = aws_api_gateway_rest_api.rest-api.id
  63. parent_id = aws_api_gateway_rest_api.rest-api.root_resource_id
  64. path_part = "{proxy+}"
  65. }
  66. resource "aws_api_gateway_method" "proxy" {
  67. rest_api_id = aws_api_gateway_rest_api.rest-api.id
  68. resource_id = aws_api_gateway_resource.proxy.id
  69. http_method = "ANY"
  70. authorization = "NONE"
  71. }
  72. resource "aws_api_gateway_integration" "proxy" {
  73. rest_api_id = aws_api_gateway_rest_api.rest-api.id
  74. resource_id = aws_api_gateway_method.proxy.resource_id
  75. http_method = aws_api_gateway_method.proxy.http_method
  76. integration_http_method = "POST"
  77. type = "AWS_PROXY"
  78. uri = aws_lambda_function.restApiLambdaFunction.invoke_arn
  79. }
  80. resource "aws_api_gateway_deployment" "rest-api-deployment" {
  81. depends_on = [aws_api_gateway_integration.proxy]
  82. rest_api_id = aws_api_gateway_rest_api.rest-api.id
  83. stage_name = var.STAGE
  84. }
  85. resource "aws_cloudwatch_event_rule" "warmup" {
  86. name = "warmup-event-rule"
  87. schedule_expression = "rate(10 minutes)"
  88. }
  89. resource "aws_cloudwatch_event_target" "warmup" {
  90. target_id = "warmup"
  91. rule = aws_cloudwatch_event_rule.warmup.name
  92. arn = aws_lambda_function.restApiLambdaFunction.arn
  93. input = "{\"httpMethod\": \"SCHEDULE\", \"path\": \"warmup\"}"
  94. }
  95. resource "aws_lambda_permission" "warmup-permission" {
  96. statement_id = "AllowExecutionFromCloudWatch"
  97. action = "lambda:InvokeFunction"
  98. function_name = aws_lambda_function.restApiLambdaFunction.function_name
  99. principal = "events.amazonaws.com"
  100. source_arn = aws_cloudwatch_event_rule.warmup.arn
  101. }
  102. resource "aws_lambda_function" "exampleFunctionOne" {
  103. filename = var.JAR_PATH
  104. function_name = "ExampleFunctionOne"
  105. role = aws_iam_role.lambda-execution-role.arn
  106. handler = "org.localstack.sampleproject.api.LambdaApi"
  107. runtime = "java11"
  108. timeout = 30
  109. source_code_hash = filebase64sha256(var.JAR_PATH)
  110. environment {
  111. variables = {
  112. FUNCTION_NAME = "functionOne"
  113. }
  114. }
  115. }
  116. resource "aws_lambda_function" "exampleFunctionTwo" {
  117. filename = var.JAR_PATH
  118. function_name = "ExampleFunctionTwo"
  119. role = aws_iam_role.lambda-execution-role.arn
  120. handler = "org.localstack.sampleproject.api.LambdaApi"
  121. runtime = "java11"
  122. timeout = 30
  123. source_code_hash = filebase64sha256(var.JAR_PATH)
  124. environment {
  125. variables = {
  126. FUNCTION_NAME = "functionTwo"
  127. }
  128. }
  129. }

Testing, Debugging and Code hot-swapping

Please read our Lambda Tools documentation to learn more about testing, debugging and code hot-swapping for JVM Lambda functions.

Last modified July 26, 2022: fix some typos (#214) (6ab8502d)