AWS Amplify recipe

AWS Amplify 101

9 min

Durante lo scorso AWS Summit ho assistito ad un’interessante sessione riguardante AWS Amplify. La bravissima speaker Marcia Villalba (AWS Developer Advocate) mi ha rapidamente convinto a provare questo framework. Pochi giorni dopo mi si è presentata l’occasione giusta: avendo la necessità di tenere mio figlio allenato con le tabelline durante queste vacanze estive, ho deciso di sviluppare una semplice web application con AWS Amplify in modo da scoprirne pregi e difetti.

Questo il link al repository GitHub e questo il link alla web application del progetto che ne è venuto fuori. Si, puoi esercitarti anche tu con le tabelline.

Cos’è Amplify?

AWS Amplify è un insieme di strumenti e servizi che permettono ad uno sviluppatore di realizzare applicazioni moderne full stack sfruttando i servizi cloud di AWS. Dovendo, per esempio, realizzare una web application React, Amplify ci consente di gestire sviluppo e deployment del frontend, dei servizi di backend e la relativa pipeline di CI/CD. Per la medesima applicazione è possibile avere più ambienti (ad esempio dev, test, stage & produzione). Amplify consente inoltre di integrare molto rapidamente alcuni servizi AWS nel proprio frontend, scrivendo pochissime righe di codice: un esempio su tutti, l’autenticazione con AWS Cognito.

Ottimo! Sembra ci sia proprio tutto per consentire ad uno sviluppatore one-man-band di realizzare rapidamente la propria applicazione! Proviamoci.

Impara le tabelline

Come prima step meglio chiarirsi le idee su cosa vogliamo realizzare: che siano quattro schizzi su un foglio di carta (il mio caso) o un buon mockup in Balsamiq, proviamo ad immaginare UI & UX della nostra applicazione.

Lo scopo principale è allenare la conoscenza delle tabelline sottoponendo l’utente ad un test: 10 moltiplicazioni relative alla medesima tabellina, scelta casualmente da 2 ad 10 (non vi devo spiegare perché la tabellina del 1 l’ho esclusa vero?).

Sarebbe interessante tenere traccia di errori e tempo speso per rispondere alle domande, in modo da avere una scoreboard con il miglior risultato per ciascuna tabellina.

Vogliamo mostrare ad ogni utente la propria scoreboard e spingerlo a migliorarsi? Dovremo quindi memorizzare la stessa e ci serve un processo di autenticazione!

Avendo la scoreboard di ciascun utente, possiamo inoltre scegliere la tabellina oggetto della prossima sfida in base ai precedenti risultati, in modo da allenare l’utente sulle tabelline per le quali ha riscontrato maggiori difficoltà.

Ed ecco apparire, come per magia, la UI della nostra applicazione.

Primi passi

Ora che abbiamo le idee più chiare su cosa realizzare, muoviamo i primi passi. Abbiamo già deciso di utilizzare React e Amplify. Vediamo i prerequisiti di quest’ultimo e creiamo il nostro progetto, come spiegato nel Tutorial ufficiale di Amplify Docs.

I prerequisiti indicati da AWS per Amplify sono questi:

E’ necessario, ovviamente, un account AWS e scopriremo poi, durante il deployment del backend, che ci servirà anche Python >= 3.8 e Pipenv.

Per installare Amplify CLI e configurarlo procediamo come indicato. La configurazione di Amplify CLI richiede la creazione di uno IAM User che sarà poi utilizzato per la realizzazione dei servizi di backend.

npm install -g @aws-amplify/cli
amplify configure

Inizializziamo ora il nostro progetto React e il relativo backend Amplify.

# Create React project
npx create-react-app amplify-101
cd amplify-101

# Run React
npm start

# Init amplify
amplify init

# Install Amplify libraries for React
npm install aws-amplify @aws-amplify/ui-react

Dovremo a questo punto editare il file src/index.js inserendo le seguenti righe:

import Amplify from "aws-amplify";
import awsExports from "./aws-exports";
Amplify.configure(awsExports);

Ottimo. Al termine di queste attività (spiegate in dettaglio nel Tutorial ufficiale di Amplify Docs) avremo un nuovo progetto React funzionante e saremo pronti a realizzare il relativo backend con Amplify.

Autenticazione con Cognito

Di cosa abbiamo bisogno? Beh, dato che l’utente è al centro della nostra applicazione, possiamo iniziare con la realizzazione del processo di autenticazione. Ed è proprio a questo punto che mi è scappato il primo “WOW” rendendomi conto di quanto sia semplice con Amplify.

Aggiungiamo “auth” al backend con il comando indicato e rispondiamo a un paio di semplici domande.

amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings?  No, I am done.

Dobbiamo poi chiedere alla CLI di Amplify di pubblicare sul Cloud il nostro backend. Il comando che andremo (spesso) ad utilizzare è:

amplify push

Amplify si occupa di tutto, andando a creare quanto necessario lato backend tramite stack CloudFormation.

Inseriamo ora la UI sul nostro frontend React: il metodo più veloce consiste nel modificare due righe di codice nel file src/App.js:

# New import for auth
import { withAuthenticator } from '@aws-amplify/ui-react'

# Change default export
export default withAuthenticator(App)

Fatto! La nostra app prevede ora un flusso completo di registrazione degli utenti, login e logout. Ovviamente gli elementi della UI sono personalizzabili e possiamo modificare il comportamento della nostra applicazione in risposta all’evento di login. Vediamo come.

<AmplifySignUp> consente di personalizzare il processo di registrazione di un nuovo utente. Ho scelto di richiedere solo l’indirizzo email (che viene verificato tramite l’invio di un codice OTP) e una password. L’indirizzo email sarà utilizzato come username per le successive login.

<AmplifySignIn> consente di personalizzare il processo di login: anche in questo caso ho specificato di considerare l’indirizzo email come username per l’accesso.

<AmplifyAuthenticator> ci consente di modificare lo stato della nostra app in risposta a login e logout, tramite handleAuthStateChange. Avremo quindi la possibilità di capire se un utente si è autenticato verificando lo stato {this.state.authState}. Potremo visualizzare il relativo username con {this.state.user.username}.

Fantastico! Ora vediamo come iniziare ad aggiungere un pò di funzionalità alla nostra app.

Storage

Quale app non ha dati da memorizzare? Con Amplify abbiamo due possibilità: contenuti statici su S3 o database NoSQL con DynamoDB. Nel caso della nostra applicazione, abbiamo la necessità di creare una tabella scoreboard per memorizzare i migliori risultati di ciascun utente. Con Amplify CLI la creazione della tabella è molto rapida:

amplify add storage

Selezionando NoSQL Database e rispondendo a qualche semplice domanda sulla struttura della tabella che si intende creare, si ottiene il deployment su AWS.

Un chiarimento: Amplify supporta la creazione di API GraphQL (AppSync) che andremo ad utilizzare negli step successivi. Specificando la direttiva @model nello schema GraphQL, è possibile delegare ad Amplify il deployment di una tabella e tutto ciò che serve per gestire le relative operazioni CRUD dal frontend. E’ una grande comodità se attorno al dato che si deve gestire non esistono complesse logiche applicative.

Nel caso della nostra scoreboard, abbiamo la necessità di gestire l’inserimento dei dati esclusivamente lato backend. Dobbiamo inoltre valutare di volta in volta i risultati di un test e aggiornare la scoreboard di conseguenza. L’accesso da parte del frontend è esclusivamente in sola lettura. Per questi motivi ho preferito non utilizzare la direttiva @model e gestire storage ed API separatamente (ma pur sempre con Amplify).

Ci servirà una seconda tabella che ho chiamato challenges: come si evince dal nome, servirà a memorizzare i risultati di una sfida “in corso” in modo da poter poi confrontarli con le risposte del nostro utente e determinare l’esito della prova. Per gli stessi motivi ho preferito gestire, anche per questa tabella, deployment ed API separatamente.

Backend functions

Iniziamo a scrivere il codice per il backend: ho scelto di realizzare in Python le Lambda function necessarie. Una delle caratteristiche di Amplify che apprezzo è il fatto di concentrare l’intero codice della nostra applicazione in un unico repository: codice frontend, backend e infrastrutturale (IaaC) possono essere agevolmente e rapidamente modificati per adeguarsi a nuovi requisiti e nuove funzionalità.

Occupiamoci quindi di realizzare la funzione principale della nostra app: la generazione di un nuovo test e la verifica dei suoi risultati. Useremo il comando:

amplify add function

Ci viene chiesto come vogliamo chiamare la funzione, quale runtime vogliamo utilizzare (Python, Node, ecc..) e se la funzione deve aver accesso a qualche risorsa precedentemente definita. Nel nostro caso la funzione newChallenge avrà accesso ad entrambe le tabelle create in precedenza.

Ho usato lo stesso comando per creare la funzione scoreboard che permette al frontend di visualizzare il contenuto della scoreboard dell’utente.

Il codice sorgente delle funzioni di backend si trova nel percorso amplify/backend/function del nostro progetto. Ogni qualvolta andremo a modificarlo, ci basterà effettuare un push della soluzione per aggiornare il backend su cloud.

Non entro qui nel merito del codice delle Lambda function realizzate, che è disponibile in questo repository GitHub. Ci basti sapere che entrambe le funzioni rispondono alle richieste GraphQL con un JSON popolato con i dati richiesti, oltre ovviamente ad implementare la logica applicativa (generazione del test, memorizzazione dei risultati e valutazione degli stessi).

Il riferimento alle tabelle DynamoDB create in precedenza viene fornito tramite variabile d’ambiente: il nome della tabella scoreboard, per esempio, è fornito in STORAGE_SCOREBOARD_NAME.

dynamodb = boto3.resource('dynamodb')
scoreboard_table = dynamodb.Table(os.environ['STORAGE_SCOREBOARD_NAME'])

L’event con il quale viene invocata la funzione fornisce invece le informazioni relative alla richiesta GraphQL a cui rispondere: il parametro typeName indica per esempio se si tratta di una Query o di una Mutation. Vediamo nel prossimo paragrafo l’ultimo step necessario a completare la nostra app, l’implementazione delle API GraphQL.

API GraphQL

Definiamo l’ultima risorsa backend necessaria: le API con le quali la nostra web application React andrà ad interagire.

amplify add api

E’ possibile specificare API Rest esistenti o scegliere di creare un endpoint GraphQL, che è la nostra preferenza. Amplify si occupa di gestirne l’implementazione e l’integrazione con il frontend.

Noi ci dobbiamo solo preoccupare di definire lo schema dei dati della nostra applicazione. Vediamolo.

Definiamo due tipi base: challenge che rappresenta un test e score che rappresenta i risultati.

La definizione delle Query è più interessante: andiamo a definire due chiamate alle funzioni Lambda precedentemente implementate. La prima ottiene un array di score per visualizzare la scoreboard ed è chiamata getScores. La seconda ottiene una nuova challenge (getChallenge). Da notare che il nome delle funzioni riporta l’ambiente ${env} di riferimento.

Il type Mutation ci consente invece di inviare i risultati di un test alle API per la relativa valutazione. La funzione Lambda richiamata è sempre newChallenge alla quale vengono passati alcuni parametri, cioè l’identificativo univoco del test e i risultati indicati dall’utente, ottenendo l’esito.

Come utilizzare queste API in React? E’ molto semplice: basta specificare gli import necessari (il cui codice è generato automaticamente da Amplify) e richiamarle nel proprio frontend.

Questo è un estratto del codice utilizzato per ottenere la propria scoreboard.

import { getScores }  from './graphql/queries';
import { API, graphqlOperation } from "aws-amplify";

......

  componentDidMount() {
    this.fetchData();  
  }

  async fetchData() {
    const data = await API.graphql(graphqlOperation(getScores));
    const scores = data.data.getScores
    console.log(scores);
    this.setState({'scores':scores});
  }

Da notare: l’utente non viene specificato nella chiamata a getScores. Grazie infatti all’integrazione con AWS Cognito, l’identità dell’utente è infatti specificata direttamente nell’evento di invocazione della Lambda function, nel parametro identity.

Nel caso di mutation, il codice utilizzato sulla submit di una challenge è il seguente:

import { API, graphqlOperation } from 'aws-amplify'
import { sendChallengeResults } from './graphql/mutations';

....

  handleClick() {
    this.setState({'loadingResults': true})

    // mutation
    const challenge_result = { id: this.props.challenge.id, results: this.state.results }

    API.graphql(graphqlOperation(sendChallengeResults, challenge_result))
      .then(data => {
        console.log({ data });
        this.setState({'score': data.data.sendChallengeResults});
      })
      .catch(err => console.log('error: ', err));
  }

Deployment

Abbiamo terminato! Tutte le componenti della nostra app sono state realizzate.

amplify status

La CLI di Amplify ci consente di effettuare il deployment della nostra web application con due semplici comandi:

amplify add hosting
amplify publish

Non ho però scelto questa strada, volendo testare le potenzialità di CI/CD che Amplify mette a disposizione. Per farlo è necessario usare la Console.

Per prima posizioniamo la soluzione su un repository GIT. Successivamente, dalla console di Amplify, occupiamoci di connettere il branch del nostro repository per implementare la pipeline.

Amplify Console

Ecco fatto! La nostra pipeline è operativa e la nostra applicazione è ora disponibile online. Al primo tentativo? Non proprio!

Purtroppo per qualche motivo a me sconosciuto, l’immagine Docker di default utilizzata per la build della soluzione non è correttamente configurata: esiste infatti un problema noto relativo alla configurazione di Python 3.8 riportato anche in questa issue (Amplify can’t find Python3.8 on build phase of CI/CD).

Per ovviare al problema, il workaround più semplice che ho individuato è stato creare un’immagine Docker con tutti i requisiti necessari. Di seguito il relativo Dockerfile.

Ho reso poi disponibile l’immagine su DockerHub e ho configurato la pipeline di CI per utilizzare tale immagine.

Build settings

Conclusioni

Siamo giunti alla fine di questo post: la realizzazione di questa semplice web application mi ha permesso di muovere i primi passi con Amplify. A quali conclusioni sono arrivato?

Giudizio sicuramente molto positivo! Amplify consente di realizzare molto rapidamente applicazioni complesse e moderne, integrando facilmente i servizi (serverless) di AWS Cloud. L’autenticazione con AWS Cognito ne è un chiaro esempio, ma esiste la possibilità di integrare tante altre funzionalità, come Analytics, Elasticsearch o AI/ML.

GraphQL ci consente di gestire semplicemente i dati della nostra applicazione, in particolare per le classiche operazioni CRUD.

La centralizzazione del codice sorgente, di frontend, backend e infrastruttura (IaaC), consente di tenere sotto controllo l’intera soluzione e garantisce di poter intervenire rapidamente per adeguarsi a nuovi requisiti e nuove funzionalità.

E’ un prodotto dedicato ad uno sviluppatore full stack che opera singolarmente? Direi, non esclusivamente! Sebbene Amplify permetta ad un singolo sviluppatore di seguire ogni aspetto della propria applicazione, semplificando anche le operazioni di DevOps, credo che anche un team di lavoro possa facilmente collaborare alla realizzazione della soluzione traendo vantaggio dall’utilizzo di Amplify.

E’ adatto allo sviluppo di qualsiasi soluzione? Direi di no! Credo che il valore aggiunti di Amplify sia, come già detto, la possibilità di gestire in modo rapido e centralizzato tutti gli aspetti della propria applicazione. Se la complessità del backend è tale da prevedere elementi non direttamente gestibili da Amplify, forse è preferibile utilizzare altri strumenti o un approccio misto.

Per i motivi qui detti, credo che l’utilizzatore ideale di Amplify siano lo sviluppatore full stack, le giovani startup o le aziende di sviluppo più “agili” che hanno la necessità di mettere in campo rapidamente nuove soluzioni o nuove funzionalità.

In conclusione, la prossima volta che dovrò realizzare una soluzione, che sia un semplice PoC o una web application più complessa, prenderò sicuramente in considerazione l’utilizzo di AWS Amplify!

Ci siamo divertiti? Alla prossima!

Leave a Comment