Nelle ultime settimane ho avuto molto a che fare con la creazione di AWS Lambda.
La difficoltà più grossa con cui ho dovuto fare i conti è stata quella di testare in locale queste funzioni. Sicuramente è possibile (anzi, necessario) scrivere unit ed integration test, ma mi piace comunque testare in un ambiente il più vicino possibile a quello di produzione.
Vorrei quindi costruire una sorta di guida per avere un reminder per i futuri sviluppi. Per fare ciò sono necessari i seguenti step:
- scrivere una lambda che fa cose
- con l’aiuto di SAM (e la sua CLI), invocare e debuggare la funzione
Let’s start!
SAM
SAM (Serverless Application Model) è un framework open source per definire, testare, ed infine deployare applicazioni serverless. Il tutto scrivendo un file .yaml (template) che SAM si occuperà di trasformare in sintassi AWS CloudFormation.
In questo caso, ci servirà per definire l’endpoint al quale la lambda risponderà. Tramite la sua CLI, riusciremo anche a lanciare (e debuggare) la lambda stessa in locale.
Un po' di documentazione nella pagina ufficiale.
Creazione delle Lambda
La SAM CLI offre una funzionalità molto comoda per creare uno stub di lambda. Basterà infatti lanciare
sam init --runtime java8 --name StuffApi
per creare un intero progetto maven con delle classi di esempio. In particolare, verrà creata questa struttura:
StuffApi
├── README.md
├── pom.xml
├── src
│ ├── main
│ │ └── java
│ │ └── helloworld
│ │ ├── App.java <-- Lambda
│ │ └── GatewayResponse.java <-- POJO per risposta API Gateway
│ └── test
│ └── java
│ └── helloworld
│ └── AppTest.java
└── template.yaml
Vi consiglio di dare una lettura soprattutto al file README.md
, che contiene molte indicazione utili sui prossimi passi da seguire.
Implementiamo una semplice lambda che restituisce un oggetto User
:
public class StuffHandler implements RequestHandler<Void, GatewayResponse<UserDto>> {
@Override
public GatewayResponse<UserDto> handleRequest(final Void input, final Context context) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return new GatewayResponse<>(new UserDto.UserDtoBuilder().setEmail("foo@bar.com").setUsername("foobar").build(),
headers,
200);
}
}
La risposta è wrappata all’interno dell’oggetto GatewayResponse
. Questo oggetto, contiene tre campi:
body
: la risposta vera e propria della lambdaheaders
: eventuali header che vogliamo dare in risposta (in questo caso ho aggiunto l’headerContent-Type
)statusCode
: il codice HTTP restituito (nel mio caso, 200)
Creazione del SAM template
Veniamo ora alla “ciccia”, il SAM template. Ecco la prima parte del template yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
StuffApi
Sample SAM Template for StuffApi
Globals:
Function:
Timeout: 20
Qui sono contenute delle informazioni generiche che servono a SAM per parsare il file (AWSTemplateFormatVersion
ad esempio). La parte più interessante riguarda la sezione Globals
: qui infatti possiamo definire tutte le variabili globali della nostra lambda.
Resources:
StuffFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: target/StuffApi-1.0.jar
Handler: dev.marcodenisi.stuff.StuffHandler::handleRequest
Runtime: java8
Events:
Stuff:
Type: Api
Properties:
Path: /stuff
Method: get
In questa seconda sezione, definiamo la Lambda Function vera e propria. Le diamo un nome (StuffFunction
), definiamo il nome del jar contenente il codice sorgente ed anche la classe handler
, con relativo metodo che si occuperà di servire il contenuto.
Sotto Events
, definiamo un API Gateway (Type: Api
) che risponde a chiamate HTTP GET (Method: get
) al path /stuff
.
Invocare e debuggare la funzione
Per prima cosa, pacchettizziamo la lambda. Nel mio caso, usando Java + Maven, basta lanciare mvn package
per creare il jar.
Per invocare la lambda, utilizziamo il comando di invoke
della SAM cli.
sam local invoke "StuffFunction" --no-event
... // log "di servizio"
{"body":{"username":"foobar","email":"foo@bar.com"},"headers":{"Content-Type":"application/json"},"statusCode":200}
Questo comando non fa nient’altro che creare un piccolo container docker. Infatti, il primo start sarà piuttosto lento a causa del download dell’immagine, per poi velocizzarsi le volte successive. Ecco la documentazione dell’immagine docker utilizzata.
L’opzione --no-event
specifica che alla lambda non serve nulla in ingresso. Nel caso dovessero servire degli eventi specifici, il comando sam local generate-event
è utile per creare degli eventi di esempio.
Per debuggare, basta aggiungere un flag:
sam local invoke "StuffFunction" --no-event -d 5858
In questo modo, la lambda non verrà eseguita fino a che un processo si attaccherà in debug alla porta specificata (la 5858, nel mio caso).