mn create-function-app example.micronaut.micronautguide --features=aws-lambda-s3-event-notification --build=gradle --lang=kotlin
Micronaut AWS Lambda and S3 Event
Learn how to generate thumbnails for images uploaded to an S3 bucket with AWS Lambda and the Micronaut framework
Authors: Sergio del Amo
Micronaut Version: 5.0.0
1. Getting Started
In this guide, we will create a Micronaut application written in Kotlin.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOMEconfigured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the App
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
If you use Micronaut Launch, select serverless function as the application type and add the aws-lambda-s3-event-notification feature.
The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.
4.1. Thumbnail Configuration
Add the following configuration:
thumbnail.width=256
thumbnail.height=256
Create an interface to encapsulate configuration:
package example.micronaut
interface ThumbnailConfiguration {
val width: Int
val height: Int
}
package example.micronaut
import io.micronaut.context.annotation.ConfigurationProperties
import jakarta.validation.constraints.Positive
@ConfigurationProperties("thumbnail") (1)
class ThumbnailConfigurationProperties : ThumbnailConfiguration {
@field:Positive (2)
override var width: Int = 0
@field:Positive (2)
override var height: Int = 0
}
| 1 | The @ConfigurationProperties annotation takes the configuration prefix. |
| 2 | You can use validation constraints in the @ConfigurationProperties objects. |
4.2. Thumbnail Generation
Create a contract for thumbnail generation. We leverage the @Pattern annotation to accept only jpg and png files.
package example.micronaut
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import java.io.InputStream
interface ThumbnailGenerator {
fun thumbnail(
inputStream: InputStream,
@NotBlank @Pattern(regexp = "jpg|png") format: String
): ByteArray?
}
Add a dependency to Thumbnailator, a thumbnail generation library for Java.
implementation("net.coobird:thumbnailator:0.4.17")
Create an implementation of ThumbnailGenerator that uses Thumbnailator.
package example.micronaut
import jakarta.inject.Singleton
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
import net.coobird.thumbnailator.Thumbnails
import org.slf4j.LoggerFactory
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
@Singleton (1)
open class ThumbnailatorThumbnailGenerator(
private val thumbnailConfiguration: ThumbnailConfiguration (2)
) : ThumbnailGenerator {
override fun thumbnail(
inputStream: InputStream,
@NotBlank @Pattern(regexp = "jpg|png") format: String
): ByteArray? {
val byteArrayOutputStream = ByteArrayOutputStream()
return try {
Thumbnails.of(inputStream)
.size(thumbnailConfiguration.width, thumbnailConfiguration.height)
.outputFormat(format)
.toOutputStream(byteArrayOutputStream)
byteArrayOutputStream.toByteArray()
} catch (e: IOException) {
LOG.warn("IOException thrown while generating the thumbnail", e)
null
}
}
companion object {
private val LOG = LoggerFactory.getLogger(ThumbnailatorThumbnailGenerator::class.java)
}
}
| 1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
| 2 | Use constructor injection to inject a bean of type ThumbnailConfiguration. |
4.3. Handler
When you select the feature aws-lambda-s3-event-notification, the application build includes the following dependency:
implementation("com.amazonaws:aws-lambda-java-events")
To inject an S3Client, add the following dependencies:
implementation("io.micronaut.aws:micronaut-aws-sdk-v2")
implementation("software.amazon.awssdk:s3")
The handler receives an S3 notification event and, for each S3 event of type ObjectCreated, creates a thumbnail if the S3 object is a PNG or JPG in the thumbnails folder of the same S3 bucket that triggered the event.
package example.micronaut
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification
import io.micronaut.function.aws.MicronautRequestHandler
import io.micronaut.serde.annotation.Serdeable
import jakarta.inject.Inject
import org.slf4j.LoggerFactory
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import java.util.Locale
@Serdeable
class FunctionRequestHandler : MicronautRequestHandler<S3EventNotification, Void?>() { (1)
@Inject
lateinit var s3Client: S3Client (2)
@Inject
lateinit var thumbnailGenerator: ThumbnailGenerator (3)
override fun execute(input: S3EventNotification): Void? {
for (record in input.records) {
LOG.info("event name: {}", record.eventName)
if (record.eventName.contains(OBJECT_CREATED)) { (4)
val s3Entity = record.s3
val bucket = s3Entity.bucket.name
val key = s3Entity.`object`.key
val index = key.lastIndexOf(DOT)
if (index != -1) {
val format = key.substring(index + 1).lowercase(Locale.ENGLISH)
if (format == PNG || format == JPG) {
s3Client.getObject(
GetObjectRequest.builder()
.bucket(bucket)
.key(key)
.build()
).use { inputStream ->
thumbnailGenerator.thumbnail(
inputStream,
format
)?.let { bytes ->
s3Client.putObject(
PutObjectRequest.builder()
.key(thumbnailKey(key))
.bucket(bucket)
.build(),
RequestBody.fromBytes(bytes)
)
}
}
}
}
}
}
return null
}
private fun thumbnailKey(key: String): String {
val index = key.lastIndexOf(SLASH)
val fileName = if (index != -1) key.substring(index + SLASH.length) else key
return "$THUMBNAILS$SLASH$fileName"
}
companion object {
private val LOG = LoggerFactory.getLogger(FunctionRequestHandler::class.java)
private const val SLASH = "/"
private const val THUMBNAILS = "thumbnails"
const val OBJECT_CREATED = "ObjectCreated"
private const val JPG = "jpg"
private const val PNG = "png"
private const val DOT = '.'
}
}
| 1 | We want to handle S3EventNotification events. |
| 2 | Injection for S3Client. |
| 3 | Injection for ThumbnailGenerator. |
| 4 | Extra verification that the event is an ObjectCreated event. |
5. S3
Create an S3 bucket.
Inside the bucket, create two folders: thumbnails and images.
6. Lambda
Create a Lambda function. As a runtime, select Java 21.
6.1. IAM Role
Grant permissions to access the S3 bucket to the IAM role associated with the Lambda.
6.2. Triggers
Create two S3 triggers, one for PNGs and one for JPGs:
Configure the trigger to:
-
Listen only to PUT events.
-
Only for files uploaded to the
imagesfolder. -
Only for files suffixed
jpg.
6.3. Upload Code
Create an executable jar including all dependencies:
./gradlew shadowJar
Upload it:
6.4. Handler
In the AWS Console, as the handler, set:
example.micronaut.FunctionRequestHandler
6.5. Test
You can test it easily: upload a JPG file to the images folder of the S3 bucket you created. You can do this directly in the AWS Console. In a few seconds, you will see a thumbnail saved in the thumbnails folder of the bucket.
Read more about:
7. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.
8. License
| All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images). |