Why update Java version on Lambda Function?


Upgrading JVM is always a "huge pain" and the more big your application is, the more "complicated" will be the upgrade.

Convincing management why the update is important, is even more difficult because they tend to suppose it is just "a cost" and probably not really required.

But they are wrong because the enhancement among different jvm is incredible, especially if we talk about memory.

AN EXAMPLE

Let's suppose we have a very simple Lambda function that just implements a partially CRUD operations for a DynamoDB table named query. The function is exposed through API gateway and the driver for the behavior is the http method.

The code is pretty simple: based on the method, a service (findService) is called to retrieve all elements (for GET) or to insert (for PUT)

The "retrieve all" is implemented with a full scan operation, which is not optimal of course but as you know this is intended to be for testing purpose.

public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

static FindService findService = FindService.of();
static ObjectMapper objectMapper = new ObjectMapper();

public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
LambdaLogger logger = context.getLogger();
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withHeaders(headers);
try {
long startTime = DateTime.now().getMillis();
String bodyResponse = this.serveRequest(input.getHttpMethod(), input.getBody());
logger.log("Execution time is " + (DateTime.now().getMillis() - startTime));
return response
.withStatusCode(200)
.withBody(bodyResponse);
} catch (IOException e) {
context.getLogger().log("There was an error " + e);
e.printStackTrace();
try {
return response
.withBody(objectMapper.writeValueAsString(new ErrorResponse(e.getMessage())))
.withStatusCode(500);
} catch (JsonProcessingException ex) {
return response
.withBody("{\"message\" : \"Unable to create error response\"}")
.withStatusCode(500);
}
}
}

private String serveRequest(String requestType, String body) throws IOException{
if (requestType.equals("GET")){
return objectMapper.writeValueAsString(findService .getUsers());
} else if (requestType.equals("PUT")){
return objectMapper.writeValueAsString(findService.insertUser(new ObjectMapper().readValue(body, User.class)));
} else {
return objectMapper.writeValueAsString(new SimpleRequest(requestType));
}
}
}

Here the findService

public class FindService {

private DynamoDB dynamoDB;

private AmazonDynamoDB amazonDynamoDB;

private FindService(){
amazonDynamoDB = AmazonDynamoDBClient.builder().withRegion("us-east-1").build();
dynamoDB = new DynamoDB(amazonDynamoDB);

};

public static FindService of(){
return new FindService();

}


public List<User> getUsers(){

//Table table = dynamoDB.getTable("users");
ScanRequest scanRequest = new ScanRequest().withTableName("users");
return amazonDynamoDB.scan(scanRequest).getItems().stream().map(stringAttributeValueMap ->
new User(Integer.parseInt(stringAttributeValueMap.get("id").getN()),
stringAttributeValueMap.get("email").getS(),
stringAttributeValueMap.get("firstName").getS(),
stringAttributeValueMap.get("lastName").getS())).collect(Collectors.toList());
}

public User insertUser(User user){
Table table = dynamoDB.getTable("users");
table.putItem(new Item().withNumber("id", user.id())
.withString("email", user.email())
.withString("firstName", user.firstName())
.withString("lastName", user.lastName()));
return user;
}
}

As you see, the pojo classes are written as they are Record, which means:

  • empty constructor private
  • public constructor with all the values
  • no setter allowed
  • getter have the signature field()

To make Jackson works, we need to add some annotations

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class User{
private int id;
@JsonProperty("id")
public int id() {return id;}

private String email;
@JsonProperty("email")
public String email() {return email;}

private String firstName;
@JsonProperty("firstName")
public String firstName() {return firstName;}

private String lastName;
@JsonProperty("lastName")
public String lastName() {return lastName;}

@JsonCreator
public User(@JsonProperty("id") int id,
@JsonProperty("email") String email,
@JsonProperty("firstName") String firstName,
@JsonProperty("lastName") String lastName){
this.id = id;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
}

The reason of this decision? It will be easy to transform to Record when we will change the code later (while the algorithm will be not changed)

The Pom is configured to use java 11

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>

The Docker is easy 

FROM public.ecr.aws/lambda/java:11

COPY target/classes /var/task/
COPY target/dependency /var/task/lib
# Command can be overwritten by providing a different command in the template directly.
CMD ["crud.App::handleRequest"]

The code is deployed with CloudFormation, so the template is this

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
lambda_queue_sam_project

Sample SAM Template for lambda_queue_sam_project

Globals:
Function:
Timeout: 60
Parameters:
ImageUriParameter:
Type: String
Default: xxxxx.dkr.ecr.us-east-1.amazonaws.com/lambda-crud:latest
Resources:
FunctionCrudDynamodbRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: FunctionCrudDynamodbRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: DynamoDbFullAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'dynamodb:*'
Resource: '*'
- PolicyName: AWSLambdaBasicExecutionRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
CrudDatabaseApi:
Type: AWS::Serverless::Api
Properties:
StageName: dev
Name: CrudDatabaseApi
CrudDatabaseFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
FunctionName: CrudDatabase
Role: !GetAtt FunctionCrudDynamodbRole.Arn
ImageUri: !Ref ImageUriParameter
Architectures:
- x86_64
Environment:
Variables:
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Events:
HelloWorldGet:
Type: Api
Properties:
Path: /crud
Method: get
RestApiId: !Ref CrudDatabaseApi
HelloWorldPut:
Type: Api
Properties:
Path: /crud
Method: put
RestApiId: !Ref CrudDatabaseApi
HelloWorldPost:
Type: Api
Properties:
Path: /crud
Method: post
RestApiId: !Ref CrudDatabaseApi
HelloWorldDelete:
Type: Api
Properties:
Path: /crud
Method: delete
RestApiId: !Ref CrudDatabaseApi

Outputs:
HelloWorldApi:
Description: Print out value
Value: !Join [ "/", [ "https:/", !Join [ ".", [!Ref "CrudDatabaseApi", execute-api,!Ref "AWS::Region", "amazonaws.com" ] ],dev, crud ] ]
CrudDatabaseFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt CrudDatabaseFunction.Arn
CrudDatabaseFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt CrudDatabaseFunction.Arn

The default Memory is 128 if no value is explicated. If we run this application and perform a Curl we obtain


So we need to set the memory to 256 and run it.

UPGRADE JAVA

To upgrade the code for using java 17 we first need to change the pom

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>

Then we need to change the code:

User will become a nice Record Java

package crud;

public record User(int id, String email, String firstName, String lastName) {
}

Nice uh?

Time to change Docker configuration to use java17

FROM public.ecr.aws/lambda/java:17

COPY target/classes /var/task/
COPY target/dependency /var/task/lib
CMD ["crud.App::handleRequest"]
In template.yaml we can now change to

MemorySize : 128

or even remove the property

So it is time to deploy and verify if everything works

TEST

After deploying the code we can perform the curl

curl -X GET https://xxxxxx.execute-api.us-east-1.amazonaws.com/Stage/crud

And the response is correct!

[{"id":3,"email":"email","firstName":"Fabrizio","lastName":"Rametta"},{"id":2,"email":"email","firstName":"Fabrizio","lastName":"Rametta"},{"id":1,"email":"email","firstName":"Fabrizio","lastName":"Rametta"}]

The table content is a little dummy :)


If we also look to the consumption we can see


The Max memory used is 122, so can use half of the memory to run the same code


CONCLUSION


Upgrading JVM is not only about using new code structure or new feature, is also about:

  • less memory consumption
  • enhancement of garbage collector
  • less vulnerability
This can be the drive for your management to understand why upgrade JVM: with lambda it will be more easy because the amount of code should be very little (and also not many external libraries which could be not compatible)

Less memory means less bill, less costs and so on

Time to upgrade

Commenti

Post popolari in questo blog

HTML and AI - A simple request, and a beautiful response for ElasticSearch without Kibana

A simple CD using AWS Pipeline

Websocket Chat with Lambda (but all in Java)