Come eseguire qualsiasi linguaggio di programmazione su AWS Lambda: i Custom Runtimes.

Come eseguire qualsiasi linguaggio di programmazione su AWS Lambda: i Custom Runtimes.
beSharp
beSharp | 10 Agosto 2020

Le Funzioni AWS Lambda (FaaS) sono diventate rapidamente un tool estremamente versatile del cloud AWS poiché è possibile utilizzarle per una moltitudine di compiti: dal backend di un’applicazione Web all’ingestion di un’applicazione AWS IoT, dalla semplice automazione dell’infrastruttura all’analisi in tempo reale dei messaggi inviati tramite AWS SQS o AWS Kinesis. Inoltre, sono economiche, funzionali, scalabili e molto semplici da installare e mantenere. 

A renderle ancor più attraenti agli occhi di sviluppatori e devops, è il numero sempre crescente di runtime Lambda offerti da AWS, che consentono di scrivere il codice in svariati linguaggi di programmazione. Al momento della stesura di questo articolo AWS Lambda supporta nativamente Java, Go, PowerShell, Node.js, C #, Python e Ruby. 

Tuttavia ci sono molti altri linguaggi di programmazione che potremmo voler usare in una funzione Lambda, per esempio per migrare alle AWS Lambda Functions applicazioni già esistenti ed attualmente deployate in locale o su istanze EC2. Poiché la riscrittura di codice esistente è spesso impossibile a causa della mancanza di tempo o della mancanza di librerie e funzionalità, AWS ha recentemente fornito una nuova possibilità: i Runtime personalizzati per Lambda che permettono di utilizzare qualsiasi linguaggio di programmazione!

Una funzione Lambda con un ambiente di runtime personalizzato differisce da una normale funzione lambda perché contiene non solo il codice che eseguirà quando verrà invocata la funzione, ma anche tutte le librerie compilate necessarie per eseguire il codice e, se il linguaggio scelto è interpretato come PHP o compilato just in time come Julia, è necessario includere anche il binario dell’interprete. Per la maggior parte dei linguaggi di programmazione, un runtime personalizzato preparato da terze parti è generalmente disponibile su github ed è spesso utilizzabile direttamente oppure può fornire una buona base per una soluzione personalizzata.

Nella sezione seguente descriveremo in dettaglio come creare un runtime generico e presenteremo due esempi creati da AWS: bash e C ++. Infine, confronteremo il tempo di risposta delle lambda dei runtime personalizzati con quello di un runtime nativo (Python 3.8). La creazione di un runtime personalizzato ci dà anche l’opportunità di capire come funziona davvero il servizio lambda.

Come funziona Lambda?

AWS Lambda è costituito da due parti principali: il servizio Lambda che gestisce le richieste di esecuzione e le micro virtual machines Amazon Linux deployate tramite AWS Firecracker che eseguono effettivamente il codice. Una VM Firecracker viene avviata la prima volta che una determinata funzione Lambda riceve una richiesta di esecuzione (il cosiddetto “Cold Start”) e non appena la VM completa il boot, inizia a eseguire il polling del servizio Lambda per ricevere i messaggi. Quando un messaggio viene ricevuto dalla VM, essa esegue il codice della funzione handler passandogli il messaggio JSON ricevuto nell’invocazione. 

Pertanto, ogni volta che il servizio Lambda riceve una richiesta di esecuzione, verifica se è disponibile una microVM Firecracker per gestire la richiesta di esecuzione e, in tal caso, recapita il messaggio alla VM da eseguire. Al contrario, se non viene trovata nessuna VM disponibile, Firecracker avvia una nuova macchina virtuale per gestire il messaggio. 

Ogni VM esegue un messaggio alla volta, quindi se molte richieste simultanee vengono inviate al servizio Lambda, ad esempio a causa di un picco di traffico ricevuto da un gateway API, verranno accese diverse nuove VM Firecracker per gestire le richieste. Per questo motivo, la latenza media delle richieste sarà maggiore poiché ogni VM impiega all’incirca un secondo per avviarsi (lambda Cold Start). 

In una funzione lambda che utilizza un runtime nativo non è necessario preoccuparsi di come la funzione eseguirà il polling dei messaggi dal servizio lambda e l’invio dei rapporti di esecuzione, il runtime nativo si occuperà di tutto ciò senza alcuna modifica necessaria da parte dello sviluppatore. Tuttavia, questo non avviene nel caso di un runtime personalizzato. Infatti, quando viene creata una funzione Lambda con un runtime personalizzato, AWS Lambda Service avvia una VM AmazonLinux di base senza librerie e pacchetti installati ad eccezione di bash e alcuni comandi unix di base (ad esempio ls, curl) . A differenza di una normale Lambda, oltre al codice e alle librerie esterne, è necessario includere nel pacchetto di distribuzione anche uno script o un eseguibile chiamato “bootstrap” che gestirà l’interazione tra la VM della funzione e il servizio Lambda. AWS Lambda Service espone una semplice interfaccia HTTP affinchè i runtime possano usarla per ricevere gli eventi di invocazione e inviare l’esito delle esecuzioni.

Il programma bootstrap deve perciò implementare le seguenti funzioni:

  1. Ottieni un evento: Invocare l’API di invocazione per ottenere l’evento successivo. Il corpo della risposta contiene i dati dell’evento. Le intestazioni di risposta contengono l’ID richiesta e altre informazioni.
  1. Propagare l’header di xray: Ottenere l’header della trace di X-Ray dall’header Lambda-Runtime-Trace-Id nella risposta API. Impostare la variabile di ambiente _X_AMZN_TRACE_ID localmente con lo stesso valore. X-Ray SDK utilizza questo valore per connettere i dati di tracing tra servizi.
  1. Creare un oggetto di contesto: crea un oggetto con informazioni di contesto da variabili di ambiente e intestazioni nella risposta API.
  1. Richiamare il gestore funzioni: passare l’evento e l’oggetto contestuale all’handler.
  1. Gestire la risposta: chiamare l’API di risposta dell’invocazione per pubblicare la risposta dell’handler.
  1. Gestisci errori: se si verifica un errore, chiamare l’API di errore.
  1. Pulizia: eliminare le risorse non utilizzate, inviare dati ad altri servizi o eseguire attività aggiuntive prima di ottenere il prossimo evento.

Lo script / eseguibile bootstrap e altre librerie ed interpreti a livello di linguaggio (ad es. Interprete PHP) possono essere incluse in un layer lambda dedicato al fine di generare un runtime personalizzato generico e portatile che può essere utilizzato con diverse funzioni Lambda.

Come creare una Lambda Bash con un runtime personalizzato

Il modo più semplice per iniziare a lavorare con i runtime personalizzati è direttamente tramite la Console di AWS: dalla dashboard del servizio Lambda basta selezionare “Crea Lambda” e nella sezione runtime selezionare Runtime personalizzato con Usa bootstrap predefinito e fare clic su Crea funzione

create lambda function

Usando queste impostazioni predefinite il servizio Lamba creerà una Lambda Bash di base con uno script bootstrap predefinito. Diamo un’occhiata allo script bootstrap pregenerato:

 bootstrap script

#!/bin/sh
set -euo pipefail

# Handler format: .
#
# The script file .sh  must be located at the root of your
# function's deployment package, alongside this bootstrap executable.
source $(dirname "$0")/"$(echo $_HANDLER | cut -d. -f1).sh"

while true
do
    # Request the next event from the Lambda runtime
    HEADERS="$(mktemp)"
    EVENT_DATA=$(curl -v -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
    INVOCATION_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

    # Execute the handler function from the script
    RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

    # Send the response to Lambda runtime
    curl -v -sS -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$INVOCATION_ID/response" -d "$RESPONSE"
done

Esaminiamo rapidamente questo script: 

  • La prima riga set -euo pipefail assicura che lo script venga terminato immediatamente in caso di eccezione o variabile vuota.

La variabile d’ambiente _HANDLER viene valorizzata all’avvio della VM e contiene il nome del file e della funzione del nostro gestore lambda nel formato <script_name>. <bash_function_name>

basic setting of the lambda function

  • source $ (dirname “$ 0”) / “$ (echo $ _HANDLER | cut -d. -f1) .sh ” carica semplicemente in memoria le variabili e le funzioni nel file dell’handler (hello.sh nel nostro caso)
  • Viene avviato un ciclo while infinito dove prima di tutto una chiamata Api viene inviata al servizio lambda endpoint (http: // $ {AWS_LAMBDA_RUNTIME_API} / 2018-06-01 / runtime / invocation / next) per consentire l’elaborazione del prossimo evento. Va notato che l’endpoint è dinamico ed è valorizzato nella variabile di ambiente AWS_LAMBDA_RUNTIME_API.
  • La risposta viene quindi valutata inoltrando l’evento alla funzione handler (RESPONSE = $ ($ (echo “$ _HANDLER” | cut -d. -F2) “$ EVENT_DATA”))
  • Infine il risultato dell’esecuzione viene restituito al Servizio Lambda tramite un’altra chiamata Api (curl -v -sS -X POST “http: // $ {AWS_LAMBDA_RUNTIME_API} / 2018-06-01 / runtime / invocation / $ INVOCATION_ID / response” -d “$ RESPONSE”)

Va notato che in un runtime personalizzato reale sarà anche necessario gestire errori ed eccezioni chiamando l’API per notificare il servizio lambda dell’errore (/ runtime / invocation / AwsRequestId / error) quando viene sollevata un’eccezione dal metodo Handler:

REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9
ERROR="{\"errorMessage\" : \"Error parsing event data.\", \"errorType\" : \"InvalidEventDataException\"}"
curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Unhandled"

Un semplice esempio c ++: il calcolo delle prime n cifre di pi in Lambda

Passiamo ora a un esempio più complesso: il calcolo delle prime n cifre di pi in Lambda usando una versione banale e inefficiente dell’algoritmo di Spigot. 

L’esecuzione di calcoli complicati nelle funzioni Lambda è spesso non banale sia per la mancanza di potenza computazionale riservata alla VM Firecracker (almeno per quelle a bassa memoria; memoria e CPU di una Vm lambda infatti scalano proporzionalmente) sia per la natura dei linguaggi dei runtime nativi, che non sono adatti a calcoli ad alte prestazioni (tranne Go). Al contrario, C++ ha una storia lunga e di successo nel calcolo ad alte prestazioni con molte librerie disponibili, dall’aritmetica di precisione arbitraria ai calcoli con matrici, dalla fluidodinamica alle collisioni di particelle.

Inoltre, questo linguaggio è un “first class citizen” in AWS con un AWS SDK completo e un generatore di Lambda Runtimes sviluppato e gestito direttamente da AWS.

Per creare il nostro esempio, possiamo semplicemente clonare il repository git AWS del generatore del runtime Lambda e creare la libreria usando i comandi (su unix):

$ git clone https://github.com/awslabs/aws-lambda-cpp.git
$ cd aws-lambda-cpp
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/lambda-install
$ make && make install

Dopo di che passiamo all’esempio api-gateway nella cartella degli esempi e cambiamo il codice nel main.cpp con:

#include 
#include 
#include 
#include 

using namespace aws::lambda_runtime;

void pi_digits(int x, Aws::SimpleStringStream &s)
{
    long x_initial = x;
    unsigned long long k = 2;
    unsigned long long a = 4;
    unsigned long long b = 1;
    unsigned long long a1 = 12;
    unsigned long long b1 = 4;
    while (x > 0) {
        unsigned long long p = k * k;
        unsigned long long q = 2 * k +1;
        k = k + 1;

        unsigned long long a1old = a1;
        unsigned long long b1old = b1;

        a1 = p * a + q * a1;
        b1 = p * b + q * b1;
        a = a1old;
        b = b1old;

        long double d = a / b;
        long double d1 = a1 / b1;

        while ((d == d1) && (x > 0)) {
            s << static_cast(floor(d));
            if (x_initial == x) {
                s << ".";
            }
            x -= 1;
            a = 10 * (a % b);
            a1 = 10 * (a1 % b1);
            d = a / b;
            d1 = a1 / b1;
        }
    }
}


invocation_response my_handler(invocation_request const& request)
{
    using namespace Aws::Utils::Json;

    JsonValue json(request.payload);
    if (!json.WasParseSuccessful()) {
        return invocation_response::failure("Failed to parse input JSON", "InvalidJSON");
    }

    auto v = json.View();
    Aws::SimpleStringStream ss;
//
//    pi_digits(10, ss);
    if (v.ValueExists("queryStringParameters")) {
        auto query_params = v.GetObject("queryStringParameters");
        pi_digits((query_params.ValueExists("number") && query_params.GetObject("number").IsString() ? stol(query_params.GetString("number")) : 10), ss);
    }

    JsonValue resp;
    resp.WithString("message", ss.str());

    return invocation_response::success(resp.View().WriteCompact(), "application/json");
}

int main()
{
    run_handler(my_handler);
    return 0;
}

L'algoritmo di Spigot utilizzato qui è una versione in C ++ di quello proposto qui.

A questo punto dobbiamo compilare il “core” della libreria C ++ dell’AWS SDK per ottenere gli strumenti di parsing JSON usati nel codice. Per fare ciò vi consiglio caldamente di usare questo comando (testato su Linux):

$ mkdir ~/install
$ git clone https://github.com/aws/aws-sdk-cpp.git
$ cd aws-sdk-cpp
$ mkdir build
$ cd build
$ cmake .. -DBUILD_ONLY="core" \
  -DCMAKE_BUILD_TYPE=Release \
  -DBUILD_SHARED_LIBS=OFF \
  -DENABLE_UNITY_BUILD=ON \
  -DCMAKE_INSTALL_PREFIX=~/install \
  -DENABLE_UNITY_BUILD=ON
$ make
$ make install

Ora si può tornare alla cartella dell'esempio e compilare la app lambda usando i comandi:

$ mkdir build
$ cd build
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=~/install
$ make
$ make aws-lambda-package-api

A questo punto è finalmente possibile caricare sul servizio Lambda il file zip generato dal comando build in modo da deployare finalmente la nostra applicazione Lambda (compatibile con Api Gateway) che calcola le cifre di pi greco in meno di un millisecondo.

lambda execution result

Lo stesso codice eseguito in Python richiede quasi 4 volte di più per essere completato!

Infine, collegando Lambda ad Api Gateway, è possibile ottenere un endpoint in grado di calcolare il pi greco usando C ++ ed il numero di cifre può essere specificato come query parameter (“number”).

Come nota finale questo script-giocattolo può calcolare solo cifre di pi fino a 10 prima dell’overflow dei contatori. Lasciamo a voi la sfida del miglioramento del codice per utilizzare GMP per l'aritmetica arbitraria al fine di ottenere Pi con un numero arbitrario di cifre. Divertitevi! 🙂

Per concludere, in questo articolo abbiamo spiegato come funzionano i runtime Lambda Custom e abbiamo presentato due semplici esempi in bash e C ++. L'uso di runtime personalizzati aggiunge molti usi possibili per il già utilissimo servizio AWS Lambda, aggiungendo la possibilità di eseguire calcoli rapidi utilizzando linguaggi ad alte prestazioni come C, C ++, Rust e Julia. Inoltre, i runtime personalizzati consentono anche di utilizzare Lambda per eseguire semplici script bash o anche per migrare al paradigma serverless Api PHP esistenti.

Se sei interessato a questo argomento, non esitare a contattarci!

beSharp
beSharp
Dal 2011 beSharp guida le aziende italiane sul Cloud. Dalla piccola impresa alla grande multinazionale, dal manifatturiero al terziario avanzato, aiutiamo le realtà più all’avanguardia a realizzare progetti innovativi in campo IT.

Lascia un commento

Ti potrebbero interessare

Costruiamo un sistema di autenticazione macchina-macchina con Amazon Cognito

Come descritto nelle specifiche OAuth 2.0, possiamo autenticare un client che presenta un ID client e un client secret validi […]
Leggi l'articolo

Sviluppiamo un’applicazione mobile di file hosting con Flutter, Amplify e AWS

Al giorno d’oggi Flutter sta ottenendo sempre più riconoscimento come soluzione per lo sviluppo di applicazioni mobile cross-platform. Inoltre, AWS […]
Leggi l'articolo

Costruiamo un backend Serverless con TypeScript, Node.js e AWS Lambda.

Su Amazon Web Services il servizio computazionale Serverless per eccellenza rimane AWS Lambda, quasi immancabile in un’architettura che utilizza questo […]
Leggi l'articolo