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

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

 

 

 

Federico Paparoni

Looking for a right "about me"...

Una risposta

  1. Febbraio 21, 2019

    […] 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 […]

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.