5
votes

I could need some help here...

I'm using Spring Cloud Function, and I want to deploy my functions on AWS Lambda, using the adapter for AWS.

My application class looks like this:

package example;

@SpringBootApplication
public class SpringCloudFunctionApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudFunctionApiGatewayApplication.class, args);
    }

}

Function 1 looks like this:

package example;

@Component
public class StoreFunction implements Consumer<Message<DemoEntity>>{

    @Override
    public void accept(Message<DemoEntity> t) {

        System.out.println("Stored entity " + ((DemoEntity)t.getPayload()).getName());
        return;
    }
}

Finally, my function handler looks like this:

package example;

public class TestFunctionHandler extends SpringBootApiGatewayRequestHandler {

}

This setup works perfectly. When deploying to Lambda, I provide example.TestFunctionHandler as handler in the AWS console, and Spring Cloud recognizes automatically that example.QueryFunction is the only function in the context.

The log output looks like this:

START RequestId: 3bd996e7-ef5e-11e8-9829-1f50e2b93b6c Version: $LATEST
20:27:45.821 [main] INFO org.springframework.cloud.function.adapter.aws.SpringFunctionInitializer - Initializing: class de.margul.awstutorials.springcloudfunction.apigateway.SpringCloudFunctionApiGatewayApplication

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

2018-11-23 20:27:48.221  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : Starting LambdaRTEntry on ip-10-153-127-174.ec2.internal with PID 1 (/var/runtime/lib/LambdaJavaRTEntry-1.0.jar started by sbx_user1060 in /)
2018-11-23 20:27:48.242  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : No active profile set, falling back to default profiles: default
2018-11-23 20:27:52.081  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : Started LambdaRTEntry in 5.941 seconds (JVM running for 7.429)
Stored entity John Doe
END RequestId: 3bd996e7-ef5e-11e8-9829-1f50e2b93b6c
REPORT RequestId: 3bd996e7-ef5e-11e8-9829-1f50e2b93b6c  Duration: 7113.98 ms    Billed Duration: 7200 ms    Memory Size: 1088 MB    Max Memory Used: 113 MB 

Now, my problem comes here. I want to have multiple functions in one project. I know, on Lambda there can be only one function per deployment. However, for code maintenance reasons (there is some shared code as well as configurations in the real project), we want to have all functions in one project, deploy the project multiple times, and define in the deployment, which is the relevant function.

Using the native AWS SDK for Lambda, that was easy (like in this example in section 4.2): One implementation of RequestStreamHandler, with multiple methods (even if RequestStreamHandler has only one handleRequest() method). The point was that one could define the relevant function as handler: package.ClassName::methodName

However, that does not work with Spring Cloud Function (as we can have only one handler, which is TestFunctionHandler in this case). The documentations mentions that multiple functions are possible, by specifying function.name in application.properties, or as Lambda environment variable FUNCTION_NAME. Anyhow, I don't get that working.

My function 2 looks like this:

package example;

@Component
public class QueryFunction implements Function<Message<String>, Message<DemoEntity>>{

    @Override
    public Message<DemoEntity> apply(Message<String> m) {

        String name = m.getPayload();

        DemoEntity response = new DemoEntity();
        response.setName(name);
        Message<DemoEntity> message = MessageBuilder
                .withPayload(response)
                .setHeader("contentType", "application/json")
                .build();
        return message;
    }
}

In my application.properties, I have this line:

function.name = example.StoreFunction

The same applies if I create an environment variable FUNCTION_NAME: example.StoreFunction

If I now deploy the library and trigger it, I get the following logs:

START RequestId: 67e64098-ef5d-11e8-bdbf-9ddadadef0ce Version: $LATEST
20:21:50.802 [main] INFO org.springframework.cloud.function.adapter.aws.SpringFunctionInitializer - Initializing: class example.SpringCloudFunctionApiGatewayApplication

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

2018-11-23 20:21:53.684  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : Starting LambdaRTEntry on ip-10-153-127-174.ec2.internal with PID 1 (/var/runtime/lib/LambdaJavaRTEntry-1.0.jar started by sbx_user1059 in /)
2018-11-23 20:21:53.687  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : No active profile set, falling back to default profiles: default
2018-11-23 20:21:57.488  INFO 1 --- [           main] lambdainternal.LambdaRTEntry             : Started LambdaRTEntry in 6.353 seconds (JVM running for 8.326)
No function defined: java.lang.IllegalStateException
java.lang.IllegalStateException: No function defined
    at org.springframework.cloud.function.adapter.aws.SpringFunctionInitializer.apply(SpringFunctionInitializer.java:134)
    at org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler.handleRequest(SpringBootRequestHandler.java:48)

END RequestId: 67e64098-ef5d-11e8-bdbf-9ddadadef0ce
REPORT RequestId: 67e64098-ef5d-11e8-bdbf-9ddadadef0ce  Duration: 7454.73 ms    Billed Duration: 7500 ms    Memory Size: 1088 MB    Max Memory Used: 130 MB 

Every help is highly appreciated!

1
Any way to have this multiple EndPoints functionality in the same AWS Lambda in runtime at the same time with Spring Cloud Function? - pellyadolfo
@pellyadolfo I think you are talking about what people say as "Monolithic Lambda" (single lambda with multiple functions inside). In this case, there should be a single function in your lambda (only that will be triggered by lambda container on event like proxy type API gateway request) and in turn this will route to different internal functions. Though it is debatable now a days in architecture perspective that which one to use "Monolithic Lambda" vs "Micro Lambda" (single lambda with single function), where each of them has its own pros and cons and entirely based on application needs. - Abhishek Chatterjee
@AbhishekChatterjee Finally I did in that way as monolitic lambda with a RouterFunction. I agree with you..... if you have plenty of resources. In my case I am the only developer of my project and do not have want to maintain a bunch of lambda functions. It works pretty well in fact. - pellyadolfo
@pellyadolfo, thats correct, it entirely depends on scenario. However, it seems that your are using env variable to route between two functions but that is static binding decided at deployment time. There is one recommended option that you can think about, using spring built-in router function. Actually we both are on the same path, trying to achieve same goal in different way. In my spring-routing way, I was facing some routing related issues regarding implementation. Please have a look if you can help, stackoverflow.com/questions/63389412/… - Abhishek Chatterjee

1 Answers

6
votes

Ok, the problem was obviously my limited knowledge about Spring Beans.

After I viewed a list of all available beans in the context, it was clear that I had to use the class name, but starting with a lower case, i. e. function.name = storeFunction or function.name = queryFunction.

Edit to explain my solution in detail:

In my project, I have several functions like this:

@Component
public class StoreFunction implements Consumer<String>{

    @Override
    public void accept(String s) {

        // Logic comes here
    }
}

@Component
public class QueryFunction implements Function<String, String>{

    @Override
    public void apply(String s) {

        return s;
    }
}

Then, I register them as beans, e. g. like this:

@SpringBootApplication
public class SpringCloudFunctionApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudFunctionApiGatewayApplication.class, args);
    }

    @Bean
    StoreFunction storeFunction(){
        return new StoreFunction();
    }

    @Bean
    QueryFunction queryFunction(){
        return new QueryFunction();
    }
}

Now, I have the two beans storeFunction and queryFunction (the names of the @Bean methods above) available in my Spring context.

Finally, I have to tell Spring which of the functions to call. That can be done by creating an environment variable FUNCTION_NAME and setting it to one of the bean names.

When I now deploy the project to AWS Lambda, I have to tell Lambda which function the invoke (as Lambda only can invoke one function per deployment).

Btw, I created a tutorial for that.