Running serverless tasks - Spring Boot on AWS Fargate

In our journey with microservices, Spring Boot emerged as our trusted service layer framework for real-time APIs, handling complex logic, and seamless integrations. It excelled in many areas, but there were moments when we needed a more serverless approach, when our tasks were brief, such as file processing, handling a specific event, cleanup tasks, generating reports, data ingestion etc basically the kind of work that executes a certain use case and then shuts down.

Your first thought might be AWS Lambda, a popular choice for serverless computing. However, AWS Lambda has its limitations, and while it’s suitable for many use cases, it didn’t align perfectly with some of our use cases – for example, a file processing use case which requires a lengthy computation.

Our rule of thumb has always been to reuse what’s available, and only build when absolutely necessary. Considering the extensive set of libraries, custom logic, domain objects, and reusable components we’ve crafted for our Spring-based microservices, which are conveniently templated for easy replication, we wanted to retain as much of this valuable work as possible.

This led us to leverage Spring Boot’s CommandLineRunner interface along with the capabilities of AWS Fargate. This combination proved to be the ideal solution for any tasks which are resource intensive and require a lengthy computation.

Let’s go through a sample project and see how we managed to do this.

CommandLineRunner

Assuming you have a Spring Boot microservice project ready, make sure to implement a CommandLineRunner to execute your task. This task could be anything, such as file processing, handling a specific event, cleanup tasks, generating reports, data ingestion etc.

Here’s a basic example of a CommandLineRunner:

package au.com.task.runner;

import au.com.task.runner.service.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;


@SpringBootApplication
@Configuration
@Slf4j
public class Application implements CommandLineRunner {

@Autowired
private ConfigurableApplicationContext context;

@Autowired
private Service service;

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

@Override
public void run(final String... args) {
try {
log.info("Application running as CommandLineRunner with args {}", args);
service.someTask();
context.close(); // Close the Spring application context
log.info("Task completed successfully and context closed. System will exit now");
System.exit(0);
} catch (Exception ex) {
log.error("Task cannot be completed successfully, error : '{}'", ex.getMessage(), ex);
context.close(); // Close the Spring application context
System.exit(1);
}
}
}

The Application class implements the CommandLineRunner , the class is lightweight and delegates the actual work to service.someTask()

Once you are done with the task, you would want to exit, but it’s always a best practise to close the Spring application context properly, so that there are minimal resource leaks like database connections, file handles or network connections. Additionally, it ensures that beans are destroyed correctly, and any necessary cleanup (e.g., handling @PreDestroy or ContextClosedEvent) is performed.

Handle Failures

You can see that in the above example when there’s an exception, we are doing a System.exit(1) this depends on different use case or how you are triggering this application. Say for an example the application gets triggered via an EventBridge and its idempotent, then telling AWS service that one or more generic errors encountered upon exit is a good practise, as most of the AWS services support retry mechanism which are configurable.

At the same time if the application’s operation isn’t idempotent, it’s best to refrain from relying on AWS retry mechanisms. Instead, opt for a System.exit(0) and utilize logging and monitoring for issue identification and resolution. This allows for manual reruns of tasks as needed.

Why Fargate?

Many of our task definitions, which run our microservices using Docker images utilize the EC2 launch type. However, we encountered issues when certain tasks required significant resources. You do not want to know the batch process which was supposed to run midnight didn’t run because of resource constraints first day in the morning or there was sudden increase in number of files which required more CPU and memory. While auto scaling could address these problems, we opted to explore AWS Fargate as an alternative optimal solution.

Fargate — an efficient serverless compute engine enabling us to run Docker containers seamlessly, eliminating the complexities of server and cluster management.

Check below blogs from AWS on Fargate

  1. Introducing AWS Fargate — Run Containers without Managing Infrastructure
  2. Building, deploying, and operating containerized applications with AWS Fargate

Conclusion

Running Spring Boot microservices as serverless components using AWS Fargate and executing them as CommandLineRunner tasks opens up new possibilities for scalable and efficient application development. This approach promotes reusability, mitigates risks, and simplifies maintenance — all while maintaining control and supporting long-running tasks.

Reference

Amazon ECS on AWS Fargate — Amazon Elastic Container Service

Information has been prepared for information purposes only and does not constitute advice.

--

--