Version housekeeping of libraries and 3rd party code is a requirement in maintaining a strong resistance to security vulnerabilities in your product. We use maven as our build tool standard, and the Maven Versions Plugin from MojoHaus to update versions on an automated basis.
For the maven build, there are three sorts of dependencies that we automate:
Maven plugins – the building blocks that our build uses.
Explicit dependencies – the libraries that our application uses
Property based dependencies – these usually relate to a set of individual dependencies that use the same version, or for build reasons the version is a reference to a maven property value.
Note that the versions plugin is limited with updating plugin dependencies. It can only produce a report of available version updates – the plugin version must be manually updated in the pom.xml.
Version update process
Versioning Maven project plugins
Run mvn versions:display-plugin-updates and a report will be generated showing version updates available and which module pom.xml needs to be updated.
Versioning Maven project dependencies
Run the commands below to update both explicit and property based <version>w.x.y.z</version> elements in all modules pom.xml. It is suggested that this is run on code that is checked into a version control system so you can see the changes easily.
Configuring the versions plugin to stop false updates
Sadly due to the age of some of the java libraries, there are “poor” versioning choices in some of the older java libraries. You can configure the versions plugin to not consider certain versions as part of its update or not decision making. This can be useful to exclude alpha, beta, release candidate (rc) style naming, etc.
See a full implementation as part of our oss-maven-standardspom.xml on GitHub which excludes some common naming issues that we have found in our development. Feel free to use our open standards for your own projects too!
This article looks at optimising a Java Spring Boot application (Cloud Function style) with AWS SnapStart, and covered advanced optimisation with lifecycle management of pre snapshots and post restore of the application image by AWS SnapStart. We cover optimising a lambda for persistent network connection style conversational resources, such as an RDBMS, SQL, legacy messaging framework, etc.
How Snap Start Works
To import start up times for a cold start, SnapStart snapshots a virtual machine and uses the restore of the snapshot rather than the whole JVM + library startup time. For Java applications built on frameworks such as Spring Boot, this provides order of magnitude time reductions on cold start time. For a comparison with raw, SnapStart and Graal Native performance see our article here.
What frameworks do we use with Spring Boot?
For our Java Lambdas we use Spring Cloud Function with the AWS Lambda Adaptor. For an example for how we set this up, and links to our development frameworks and code, see our article AWS SnapStart for Faster Java Lambdas
Default SnapStart: Simple Optimisation of the Lambda INIT phase
When the lambda version is published SnapStart will run up the Java application to the point that the lambda is initialised. For a spring cloud function application, this will complete the Spring Boot lifecycle to the Container Started phase. In short, all your beans will be constructed, injected and started from a Spring Container perspective.
SnapStart will then snapshot the virtual machine with all the loaded information. When the image is restored, the exact memory layout of all classes and data in the JVM is restored. Thus any data loaded in this phase as part of a Spring Bean Constructor, @PostCreate annotated methods and ContextRefresh event handlers will have been reloaded as part of the restore.
Issues with persistent network connections
Where this breaks down is if you wish to use a “persistent” network connection style resource, such as a RDBMS connection. In this example, usually in a Spring Boot application a Data Source is configured and the network connections initialised pre container start. This can cause significant slow downs when restoring an image, perhaps weeks after its creation, as all the network connections will be broken.
For a self healing data source, when a connection is requested the connection will check, timeout and have to reconnect the connection and potentially start a new transaction for the number of configured connections in the pool. Even if you smartly set the pool size to one, given the single threaded lambda execution model, that connection timeout and reconnect may take significant time depending on network and database settings.
Project CRaC, Co-ordinated Restore at Checkpoint, is a JVM project that allows responses to the host operating system having a checkpoint pre a snapshot operation, and the signal that a operating system restore has occurred. The AWS Java Runtime supports integration with CRaC so that you can optimise your cold starts even under SnapStart.
At the time of our integration, we used the CRaC library to create a base class that could be used to create a support class that can handle “manual” tailoring of preSnapshot and postRestore events. Newer versions of boot are integrating CRaC support – see here for details.
We have created a base class, SnapStartOptimizer, that can be used to create a spring bean that can respond to preSnapshot and postRestore events. This gives us two hooks into the lifecycle:
Load more data into memory before the snapshot occurs.
Restore data and connections after we are running again.
Optimising pre snapshot
In this example we have a simple Spring Component that we use to exercise some functionality (http based) to load and lazy classes, data, etc. We also exercise the lookup of our spring cloud function definition bean.
@Component
@RequiredArgsConstructor
public class SnapStartOptimisation extends SnapStartOptimizer {
private final UserManager userManager;
private final TradingAccountManager accountManager;
private final TransactionManager transactionManager;
@Override
protected void performBeforeCheckpoint() {
swallowError(() -> userManager.fetchUser("thisisnotatoken"));
swallowError(() -> accountManager.accountsFor(new TradingUser("bob", "sub")));
final int previous = 30;
final int pageSize = 10;
swallowError(() -> transactionManager.query("435345345",
Instant.now().minusSeconds(previous),
Instant.now(),
PaginatedRequest.of(pageSize)));
checkSpringCloudFunctionDefinitionBean();
}
}
Optimising post restore – LambdaSqlConnection class.
In this example we highlight our LambdaSqlConnection class, which is already optimised for SnapStart. This class exercises a delegated java.sql.Connection instance preSnapshot to confirm connectivity, but replaces the connection on postRestore. This class is used to implement a bean of type java.sql.Connection, allowing you to write raw JDBC in lambdas using a single RDBMS connection for the lambda instance.
Note: Do not use default Spring Boot JDBC templates, JPA, Hibernate, etc in lambdas. The overhead of the default multi connection pools, etc is inappropriate for lambda use. For heavy batch processing a “Run Task” ECS image is more appropriate, and does not have 15 minute timeout constraints.
So how does it work?
The LambdaSqlConnection class manages the Connection bean instance.
When preSnapshot occurs, LambdaSqlConnection closes the Connection instance.
When postRestore occurs, LambdaSqlConnection reconnects the Connection instance.
Because LambdaSqlConnection creating a dynamic proxy as the Connection instance, it can manage the delegated connection “behind” the proxy without your injected Connection instance changing.
Using Our SQL Connection replacement in Spring Boot
@Import(LambdaSqlConnection.class)
@SpringBootApplication
public class MySpringBootApplication {
You can now remove any code that is creating a java.sql.Connection and simply use a standard java.sql.Connection instance injected as a dependency in your code. This configuration creates a java.sql.Connection compatible bean that is optimised with SnapStart and delegates to a real SQL connection.
We deploy and debug our Java Lambda on development machines using Localstack to emulate and Amazon Web Services (AWS) account. This article walks through the architecture, deployment using our open source java framework to local stack and enabling a debug mode for remote debugging using any Java integrated development environment (IDE).
These capabilities live in our test-utilities module, LambdaSupport.java.
Localstack development architecture
Our build framework uses Docker to deploy a Localstack image, then we use AWS Api calls to deploy a zip of our lambda java classes to the Localstack lambda engine. Due to the size of the zip files, we need to deploy the lambda using a S3 url. We use Localstack’s S3 implementation to emulate the process.
When the lambda is deployed, the Localstack Lambda engine will pull the AWS Lambda Runtime image from public ECR and then perform the deployment steps. Using the Localstack endpoint for lambda we now have a full environment where we can perform a lambda.invoke to test the deployed function.
Viewing lambda logs
With the appropriate Localstack configuration we can view lambda logs for both startup and run of the lambda. Note these logs appear in the docker logs for the AWS Lambda Runtime Container. This container spins up when the lambda is deployed.
The easiest method we use to see the logs is to:
Run the Junit test in debug, with a breakpoint after the lambda invoke.
When the breakpoint is hit, use docker ps and docker logs to see the output of the Lambda Runtime.
In IntelliJ Ultimate, you can see the containers deployed via the Services pane after connecting to your docker daemon.
Using the architecture in debug mode
We can use this architecture to remote debug the deployed lambda. Our LambdaSupport class includes configuration on deploy to enable debug mode as per the Localstack documentation https://docs.localstack.cloud/user-guide/lambda-tools/debugging/. With our support class you simply switch from java() to javaDebug() and the deploy will configure the runtime for debug mode (port 5050 by default).
In your docker-compose.yml, set the environment variable LAMBDA_DOCKER_FLAGS=-p 127.0.0.1:5050:5050 -e LS_LOG=debug.
This enables port passthrough for the java debugger from localhost to port 5050 of the container (assuming that is where the JVM debugging is configured for).
Do not commit this code as it will BLOCK test threads until a debugger is connected (port 5050 by default).
Loading the lambda as a static variable in a unit test.
We recommend a static initialised once a junit setup function due to the time to deploy the lambda.
The LambdaSupport.java method performs deployment of the supplied module zip to Localstack S3, then invokes the AWS Lambda API to confirm that the lambda has started cleanly (state == Active).
private static Lambda LAMBDA;
...
// environment variables for the lambda configuration
final Map<String, String> environment = Map.of(
"SPRING_PROFILES_ACTIVE", "integration-test"
"SPRING_CLOUD_FUNCTION_DEFINITION","get"
);
// using the lambda zip that was built in module ../jar-lambda-poc
LAMBDA = lambdaSupport.java("../jar-lambda-poc",
LimeAwsLambdaConfiguration.LAMBDA_HANDLER,
environment);
Invoking the lambda for black box testing
This example is using a static variable for the Lambda, JUnit 5 and assert4J. An AWS API Gateway event JSON is loaded and invoked to the deployed lambda. The result is asserted.
@Test
public void shouldCallTransactionPostOkApiGatewayEvent() {
final APIGatewayV2HTTPEvent event = json.loadLambdaEvent("/events/postApiEvent.json",
APIGatewayV2HTTPEvent.class);
final APIGatewayV2HTTPResponse response = lambdaSupport.invokeLambdaEvent(LAMBDA,
event,
APIGatewayV2HTTPResponse.class);
assertThat(response.getStatusCode()).isEqualTo(200);
String output = json.parse(response.getBody(), String.class);
assertThat(output).isEqualTo("world");
}
Localstack lambda deployment debug example
We alter the setup to use the deprecated javaDebug function. Do not commit this code as it will BLOCK test threads until a debugger is connected (port 5050 by default).
// using the lambda zip that was built in module ../jar-lambda-poc
LAMBDA = lambdaSupport.javaDebug("../jar-lambda-poc",
LimeAwsLambdaConfiguration.LAMBDA_HANDLER,
environment);
Need to bend Maven to your will wthout writing a maven plugin? Some hackery with Ant and Ant-Contrib‘s if task can solve many problems.
Lime Mojito’s approach to avoiding multiple build script technologies
We use maven at Lime Mojito for most of our builds due to the wealth of maven plugins available and “hardened” plugins that are suited to our “fail fast” approach to our builds. Checkstyle, Enforcer, Jacoco Coverage are a few of the very useful plugins we use. However, sometimes you need some custom build script and doing that in maven without using exec and a shell script can be tricky.
For more details see our post on Maintainable Builds with Maven to see how we keep our “application level” pom files so small.
We try and avoid having logic “spread” amongst different implementation technologies, and reduce cognitive load when maintining software by having the logic in one place. Usually this is a parent POM from a service’s perspective, but we try to avoid the “helper script” pattem as much as possible. We also strive to reduce the number of technologies in play so that maintaining services doesn’t require learning 47 different techologies to simply deploy a cloud service.
So how can you program Maven?
Not easily. Maven is “declarative” – you are meant to declare how the plugins are executed in order inside maven’s pom.xml for a source module. If you want to include control statements, conditionals, etc like a programming language maven is not the technology to do this in.
However, there is a maven plugin, ant-run, which allows us to embed Ant tasks and their “evil” logic companion, Ant Contrib, into our maven build.
Ant in Maven! Why would you do this?
Because maven is essentially an XML file. Ant instructions are also XML and embedding in the maven POM maintain a flow while editing. Property replacement between maven and ant is almost seamless, and this gives a good maintenance experience.
And yes, the drawback is that xml can become quite verbose. If our xml gets too big we consider it a “code smell” that we may need to write a proper maven plugin.
See our post on maintainable maven for tips on how we keep our service pom files so small.
Setting up the AntRun Maven plugin to use Ant Contrib.
We configure the maven plugin, but add a dependency for the ant-contrib library. That library is older, and has a poor dependency with ant in it’s POM so we exclude the ant jar as below. Once enabled, we can add any ant-contrib task using the XML namespace antlib:net.sf.antcontrib.
For a quick tutorial on XML and namespaces, see W3 Schools here.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<!--
This allows us to use things like if in ant tasks. We use ant to add some control flow to
maven builds.
<configuration>
<target xmlns:ac="antlib:net.sf.antcontrib">
<ac:if>
...
-->
<dependencies>
<dependency>
<groupId>ant-contrib</groupId>
<artifactId>ant-contrib</artifactId>
<version>1.0b3</version>
<exclusions>
<exclusion>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</plugin>
Example one – Calculation to set a property in the pom.xml
Here we configure the ant plugin’s execution to calculate a property to be set in the maven POM.xml. This allows later tasks, and even other maven plugins, to use the set property to alter their execution. We use skip configurations a lot to separate our “full” builds from “fast builds” where a fast build might skip most checks and just produce a deliverable for manual debugging. This plugin’s execution runs in the Maven process-resources phase – before you write executions a solid understanding of the Maven Lifecycle is required.
Because the file links back to our parent pom, we do not need to repeat version, ant-contrib setup, etc. This example does not need ant-contrib.
The main trick is that we set exportAntProperties on the plugin execution so that properties we set in ant are set in the maven project object model. The Maven property <test.compose.location> is set in the <properties> section of the POM. It is replaced in the ant script before it is executed by ant seamlessly by the maven-antrun-plugin.
Note that the XML inside the <target> tag is Ant XML. We are using the condition task and an available task to see if a file exists. If it does then we set the property docker.compose.skip to true.
This example is in our java-development/pom.xml which is the base POM for all our various base POMs for jars, spring boot, Java AWS Lambdas, etc.
Example 2 – If we are not skipping, wait for Docker Compose up before integration test start
We use another plugin to manage docker compose before our integration test phase using failsafe for our Java integration tests. This older plugin was before docker had healthcheck support in compose files – we recommend this compose healthcheck approach in modern development. Our configuration of this plugin uses docker.compose.skip property to skip execution if set to true.
However we can specify a port in our Maven pom.xml and the build will wait until that port responds 200 on http://localhost. As ant-run is before the failsafe plugin in our declaration, its execution happens before the failsafe test run.
Note that the XML inside the <target> tag is Ant XML. The <ac:if> is using the namespace defined in the <target> element that tells ant to use the ant-contrib jar for the task. We are using the ant-contrib if task to only perform a waitFor if the docker.compose.skip property is set to false. This was performed earlier in the lifecucle by the example above.
This example is in our java-development/pom.xml which is the base POM for all our various base POMs for jars, spring boot, Java AWS Lambdas, etc.
This aproach of ant hackery can produce small pieces of functionality in a maven build that can smooth the use of other plugins. Modern Ant has some support for if in a task dependency manner, but the older contrib tasks add a procedural approach that make the build cleaner in our opinion.
Why not Gradle? We have a lot of code in maven, and most of our projects fall into out standard deliverables of jars, Spring Boot jars or AWS Java Lambdas that are all easy to build in Maven. Our use of Java AWS CDK also uses maven so it ties nicely together from a limiting the number of technologies perspective. Given our service poms are so small due to our Maintainable Maven approach the benefits of Gradle seem small.
To test a highly AWS integrated solution, such as deployments on AWS Lambda, you can test deployments on an AWS “stub”, such as LocalStack or an AWS account per developer (or even per solution). Shared AWS account models are flawed for development as the environment can not be effectively shared with multiple developers without adding a lot of deployment complexity such as naming conventions.
What are the pros and cons of choosing a stub such as LocalStack versus an account management policy such as an AWS account per developer?
When is LocalStack a good approach?
LocalStack allows a configuration of AWS endpoints to point to a local service running stub AWS endpoints. These services implement most of the AWS API allowing a developer to check that their cloud implementations have basic functionality before deploying to a real AWS Account. LocalStack runs on a developer’s machine standalone or as a Docker container.
For example, you can deploy a Lambda implementation that uses SQS, S3, SNS, etc and test that connectivity works including subscriptions and file writes on LocalStack.
As LocalStack mimics the AWS API, it can be used with AWS-CLI, AWS SDKs, Cloudformation, CDK, etc.
LocalStack (at 28th July 2024) does not implement IAM security rules so a developer’s deployment code will not be tested for the enforcement of IAM policies.
Some endpoints (such as S3) require configuration so that the AWS API responds with URLs that can be used by the application correctly.
Using a “fresh” environment for development pipelines can be simulated by running a “fresh” LocalStack container. For example you can do a System Test environment by using a new container, provisioning and then running system testing.
If you have a highly managed and siloed corporate deployment environment, it may be easier, quicker and more pragmatic to configure LocalStack for your development team then attempt to have multiple accounts provisioned and managed by multiple specialist teams.
When is an AWS Account per developer a good approach?
An AWS account per developer can remove a lot of complexity in the build process. Rather than managing the stub endpoints and configuration, developers can be forced to deploy with security rules such as IAM roles and consider costing of services as part of the development process.
However this requires a high level of account provisioning and policy automation. Accounts need to be monitored for cost control and features such as account destruction and cost saving shutdowns need to be implemented. Security scans for policy issues, etc can be implemented across accounts and policies for AWS technology usage can be controlled using AWS Organisations.
An account per developer opens a small step to account per environment which allows the provisioning of say System Test environments on an ad hoc basis. AWS best practices for security also suggest account per service to limit blast radius and maintain separate controls for critical services such as payment processing.
If the organisation already has centralised account policy management and a strong provisioning team(s), this may be an effective approach to reduce the complexity in development while allowing modern automated pipeline practices.
Conclusion
Approach
Pros
Cons
LocalStack
Can be deployed on a developer’s machine.
Does not require using managed environments in a corporate setting.
Can be used with AWS-SDKs, AWS-CLI, Cloudformation, SAM, CDK for deployments
Development environments are separated without naming conventions in shared accounts, etc.
Fresh LocalStacks can be used to mimic environments for various development pipeline stages.
Development environment control within the development team.
Requires application configuration to use LocalStack.
Does not test security policies before deployment.
Maven is known to be a verbose, opinionated framework for building applications, primarily for a Java Stack. In this article we discuss Lime Mojito’s view on maven, and how we use it to produce maintainable, repeatable builds using modern features such as automated testing, AWS stubbing (LocalStack) and deployment. We have OSS standards you can use in your own maven builds at https://bitbucket.org/limemojito/oss-maven-standards/src/master/ and POM’s on maven central.
Before we look at our standards, we set the context of what drives our build design by looking at our technology choices. We’ll cover why our developer builds are setup this way, but not how our Agile Continuous Integration works in this post.
Lime Mojito’s Technology Choices
Lime Mojito uses a Java based technology stack with Spring, provisioned on AWS. We use AWS CDK (Java) for provisioning and our lone exception is for web based user interfaces (UI), where we use Typescript and React with Material UI and AWS Amplify.
Our build system is developer machine first focused, using Maven as the main build system for all components other than the UI.
Build Charter
The build enforces our development standards to reduce the code review load.
The build must have a simple developer interface – mvn clean install.
If the clean install passes – we can move to source Pull Request (PR).
PR is important, as when a PR is merged we may automatically deploy to production.
Creating a new project or module must not require a lot of configuration (“xml hell”).
A module must not depend on another running Lime Mojito module for testing.
Any stub resources for testing must be a docker image.
This example will do all the below with only 6 lines of extra XML in your maven pom.xml file:
enforce your dependencies are a single java version
resolve dependencies via the Bill of Materials Library that we use too smooth out our Spring + Spring Boot + Spring Cloud + Spring Function + AWS SDK(s) dependency web.
Enable Lombok for easier java development with less boilerplate
Configure code signing
Configure maven repository deployment locations (I suggest overriding these for your own deployments!)
When you add dependencies, common ones that are in or resolved via our library pom.xml do not need version numbers as they are managed by our modern Bill of Materials (BOM) style dependency setup.
Our Open Source Standards library supports the following module types (archetypes) out of the box:
Type
Description
java-development
Base POM used to configure deployment locations, checkstyle, enforcer, docker, plugin versions, profiles, etc. Designed to be extended for different archetypes (JAR, WAR, etc.).
jar-development
Build a jar file with test and docker support
jar-lamda-development
Build a Spring Boot Cloud Function jar suitable for lambda use (java 17 Runtime) with AWS dependencies added by default. Jar is shaded for simple upload.
spring-boot-development
Spring boot jar constructed with the base spring-boot-starter and lime mojito aws-utilities for local stack support.
Available Module Development Types
We hope that you might find these standards interesting to try out.
We have a web service responding to web requests. The service has a thread pool where each web request uses one operating system thread. The requests are then managed by a multi-core CPU that time-slices between the various threads using the operating system scheduler.
This example is very similar to how Tomcat (Spring Boot MVC) works out of the box when servicing requests with servlets in the Java web server space. The Java VM (v17) matches a Java Thread to an operating system thread that is then scheduled for execution by a core.
So what happens when we have a lot of requests?
Many threads here are sliced between the 4 cores. This slicing of threads where a core works on one for a while, then context switches to another thread, can scale to any level. However, there is an expense in CPU time to switch between one thread to another. This context switch is expensive as it involves both memory and CPU manipulation.
Given enough threads, the CPU cores can quickly spend a significant amount of time context switching when compared to the actual amount of time processing the request.
How do we reduce context switching?
We can trade off context switching for latency by blocking a request thread until a vCPU is available to do the work. Provided the work is largely CPU bound this may reduce the overall throughput time if the context switching has become a major use of the available vCPU resources.
For our Java spring boot based application we introduce one of the standard Executors to provide a blocking task service. We use a WorkStealingPool which is an executor that defaults the worker threads to the number of CPUs available with an unlimited queue depth.
We now move the CPU heavy process into a task that can be scheduled onto the executor by a given thread. The thread will then block on the Future returned from submitting the task – this blocking occurs until a worker thread has completed the task’s job and returned a result.
On our application, this returned a 5X improvement to average throughput times for the same work being submitted to a single microservice performing the request processing. This goes to show that in our situation the majority of CPU was being spent on context switching between requests rather than servicing the CPU intensive task for each request.
In our case this translated to 5X less CPU required and a similar reduction in our AWS EC2 costs for this service as we needed less instances provisioned to support the same load.
After finding Native Java Lambda to be too fragile for runtimes we investigated AWS Snap Start to speed up our cold starts for Java Lambda. While not as fast as native, Snap Start is a supported AWS Runtime mode for Lambda and it is far easier to build and deploy compared to the requirements for native lambda.
How does Snap Start Work?
Snap Start runs up your Java lambda in the initialisation phase, then takes a VM snapshot. That snapshot becomes the starting point for a cold start when the lambda initialises, rather than the startup time of your java application.
With Spring Boot this shows a large decrease in cold start time as the JVM initialisation, reflection and general image setup is happening before the first request is sent.
Snap Start is configured by saving a Version of your lambda. This version phase takes the VM snapshot and loads that instead of the standard java runtime initialisation phase. The runtime required is the offical Amazon Lambda Runtime and no custom images are required.
What are the trade offs for Snap Start?
Version Publishing needs to be added to the lambda deployment. The deployment time is longer as that image needs to be taken when the version is published.
VM shared resources may behave differently to development as they are re-hydrated before use in the cold start case. For example DB connection pools will need to fail and reconnect as they be begin at request time in a disconnected state. However see AWS RDS Proxy for this serverless use case.
As at 26th August 2023 SnapStart is limited to the x86 Architecture for Lambda runtimes.
What are the speed differences?
After warm up there was no difference between a hot JVM and the native compiled hello world program. Cold start however showed a marked difference from memory settings of 512MB and higher due to the proportional allocation of more vCPU.
Times below are in milliseconds.
Architecture
256
512
1024
Java
5066
4054
3514
SnapStart
4689.22
2345.2
1713.82
Native
1002
773
670
Comparison of Architecture v Lambda Memory Configuration
At 1GB with have approximately 1 vCPU for the lambda runtime which makes a significant difference to the cold start times. Memory settings higher than 1vCPU had little effect.
While native is over twice as fast as SnapStart the fragility of deployment for lambda and the massive increase in build times and agent CPU requirements due to compilation was un productive for our use cases.
Snap start adds around 3 minutes to deployments to take the version snapshot (on AWS resources) which we consider acceptable compared to the build agent increase that we needed to do for native (6vCPU and 8GB). As we are back to Java and scripting our agents are back down to 2vCPU and 2GB with build times less than 10 minutes.
How do you integrate Snap Start with AWS CDK?
This is a little tricky as there are not specific CDK Function props to enable SnapStart (as at 26th August 2023). With CDK we have to fall back to a cloud formation primitive to enable snap start and then take a version
final IFunction function = new Function(this,
LAMBDA_FUNCTION_ID,
FunctionProps.builder()
.functionName(LAMBDA_FUNCTION_ID)
.description("Lambda example with Java 17")
.role(role)
.timeout(Duration.seconds(timeoutSeconds))
.memorySize(memorySize)
.environment(Map.of())
.code(assetCode)
.runtime(JAVA_17)
.handler(LAMBDA_HANDLER)
.logRetention(RetentionDays.ONE_DAY)
.architecture(X86_64)
.build());
CfnFunction cfnFunction = (CfnFunction) function.getNode().getDefaultChild();
cfnFunction.setSnapStart(CfnFunction.SnapStartProperty.builder()
.applyOn("PublishedVersions")
.build());
IFunction snapstartVersion = new Version(this,
LAMBDA_FUNCTION_ID + "-snap",
VersionProps.builder()
.lambda(function)
.description("Snapstart Version")
.build());
In CDK because Version and Function both implement IFunction, you can pass a Version to route constructs as below.
String apiId = LAMBDA_FUNCTION_ID + "-api";
HttpApi api = new HttpApi(this, apiId, HttpApiProps.builder()
.apiName(apiId)
.description("Public API for %s".formatted(LAMBDA_FUNCTION_ID))
.build());
HttpLambdaIntegration integration = new HttpLambdaIntegration(LAMBDA_FUNCTION_ID + "-integration",
snapstartVersion,
HttpLambdaIntegrationProps.builder()
.payloadFormatVersion(
VERSION_2_0)
.build());
HttpRoute build = HttpRoute.Builder.create(this, LAMBDA_FUNCTION_ID + "-route")
.routeKey(HttpRouteKey.with("/" + LAMBDA_FUNCTION_ID, HttpMethod.GET))
.httpApi(api)
.integration(integration)
.build();
Note in the HttpLambdaIntegration that we pass a Version rather than the Function object. This produces the Cloudformation that links the API Gateway integration to your published Snap Start version of the Java Lambda.
Update: 20/8/2023: After the CDK announcement that node 16 is no longer supported after September 2023 we realised that we can’t run CDK and node on Amazon Linux2 for our build agents. We upgraded our agents to AL2023 and found out the native build produces incompatible binaries due to GLIBC upgrades, and Lambda does not support AL2023 runtimes. We have given up with this native approach due to the fragility of the platform and are investigating AWS Snapstart which now has Java 17 support.
Update: 02/9/2023: We have switched to AWS Snap Start as it appears to be a better trade off for application portability. Short builds and no more binary compatibility issues.
Native Java AWS Lambda refers to Java program that has been compiled down to native instructions so we can get faster “cold start” times on AWS Lambda deployments.
Cold start is the initial time spent in a Lambda Function when it is first deployed by AWS and run up to respond to a request. These cold start times are visible to a caller has higher latency to the first lambda request. Java applications are known for their high cold start times due to the time taken to spin up the Java Virtual Machine and the loading of various java libraries.
We built a small framework that can assemble either a AWS Lambda Java runtime zip, or a provided container implementation of a hello world function. The container provided version is an Amazon Linux 2 Lambda Runtime with a bootstrap shell script that runs our Native Java implementation.
Note that these timings were against the raw hello java lambda (not the spring cloud function version).
@Slf4j
public class MethodHandler {
public String handleRequest(String input, Context context) {
log.info("Input: " + input);
return "Hello World - " + input;
}
}
Native Java AWS Lambda timings
We open with a “Cold Start” – the time taken to provision the Lambda Function and run the first request. Then a single request to the hot lambda to get the pre-JIT (Just-In-Time compiler) latency. Then ten requests to warm the lambda further so we have some JIT activity. Max Memory use is also shown to get a feel system usage. We run up to 1GB memory sizing to approach 1vCPU as per various discussions online.
Note that we run the lambda at various AWS lambda memory settings as there is a direct proportional link between vCPU allocation and the amount of memory allocated to a lambda (see AWS documentation).
This first set of timings is for a Java 17 Lambda Runtime container running a zip of the hello world function. Times are in milliseconds.
Java Container
128
256
512
1024
Cold Start
6464
5066
4054
3514
1
90
52
16
5
10X
60
30
5
4
Max Mem
126
152
150
150
Java Container Results
Native Java
128
256
512
1024
Cold
1427
1002
773
670
1
10
4
4
5
10X
4
4
3
3
Max Mem
111
119
119
119
Native Java Results
The comparison of the times below show the large performance gains for cold start.
Conclusion
From our results we have a 6X performance improvement in cold starts leading to sub second performance for the initial request.
The native version shows a more consistent warm lambda behaviour due to the native lambda compilation process. Note that the execution times seem to trend for both Java and native down to sub 10ms response times.
While there is a reduction in memory usage this is of no realisable benefit as we configure a larger memory size to get more of a vCPU allocation.
However be aware thatbuild times increased markedly due to the compilation phase (from 2 minutes to 8 for a hello world application). This compilation phase is very CPU and memory intensive so we had to increase our build agents to 6vCPU and 8GB for compiles to work.
How to integrate AWS Cognito and Spring Security using JSON Web Tokens (JWT), Cognito groups and mapping to Spring Security Roles. Annotations are used to secure Java methods.
AWS Cognito Configuration
Configure a user pool.
Apply a web client
Create a user with a group.
The user pool can be created from the AWS web console. The User Pool represents a collection of users with attributes, for more information see the amazon documentation.
An app client should be created that can generate JWT tokens on authentication. An example client configuration is below, and can be created from the pool settings in the Amazon web console. This client uses a simple username/password flow to generate id, access and refresh tokens on a successful auth.
Note this form of client authentication flow is not recommended for production use.
We can now add a group so that we can bind new users to a group membership. This is added from the group tab on the user pool console.
Creating a user
We can easily create a user using the aws command line.
The curl example below will generate a token for our hello test user. Note that you will need to adjust the URL to the region your user pool is in, and the client id as required. The client ID can be retrieved from the App Client Information page in the AWS Cognito web console.
We start by configuring a Spring Security OAuth 2.0 Resource server. This resource server represents our service and will be guarded by the AWS Cognito access token. This JWT contains the cognito claims as configured in the Cognito User Pool.
This configuration is simply to point the issuer URL (JWT iss claim) to the Cognito Issuer URL for your User Pool.
The following security configuration enables Spring Security method level authorisation using annotations, and configures the Resource Server to split the Cognito Groups claim into a set of roles that can be mapped by the Spring Security Framework.
This Spring Security configuration maps a default role, “USER” to all valid tokens, plus each of the group names in the JWT claim cognito:groups is mapped a a spring role of the same name. As per spring naming conventions, each role has the name prefixed with “ROLE_”. We also allow spring boot actuator in this example to function without any authentication, which gives us a health endpoint, etc. In production you will want to bar access to these URLs.
A now a code example of the annotations used to secure a method. The method below, annotated by PreAuthorize, requires a group of Admin to be linked to the user calling the method. Note that the role “Admin” amps to the spring security role “ROLE_Admin” which will be sourced from the Cognito group membership of “Admin” as previously configured in our Cognito setup above.
That’s it! You now have a working example for configuring cognito and Spring Security to work together. As this is based on the Authorisation header with a bearer token, it will work with minimal configuration of API Gateway, Lambda, etc.