Plantilla de AWS Lambda a través de Spring Native
El arranque en frío de Java no es excelente en AWS Lambda en comparación con lenguajes como Python y NodeJS. Además, si decidimos usar frameworks como Spring Native, el tiempo de calentamiento será aún mayor.
Estoy bastante acostumbrado a usar Spring Native y quería aprovechar esta experiencia mientras desarrollaba funciones en AWS usando Spring Cloud Function. Entonces, ¿cómo podemos justificar el costo de usar Java (y Spring)?
Bueno, me topé con un video que describe Spring Native y pensé que esta podría ser una solución.
spring-native-aws-lambda .mvn/wrapper # for building without installing maven mvnw # build on unix like OS mvnw.cmd # build on windows Dockerfile # Building for aws environment using amazonlinux image pom.xml # project dependencies src assembly native.xml # Used by maven-assembly-plugin to zip the native image and shell/native/bootstrap shell native bootstrap # bootstraps the function in AWS Lambda env main java com.coffeebeans.springnativeawslambda
Naveguemos por el código en estos archivos.
Código
Dependencias de funciones de Spring Cloud
1. Gestión de Dependencias
La función Spring Cloud (SCF) proporciona una dependencia pom que podemos importar al elemento dependencyManagement
<gestión de dependencias> <dependencias> <dependencia> <groupId>org.springframework.cloud</groupId> <artifactId>primavera-nube-dependencias</artifactId> <versión>2020.0.2</versión> <tipo>pom</tipo> <ámbito>importar</ámbito> </dependencia> </dependencias> </administración de dependencias>
Esto nos ayuda a importar una versión compatible para SCF
2. Dependencias
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-function-adapter-aws</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.reactivestreams</groupId> <artifactId>reactive-streams</artifactId> <version>1.0.0</version> </dependency> </dependencies>
NOTA: que usaremos el modo reactivo, por lo tanto, org.springframework.boot:spring-boot-starter-webflux
y org.reactivestreams:reactive-streams
—
Dependencias nativas de Spring
1. Gestión de Dependencias
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native</artifactId> <version>0.10.0</version> </dependency> </dependencies> </dependencyManagement>
2. Dependencia
<dependencies> <dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native</artifactId> </dependency> </dependencies>
3. Complementos
<build> <plugins> <plugin> <groupId>org.springframework.experimental</groupId> <artifactId>spring-aot-maven-plugin</artifactId> <version>${spring-native.version}</version> <executions> <execution> <id>test-generate</id> <goals> <goal>test-generate</goal> </goals> </execution> <execution> <id>generate</id> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
4. Perfil
<profiles> <!-- Enable building a native image using a local installation of native-image with GraalVM native-image-maven-plugin --> <profile> <id>native-image</id> <properties> <!-- Avoid a clash between Spring Boot repackaging and native-image-maven-plugin --> <classifier>exec</classifier> </properties> <build> <plugins> <plugin> <groupId>org.graalvm.nativeimage</groupId> <artifactId>native-image-maven-plugin</artifactId> <version>21.1.0</version> <configuration> <imageName>${project.artifactId}</imageName> <buildArgs>${native.build.args}</buildArgs> </configuration> <executions> <execution> <goals> <goal>native-image</goal> </goals> <phase>package</phase> </execution> </executions> </plugin> <plugin> <!-- packages native image and bootstrap file for uploading to AWS --> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <id>native-zip</id> <phase>package</phase> <goals> <goal>single</goal> </goals> <inherited>false</inherited> </execution> </executions> <configuration> <descriptors> <descriptor>src/assembly/native.xml</descriptor> </descriptors> </configuration> </plugin> </plugins> </build> </profile> </profiles>
Configuración de la aplicación
La aplicación está configurada como cualquier otra aplicación Spring Boot con muy pocas diferencias.
- Utiliza
@SpringBootConfiguration
en lugar de@SpringBootApplication
FunctionalSpringApplication#run(Class, String[])
en lugar de SpringApplication#run(Class, String[])SpringApplication#run(Class, String[])
- Hicimos una sugerencia nativa a nuestra aplicación para serializar nuestros modelos correctamente
@SerializationHint(types = {Request.class, Response.class}) @SpringBootConfiguration public class SpringNativeAwsLambdaApplication implements ApplicationContextInitializer<GenericApplicationContext> { public static void main(final String[] args) { FunctionalSpringApplication.run(SpringNativeAwsLambdaApplication.class, args); } @Override public void initialize(final GenericApplicationContext context) { context.registerBean("exampleFunction", FunctionRegistration.class, () -> new FunctionRegistration<>(new ExampleFunction()).type(ExampleFunction.class)); } }
Configuraciones de tiempo de ejecución personalizadas de AWS Lambda
#this is required to be true for custom runtime where we will run our native-image, override it to false if you deploying spring cloud function to aws without spring-native spring.cloud.function.web.export.enabled=true
NOTA: Spring Cloud Function admite un estilo “funcional” de declaraciones de beans para aplicaciones pequeñas en las que necesita un inicio rápido. Por lo tanto, lo usamos en nuestra aplicación (este es el método de initialize
alización que también implementa ApplicationContextInitializer
)
La función
Spring Cloud Functions nos permite crear funciones extendiendo las interfaces funcionales principales de java8 3
java.util.function.Function
java.util.function.Consumer
java.util.function.Supplier
Puede encontrar más información sobre cada caso de uso aquí
Para nuestro ejemplo, extendimos java.util.function.Function
NOTA: colocamos nuestra clase en un paquete llamado functions
. De esta forma, se escaneará automáticamente incluso sin la anotación @Component
.
@Slf4j public class ExampleFunction implements Function<Request, Response> { @Override public Response apply(final Request request) { log.info("Converting request into a response..."); final Response response = Response.builder() .name(request.getName()) .saved(true) .build(); log.info("Converted request into a response."); return response; } }
Modelos
Nuestros modelos están implementando Serializable
Configuración.xml
Un último paso es asegurarnos de que podamos obtener dependencias y complementos de org.springframework.experimental
. Para esto, necesitamos agregar https://repo.spring.io/release
como repositorio y pluginrepository
en settings.xml
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <profiles> <profile> <id>spring-native-demo</id> <repositories> <repository> <id>spring-releases</id> <name>Spring Releases</name> <url>https://repo.spring.io/release</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-releases</id> <name>Spring Releases</name> <url>https://repo.spring.io/release</url> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>spring-native-demo</activeProfile> </activeProfiles> </settings>
Construyendo
Construyendo para el Medio Ambiente Local
./mvnw -ntp clean package -Pnative-image target/spring-native-aws-lambda
Creación para el entorno de AWS
- Ejecute los siguientes comandos (en la terminal local o en la canalización de CI/CD)
mkdir targetdocker build --no-cache \ --tag spring-native-aws-lambda:0.0.1-SNAPSHOT \ --file Dockerfile .docker ps -a docker rm spring-native-aws-lambda docker run --name spring-native-aws-lambda spring-native-aws-lambda:0.0.1-SNAPSHOT docker cp spring-native-aws-lambda:app/target/ .
- Cargue el archivo
spring-native-aws-lambda-0.0.1-SNAPSHOT-native-zip.zip
en aws lambda, - Establezca el controlador en
exampleFunction
- Prueba, y
- Et voila! Funciona con 500 ms para arranque en frío (supongo que no está mal para la aplicación Spring Boot)
NOTA: Necesitamos construir dentro de una arquitectura de sistema operativo lo más cerca posible del sistema operativo de Lambda.
Esto se debe a que la imagen nativa se construirá para esa arquitectura. Es decir, si lo construimos en MacOS, no se ejecutará en Amazon Linux.
Esta es la razón por la que estamos construyendo en su lugar un contenedor amazonlinux2
usando el Dockerfile
Probando localmente
Puede acceder a la aplicación en cualquier puerto en el que la inició utilizando la solicitud http.
Recuerde, para probar localmente, asegúrese de que spring.main.web-application-type
esté configurado como reactive
. Para el entorno de AWS, debe establecerse en none
.
curl --location --request POST 'http://localhost:8080/exampleFunction' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "CoffeeBeans" }'
Código fuente del proyecto
Si le interesa, puede echar un vistazo a algunos de los otros artículos que he escrito recientemente sobre Laravel: