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
Posta un commento