Skip to content

Commit 626b770

Browse files
committed
Initial project setup
1 parent 905b639 commit 626b770

File tree

18 files changed

+539
-6
lines changed

18 files changed

+539
-6
lines changed

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
*.iml
3+
runtime.zip
4+
5+
aws-lambda-java-libs/
6+
.idea/
7+
target/
8+
cdk.out/
9+
10+

Dockerfile

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
FROM --platform=linux/amd64 amazonlinux:2 AS packer
2+
3+
# Add the Amazon Corretto repository
4+
RUN rpm --import https://yum.corretto.aws/corretto.key
5+
RUN curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo
6+
7+
# Update the packages and install Amazon Corretto 18, Maven and Zip
8+
RUN yum -y update
9+
RUN yum install -y java-18-amazon-corretto-devel maven zip
10+
11+
# Set Java 18 as the default
12+
RUN update-alternatives --set java "/usr/lib/jvm/java-18-amazon-corretto/bin/java"
13+
RUN update-alternatives --set javac "/usr/lib/jvm/java-18-amazon-corretto/bin/javac"
14+
15+
# Copy the software folder to the image and build the function
16+
COPY software software
17+
WORKDIR /software/example-function
18+
RUN mvn clean package
19+
20+
# Find JDK module dependencies dynamically from our uber jar
21+
RUN jdeps \
22+
# dont worry about missing modules
23+
--ignore-missing-deps \
24+
# suppress any warnings printed to console
25+
-q \
26+
# java release version targeting
27+
--multi-release 18 \
28+
# output the dependencies at end of run
29+
--print-module-deps \
30+
# pipe the result of running jdeps on the function jar to file
31+
target/function.jar > jre-deps.info
32+
33+
# Create a slim Java 18 JRE which only contains the required modules to run this function
34+
RUN jlink --verbose \
35+
--compress 2 \
36+
--strip-java-debug-attributes \
37+
--no-header-files \
38+
--no-man-pages \
39+
--output /jre18-slim \
40+
--add-modules $(cat jre-deps.info)
41+
42+
43+
# Use Javas Application Class Data Sharing feature
44+
# It creates the file /jre18-slim/lib/server/classes.jsa
45+
RUN /jre18-slim/bin/java -Xshare:dump -Xbootclasspath/a:/software/example-function/target/function.jar
46+
47+
# Package everything together into a custom runtime archive
48+
WORKDIR /
49+
50+
COPY bootstrap bootstrap
51+
RUN chmod 755 bootstrap
52+
RUN cp /software/example-function/target/function.jar function.jar
53+
RUN zip -r runtime.zip \
54+
bootstrap \
55+
function.jar \
56+
/jre18-slim

README.md

+80-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,86 @@
1-
## My Project
1+
# AWS Lambda custom runtime based on minimal Java 18 JRE
2+
An AWS Lambda custom runtime to enable Java 18 support on a minimalistic JRE, which only includes the Java modules required by the application.
23

3-
TODO: Fill this README out!
4+
![overview](docs/overview.png)
45

5-
Be sure to:
6+
1. Use the application code (uber.jar), download the preferred Java version and create a bootstrap file with optimized starting instructions for the application.
7+
2. Take advantage of using jdeps and jlink to create a minified Java runtime based on the application needs, start the app with Class Data Sharing enabled to get the classes file for later start optimizations and build the runtime.zip out of those components
8+
3. Deploy the runtime, including the app, to AWS Lambda via AWS CDK
69

7-
* Change the title in this README
8-
* Edit your repository description on GitHub
10+
## Getting started
11+
12+
1. Download or clone the repository.
13+
14+
2. install prerequisite software:
15+
1. Install [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html)
16+
2. Install [Docker](https://docs.docker.com/get-docker/)
17+
18+
3. Build and package the AWS Lambda function and create the AWS Lambda custom runtime using Docker:
19+
20+
```bash
21+
./build.sh
22+
```
23+
24+
4. Provision the AWS infrastructure (Amazon API Gateway, AWS Lambda and Amazon DynamoDB) using AWS CDK:
25+
26+
```bash
27+
./provision-infrastructure.sh
28+
```
29+
30+
The API Gateway endpoint URL is displayed in the output and saved in the file `infrastructure/target/outputs.json`. The contents are similar to:
31+
32+
```
33+
{
34+
"LambdaCustomRuntimeMinimalJRE18InfrastructureStack": {
35+
"apiendpoint": "https://<API_ID>.execute-api.<AWS_REGION>.amazonaws.com"
36+
}
37+
}
38+
```
39+
40+
## Using Artillery to test the changes
41+
42+
First, install prerequisites:
43+
44+
1. Install [jq](https://stedolan.github.io/jq/) and [Artillery Core](https://artillery.io/docs/guides/getting-started/installing-artillery.html)
45+
2. Run the following script from the projects root directory:
46+
47+
```bash
48+
artillery run -t $(cat infrastructure/target/outputs.json | jq -r '.LambdaCustomRuntimeMinimalJRE18InfrastructureStack.apiendpoint') -v '{ "url": "/custom-runtime" }' infrastructure/loadtest.yml
49+
```
50+
51+
52+
### Check results in Amazon CloudWatch Insights
53+
54+
1. Navigate to Amazon **[CloudWatch Logs Insights](https://console.aws.amazon.com/cloudwatch/home?#logsV2:logs-insights)**.
55+
2. Select the log groups `/aws/lambda/custom-runtime-java-18` from the drop-down list
56+
3. Copy the following query and choose **Run query**:
57+
58+
```
59+
filter @type = "REPORT"
60+
| parse @log /\d+:\/aws\/lambda\/(?<function>.*)/
61+
| stats
62+
count(*) as invocations,
63+
pct(@duration+coalesce(@initDuration,0), 0) as p0,
64+
pct(@duration+coalesce(@initDuration,0), 25) as p25,
65+
pct(@duration+coalesce(@initDuration,0), 50) as p50,
66+
pct(@duration+coalesce(@initDuration,0), 75) as p75,
67+
pct(@duration+coalesce(@initDuration,0), 90) as p90,
68+
pct(@duration+coalesce(@initDuration,0), 95) as p95,
69+
pct(@duration+coalesce(@initDuration,0), 99) as p99,
70+
pct(@duration+coalesce(@initDuration,0), 100) as p100
71+
group by function, ispresent(@initDuration) as coldstart
72+
| sort by coldstart, function
73+
```
74+
75+
![AWS Console](docs/insights-query.png)
76+
77+
You see results similar to:
78+
79+
![Resuts](docs/results.png)
80+
81+
For cold-starts only you will see results similar to:
82+
83+
![Resuts](docs/cold-start-only.png)
984

1085
## Security
1186

@@ -14,4 +89,3 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform
1489
## License
1590

1691
This library is licensed under the MIT-0 License. See the LICENSE file.
17-

bootstrap

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
3+
$LAMBDA_TASK_ROOT/jre18-slim/bin/java \
4+
--add-opens java.base/java.util=ALL-UNNAMED \
5+
-XX:+TieredCompilation \
6+
-XX:TieredStopAtLevel=1 \
7+
-Xshare:on \
8+
-XX:+UseSerialGC \
9+
-jar function.jar "$_HANDLER"

build.sh

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh
2+
# remove a maybe earlier build custom runtime archives
3+
rm runtime.zip
4+
5+
# Build the docker image which will:
6+
# 1. Use the latest Amazon Linux 2 image and install Amazon Corretto 18
7+
# 2. Copy the software directory into the Docker container and run the build using Maven, which creates an uber jar
8+
# 3. run jdeps to calculate the module dependencies for this uber jar
9+
# 4. feeding the jdeps result into jlink, creating a minimal Java 18 JRE which only contains the necessary modules to run this jar
10+
# 5. Use Javas Application Class Data Sharing for further optimizations
11+
# 6. Create the runtime.zip archive, based on the AWS Lambda custom runtime specification
12+
docker build -f Dockerfile --progress=plain -t lambda-custom-runtime-minimal-jre-18-x86 .
13+
# Extract the runtime.zip from the Docker container and store it locally
14+
docker run --rm --entrypoint cat lambda-custom-runtime-minimal-jre-18-x86 runtime.zip > runtime.zip

docs/cold-start-only.png

29.1 KB
Loading

docs/insights-query.png

75.5 KB
Loading

docs/overview.png

42.8 KB
Loading

docs/results.png

43.4 KB
Loading

infrastructure/.gitignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.classpath.txt
2+
target
3+
.classpath
4+
.project
5+
.idea
6+
.settings
7+
.vscode
8+
*.iml
9+
10+
# CDK asset staging directory
11+
.cdk.staging
12+
cdk.out
13+
outputs.json
14+

infrastructure/cdk.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"app": "mvn -e -q compile exec:java",
3+
"context": {
4+
"@aws-cdk/core:enableStackNameDuplicates": "true",
5+
"aws-cdk:enableDiffNoFail": "true",
6+
"@aws-cdk/core:stackRelativeExports": "true",
7+
"@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
8+
"@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
9+
"@aws-cdk/aws-kms:defaultKeyPolicies": true,
10+
"@aws-cdk/aws-s3:grantWriteWithoutAcl": true
11+
}
12+
}

infrastructure/loadtest.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
config:
2+
phases:
3+
- duration: 60
4+
arrivalRate: 120
5+
scenarios:
6+
- flow:
7+
- post:
8+
url: "{{ url }}"

infrastructure/pom.xml

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
3+
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<groupId>example</groupId>
7+
<artifactId>infrastructure</artifactId>
8+
<version>0.1</version>
9+
10+
<properties>
11+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
12+
<cdk.version>1.132.0</cdk.version>
13+
<junit.version>5.7.0</junit.version>
14+
</properties>
15+
16+
<build>
17+
<plugins>
18+
<plugin>
19+
<groupId>org.apache.maven.plugins</groupId>
20+
<artifactId>maven-compiler-plugin</artifactId>
21+
<version>3.8.1</version>
22+
<configuration>
23+
<source>9</source>
24+
<target>9</target>
25+
</configuration>
26+
</plugin>
27+
28+
<plugin>
29+
<groupId>org.codehaus.mojo</groupId>
30+
<artifactId>exec-maven-plugin</artifactId>
31+
<version>3.0.0</version>
32+
<configuration>
33+
<mainClass>com.amazon.aws.example.InfrastructureApp</mainClass>
34+
</configuration>
35+
</plugin>
36+
</plugins>
37+
</build>
38+
39+
<dependencies>
40+
<!-- AWS Cloud Development Kit -->
41+
<dependency>
42+
<groupId>software.amazon.awscdk</groupId>
43+
<artifactId>core</artifactId>
44+
<version>${cdk.version}</version>
45+
</dependency>
46+
47+
<dependency>
48+
<groupId>software.amazon.awscdk</groupId>
49+
<artifactId>lambda</artifactId>
50+
<version>${cdk.version}</version>
51+
</dependency>
52+
<dependency>
53+
<groupId>software.amazon.awscdk</groupId>
54+
<artifactId>apigatewayv2</artifactId>
55+
<version>${cdk.version}</version>
56+
</dependency>
57+
<dependency>
58+
<groupId>software.amazon.awscdk</groupId>
59+
<artifactId>apigatewayv2-integrations</artifactId>
60+
<version>${cdk.version}</version>
61+
</dependency>
62+
<dependency>
63+
<groupId>software.amazon.awscdk</groupId>
64+
<artifactId>dynamodb</artifactId>
65+
<version>${cdk.version}</version>
66+
</dependency>
67+
68+
69+
</dependencies>
70+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.amazon.aws.example;
2+
3+
import software.amazon.awscdk.core.App;
4+
5+
public class InfrastructureApp {
6+
public static void main(final String[] args) {
7+
App app = new App();
8+
9+
new InfrastructureStack(app, "LambdaCustomRuntimeMinimalJRE18InfrastructureStack");
10+
11+
app.synth();
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.amazon.aws.example;
2+
3+
import software.amazon.awscdk.core.CfnOutput;
4+
import software.amazon.awscdk.core.CfnOutputProps;
5+
import software.amazon.awscdk.core.Construct;
6+
import software.amazon.awscdk.core.Duration;
7+
import software.amazon.awscdk.core.Stack;
8+
import software.amazon.awscdk.core.StackProps;
9+
import software.amazon.awscdk.services.apigatewayv2.AddRoutesOptions;
10+
import software.amazon.awscdk.services.apigatewayv2.HttpApi;
11+
import software.amazon.awscdk.services.apigatewayv2.HttpApiProps;
12+
import software.amazon.awscdk.services.apigatewayv2.HttpMethod;
13+
import software.amazon.awscdk.services.apigatewayv2.PayloadFormatVersion;
14+
import software.amazon.awscdk.services.apigatewayv2.integrations.LambdaProxyIntegration;
15+
import software.amazon.awscdk.services.apigatewayv2.integrations.LambdaProxyIntegrationProps;
16+
import software.amazon.awscdk.services.dynamodb.Attribute;
17+
import software.amazon.awscdk.services.dynamodb.AttributeType;
18+
import software.amazon.awscdk.services.dynamodb.BillingMode;
19+
import software.amazon.awscdk.services.dynamodb.Table;
20+
import software.amazon.awscdk.services.dynamodb.TableProps;
21+
import software.amazon.awscdk.services.lambda.*;
22+
import software.amazon.awscdk.services.lambda.Runtime;
23+
import software.amazon.awscdk.services.logs.RetentionDays;
24+
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
28+
import static java.util.Collections.singletonList;
29+
30+
public class InfrastructureStack extends Stack {
31+
public InfrastructureStack(final Construct scope, final String id) {
32+
this(scope, id, null);
33+
}
34+
35+
public InfrastructureStack(final Construct scope, final String id, final StackProps props) {
36+
super(scope, id, props);
37+
38+
Table exampleTable = new Table(this, "ExampleTable", TableProps.builder()
39+
.partitionKey(Attribute.builder()
40+
.type(AttributeType.STRING)
41+
.name("id").build())
42+
.billingMode(BillingMode.PAY_PER_REQUEST)
43+
.build());
44+
45+
Function customJava18Function = new Function(this, "LambdaCustomRuntimeJava18", FunctionProps.builder()
46+
.functionName("custom-runtime-java-18")
47+
.handler("com.amazon.aws.example.ExampleDynamoDbHandler::handleRequest")
48+
.runtime(Runtime.PROVIDED_AL2)
49+
.architecture(Architecture.X86_64)
50+
.code(Code.fromAsset("../runtime.zip"))
51+
.memorySize(512)
52+
.environment(Map.of("TABLE_NAME", exampleTable.getTableName()))
53+
.timeout(Duration.seconds(20))
54+
.logRetention(RetentionDays.ONE_WEEK)
55+
.build());
56+
57+
exampleTable.grantWriteData(customJava18Function);
58+
59+
HttpApi httpApi = new HttpApi(this, "ExampleApi", HttpApiProps.builder()
60+
.apiName("ExampleApi")
61+
.build());
62+
63+
httpApi.addRoutes(AddRoutesOptions.builder()
64+
.path("/custom-runtime")
65+
.methods(singletonList(HttpMethod.POST))
66+
.integration(new LambdaProxyIntegration(LambdaProxyIntegrationProps.builder()
67+
.handler(customJava18Function)
68+
.payloadFormatVersion(PayloadFormatVersion.VERSION_2_0)
69+
.build()))
70+
.build());
71+
72+
new CfnOutput(this, "api-endpoint", CfnOutputProps.builder()
73+
.value(httpApi.getApiEndpoint())
74+
.build());
75+
}
76+
}

0 commit comments

Comments
 (0)