Serverless in Java : Azure Functions
Dopo aver visto AWS Lambda nel precedente articolo è ora il turno di Azure Functions
Azure Functions è la proposta di Microsoft in ambito Servless/FaaS e permette di realizzare una funzione/applicazione all’interno di Azure senza preoccuparsi troppo dell’infrastruttura circostante. Allo stato attuale i linguaggi supportati sono C#, F#, Node.js, Java e PHP, mentre per quanto riguarda il modello di pagamento previsto per queste funzioni è possibile utilizzare quello a consumo (come le AWS Lambda) oppure un piano di servizio (App Service plan) tipico di Azure dove si paga un fisso per una serie di servizi con certe caratteristiche.
Le integrazioni previste da Azure Functions sono principalmente quelle relative alla piattaforma Azure:
- Azure Cosmos DB
- Azure Event Hubs
- Azure Event Grid
- Azure Mobile Apps (tables)
- Azure Notification Hubs
- Azure Service Bus (queues and topics)
- Azure Storage (blob, queues, and tables)
- GitHub (webhooks)
- On-premises (using Service Bus)
- Twilio (SMS messages)
Un punto di forza di questa piattaforma è la possibilità di avere il runtime in locale, ovvero di poter lanciare e testare le funzioni prima sulla nostra macchina e successivamente effettuare il deploy su Azure. A partire da qualche mese è stato supportato anche Java tra i linguaggi che dispongono di un runtime per Azure Functions e ora vedremo come sia possibile realizzare una prima semplice funzione. Se non avete a disposizione un account su Azure potete richiederne uno gratuito a questo link
Hello Azure
Prima di iniziare dobbiamo installare una serie di tool necessari per lo sviluppo di Azure Functions con Java
- .NET Core, ultima versione.
- Java Developer Kit, versione 8.
- Interfaccia della riga di comando di Azure
- Apache Maven, versione 3.0 o successiva.
- Node.js, versione 8.6 o successiva.
Una volta completata l’installazione di questi tool possiamo iniziare ad installare il runtime locale di Azure Functions utilizzando npm per aggiungere al nostro ambiente alcuni programmi necessari per lo sviluppo/test di funzioni. Andiamo quindi in una console dei comandi e aggiungiamo il seguente modulo
npm install -g azure-functions-core-tools@core
Possiamo quindi generare il nostro progetto Java utilizzando un archetipo Maven
mvn archetype:generate \ -DarchetypeGroupId=com.microsoft.azure \ -DarchetypeArtifactId=azure-functions-archetype
durante la generazione del progetto ci verrà chiesto il nome della nuova applicazione e la regione geografica dove sarà creata
Confirm properties configuration: groupId: com.javastaff artifactId: azure.functions version: 1.0-SNAPSHOT package: com.javastaff.azure.functions appName: testazure-func0001 appRegion: westus
Il progetto generato avrà al suo interno il nostro primo esempio di funzione
package com.javastaff.azure.functions; import java.util.*; import com.microsoft.azure.serverless.functions.annotation.*; import com.microsoft.azure.serverless.functions.*; /** * Azure Functions with HTTP Trigger. */ public class Function { /** * This function listens at endpoint "/api/hello". * Two ways to invoke it using "curl" command in bash: * 1. curl -d "HTTP Body" {your host}/api/hello * 2. curl {your host}/api/hello?name=HTTP%20Query */ @FunctionName("hello") public HttpResponseMessage<String> hello( @HttpTrigger(name = "req", methods = {"get", "post"}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request, final ExecutionContext context) { context.getLogger().info("Java HTTP trigger processed a request."); // Parse query parameter String query = request.getQueryParameters().get("name"); String name = request.getBody().orElse(query); if (name == null) { return request.createResponse(400, " Please pass a name on the query string or in the request body"); } else { return request.createResponse(200, "Hello, " + name); } } }
Il fatto che sia una funzione viene evidenziato dall’annotation FunctionName, dove indichiamo il nome da associare. L’altra informazione essenziale è il modo in cui viene scatenata l’esecuzione di questa funzione ed in questo caso si tratta di un trigger azionato da una chiamata HTTP, che viene dichiarato attraverso l’annotation HttpTrigger. Il metodo quindi scatta in risposta ad una chiamata HTTP e restituisce una stringa. Per lanciarlo prima di tutto lo impacchettiamo
mvn clean package
e poi utilizziamo il plugin Maven di Azure (che ci troviamo automaticamente aggiunto nel POM) per lanciare la funzione in locale sul nostro pc
mvn azure-functions:run
Se tutto è andato a buon fine troveremo il seguente output
[1/30/18 2:18:21 PM] Generating 1 job function(s) [1/30/18 2:18:21 PM] Starting Host (HostId=ip1723132112-1437871540, Version=2.0.11415.0, ProcessId=996, Debug=False, ConsecutiveErrors=0, StartupCount=1, FunctionsExtensionVersion=) [1/30/18 2:18:21 PM] Found the following functions: [1/30/18 2:18:21 PM] Host.Functions.hello [1/30/18 2:18:21 PM] [1/30/18 2:18:21 PM] Job host started Listening on http://localhost:7071/ Hit CTRL-C to exit... Http Functions: hello: http://localhost:7071/api/hello
e possiamo richiamare la nostra funzione con una semplice chiamata HTTP. Una volta testata l’applicazione in locale passiamo al deploy su Azure. Dalla console prima dobbiamo autenticarci con Azure seguendo le indicazioni
az login To sign in, use a web browser to open the page https://aka.ms/devicelogin and enter the code D6SUMGJCE to authenticate.
In questo modo saremo autenticati e potremo lanciare il comando del plugin Maven per effettuare il deploy
mvn azure-functions:deploy [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Azure Java Functions 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- azure-functions-maven-plugin:0.1.10:deploy (default-cli) @ azure.functions --- AI: INFO 30-01-2018 15:41, 1: Configuration file has been successfully found as resource AI: INFO 30-01-2018 15:41, 1: Configuration file has been successfully found as resource [INFO] Starting deploying to Function App testazure-func0001... [INFO] Authenticate with Azure CLI 2.0 [INFO] Target Function App does not exist. Creating a new Function App ... [INFO] Successfully created Function App testazure-func0001 [INFO] [INFO] Step 1 of 4: Creating ZIP package... [INFO] Successfully saved ZIP package at /home/ubuntu/azure.functions/target/azure-functions/testazure-func0001.zip [INFO] [INFO] Step 2 of 4: Uploading ZIP package to Azure Storage... [INFO] Successfully uploaded ZIP package to https://eaae245b429b4749bc12.blob.core.windows.net/java-functions-deployment-packages/testazure-func0001.20180130154235272.zip [INFO] [INFO] Step 3 of 4: Deploying Function App with package... [INFO] Azure Resource Manager read/write per hour limit reached. Will retry in: 5 seconds [INFO] Successfully deployed Function App with package. [INFO] [INFO] Step 4 of 4: Deleting deployment package from Azure Storage... [INFO] Successfully deleted deployment package testazure-func0001.20180130154235272.zip [INFO] Successfully deployed Function App at https://testazure-func0001.azurewebsites.net
A questo punto potremo richiamare la nostra applicazione dall’url https://testazure-func0001.azurewebsites.net/api/hello
curl -w '\n' -d AzureFunctions https://testazure-func0001.azurewebsites.net/api/hello Hello, AzureFunctions
Trigger, Input e Output
Esplorando i trigger che possiamo utilizzare uno interessante è sicuramente quello relativo ai timer. Possiamo definire un timer attraverso la classica notazione stile crontab e vedere la nostra funzione richiamata ogni volta che scade il timer
package com.javastaff.azure.functions; import java.util.UUID; import com.microsoft.azure.serverless.functions.ExecutionContext; import com.microsoft.azure.serverless.functions.annotation.*; public class TimerFunction { @FunctionName("timerFunction") @QueueOutput(name = "queueItem", queueName = "codaMessaggiInTransito", connection = "AzureWebJobsStorage") public String functionHandler( @TimerTrigger(name = "timerInfo", schedule = "*/30 * * * * *") String timerInfo, final ExecutionContext executionContext) { String randomString=UUID.randomUUID().toString(); executionContext.getLogger().info( "Funzione attivata dal trigger: " + timerInfo); executionContext.getLogger().info( "Inserisco messaggio random nella coda: " + randomString); return randomString; } }
Nell’esempio utilizziamo prima di tutto l’annotation TimerTrigger per specificare che la nostra funzione sarà richiamato ogni 30 secondi. Nel metodo della funzione viene creata una stringa random e ritornata in output. Attraverso l’utilizzo dell’annotation QueueOutput quello che ritorna la nostra funzione sarà inserito all’interno di una coda di messaggi su Azure. Microsoft mette a disposizione un servizio Storage, il quale include code, tabelle NoSQL chiave-valore, blob e file.
In questo esempio il risultato finirà in una coda di messaggi, con nome codaMessaggiInTransito ottenuta dalla connessione AzureWebJobsStorage. Quest’ultima è un’informazione che viene recuperata in fase di test dal file local.settings.json e ci permette di sapere a chi deve collegarsi la nostra funzione.
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "<connection string>", "AzureWebJobsDashboard": "<connection string>" }, "Host": { "LocalHttpPort": 7071, "CORS": "*" }, "ConnectionStrings": { "SQLConnectionString": "Value" } }
Questo file viene generato automaticamente dal plugin Azure Maven ma se poi vogliamo aggiungere configurazione specifiche o comunque customizzare lo storage utilizzato dovremo aggiungere la seguente configurazione al plugin
<plugin> <groupId>com.microsoft.azure</groupId> <artifactId>azure-functions-maven-plugin</artifactId> <configuration> <resourceGroup>azfuncgroup</resourceGroup> <appName>${functionAppName}</appName> <region>${functionAppRegion}</region> <appSettings> <property> <name>FUNCTIONS_EXTENSION_VERSION</name> <value>beta</value> </property> <property> <name>AzureWebJobsStorage</name> <value>STRINGA_DI_CONNESSIONE</value> </property> <property> <name>PersonalStorage</name> <value>ALTRO_STORAGE</value> </property> </appSettings> </configuration> <executions> <execution> <id>package-functions</id> <goals> <goal>package</goal> </goals> </execution> </executions> </plugin>
Queste informazioni poi saranno dopo il deploy tranquillamente aggiornabili dal tab Application Settings sulla nostra Azure Function App. Quindi ora abbiamo una funzione che viene azionata da un trigger ogni 30 secondi ed inserisce una stringa random sulla coda codaMessaggiInTransito. A questo punto vediamo come far scattare una seconda funzione con l’annotation QueueTrigger
package com.javastaff.azure.functions; import com.microsoft.azure.serverless.functions.ExecutionContext; import com.microsoft.azure.serverless.functions.annotation.*; public class CascadeQueueFunction { @FunctionName("cascadeQueueFunction") @QueueOutput(name = "myQueueItemOut", queueName = "codaMessaggiInArrivo", connection = "AzureWebJobsStorage") public String functionHandler( @QueueTrigger(name = "myQueueItem", queueName = "codaMessaggiInTransito", connection = "AzureWebJobsStorage") String myQueueItem, final ExecutionContext executionContext) { executionContext.getLogger().info("Queue trigger input: " + myQueueItem); return myQueueItem.toUpperCase(); } }
Questa funzione viene risvegliata ogni volta che è inserito un elemento sulla coda codaMessaggiInTransito, effettua l’uppercase ed inserisce il messaggio nella nuova coda codaMessaggiInArrivo. Per concludere la catena c’è l’ultima funzione che scoda il messaggio e lo stampa nel log.
package com.javastaff.azure.functions; import com.microsoft.azure.serverless.functions.ExecutionContext; import com.microsoft.azure.serverless.functions.annotation.*; public class QueueFunction { @FunctionName("queueFunction") public void functionHandler( @QueueTrigger(name = "queueItem", queueName = "codaMessaggiInArrivo", connection = "AzureWebJobsStorage") String queueItem, final ExecutionContext executionContext) { executionContext.getLogger().info( "Arrivato il seguente messaggio in coda: " + queueItem); } }
Conclusioni
Azure Functions è sicuramente interessante per lo sviluppo di applicazioni che potrebbero rientrare nella categoria serverless, soprattutto per chi già si trova a lavorare su Azure in quanto le integrazioni su questo sistema sono quelle primarie. Dal punto di vista dell’ambiente di sviluppo è molto utile la possibilità di testare le funzioni in locale ma purtroppo Java ancora non dispone di tutti i trigger/input/ouput che sono destinati a tutti i linguaggi sulla piattaforma (probabilmente perchè è stato appena inserito il suo supporto). Trigger e bindings attualmente supportati dalla piattaforma possono essere trovati a questa pagina, mentre per il riferimento per lo sviluppo Java è questo.
Di seguito trovate il solito progetto d’esempio Github

Looking for a right “about me”…
Una risposta
[…] 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 […]