Serverless in Java : AWS Lambda
Serverless (o FaaS) è una delle buzzword del momento, quindi perchè non vedere di cosa si tratta e cosa è possibile fare in Java, utilizzando in questo caso AWS ?
WTF Serverless?
Negli ultimi anni sono argomenti ricorrenti i microservizi e la possibilità di cercare di ridurre la complessità delle applicazioni, principalmente per aumentare la qualità generale dei progetti attraverso una corretta separazione delle competenze (separation of concerns) e un più veloce time to market. Uno degli ultimi trend è appunto quello del Servless, noto anche come FaaS (Function as a Service), dove viene estremizzato il concetto di servizio esposto riducendo il compito dello sviluppatore al codice ed ad una minima configurazione.
Ovviamente un qualche tipo di “server“ dietro le quinte esiste, solo che rispetto al PaaS (Platform as a Service) dove ci si deve occupare in maniera più dettagliata del bilanciamento del carico che deve gestire la nostra piattaforma, con FaaS possiamo pensare solo alla semplice esecuzione del nostro codice, preoccupandoci principalmente della durata che impiegherà la nostra funzione a terminare in quanto spesso è quello che viene preso come parametro per regolare il costo del servizio.
Il limite tra FaaS e PaaS può essere abbastanza sottile in alcuni casi e quindi bisogna capire in base al nostro caso d’uso quale sia lo strumento più adatto. Sicuramente l’utilizzo di un FaaS in una fase embrionale di un determinato progetto può essere molto utile per circoscrivere costi e tempi di sviluppo.
AWS Lambda
AWS Lambda è una piattaforma serverless fornita da Amazon a partire dal 2014, che permette di definire delle cosiddette funzioni, ovvero applicazioni che vengono eseguite quando si scatenano determinati eventi. Il nome di questa piattaforma deriva dal concetto di funzioni anonime o Lambda expression, dalle quali si vorrebbe ereditare la caratteristica di piccole funzioni che vengono eseguite per un breve periodo di tempo.
Come in altre piattaforme serverless, il concetto principale è il codice che deve essere eseguito (la funzione) e non la gestione del server che viene fatta in automatico dalla piattaforma. Le funzioni Lambda e le sorgenti di eventi sono i componenti principali in AWS Lambda : la sorgente di eventi è l’entità che pubblica gli eventi mentre la funziona Lambda è il codice che si occupa di gestire lo specifico evento.
Sulla piattaforma Amazon sono presenti molti modi per poter richiamare una funzione Lambda, che possiamo raggruppare nelle seguenti tre categorie:
- Servizi Amazon che generano sorgenti eventi di vario tipo (S3, DynamoDB, SNS, CloudTrail)
- Invocazione diretta attraverso HTTPS (Amazon API Gateway)
- Eventi schedulati (CloudWatch)
Hello Serverless
Le possibilità offerte da AWS sono talmente tante che potremmo scrivere un libro su tutti i modi in cui poter intrecciare Lambda con tutti i servizi disponibili sulla piattaforma. Di sicuro dobbiamo prima vedere il classico Hello World utilizzando Java e per poterlo fare possiamo sfruttare tre diverse opzioni disponibili per questo linguaggio:
- Implementare l’interfaccia RequestHandler
- Creare una classe custom
- Implementare l’interfaccia RequestStreamHandler
Prima di vedere come implementare un esempio nelle tre diverse diverse modalità, dobbiamo definire la richiesta d’esempio che riceveremo, che è un semplice oggetto Java dove verranno deserializzate le informazioni presenti nel JSON utilizzato come payload della richiesta
package com.javastaff.aws.lambda.hello; public class HelloRequest { private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
In questo caso è un semplice oggetto con la proprietà username. Passiamo ora alla definizione della funzione usando l’interfaccia RequestHandler, che ci obbliga a definire l’input e l’output della nostra funzione Lambda, presenti nella definizione dell’interfaccia come tipi generici
package com.javastaff.aws.lambda.hello; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; public class HelloWorldRequestHandler implements RequestHandler<HelloRequest, String> { public String handleRequest(HelloRequest input, Context context) { context.getLogger().log("Stringa input: " + input.getUsername()); return "Hello World " + input.getUsername(); } }
Il metodo handleRequest non fa altro che loggare la proprietà definita nella nostra richiesta e poi tornare una stringa in output. Dobbiamo ovviamente renderci conto che l’output verrà preso in considerazione solo da quei servizi che richiamano il nostro codice in maniera sincrona, mentre verrà ignorata quando ad esempio la funzione Lambda è configurata per scattare in base ad un evento relativo ad S3 che scatta in maniera asincrona.
L’approccio successivo che possiamo usare è una classe custom, che non implementa nessuna interfaccia, ma che avrà un metodo richiamato dal contesto Lambda dove avrà come primo parametro l’input al nostro servizio e come tipo di ritorno l’output
package com.javastaff.aws.lambda.hello; import com.amazonaws.services.lambda.runtime.Context; public class CustomHelloHandler { public String myHandler(HelloRequest request, Context context) { return String.format("Hello World custom %s.", request.getUsername()); } }
L’ultimo esempio che vediamo prima di provare le nostre funzioni su AWS è quello relativo all’interfaccia RequestStreamHandler, dove appunto ragioniamo con gli stream di input/output invece che con gli oggetti request/response
package com.javastaff.aws.lambda.hello; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; import com.amazonaws.services.lambda.runtime.Context; public class HelloWorldRequestStreamHandler implements RequestStreamHandler{ public void handleRequest(InputStream inputStream , OutputStream outputStream, Context context) throws IOException { int letter; outputStream.write("Hello World stream ".getBytes()); while((letter = inputStream.read()) != -1){ outputStream.write(Character.toUpperCase(letter)); } } }
Con questo ultimo esempio abbiamo visto quali sono i diversi modi per definire una funzione su AWS Lambda in Java, ora dobbiamo vedere come impacchettarla.
Struttura progetto
Dopo aver capito come è possibile definire la nostra funzione Lambda, vediamo come creare il pacchetto per poter fare il “deploy” su AWS. Definiamo un semplice progetto Maven JAR, dove andiamo ad inserire la seguente dipendenza
<dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-core</artifactId> <version>1.2.0</version> </dependency>
che ci servirà principalmente per le classi interfaccia di AWS che dovremo implementare. Oltre a questo nel pom del progetto dovremo importare il plugin Maven Shade per creare un unico JAR finale con tutte le dipendenze
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <configuration> <createDependencyReducedPom> false </createDependencyReducedPom> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Test su AWS
Ora che siamo abbiamo a disposizione il nostro JAR, che contiene le funzioni d’esempio, dobbiamo accedere ad una console AWS, che potete ottenere facilmente con il free-tier. Dalla vostra console vedrete potete accede ai servizi Lambda
e successivamente creare una nuova funzione. Le informazioni base di cui abbiamo bisogno sono il nome della funzione, il runtime (Java nel nostro caso) e il ruolo. Quest’ultimo è importante in quanto in base al ruolo e ai permessi associati potremo poi accedere a determinate risorse su AWS
Nella screenshot potete vedere che in questo caso abbiamo creato un ruolo specifico a partire da alcuni template già presenti e serve poi per avere la possibilità di accedere a determinati servizi all’interno della funzione. Una volta creata la nostra prima Lambda, dalla schermata principale dobbiamo effettuare l’upload del nostro JAR e indicare le “coordinate” della nostra funzione utilizzando la seguente sintassi
package.class-name::handler o package.class-name
quindi se vogliamo richiamare il primo esempio basta la seguente stringa
com.javastaff.aws.lambda.hello.HelloWorldRequestHandler
Quindi una volta che abbiamo finito l’upload riceveremo il seguente messaggio
Congratulations! Your Lambda function “primaFunzione” has been successfully created. You can now change its code and configuration. Click on the “Test” button to input a test event when you are ready to test your function.
In questo caso non abbiamo configurato nessuna sorgente che permette di far scattare la nostra funzione e quindi andiamo semplicemente a cliccare su Test che ci permetterà di invocarla per verificare il corretto funzionamento. Ci verrà chiesto di scegliere un evento di test oppure di definirne uno nostro. Avendo specificato che il nostro componente riceverà come input un oggetto della classe HelloWorldRequest, possiamo usare un semplice JSON
{ "username": "JavaStaff" }
Effettuando il test riceveremo il risultato positivo. Anche se abbastanza laborioso per un semplice Hello World, a questo punto abbiamo chiaro come configurare un applicazione serverless su AWS, nei prossimi paragrafi vedremo l’integrazione con gli altri servizi.
Evento schedulato
Per gestire la schedulazione di un evento su AWS possiamo utilizzare il servizio di CloudWatch, che può essere utilizzato per organizzare un allarme ricorrente da poter sfruttare come punto d’invocazione della nostra funzione. Per fare ciò nella creazione guidata della funzione è possibile selezionare un trigger di tipo CloudWatch Events e nella configurazione seguente possiamo creare uno scheduler come quella riportata nel seguente screenshot
In questo modo verrà invocata la nostra funzione ogni minuto. Per mappare l’evento di richiesta possiamo utilizzare la classe ScheduledEvent presente in un JAR che dobbiamo aggiungere alle nostre dipendenze
<dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-lambda-java-events</artifactId> <version>2.0.2</version> </dependency>
In questa libreria sono presenti tutti i vari model che mappano gli eventi standard che possono essere intercettati tramite Lambda su AWS. La nostra funzione sarà quindi la seguente
package com.javastaff.aws.lambda.schedule; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; public class ExampleScheduledRequestHandler implements RequestHandler<ScheduledEvent, String>{ public String handleRequest(ScheduledEvent event, Context context) { String logString=String.format("Id %s source %s", event.getId(),event.getSource()); context.getLogger().log(logString); return "Scheduled handler returns: " + logString; } }
In questo caso possiamo testare anche con l’invocazione diretta come abbiamo fatto precedentemente, passando un JSON corretto che contenga le informazioni richieste, ma il test vero sarà quello che faremo attivando lo scheduler CloudWatch e andando a vedere sui log generati.
Evento schedulato e salvataggio su S3
Passiamo quindi ad un esempio che possa somigliare leggermente ad un possibile caso d’uso di AWS Lambda. Immaginiamo di avere uno evento schedulato che richiama la nostra funzione e quest’ultima si occuperà di scaricare periodicamente il feed RSS che ci interessa, estrarre il testo dagli articoli, creare un PDF e archiviarlo su S3 (servizio di storage di AWS). Come caso d’uso potrebbe essere anche suddiviso in un paio di diverse funzioni Lambda, però per semplicità e brevità lo realizzeremo tutto insieme.
La funzione che creiamo dovrà avere un ruolo che possa accedere ad S3, quindi potremo crearne uno utilizzando i template disponibili di S3. Passiamo quindi alla definizione della classe
package com.javastaff.aws.lambda.schedule; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.Charset; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import com.amazonaws.services.s3.model.ObjectMetadata; import com.itextpdf.text.Chunk; import com.itextpdf.text.Document; import com.itextpdf.text.Font; import com.itextpdf.text.FontFactory; import com.itextpdf.text.Paragraph; import com.itextpdf.text.pdf.PdfWriter; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.io.SyndFeedInput; import de.l3s.boilerpipe.extractors.ArticleExtractor; public class PrintServiceHandler implements RequestHandler<ScheduledEvent, String>{ public String handleRequest(ScheduledEvent event, Context context) { try { URL feedUrl = new URL("http://www.repubblica.it/rss/homepage/rss2.0.xml"); SyndFeedInput input = new SyndFeedInput(); HttpURLConnection conn = (HttpURLConnection) feedUrl.openConnection(); InputStream in = conn.getInputStream(); SyndFeed feed = (SyndFeed) input.build( new InputStreamReader(in, Charset.forName("UTF-8"))); //PDF ByteArrayOutputStream baos=new ByteArrayOutputStream(); Document document = new Document(); PdfWriter.getInstance(document, baos); document.open(); Font chapterFont = FontFactory.getFont( FontFactory.HELVETICA, 16, Font.BOLDITALIC); Font paragraphFont = FontFactory.getFont( FontFactory.HELVETICA, 12, Font.NORMAL); Paragraph intestazione = new Paragraph(" Giornale generato da feed RSS di Repubblica", chapterFont); document.add(intestazione); document.add( Chunk.NEWLINE ); document.add( Chunk.NEWLINE ); //Scorro gli articoli del feed rss for(SyndEntry entry:feed.getEntries()) { System.out.println(entry.getTitle()+" "+entry.getLink()); URL tempUrl=new URL(entry.getLink()); HttpURLConnection conn2 = (HttpURLConnection)tempUrl.openConnection(); InputStream in2 = conn2.getInputStream(); //Per ogni articolo ottengo il testo principale //utilizzando Boilerpipe String text=ArticleExtractor.INSTANCE.getText( new InputStreamReader(in2)); //Aggiungo l'articolo al PDF document.add(new Paragraph(entry.getTitle(), chapterFont)); document.add(new Paragraph(text, paragraphFont)); document.add( Chunk.NEWLINE ); document.add( Chunk.NEWLINE ); //document.add(chapter); } document.close(); //Collegamento ad S3 AmazonS3 s3 = AmazonS3ClientBuilder.standard() .withRegion("eu-west-3") .build(); ByteArrayInputStream bais=new ByteArrayInputStream( baos.toByteArray()); ObjectMetadata meta = new ObjectMetadata(); meta.setContentLength(bais.available()); //Salvataggio del pdf su S3 s3.putObject( "giornali", "giornale-"+System.currentTimeMillis()+".pdf", bais, meta); } catch (Exception ex) { ex.printStackTrace(); System.out.println("ERROR: "+ex.getMessage()); } return "Giornale consegnato"; } }
Abbiamo definito una classe implementando l’interfaccia RequestHandler per l’evento di tipo ScheduledEvent. Poi utilizzando la libreria ROME abbiamo scaricato il feed RSS di Repubblica e inizializzato un nuovo documento PDF con iText. Successivamente per ogni articolo presente nel feed, sfruttando Boilerpipe che estrae il contenuto principale da una pagina HTML, abbiamo ottenuto il testo principale di ogni articolo e l’abbiamo aggiunto al PDF.
L’integrazione con AWS c’è quando inizializziamo il client per S3
AmazonS3 s3 = AmazonS3ClientBuilder.standard() .withRegion("eu-west-3") .build();
e successivamente quando salviamo il PDF risultante
s3.putObject("giornali", "giornale-"+System.currentTimeMillis()+".pdf", bais, meta);
questo ovviamente sarà possibile se saranno forniti alla nostra funzioni i ruoli corretti per poter accedere ad S3. Per poter dialogare con S3 dovremo aggiungere al pom del nostro progetto la rispettiva libreria
Conclusioni
Abbiamo visto la semplice definizione di una funzione su AWS Lambda e alcuni esempi d’integrazione con gli altri servizi presenti su AWS. Lambda è sicuramente un servizio interessante per gestire alcune funzionalità particolari all’interno della nostra architettura software ed esplorando gli altri innumerevoli servizi presenti su AWS sicuramente possiamo renderci conto delle potenzialità di questa piattaforma. Al seguente link Github è possibile scaricare il progetto d’esempio relativo all’articolo

Looking for a right “about me”…
Una risposta
[…] Lo schema architetturale adottato da Apache OpenWhisk è molto simile a quello visto con AWS Lambda e Azure Function, anche se cambia leggermente per quanto riguarda la […]