fbpx

AWS CDK: Rest API con OpenAPI

AWS CDK è uno dei migliori strumenti per la creazione di infrastrutture su AWS, tramite linguaggi di programmazione come Python o Javascript. Molto velocemente sta diventando sempre più stabile e completo, tuttavia presenta ancora una piccola mancanza nella creazione di API tramite lo standard OpenApi.
Distribuire una API tramite una specifica OpenApi è sicuramente la via migliore considerando tutto l’ecosistema che si è creato interno a questo standard. Purtroppo il suo utilizzo in AWS CDK rimane ancora acerbo. Infatti se distribuire, tramite costrutti CDK, una API tramite il servizio AWS API Gateway è di base relativamente semplice ma prolisso, può diventare intricato utilizzando un file OpenApi.

In questo articolo vedremo come poter risolvere l’integrazione con una Lambda function creata tramite CDK e daremo una possibile soluzione per l’integrazione di un Authorizer tramite Cognito UserPool.

Problema

CDK supporta perfettamente la creazione di una API, tramite il servizio AWS API Gateway, utilizzando lo standard OpenApi e le varie estensioni messe a disposizione. Tuttavia sorgono numerosi problemi nel momento in cui si vanno ad effettuare delle integrazioni dinamiche con servizi come, ad esempio, Lambda o Cognito. Infatti nella specifica del nostro file OpenApi si richiede l’ARN della funzione Lambda o l’ID della User Pool di Cognito, ma questi dati non sono disponibili immediatamente (al primo deploy) in CDK.
Nello specifico vedremmo come l’ARN o l’ID non sarebbero disponibili per popolare il nostro yaml o json, della specifica OpenAPI, il quale viene convertito come asset durante la procedura di Synth della CDK cli, prima che venga fatto il deploy dei servizi. Essendo, quindi, questi dati disponibili solo dopo la creazione dei servizi non possiamo effettuare il rilascio di tutti i servizi, almeno non in contemporanea.

Esempio di integrazione AWS API Gateway + Lambda, tramite OpenAPI:

Tramite questo spezzone di definizione si va ad indicare ad API Gateway di collegare l’endpoint /users alla funzione Lambda con l’ARN definito nel campo uri ( ${your-lambda-arn} ) ed usando un tipo di integrazione aws_proxy (specifiche dell’estenzione AWS).
Tramite questo esempio ci è forse subito chiaro il problema: dobbiamo conoscere l’ARN della nostra funzione Lambda affinché funzioni l’integrazione, cosa che non sappiamo poiché verrà creata dinamicamente da CDK.

Il team di AWS CDK è a conoscenza di questo problema (vedi la pull-request Github #1461), tuttavia questa issue è aperta da quasi tre anni, quindi nel frattempo abbiamo dovuto trovare una buona soluzione alternativa.

Metodi di risoluzione

Sono state pubblicate diverse soluzioni alternative per questo problema:

  • Questo post sul blog di Martin Müller in cui sostanzialmente distribuisce l’intero stack due volte: una volta senza le specifiche OpenAPI e una volta con esse. Non è il tipo di approccio che ci piace utilizzare.
  • Questa risposta StackOverflow in cui l’autore utilizza la funzione Fn::Sub nel file OpenAPI per sostituire un segnaposto dell’ARN Lambda con l’ARN effettivo. Questo metodo sarebbe stato il migliore, ma sfortunatamente anche dopo numerosi tentativi non siamo riusciti a farlo funzionare. Questa soluzione avrebbe permesso la sostituzione del segnaposto come succede per gli altri AWS::Partion, AWS::Region, AWS::Account, etc che sono integrati in AWS e disponibili al rilascio del servizio Api Gateway.

Avendo scartato queste due soluzioni abbiamo approcciato quella che attualmente va per la maggiore tra gli utenti, ovvero sostituire l’ARN della Funziona Lambda al momento del Synthesize CDK e quindi al momento della creazione dell’asset di definizione OpenApi. L’unico “svantaggio” che comporta questa soluzione è il dover anticipatamente aver definito in maniera statica il nome della Lambda.
Anche se questa è una soluzione subottimale, la riteniamo ad ogni modo migliore delle altre riportate.

Implementazione soluzione

La definizione OpenAPI è stata creata in un modello Jinja2 in modo che i segnaposto, nel nostro caso il nome della Lambda, possa essere sostituito. Oltre alla sostituzione del nome possiamo così applicare costrutti Jinja, come loop o if, per la creazione di definizioni diverse in base all’ambiente di rilascio.

Esempio di integrazione AWS API Gateway + Lambda, tramite OpenAPI e Jinja2:

Come si può notare nel campo uri abbiamo aggiunto un ARN che verrà per la maggior parte sostituito automaticamente da AWS e, per la restante parte, tramite Jinja nei segnaposto racchiusi nelle doppie parentesi graffe. In questo caso avremo bisogno di una costante con il nome della Lambda e un’altra con il nome della IAM Role da assegnare al metodo Api-Gateway per poterla eseguire.

Vediamo quindi un esempio (più o meno completo) di uno Stack CDK scritto in Python:

Descriviamo le parti principali del codice: la nostra definizione OpenApi (open-api-definition/openapi3.yaml) viene processata tramite Jinja e quindi esportata una copia dove avremo i nostri due campi (lambda_a, lambda_exec_role)  sostituiti (open-api-definition/openapi3_rendered.yaml). Il file viene quindi utilizzato per creare la nostra API tramite il costrutto CDK SpecRestApi(). Infine viene definita la funzione Lambda e tramite il metodo add_permission() facciamo in modo che API-Gateway abbia il permesso di invocare la funzione.
Come è facilmente intuibile questa soluzione non è sicuramente molto elegante ed aggiunge la necessità di definire a priori il nome di tutte le Lambda.

In aggiunta è bene notare che in alcuni casi, dove la IAM Role ha dei permessi molto lascivo, è necessario aggiungere esplicitamente il permesso dell’API-Gateway per invocare ogni singola Lambda. Questo porta inesorabilmente ad un codice più prolisso e statico.

OpenApi Authorizer integrazione Cognito

AWS Cognito è molto utile nel caso si voglia aggiungere un fattore di autenticazione, tramite Token JWT, per le chiamate all’API. I due servizi: Cognito ed API-Gateway, sono perfettamente integrati in AWS e l’integrazione tramite AWS Web-Console risulta relativamente semplice ed immediata.

Tuttavia non possiamo utilizzare lo stesso metodo visto precedentemente per le Lambda per integrare Cognito come Authorizer in API-Gateway. Infatti ci è impossibile conoscere l’ID della UserPool prima che questa venga creata. L’unico modo attualmente possibile è quello di rilanciare successivamente il deploy, questa volta inserendo l’ID della UserPool precedentemente creata e restituitaci negli output di CloudFormation.

Questo un esempio di integrazione AWS API-Gateway + OpenAPI + AWS Cognito:

La variabile user_pool_id dovrà essere al primo rilascio casuale e successivamente quella corretta. Al secondo deploy potremmo passare la variabile tramite context di CDK: cdk deploy -c user_pool_id=xxxxxx.

Conclusione

L’uso di OpenAPI risulta ancora generalmente un pochino acerbo con il cloud di AWS per via di alcune limitazioni come quelle presentate precedentemente. Tuttavia le definizioni OpenAPI ben strutturate e complete portano alla semplificazione del lavoro di ingegnerizzazione e codifica di una REST API ed allo stesso tempo permettono, a tutti i membri dei team, una facile comprensione su come integrare i vari servizi.

 

I hope to see you again, cheerio!

Andrea Salvatori
Andrea Salvatori
Articoli: 4

Lascia una risposta

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.