Serveless in Java : Apache OpenWhisk
Prosegue il tour delle soluzioni Servless con Apache OpenWhisk
Apache OpenWhisk è una soluzione per realizzare servizi Servless, donata da IBM e Adobe al mondo opensource di Apache. La piattaforma, realizzata principalmente in Scala, permette la definizione di servizi in modalità Serverless, ovvero la creazione e gestione di funzioni utilizzabili per la realizzazione di un sistema che può sfruttare l’invocazione in modalità asincrona con ottime prestazioni. 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 terminologia
Abbiamo quindi i seguenti concetti
- Action : sono le funzioni che possono essere definite e richiamate all’interno del sistema
- Event source : sorgenti generiche di eventi, i quali possono essere iniettati nel sistema
- Trigger : quando si verifica un determinato evento il trigger associato a quella classe di eventi scatta
- Rule : la regola permette di associare un trigger ad un action. In questo modo quando scatta il trigger viene richiamata la corrispondente action
Le azioni, che sono quindi il cuore della piattaforma, possono essere realizzate nei seguenti linguaggi
Setup ambiente
Per utilizzare OpenWhisk possiamo creare un account gratuito sulla piattaforma IBM BlueMix, con una macchina Vagrant, Docker su Mac o Docker su Ubuntu. La cosa più semplice è quella di utilizzare l’implementazione cloud di IBM, che possiamo utilizzare creando un account gratuito. Per installare la console di IBM Cloud Functions possiamo quindi eseguire i seguenti comandi
curl -fsSL https://clis.ng.bluemix.net/install/linux | sh bx plugin install Cloud-Functions -r Bluemix bx login -a api.eu-gb.bluemix.net -o miaemail -s dev bx wsk action invoke /whisk.system/utils/echo -p message hello --result
Con il primo comando scarichiamo la console di IBM BlueMix (che ovviamente è molto di più rispetto a quello che stiamo vedendo) e la installiamo. Il secondo comando installa il plugin per utilizzare le Cloud Functions e con il terzo andiamo a fare il login sulla piattaforma. L’ultimo comando esegue un primo hello world sulla piattaforma, invocando l’azione di utility /whisk.system/utils/echo.
Hello OpenWhisk
Vediamo quindi come realizzare un semplice Hello World in Java su questa piattaforma. Come prima cosa dobbiamo sapere che l’input e l’output che avremo nella nostra funzione sarà sempre un oggetto JSON, che verrà gestito dalla libreria GSON di Google inclusa come dipendenza nel nostro progetto
<dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.6.2</version> </dependency>
Passiamo quindi alla classe d’esempio, che deve prevedere un metodo statico main, che come input e output avrà l’oggetto JSON che abbiamo detto.
package com.javastaff.openwhisk; import com.google.gson.JsonObject; public class HelloAction { public static JsonObject main(JsonObject args) { String nome = args.getAsJsonPrimitive("nome")!=null ? args.getAsJsonPrimitive("nome").getAsString() : "OpenWhisk"; JsonObject response = new JsonObject(); response.addProperty("risposta", "Ciao " + nome + "!"); System.out.println("HelloAction invocatata con parametro "+nome); return response; } }
Il metodo recupera il parametro nome se presente, risponde con un JSON che ha la property risposta. Come visto su AWS Lambda e Azure Function, il progetto deve creare un JAR con tutte le dipendenze, quindi dovremo utilizzare il plugin Maven Shade
<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>
quindi per creare il pacchetto JAR, l’action e invocarla possiamo utilizzare i seguenti comandi in sequenza
mvn clean package bx wsk action create hello-java target/openwhisk-1.0-SNAPSHOT.jar \ --main com.javastaff.openwhisk.HelloAction bx wsk action invoke hello-java --result
In questo modo la nostra action è invocabile attraverso la console di BlueMix ma possiamo anche creare un API gateway che richiami questa action. Prima di farlo dobbiamo aggiornare il nostro hello world specificando che deve essere un action di tipo web
bx wsk action update hello-java --web true
e poi creare un API che richiami l’action
bx wsk api create /hello /world get hello-java --response-type json ok: created API /hello/world GET for action /_/hello-java https://service.eu.apiconnect.ibmcloud.com/gws/apigateway/api/ 65ddc8f7df4045c9af66b5da80b6df34c289a1c0d401a7408a3c5e2a80466bf8/hello/world
L’indirizzo fornito come output potrà quindi essere utilizzato per richiamare la nostra action attraverso una semplice chiamate HTTP
Sequenze
Diverse action possono essere collegate in una sequenza più lunga, dove l’output di una diventa l’input di quella successiva. Vediamo quindi un esempio di sequenza dove il primo step recupera un parametro di tipo stringa dalla request ed effettua banalmente l’uppercase
package com.javastaff.openwhisk; import com.google.gson.JsonObject; /*** * Action che prende in input un testo e lo restituisce in uppercase */ public class UpperCaseAction { public static JsonObject main(JsonObject args) { String testo = args.getAsJsonPrimitive("testo").getAsString(); JsonObject response = new JsonObject(); response.addProperty("testo", testo.toUpperCase()); return response; } }
e lo aggiungiamo al nostro sistema
bx wsk action create uppercase target/openwhisk-1.0-SNAPSHOT.jar \ --main com.javastaff.openwhisk.UpperCaseAction
Il secondo step prende una stringa in input e genera un PDF che contiene come testo la stringa. In questo caso il PDF viene restituito sempre dentro all’oggetto JSON di risposta ma utilizzando l’encoding Base64
package com.javastaff.openwhisk; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Base64; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; import com.google.gson.JsonObject; /*** * Action che prende in input un testo e crea il pdf * risultante restituendolo in Base64 */ public class PdfAction { public static JsonObject main(JsonObject args) throws IOException { String testo = args.getAsJsonPrimitive("testo").getAsString(); // Inizializza PDF e caratteristiche collegate PDDocument document = new PDDocument(); PDPage page = new PDPage(); document.addPage(page); PDFont pdfFont = PDType1Font.HELVETICA; float fontSize = 13; float leading = 1.5f * fontSize; PDRectangle mediabox = page.getMediaBox(); float margin = 72; float width = mediabox.getWidth() - 2 * margin; float startX = mediabox.getLowerLeftX() + margin; float startY = mediabox.getUpperRightY() - margin; // Splitta il testo in diverse linee List<String> lines = splitLines(testo, fontSize, pdfFont, width); PDPageContentStream contentStream = new PDPageContentStream(document, page); contentStream.beginText(); contentStream.setFont(pdfFont, fontSize); contentStream.newLineAtOffset(startX, startY); // Aggiunge ogni linea al PDF for (String line : lines) { contentStream.showText(line); contentStream.newLineAtOffset(0, -leading); } contentStream.endText(); contentStream.close(); // Salva output ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); document.close(); JsonObject response = new JsonObject(); response.addProperty("pdffile", Base64.getEncoder().encodeToString(baos.toByteArray())); return response; } /** * Splitta il testo da inserire nel PDF in diverse linee * * @param testo * @param fontSize * @param pdfFont * @param width * @return * @throws IOException */ public static List<String> splitLines( String testo, float fontSize, PDFont pdfFont, float width) throws IOException { List<String> lines = new ArrayList<String>(); int lastSpace = -1; while (testo.length() > 0) { int spaceIndex = testo.indexOf(' ', lastSpace + 1); if (spaceIndex < 0) spaceIndex = testo.length(); String subString = testo.substring(0, spaceIndex); float size = fontSize * pdfFont.getStringWidth(subString) / 1000; if (size > width) { if (lastSpace < 0) lastSpace = spaceIndex; subString = testo.substring(0, lastSpace); lines.add(subString); testo = testo.substring(lastSpace).trim(); lastSpace = -1; } else if (spaceIndex == testo.length()) { lines.add(testo); testo = ""; } else { lastSpace = spaceIndex; } } return lines; } }
Per realizzare il PDF è stato utilizzato PDFBox che abbiamo incluso come dipendenza e che quindi sarà presente nel JAR risultante. Creiamo quindi l’action per la generazione del PDF
bx wsk action create createpdf target/openwhisk-1.0-SNAPSHOT.jar \ --main com.javastaff.openwhisk.PdfAction
L’ultimo step prende come parametro d’input un PDF passato come array di byte in Base64, applica un logo in alto a destra alla prima pagina del PDF e lo ritorna in output
package com.javastaff.openwhisk; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Base64; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import com.google.gson.JsonObject; /*** * Action che prende in input un pdf base64 * applica un immagine come logo e ritorna il pdf modificato come base64 */ public class LogoStamper { public static JsonObject main(JsonObject args) throws Exception { byte[] pdffile=Base64.getDecoder().decode( args.getAsJsonPrimitive("pdffile").getAsString()); byte[] imageFile=readByte( "https://github.com/fpaparoni/OpenWhisk/raw/master/logo.png"); String filename = "logo.png"; PDDocument document = PDDocument.load(pdffile); float scale = 0.5f; PDImageXObject ximage = PDImageXObject.createFromByteArray(document, imageFile, filename); float deltaX = ximage.getWidth() * scale; float deltaY = ximage.getHeight() * scale; PDPage page = document.getDocumentCatalog().getPages().get(0); PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); contentStream.drawImage( ximage, page.getMediaBox().getUpperRightX() - deltaX, page.getMediaBox().getUpperRightY() - deltaY, deltaX, deltaY); contentStream.close(); ByteArrayOutputStream baos=new ByteArrayOutputStream(); document.save(baos); document.close(); String pdfreturn=Base64.getEncoder().encodeToString( baos.toByteArray()); JsonObject response = new JsonObject(); response.addProperty("pdffile", pdfreturn); return response; } /** * Legge un file da un url e lo converte in array di byte * * @param fileUrl * @return * @throws Exception */ public static byte[] readByte(String fileUrl) throws Exception { URL url=new URL(fileUrl); ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream is = null; try { is = url.openStream (); byte[] byteChunk = new byte[4096]; int n; while ( (n = is.read(byteChunk)) > 0 ) { baos.write(byteChunk, 0, n); } } catch (IOException e) { System.err.printf ("Failed while reading bytes from %s: %s", url.toExternalForm(), e.getMessage()); e.printStackTrace (); } finally { if (is != null) { is.close(); } } return baos.toByteArray(); } }
Anche in questo caso generiamo l’action relativa
bx wsk action create pdfstamp target/openwhisk-1.0-SNAPSHOT.jar \ --main com.javastaff.openwhisk.LogoStamper
e passiamo alla creazione della sequenza, dove dovremo appunto specificare quali action e in che ordine richiamarle
bx wsk action create sequenza --sequence uppercase,createpdf,pdfstamp
la sequenza in realtà è un wrapper che agisce come action e chiama le action che contiene. Per invocare la sequenza possiamo quindi richiamarla come una semplice action e passare il parametro iniziale di cui ha bisogno, in questo caso una stringa che sarà presente nel PDF di output
bx wsk action invoke sequenza -p testo "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum" --result
Il risultato, che evito di riportare nell’articolo per ovvi motivi, è in Base64 quindi se lo volete visualizzare dovete effettuare la decodifica.
Rules e Trigger
I trigger e le regole di OpenWhisk forniscono alla piattaforma la sua caratteristica event-driven. La creazione di questi elementi possono essere fatte tutte tramite la console quindi non hanno niente di specifico relativo a Java o ad altri linguaggi, però vale comunque la pena capire come definirli per poter poi agganciare delle action. Per creare un trigger che scatta ogni 20 secondi possiamo utilizzare il seguente comando
bx wsk trigger create every-20-seconds \ --feed /whisk.system/alarms/alarm \ --param cron '*/20 * * * * *' \ --param maxTriggers 15
In questo modo abbiamo associato il trigger ad un feed di eventi che un action di utility che fornisce degli eventi. A questa utility alarm abbiamo detto di scattare ogni 20 secondi e che il trigger potrà partire al massimo 15 volte. Ora definiamo la regola che permette di agganciare questo trigger al nostro hello world
bx wsk rule create \ invoke-periodically \ every-20-seconds \ hello-java
la regola, come già detto, lega un trigger ad una action e quindi dobbiamo riportare queste due informazioni quando la creiamo. Per vedere che il nostro hello world viene richiamato possiamo lanciare il seguente comando
bx wsk activation poll
Conclusioni
Anche Apache OpenWhisk sembra essere un’ottima soluzione per realizzare dei componenti fruibili in modalità Serverless. Nella sua versione targata IBM, ovvero IBM Cloud Functions, abbiamo un’ambiente simile a quelli visti con AWS e Azure, ma sembra essere molto interessante anche la possibilità di creare una propria versione hosted di Apache OpenWhisk visto che stiamo parlando di una completamente piattaforma opensource
https://github.com/fpaparoni/OpenWhisk

Looking for a right “about me”…
Commenti recenti