source code

Linting e test end-to-end Cypress in AWS Amplify

6 min

Di rientro dalla pausa estiva, ho ripreso l’approfondimento di AWS Amplify. In un precedente articolo avevo trattato la realizzazione di una semplice web application utilizzando questo framework di AWS. In questo post voglio invece trattare l’implementazione di linter per il codice sorgente e dei test end-to-end con Cypress, ovviamente automatizzati nella pipeline CI/CD di AWS Amplify.

Questo il link al repository GitHub e questo il link alla web application del progetto.

Linter

Il linter è uno strumento che analizza il codice sorgente per contrassegnare errori di programmazione, bug, errori stilistici e costrutti sospetti.

(wikipedia)

Utilizzare uno o più strumenti linter ci consente di aumentare la qualità del codice sorgente prodotto. Utilizzare tali strumenti in una pipeline di CI/CD ci permette inoltre di effettuare il deployment di un’applicazione solo quando il codice sorgente della stessa rispetta determinati livelli qualitativi.

La web application che ho realizzato prevede un frontend React e un backend lambda Python. Avremo quindi bisogno due differenti strumenti, specifici per le due tecnologie utilizzate.

ESLint

Uno dei migliore linter che ho avuto la possibilità di testare per JavaScript è ESLint: le sue funzionalità permettono non solo di testare ma anche di porre rimedio (fix automatico) ad un elevato numero di problemi.

Ho utilizzato quindi ESLint per verificare il codice del frontend. La sua installazione è semplice: dalla directory principale del nostro progetto, possiamo installare il linter con il seguente comando.

npm install eslint --save-dev

Terminata l’installazione, il wizard per la prima configurazione si avvia in questo modo.

npx eslint --init

Verranno richieste diverse informazioni: in generale è possibile scegliere se utilizzare ESLint per:

  • la verifica della sintassi
  • la verifica della sintassi e la ricerca di problemi
  • la verifica della sintassi, la ricerca di problemi e il rispetto stilistico nella scrittura del codice

Quest’ultima opzione è interessante e consente di “aderire” ad uno stile tra i più popolari, come quello scelto da Airbnb o da Google. Terminata la configurazione, verranno installati i packages richiesti. Nel mio caso:

"devDependencies": {
    "eslint": "^7.8.1",
    "eslint-config-google": "^0.14.0",
    "eslint-plugin-react": "^7.20.6"
  }

Ora che abbiamo configurato ESLint, procediamo alla verifica del codice:

npx eslint src/*.js

A seconda delle opzioni scelte durante la configurazione, il risultato potrebbe non essere dei più ottimisti, rilevando una lunga serie di problemi.

Come però dicevo inizialmente, ESLint ci consente di correggere automaticamente alcuni di questi, utilizzando il seguente comando:

npx eslint src/*.js --fix

Ottimo! Degli 87 problemi rilevati inizialmente, solo 6 richiedono il nostro intervento per essere corretti.

Possiamo inoltre decide di ignorare alcuni problemi specifici. Volendo, per esempio, evitare la segnalazione per l’assenza dei commenti JSDoc, si deve modificare la sezione rules del file .eslintrc.js di configurazione di ESLint.

  'rules': {
    "require-jsdoc": "off"
  },

Rimane solo un errore, relativo ad una linea troppo lunga e che andremo direttamente a correggere.

Abbiamo svolto manualmente l’operazione di linting del codice. L’obiettivo prefissato è inoltre automatizzare questo processo nella pipeline di CI/CD, in modo da interrompere il deployment nel caso alcuni errori vengano rilevati.

Configuriamo la pipeline modificando il file amplify.yml in questo modo:

frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npx eslint src/*.js 
        - npm run build
  artifacts:
    baseDirectory: build
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

Tra i comandi della fase di build del frontend, inseriamo il comando per eseguire ESLint. Qualora lo stesso rilevi un problema, la pipeline si interromperà automaticamente.

Per quanto riguardo il frontend è tutto! Per eventuali approfondimenti nell’uso di ESLint, consiglio di consultare l’ottima documentazione.

Pylint

E’ arrivato il momento di verificare il codice di backend. Avendo realizzato alcune funzioni AWS Lambda in Python, ho scelto Pylint per questo scopo. L’installazione si effettua normalmente con PIP:

pip install pylint

Per analizzare un file con Pylint basta eseguirlo specificando il nome del file come argomento.

A differenza del tool visto in precedenza, Pylint attribuisce un punteggio al codice analizzato. Il punteggio si basa ovviamente sui problemi rilevati.

Alcuni problemi possono essere ignorati andando a modificare il file di configurazione. Per generare un file di configurazione da personalizzare successivamente con le proprio impostazioni, è necessario eseguire il comando:

pylint --generate-rcfile

Il file generato è ben documentato e comprensibile.

Al fine di integrare Pylint nella pipeline di AWS Amplify, le operazioni da compiere sono più complicate di quanto visto in precedenza per ESLint.

Per prima cosa è necessario prevedere l’installazione di Pylint nell’immagine Docker utilizzata per la fase di build del backend. Come spiegato nel precedente articolo che ho scritto a riguardo di AWS Amplify,  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.

Installato Pylint, andremo a configurare la pipeline modificando il file amplify.yml in questo modo:

backend:
  phases:
    build:
      commands:
        - '# Evaluate backend Python code quality'
        - find amplify/backend/function -name index.py -type f | xargs pylint --fail-under=5 
        - '# Execute Amplify CLI with the helper script'
        - amplifyPush --simple

Utilizziamo find per sottoporre a Pylint tutti i file index.py delle nostre lambda function di backend. Particolare attenzione deve essere posta al parametro –fail-under: questo istruisce Pylint per considerare fallita una valutazione del codice sorgente inferiore a 5. In questo caso l’exit code di Pylint sarà diverso da zero e quindi interromperà la pipeline di CI/CD. Con una valutazione superiore a 5, l’exit code sarà invece pari a zero. La soglia di default è 10, corrispondente ad un codice sorgente “perfetto” e onestamente molto difficile da ottenere in presenza di applicazioni complesse.

Abbiamo completato l’introduzione dei linter nella nostra pipeline. Vediamo ora come automatizzare il test della nostra applicazione.

End-to-end test con Cypress

Cypress è una soluzione che permette di effettuare test automatici della nostra web application, simulando le operazioni che un utente esegue tramite UI. I test sono raccolti in suite.

Il superamento dei test garantisce il corretto funzionamento dal punto di vista dell’utente.

La console Amplify offre un’integrazione con Cypress (un framework di test end-to-end) per test basati su browser. Per le app Web che utilizzano Cypress, la console Amplify rileva automaticamente i comandi di test sulla connessione repo, esegue i test e fornisce accesso a video, schermate e qualsiasi altro artefatto reso disponibile durante la fase di compilazione.

(AWS)

L’installazione è molto semplice:

npm install cypress

Con il seguente comando avviamo per la prima Cypress:

npx cypress open

Questo comporterà la creazione di una directory cypress nel nostro progetto che include tutti i file di configurazione e le test suite. Alcune di esempio sono presenti in cypress/integration. Non mi addentro nei dettagli di come realizzare una test suite perché esiste già un’immensa documentazione a riguardo.

Configurazione di Cypress per AWS Amplify e Cognito

Vediamo come configurare correttamente il nostro progetto per integrarsi con AWS Amplify.

Il primo file che andremo a modificare è cypress.json presente ora nella root del progetto.

{ 
    "baseUrl": "http://localhost:3000/", 
    "experimentalShadowDomSupport": true
}

Il primo parametro indica l’URL base della nostra web application. Il secondo parametro experimentalShadowDomSupport è invece molto importante nel caso di applicazioni React che utilizzando il backend di autenticazione Cognito di AWS Amplify e i relativi componenti UI. In estrema sintesi, non abilitando il supporto Shadow DOM, non saremo in grado di autenticarci nella nostra applicazione durante le fasi di testing.

La test suite utilizzata per verificare le funzionalità di login e logout della web application è il seguente:

Per motivi di sicurezza username e password da utilizzare per i test non sono specificate nello script ma andranno definite in variabili di ambiente direttamente sulla console di AWS Amplify. Le variabili sono CYPRESS_username e CYPRESS_password.

Configurazione della Pipeline

Non resta che configurare la pipeline per l’esecuzione dei test, così come indicato da AWS. Ed ecco il file amplify.yml al completo.

Anche in questo caso, come per i linter, la nostra pipeline di deployment si interromperà se i test non avranno esito positivo.

E’ possibile vedere un filmato relativo all’esecuzione dei vari test direttamente dalla console di AWS Amplify, effettuando il download degli artefatti. Di seguito un esempio.

Conclusioni

L’utilizzo combinato di linter e tool di test end-to-end ci consente di rilasciare codice di qualità e verificato dal punto di vista dell’utilizzatore finale. Con AWS Amplify abbiamo la possibilità di integrare questi tool semplificando le operazioni di DevOps e mantenendo un controllo puntuale delle fasi di CI/CD.

In conclusione confermo le impressioni del primo post: 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! Ovviamente affiancato da Cypress e da uno o più linter.

Ci siamo divertiti? Alla prossima!

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!

Add to Slack – Monetizzare le proprie API

7 min

La monetizzazione delle API è un trend in forte crescita. E’ ormai un business consolidato delle aziende digitali far utilizzare, a pagamento, le API che accedono ai dati aziendali o ad alcuni servizi già erogati secondo canali differenti.

Un possibile canale di diffusione delle proprie API e dei propri servizi è l’integrazione con piattaforme esistenti e molto note, come ad esempio Slack. Se il proprio servizio o le proprie API possono essere utili e di valore nell’ambito di un meeting, di una chat di progetto o durante le attività di customer care, allora realizzare un’integrazione per Slack potrebbe rivelarsi redditizia.

Ma cosa c’è dietro l’integrazione di API esistenti con Slack? Quanto è complesso pubblicare il classico pulsante “Add to Slack” sul proprio sito web? Vediamolo in questa guida passo passo.

Creare un App

In Slack è possibile creare le proprie app o utilizzarne di esistenti. Le più complete e professionali sono elencate nel App Directory. Si trova ogni cosa, dall’integrazione con Google Calendar, a Zoom e GitHub. Normalmente le app consentono di aggiungere funzionalità ai propri canali, fornendo l’integrazione con servizi già esistenti. Un’app può dialogare con i partecipanti ad un canale inviando messaggi di suggerimento e notifiche oppure rispondendo a comandi ben specifici. L’app può “esprimersi” sotto forma di bot oppure agire come se fossimo direttamente noi a inviare messaggi. Insomma: le potenzialità sono illimitate!

Creare la propria app è semplice: basta raggiungere il portale api e seguire le indicazioni di “Create New App”.

Prima di addentrarci negli step successivi di creazione di un app, è meglio chiarire come questa può essere distribuita su Slack.

Installazione nel proprio Workspace

Di default, quando creiamo un App su Slack, la stessa può essere installata sul proprio workspace. Chiunque sia invitato a partecipare al medesimo, può quindi interagire e usufruire della stessa. Se intendiamo realizzare un applicazione dedicata esclusivamente al nostro team, non è richiesto altro effort.

Distribuzione

Se l’app che intendiamo realizzare deve poter essere installata e condivisa su altri workspace, per esempio a livello aziendale, è necessario attivare la “distribuzione” dell’applicazione. Questo consente la creazione del classico pulsante “Add to Slack” e comporta l’implementazione di un flusso OAuth 2.0 per gestire il processo di autorizzazione.

App Directory

L’App Directory è una sorta di marketplace di Slack dove vengono elencate per categoria tutte le applicazioni che hanno superato un processo di verifica. Lo stesso è condotto direttamente da un Team di Slack che si assicura che l’applicazione abbia i necessari requisiti tecnici e qualitativi. E’ ovvio che se intendiamo monetizzare le nostre API tramite un’integrazione, la pubblicazione su App Directory ci consentirà di avere un pubblico decisamente più ampio e ne gioverà anche la reputazione dell’applicazione, in quanto già verificata da Slack stessa.

Pubblicare la propria app nella Directory ufficiale è però ovviamente un processo più complesso, che prevede un lungo elenco di requisiti da soddisfare.

Interazione

Abbiamo creato la nostra app, che però non svolge ancora alcuna funzione. A seconda del servizio che vogliamo integrare in Slack, per prima cosa è meglio decidere come la nostra applicazione andrà ad interagire sui canali Slack. Esistono diverse possibilità: l’app potrà essere invocata con uno “slash command” oppure nel caso di specifici eventi.

Gli “slash command” sono i tipici comandi di Slack preceduti appunto da una slash come, ad esempio /INVITE. Nel caso invece degli eventi, è possibile richiamare l’app quando la stessa viene per esempio citata (@nomeapp) durante una conversazione oppure quando le vengono inviati dei messaggi diretti.

Personalmente ho scelto questo secondo approccio, basato su eventi, ma non esiste una sola soluzione ottimale: dipende dal servizio che stiamo integrando e dalle UX che vogliamo fornire. Nessuno ci vieta, inoltre, di prevedere entrambe le modalità.

Integrazione

Come integrare il nostro servizio e le relative API esistenti? Sarà necessario realizzare un layer dedicato, in grado di rispondere alle richieste da parte di Slack e interagire con gli utenti, da un lato, e in grado di richiamare le nostre API dall’altro lato.

Di cosa si deve occupare questo layer di integrazione? Vediamolo:

  • Ricevere gli eventi da parte di Slack e rispondere di conseguenza, erogando il servizio fornito dalle nostre API
  • Gestire il processo di autorizzazione OAuth 2.0
  • Gestire subscription e billing o integrarsi con una piattaforma di E-Commerce

Sottoscrizione agli eventi

Abbiamo deciso di ricevere delle notifiche quando si verifica un evento di interesse per la nostra app. Dobbiamo innanzitutto specificare l’endpoint a cui verranno notificati questi eventi.

Dovremo rispondere alla richiesta utilizzando il parametro challenge come indicato per validare il nostro endpoint. Dopo averlo Impostato potremo quindi specificare quali notifiche vogliamo ricevere.

Gli eventi sono ben descritti: message.im si verifica, per esempio, quando viene inviato un messaggio direct alla nostra app, tipicamente nel relativo channel privato.

Il concetto di Scope è fondamentale da comprendere: per poter ricevere alcune notifiche è necessario che la nostra applicazione abbia ottenuto l’autorizzazione ad un particolare scope; nell’esempio precedente, im:history. Vedremo che tale autorizzazione verrà richiesta durante il processo di autorizzazione dell’app.

Tutto qui? Non direi. E’ opportuno fare in modo che il nostro layer di integrazione verifichi inoltre le richieste per assicurarsi che provengano proprio dai server di Slack: non vogliamo certo pubblicare un servizio a pagamento e poi essere frodati giusto?

L’algoritmo di verifica delle richieste è indicato in questo articolo tratto dalla documentazione ufficiale. Richiede l’utilizzo del Signing Secret, un parametro che Slack attribuisce ad ogni applicazione e che è possibile trovare nella sezione App Credentials della propria applicazione.

Personalmente mi sono occupato di realizzare un layer di integrazione totalmente serverless, utilizzando API Gateway e AWS Lambda. Ecco riportata un estratto della funzione di risposta agli eventi, che prevede inoltre l’eventuale decodifica Base64.

Le notifiche sono in formato JSON e ben documentate. Un esempio di payload relativo a message:im è qui.

Cos’è l’evento app_home_opened? Si verifica quando l’utente che ha installato la nostra app ne apre la sezione corrispondente. E’ utilizzabile per inviare un messaggio di benvenuto e spiegare, per esempio, come utilizzare l’app. E’ bene ricordarsi di questo evento perché la sua corretta gestione è uno dei requirement alla pubblicazione nel App Directory.

Gli eventi disponibili sono molti e permettono di personalizzare la UX della propria integrazione.

Autorizzazione

Il processo di autorizzazione OAuth 2.0 è documentato molto chiaramente.

Il processo di autorizzazione è composto dai seguenti step:

  1. L’utente avvia il processo di installazione cliccando su “Add to Slack”. Il codice HTML del pulsante può essere facilmente ottenuto dalla pagina della propria app. L’URL richiamata dal pulsante sarà gestita dal nostro integration layer che si occuperà di rispondere con un redirect (302) verso l’URL di autorizzazione di Slack (https://slack.com/oauth/v2/authorize) specificando almeno i parametri client_id e scope che sono obbligatori. Il primo parametro identifica la nostra app e il suo valore è disponibile nella sezione App Credentials. Il secondo specifica invece le varie autorizzazioni che intendiamo richiedere per la nostra app (scope).
  2. L’utente autorizza l’installazione dell’applicazione
  3. Un primo code di autorizzazione viene fornito al nostro integration layer alla redirect URL specificata nella sezione OAuth & Permissions della nostra applicazione
  4. L’integration layer si occupa di richiedere il token di autorizzazione richiamando l’URL dedicato (https://slack.com/api/oauth.v2.access) e specificando i parametri client_id e client_secret, oltre al code appena ottenuto
  5. In caso di risposta positiva, il token viene memorizzato dall’integration layer in modo da poter essere utilizzato per interagire con il workspace relativo

Ecco un esempio di una Lambda function in grado di gestire il processo, memorizzando i token ottenuti in una tabella DynamoDB.

UX & Pagamenti

Abbiamo terminato? No, siamo solo all’inizio! Abbiamo realizzato due funzioni, la prima delle quali è in grado di ricevere le notifiche relative agli eventi che ci interessa gestire con la nostra app. La seconda si occupa invece di gestire correttamente il processo di autorizzazione.

Dobbiamo ora rimboccarci le maniche e implementare la vera integrazione tra Slack e il nostro servizio o API. Il package Python slackclient ci permette di interagire con le API Web Client in modo da poter, per esempio, postare un messaggio in risposta ad uno specifico direct. Come già detto all’inizio di questo post, le possibilità sono infinite e dipendono molto dal servizio che si vuole integrare.

I messaggi Slack possono essere arricchiti di elementi per renderli più funzionali e accattivanti. La struttura di ciascun messaggio è composto da blocchi. L’ottimo editor consente di sperimentare il layout dei messaggi prima di implementarli nella propria app.

Dovremo inoltre gestire l’integrazione con la piattaforma di E-Commerce al fine di gestire le sottoscrizioni al nostro servizio. Personalmente ho utilizzato Paddle.

Pubblicazione su App Directory

La nostra App è pronta? Abbiamo testato le sue funzionalità, il processo di installazione e l’integrazione con la piattaforma di E-Commerce? Benissimo! Siamo pronti alla pubblicazione in App Directory? Non ancora.

Il processo di pubblicazione prevede la verifica, da parte del team di Slack, di una serie di requisiti che la nostra App deve soddisfare. Ce ne sono molti, divisi in sezioni e che consiglio di leggere attentamente.

Il processo di verifica è molto scrupoloso e ci possono volere alcuni giorni per ottenere un feedback. Se anche solo uno dei requisiti non è soddisfatto, viene segnalato e si deve procedere ad una nuova submission, ovviamente dopo aver corretto la propria app.

Prepariamoci quindi a dover realizzare, ad esempio, landing page per il supporto al cliente e per la propria privacy policy. Un attenzione particolare va riposta agli scope richiesti, che andranno motivati dettagliatamente.

Se abbiamo realizzato tutto nella maniera corretta, la nostra app sarà finalmente presente nella Directory di Slack e inizieremo a monetizzare il nostro servizio.. o almeno speriamo!

Ci siamo divertiti? Alla prossima!

Chromium & Selenium

Chromium e Selenium in AWS Lambda

4 min

Vediamo insieme come sia possibile utilizzare Chromium e Selenium in una funzione AWS Lambda; prima però, qualche informazione per chi non conosce questi due progetti.

Chromium è il browser open source da cui deriva Google Chrome. I browser condividono la maggior parte del codice e funzionalità. Differiscono però per i termini di licenza e Chromium non supporta Flash, non ha un sistema automatico di aggiornamento e non raccoglie statistiche di utilizzo e crash. Vedremo che queste differenze non incidono minimamente sulle potenzialità del progetto e che, grazie a Chromium, possiamo svolgere molti task interessanti nell’ambito delle nostre Lambda function.

Selenium è un notissimo framework dedicato al test di applicazioni web. A noi interessa in particolare Selenium WebDriver, un tool che consente di comandare a proprio piacimento tutti i principali browser oggi disponibili, tra cui anche Chrome/Chromium.

Perché usare Chrome in una Lambda function?

Qual’è lo scopo di utilizzare un browser in un ambiente (AWS Lambda) che non prevede una GUI? In realtà ne esistono diversi. Comandare un browser consente di automatizzare una serie di task. E’ possibile, per esempio, testare la propria web application in modo automatico realizzando una pipeline CI. Oppure, non meno importante, effettuare web scraping.

Il web scraping è una tecnica che consente di estrarre dati da un sito web e le sue possibili applicazioni sono infinite: monitorare i prezzi dei prodotti, verificare la disponibilità di un servizio, costruire base di dati acquisendo records da più fonti.

In questo post vedremo come utilizzare Chromium e Selenium in una Lambda function Python per effettuare il rendering di una qualsiasi URL.

Lambda function di esempio

La Lambda Function che andiamo a realizzare ha uno scopo specifico: data una URL, verrà utilizzato Chromium per effettuare il rendering della relativa pagina web e verrà catturata una screenshot del contenuto in formato PNG. Andremo a salvare l’immagine in un bucket S3. Eseguendo la funzione periodicamente, saremo in grado di “storicizzare” i cambiamenti di un qualsiasi sito, ad esempio la homepage di un quotidiano di informazioni online.

Ovviamente la nostra Lambda ha moltissime possibilità di miglioramento: lo scopo è mostrare le potenzialità di questo approccio.

Iniziamo con i pacchetti Python richiesti (requirements.txt).

selenium==2.53.0
chromedriver-binary==2.37.0

Non sono packages normalmente disponibili nell’ambiente AWS Lambda Python: andremo quindi a creare uno zipfile per distribuire la nostra function comprensiva di tutte le dipendenze.

Otteniamo Chromium, nella sua versione Headless, cioè in grado di essere eseguito in ambienti server. Ci servirà inoltre il relativo driver Selenium. Qui di seguito i link che ho utilizzato.

Come usare Selenium Webdriver

Un semplice esempio di come usare Selenium in Python: il metodo get consente ti indirizzare il browser verso l’URL indicata, mentre il metodo save_screenshot ci permette di salvare su file un’immagine PNG del contenuto. L’immagine avrà le dimensioni della finestra di Chromium, che è impostata con l’argomento window-size a 1280×1024.

Ho deciso di realizzare una classe wrapper per il nostro scopo: effettuare il rendering di una pagina, in tutta la sua altezza. Andremo quindi a calcolare la dimensione della finestra necessaria a contenere tutti gli elementi utilizzando un trucco, uno script che eseguiremo a caricamento avvenuto. Anche in questo caso Webdriver espone un metodo execute_script che fa al caso nostro.

La soluzione non è molto elegante ma funzionale: l’URL richiesta deve essere caricata due volte. La prima volta per determinare la dimensione della finestra, la seconda per ottenere la screenshot. Lo script JS utilizzato è stato preso da questo interessante post.

Il wrapper è inoltre già “Lambda ready”, cioè gestisce correttamente i percorsi temporanei e la posizione dell’eseguibile headless-chromium specificando le chrome_options necessarie.

Vediamo come è la nostra funzione Lambda:

L’event handler della funzione si occupa semplicemente di istanziare il nostro oggetto WedDriverScreenshot e lo utilizza per la generazione di due screenshot: il primo con dimensione fissa della finestra (1280×1024 pixel). Per il secondo viene invece omesso il parametro Height che sarà determinato automaticamente dal nostro wrapper.

Ecco le due immagini risultanti, messe a confronto tra loro, relative al sito www.repubblica.it

Deployment della funzione

Ho raccolto in questo repository Github tutti i file necessari al deployment del progetto su AWS Cloud. Oltre ai file già analizzati in precedenza, è presente un template CloudFormation per il deployment dello stack. La sezione più importante riguarda ovviamente la definizione della ScreenshotFunction: alcune variabili di ambiente come PATH e PYTHONPATH sono fondamentali per la corretta esecuzione della function e di Chromium. E’ necessario, inoltre, tenere in considerazione i requisiti di memoria ed il timeout: il caricamento di una pagina può impiegare infatti diversi secondi. Il percorso lib include alcune library necessarie all’esecuzione di Chromium e non presenti di default nell’ambiente Lambda.

Come di consueto mi sono affidato ad un Makefile per l’esecuzione delle operazioni principali.

## download chromedriver, headless-chrome to `./bin/`
make fetch-dependencies

## prepares build.zip archive for AWS Lambda deploy 
make lambda-build		

## create CloudFormation stack with lambda function and role.
make BUCKET=your_bucket_name create-stack 

Il template CloudFormation prevede un bucket S3 da utilizzare come source per il deployment della Lambda function. Successivamente lo stesso bucket verrà utilizzato per memorizzare gli screenshot PNG.

Conclusioni

Abbiamo utilizzato Selenium e Chromium per effettuare il rendering di una pagina web. Si tratta di una possibile applicazione di questi due progetti in ambito serverless. Come anticipato, un’altra applicazione molto interessante è il web scraping. In questo caso, l’attributo page_source di Webdriver consente di accedere ai sorgenti della pagina e un package come BeautifulSoup può essere molto utile per estrarre i dati che intendiamo raccogliere. Il pacchetto Selenium per Python mette a disposizione diversi metodi per automatizzare altre operazioni: consiglio di guardarsi l’ottima documentazione.

Vedremo un esempio di web scraping in uno dei prossimi post!

Ci siamo divertiti? Alla prossima!

pipeline

AWS Lambda CI pipeline con DGoss e TravisCI

4 min

Ho recentemente parlato di come sviluppare funzioni AWS Lambda offline, utilizzando le immagini docker di LambdaCI. Partendo dall’ambiente di sviluppo Python utilizzato per lo sviluppo di una funzione Lambda di esempio, vediamo come realizzare una pipeline di Continuous Integration che si occupi di verificarne il corretto funzionamento ad ogni nuovo commit. Utilizzeremo Goss come strumento di test e TravisCI per l’implementazione della pipeline di CI.

AWS Lambda in un container Docker

Vediamo rapidamente come eseguire una funzione AWS Lambda in un container Docker.

Utilizziamo la seguente funzione Python di esempio, che si occupa di processare dei messaggi accodati su SQS e che richiede un package (PILLOW) normalmente non presente nell’ambiente Lambda.

Per creare l’immagine docker necessaria all’esecuzione della nostra Lambda function, il relativo Dockerfile è il seguente, che partendo dall’immagine lambci/lambda:python3.6, consente l’installazione di eventuali pacchetti Python aggiuntivi previsti in requirements.txt

Consiglio di vedere questo post per maggiori dettagli; ora che abbiamo la nostra immagine Docker e la nostra Lambda function viene eseguita correttamente in un container, possiamo iniziare a realizzare la test suite.

Goss e DGoss

Ho scelto di utilizzare Goss come strumento di test principalmente per due motivi: grazie al wrapper DGoss è possibile interagire direttamente con un container docker sia per la fase di esecuzione dei test che durante lo sviluppo (o editing) degli stessi. Lo scopo principale di Goss è validare la configurazione di un server, che nel nostro caso è proprio il container che ospita la Lambda function. La suite di test può essere generata direttamente dallo stato corrente del server (o container) rendendo di fatto l’operazione molto veloce e pratica.

La definizione della test suite è basata su file YAML.

Procediamo molto rapidamente con l’installazione di Goss e DGoss e avviamo il container che ci permetterà di realizzare la test suite, utilizzando la sintassi Makefile.

Il comando avvia il container docker in modalità “STAY OPEN”, cioè avviando un API server in ascolto sulla porta 9001 e alla quale risponde la nostra funzione Lambda. Il parametro edit indica a DGoss che andremo ad utilizzare il container per la realizzazione della test suite. La directory test contatterà i file necessari all’esecuzione dei test stessi. Ad esempio, volendo verificare che la funzione processi correttamente i messaggi SQS, alcuni eventi di esempio (in formato JSON) saranno salvati in questa folder per essere poi utilizzati durante i test.

A questo punto possiamo utilizzare tutti i comandi di Goss per realizzare la nostra test suite: se vogliamo, per esempio, verificare la presenza del handler di funzione, si utilizza il comando:

goss add file /var/task/src/lambda_function.py

L’opzione ADD aggiunge un nuovo test al file di configurazione goss.yaml, nel nostro caso per verificare la presenza del file specificato. Goss consente di verificare moltissimi parametri di configurazione dell’ambiente in cui viene eseguito, come la presenza di file, pacchetti, processi, risoluzione dns, ecc. Il relativo manuale è molto esaustivo.

Realizziamo ora uno script che ci consenta di verificare la risposta della nostra funzione Lambda.

#!/bin/sh
# /var/task/test/test-script.sh

curl --data-binary "@/var/task/test/test-event.json" http://localhost:9001/2015-03-31/functions/myfunction/invocations

Utilizziamo curl per interrogare l’endpoint della nostra Lambda function inviando un evento SQS di test (il file test-event.json). Aggiungiamo quindi un ulteriore test Goss, questa volta di tipo command. Il file goss.yaml diventerà come il seguente.

La sezione command prevede l’esecuzione dello script che interroga la Lambda function. Ci aspettiamo su stdout un JSON dal contenuto specifico, che conferma la corretta esecuzione dalla funzione. Ovviamene questo test è solo un esempio esplicativo di ciò che può essere fatto con Goss.

Quanto abbiamo terminato le definizione della test suite, uscendo dal container (exit), DGoss si occuperà di copiare sul nostro host locale il file goss.yaml appena editato, in modo che persista dopo l’eliminazione del container stesso. Avviando nuovamente la procedura di edit, il file sarà copiato nel nuovo container e nuovamente sincronizzato al termine.

E’ arrivato il momento di eseguire il test! Al posto di edit, andremo semplicemente a specificare il comando run di DGoss.

DGoss si occupare di creare un nuovo container ed eseguire i test specificati. Di seguito un esempio.

Ottimo! La nostra test suite è pronta, procediamo ad integrarla nella nostra pipeline CI.

AWS Lambda CI in TravisCI

E’ arrivato il momento di configurare la pipeline CI. Ho scelto TravisCI per la sua semplicità e perché consente di effettuare la build di immagini docker gratuitamente. L’ho utilizzato anche in questo post con Ansible ed è tra gli strumenti di CI che preferisco. Per automatizzare il processi di test ad ogni commit andremo a configurare TravisCI per monitorare il repository GitHub/Bitbucket che ospita i sorgenti.

Un semplice file .travis.yaml realizza la pipeline.

Analizziamolo: la direttiva services indica a TravisCI di utilizzare il servizio docker. Prime di avviare i test, viene installato Goss/DGoss e viene effettuata la build della nostra immagine AWS Lambda, come specificato in before_install. Per ultimo, con script viene indicato il comando per l’effettiva esecuzione della test suite che abbiamo preparato in precedenza.

Ecco il risultato:

Fantastico! La nostra pipeline è realizzata!

Conclusioni

Ci siamo occupati di sviluppare e automatizzare il test di una semplice funzione AWS Lambda senza che questa sia mai realmente entrata nel cloud AWS: ho utilizzato questa modalità per alcuni progetti su cui ho lavorato e credo che permetta di velocizzare notevolmente lo sviluppo ed il mantenimento dei sorgenti, consentendo di effettuare il deployment su AWS di codice già testato e di qualità.

Questo repository GitHub raccoglie quanto trattato in questo articolo e può essere utilizzato come base di partenza per realizzare la propria pipeline CI con Goss.

Ci siamo divertiti? Alla prossima!

AWS Lambda in Docker

Sviluppo offline di funzioni AWS Lambda

3 min

I progetti a cui lavoro sono sempre più indirizzati verso il paradigma serverless e sempre più spesso implementati su piattaforma AWS Lambda. Poter sviluppare una funzione AWS Lambda offline, cioè rimanendo comodamente nel proprio IDE preferito senza dover effettuare l’upload del codice per poterlo testare, consente di velocizzare notevolmente le attività ed essere più efficienti.

AWS Lambda environment in docker

Esatto! La soluzione che ci consente di sviluppare codice AWS Lambda in modalità offline è utilizzare un’immagine docker che replica in maniera pressoché identica l’ambiente live di AWS. Le immagini docker disponibili presso DockerHub costituiscono una sandbox all’interno della quale eseguire la propria funzione, sicuri di trovare le medesime librerie, struttura file e relativi permessi, variabili d’ambiente e contesto di produzione. Fantastico!

Raramente una Lambda function è “indipendente” da altre risorse: spesso ha la necessità di accedere a degli oggetti memorizzati in un bucket S3, accodare messaggi su SQS o accedere ad una tabella DynamoDB. L’aspetto interessante di questa soluzione è la possibilità di sviluppare e testare il proprio codice offline, interagendo comunque con servizi e risorse reali di AWS, semplicemente andando a specificare una coppia di chiavi di accesso AWS nelle variabili di ambiente AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY.

Il progetto LambdaCI è aggiornato frequentemente e ben documentato: prevede diversi ambienti di runtime tra quali Python, che andremo ad utilizzare nei prossimi paragrafi. L’ambiente che ho utilizzato per lo sviluppo è disponibile in questo repository.

Funzione di esempio

Supponiamo di lavorare ad una semplice funzione Python che si occupi di processare dei messaggi SQS e che utilizzi un package normalmente non installato nell’ambiente AWS Lambda Python. ll codice di esempio è il seguente.

Per prima cosa viene istanziato l’oggetto Logger che andremo ad utilizzare per tracciare l’evento SQS. Nel body del messaggio sono previsti degli addendi che andremo a sommare ed il risultato verrà riportato nei log. Andremo inoltre a tracciare la versione del package PILLOW, normalmente non previsto di default nell’ambiente AWS Lambda, per verificare che l’installazione dei pacchetti aggiuntivi avvenga correttamente. Per finire restituiremo un messaggio testuale (All the best for you) al termine dell’esecuzione della funzione.

Vediamo ora come eseguire la Lambda function in Docker.

Dockerfile e Docker-Compose

Per prima cosa dobbiamo preoccuparci di come installare i pacchetti Python aggiuntivi, nel nostro esempio PILLOW. Andremo quindi a creare una nuova immagine Docker grazie ad un dockerfile che parta dall’immagine lambci/lambda:python3.6 e che si occupi di installare tutti i pacchetti aggiuntivi specificati nel file requirements.txt

Per finire, con un file docker-compose.yml andremo a definire un servizio lambda da utilizzare per il debugging offline. Lo scopo è mappare la directory host src per i sorgenti e impostare PYTHONPATH per l’utilizzo dei pacchetti aggiuntivi in /var/task/lib

Come primo test basterà avviare docker-compose per eseguire la nostra funzione Lambda.

docker-compose run lambda src.lambda_function.lambda_handler

Eventi

La nostra funzione si aspetta un evento SQS da processare. Come inviarlo? Per prima cosa dobbiamo procurarci un JSON di test e salvarlo in un file (ad esempio event.json). Andremo poi a specificarlo nella chiamata a docker-compose.

docker-compose run lambda src.lambda_function.lambda_handler "$(cat event.json)"

Vediamo il risultato dell’esecuzione.

Perfetto! La nostra funzione viene eseguita correttamente ed il risultato corrisponde a quanto atteso. L’avvio del container Docker corrisponde ad un “cold start” di AWS Lambda. Vediamo come sia possibile mantenere attivo il container per richiamare più volte la funzione.

Mantenere in esecuzione

In alternativa è possibile avviare e mantenere in esecuzione il container della nostra funzione Lambda in modo da poter effettuare velocemente diverse chiamate consecutive senza attendere i tempi di “cold start”. In questa modalità viene avviato un server API che risponde di default alla porta 9001.

docker-compose run -e DOCKER_LAMBDA_STAY_OPEN=1 -p 9001:9001 lambda src.lambda_function.lambda_handler

Andremo a richiamare la nostra funzione usando, ad esempio, curl.

curl --data-binary "@event.json" http://localhost:9001/2015-03-31/functions/myfunction/invocations

All’endpoint indicato risponde l’handler di default della nostra Lambda function. Il parametro data-binary consente di inviare il contenuto del file JSON relativo all’evento SQS di esempio.

Conclusioni

Ho raccolto in questo repository GitHub i file necessari a ricreare l’ambiente Docker che utilizzo per lo sviluppo ed il debugging offline di funzioni AWS Lambda in Python. Per comodità ho raccolto in un Makefile le operazioni più frequenti.

Il comando make lambda-build realizza il pacchetto di deployment della funzione, comprensivo dei pacchetti aggiuntivi.

Di seguito un esempio di deployment della nostra Lambda function con CloudFormation.

Altri comandi disponibili nel Makefile sono:

## create Docker image with requirements
make docker-build

## run "src.lambda_function.lambda_handler" with docker-compose
## mapping "./tmp" and "./src" folders. 
## "event.json" file is loaded and provided to function  
make lambda-run

## run API server on port 9001 with "src.lambda_function.lambda_handler" 	 
make lambda-stay-open

Ci siamo divertiti? Ci piacerebbe utilizzare questo ambiente per realizzate una pipeline di CI che si occupi di testare la nostra funzione? Nel prossimo post!

disposable email

Disposable: la tua mail “usa e getta” su AWS

11 min

Il 90% dei servizi online prevede un processo di registrazione durante il quale fornire il proprio indirizzo di posta elettronica. Questo andrà poi confermato ricevendo una email provvista di link sul quale cliccare. A questo punto avremo accesso al servizio al quale eravamo interessati ma contemporaneamente daremo il via alla ricezione di newsletter, sondaggi, promozioni, ecc.

Un indirizzo “usa e getta” è utile proprio in queste situazioni: ci consente di portare a termine il processo di registrazione e ci evita di ricevere in futuro email indesiderate, mantenendo “pulita” la nostra mailbox principale, che utilizziamo per lavoro o per scrivere agli amici.

NOVITA’!!

Sei alla ricerca di un servizio email “usa e getta” pronto all’uso, dotato di API fantastiche e integrato con Slack?

my24h.email

Esistono diversi servizi online che offrono la possibilità di creare indirizzi email “usa e getta” ed il più famoso è probabilmente Mailinator. Sono però servizi che poco si prestano ad un utilizzo professionale in ambito aziendale e sono spesso caratterizzati da alcune limitazioni. Il dominio di posta utilizzato, per esempio, è spesso riconosciuto per “usa e getta” e non considerato valido durante il processo di registrazione.

Andremo a realizzare Disposable-mail, un servizio di mail monouso personalizzabile con il proprio dominio internet, utilizzando i building block AWS. Il servizio è totalmente serverless: non sono infatti necessarie istanze EC2 ma solo semplici lambda functions.

Disposable-mail funziona così:

  1. Val sul sito
  2. Scegli un indirizzo email e lo digiti
  3. La mailbox che hai scelto viene immediatamente attivata e puoi iniziare a ricevere email

Se vuoi provarlo lo trovi qui: http://disposable.aws.gotocloud.it

Non serve registrarsi o scegliere una password. La mailbox ha validità di un’ora e allo scadere dei 60 minuti tutti i messaggi ricevuti saranno eliminati e la mailbox non sarà più attiva.

Non è possibile inviare email, ma solo riceverle. Gli allegati non vengono visualizzati (ma forse lo saranno nella prossima versione).

Per evitare abusi utilizziamo Google reCAPTCHA nella form di login, in versione hidden.

Requisiti

Per realizzare il tuo servizio di email “usa e getta” sul tuo dominio personale ti serve:

  • un account AWS
  • un dominio internet (meglio un terzo livello) su Route53 o comunque per il quale ti sia possibile configurare la relativa zona DNS
  • una coppia di chiavi (site/private) di Google reCAPTCHA validi per il tuo dominio (reCAPTCHA console)

Tutto qui.

Architettura della soluzione

Come anticipato, Disposable-mail è totalmente serverless e, data la natura di questi building blocks AWS, non ci dovremo preoccupare della loro gestione, la scalabilità è garantita e il costo è “per utilizzo”.

Dal layout si comprende come esistano essenzialmente due workflow: il primo, totalmente automatizzato, di ricezione, verifica, memorizzazione e cancellazione delle email. Il secondo, dato dall’utente che accede all’applicazione React e consulta la propria mailbox. L’elemento comune dei due è rappresentato dal bucket S3 dove vengono memorizzate le email ricevute e le tabelle DynamoDB dove vengono memorizzati i relativi metadati.

Per la ricezione delle email ci affidiamo al servizio Amazon SES (Simple Email Service): ci occuperemo della sua configurazione nel paragrafo successivo. Alcune funzioni lambda si occupano di gestire il flusso delle email. Route53 è utilizzato per ospitare la zona relativa al nostro dominio internet.

La web application è ospitata direttamente in un bucket S3 pubblico e l’endpoint API è gestito dal servizio Amazon API Gateway. Alcune funzioni lambda si occupano di processare le richieste da parte dei client.

Ci affidiamo a CloudWatch per eseguire periodicamente una funzione lambda il cui scopo è disattivare ed eliminare le mailbox scadute.

Primi passi

Il primo passo per la realizzazione della soluzione è la corretta configurazione del servizio Amazon SES. Il template CloudFormation che utilizzeremo per la realizzazione di tutta l’infrastruttura di backend presuppone che il dominio email da noi scelto sia già correttamente configurato e verificato.

Iniziamo la configurazione: dalla console AWS è necessario accedere al servizio SES. Non tutte le Region AWS dispongono di questo servizio: è possibile quindi che si debba utilizzare una Region differente da quella che si utilizza normalmente. L’intera soluzione sarà ospitata, per semplicità, sulla medesima Region.

In Domains viene visualizzato l’elenco di tutti i domini internet per i quali il servizio SES è stato configurato. Dovremo aggiungere il nostro dominio ed effettuare il processo di verifica.

Cliccando su Verify a New Domain andiamo ad aggiungere il nostro dominio specificandone il nome. Come detto in precedenza, meglio utilizzare un dominio di terzo livello perché tutte le mailbox di questo saranno gestite da Disposable-mail.

Una volta aggiunto il dominio dobbiamo occuparci della configurazione delle relativa zona DNS. Si dovranno inserire due record:

  • un record TXT per la verifica da parte di AWS
  • un record MX per abilitare la ricezione delle email

Se la zona DNS è gestita da Route53, tutto quello che dobbiamo fare è cliccare su Use Route 53: la configurazione dei due record sarà automatica. In caso contrario è necessario fare riferimento al proprio provider per modificare la zona come indicato.

Una volta che il dominio sarà stato verificato, saremo in grado di ricevere email tramite Amazon SES.

Backend – Ricezione emails

Come vedremo, il template CloudFormation crea una nuova regola di ricezione email (Rule Set) di tipo “catch-all”, cioè valida per tutti gli indirizzi email del tipo <qualsiasi.cosa>@<dominio.scelto>

Il “Recipient” specificato corrisponde infatti al nostro dominio.

La prima Action si occupa di invocare una funziona lambda e di verificarne il responso. La funzione IncomingMailCheckFunction si occupa di verificare che l’indirizzo di destinazione della mail sia valido e attivo. Indica poi al servizio SES se proseguire (CONTINUE) o interrompere l’esecuzione di ulteriori azioni (STOP_RULE_SET).

La seconda Action, eseguita esclusivamente nel caso in cui la precedente lo abbia consentito, comporta il salvataggio della email nel bucket S3, il cui nome corrisponde a “incoming.disposable.<nome_dominio>” e la notifica dell’avvenuto salvataggio ad un topic SNS “IncomingMailTopic“.

Come avviene la verifica dell’indirizzo email? Una tabella DynamoDB è utilizzata per memorizzare l’elenco degli indirizzi email attivi e la relativa scadenza (TTL). La chiave della tabella è proprio l’indirizzo email (address).

## part of IncomingMailCheckFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ['addresses_table_name'])

def address_exists(address):
  exists = False
  response = table.get_item(Key={'address': address})
  if 'Item' in response:
      item = response['Item']
      if item['TTL'] > int(time.time()):
         exists = True

def lambda_handler(event, context):
  for record in event['Records']:
    to_address = record['ses']['mail']['destination'][0]
    if address_exists(to_address):
       return {'disposition': 'CONTINUE'}
    else:
       return {'disposition': 'STOP_RULE_SET'}
    break;

Per completare l’analisi del flusso di ricezione delle email, vediamo la funzione StoreEmailFunction. Questa funzione si occupa di memorizzate i dati della email ricevuta nel database DynamoDB, nella tabella “emails”. La tabella ha come Primary Key la stringa “destination” che corrisponde all’indirizzo email ed una Sort Key che corrisponde invece al “messageId” della email stessa. La funzione lambda riceve le notifiche dal topic SNS.

## part of StoreEmailFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ['emails_table_name'])

## Store email details in database
def store_email(email, receipt):
   response = table.put_item(
                  Item= {'destination': email['destination'][0],
                         'messageId': email['messageId'],
                         'timestamp': email['timestamp'],
                         'source': email['source'],
                         'commonHeaders': email['commonHeaders'],
                         'bucketName': receipt['action']['bucketName'],
                         'bucketObjectKey': receipt['action']['objectKey'],
                         'isNew': "true"
                  }
   )

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    store_email(message['mail'], message['receipt'])

Nella notifica SNS è presente il nome del bucket S3 e l’objectKey utilizzato per memorizzarla. Salvando le informazioni nella tabella DynamoDB, sarà possibile successivamente recuperare il contenuto della email per la visualizzazione.

La funzione CleanUpFunction viene invocata periodicamente da CloudWatch al fine di svuotare e disattivare le mailbox scadute. Si occupa inoltre di eliminare i record di sessione anch’essi scaduti.

Viene effettuato uno scan sulle tabelle addresses e sessions verificando il valore dell’attributo TTL.

## part of CleanUpFunction, see Github repository for 
## the complete functions code.

def cleanup():
    ## get expired addresses
    try:
        response = addresses_table.scan(
            FilterExpression = Attr('TTL').lt(int(time.time())),
            ProjectionExpression = "address"
        )
    except ClientError as e:
        logger.error('## DynamoDB Client Exception')
        logger.error(e.response['Error']['Message'])
    else:
        for I in response['Items']:
            find_emails(I['address'])
            delete_address_item(I['address'])

Backend – API

API Gateway viene utilizzato come endpoint REST (regional) per la web application.

Viene utilizzata l’integrazione lambda ed esistono tre funzioni lambda dedicate alla creazione di una nuova mailbox (CreateEmailFunction), alle consultazione dell’elenco delle email presenti (GetEmailsListFunction) e al download del contenuto di una email specifica (GetEmailFileFunction).

La funzione CreateEmailFunction risponde al metodo GET della risorsa /create e prevede due parametri:

  • address – l’indirizzo email da creare
  • captcha – la chiave necessaria per la verifica di reCAPTCHA

Entrambi i parametri vengono validati e nel caso in cui l’indirizzo email non esista, lo stesso viene creato inserendo il relativo record nella tabella addresses. La funzione restituisce nel body un parametro sessionID che sarà utilizzato dal client per le ulteriori chiamate alle API. Gli ID di sessione validi e la relativa scadenza sono memorizzati nella tabella sessions.

## part of CreateEmailFunction, see Github repository for  
## the complete functions code.

import http.client, urllib.parse
import os
import re

valid_domains = os.environ['valid_domains'].split(',')
recaptcha_key = os.environ['recaptcha_key']

## A very simple function to validate email address
def validate_email(address):
    valid = False
    # Do some basic regex validation 
    match = re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', address)
    if (match != None):
        domain = address.split("@")[-1]
        if domain in valid_domains:
            valid = True
    return valid

## reCAPTCHA validation function
def validate_recaptcha(token):
    conn = http.client.HTTPSConnection('www.google.com')
    headers = {"Content-type": "application/x-www-form-urlencoded"}
    params = urllib.parse.urlencode({'secret': recaptcha_key, 'response': token}) 
    conn.request('POST', '/recaptcha/api/siteverify', params, headers)
    response = conn.getresponse()
    r = response.read().decode() 
    r = json.loads(r)
    return r['success']

La funzione GetEmailsListFunction risponde al metodo GET della risorsa /{destination} dove il parametro {destination} è l’indirizzo della mailbox. La funzione ritorna l’elenco delle email presenti nella mailbox, dopo aver validato il sessionID fornito nella richiesta (queryString).

L’elenco delle email è ottenuto tramite una Query sulla tabella emails.

## part of GetEmailsListFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
emails_table = dynamodb.Table(os.environ['emails_table_name'])
sessions_table = dynamodb.Table(os.environ['sessions_table_name'])

## Get emails list from DB
def get_emails(destination):
    items = None
    try:
        filtering_exp = Key('destination').eq(destination)
        response = emails_table.query(KeyConditionExpression=filtering_exp)        
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        #Remove private bucket details        
        for i in response['Items']:
            i.pop('bucketObjectKey', None)
            i.pop('bucketName', None)
        items = {'Items' : response['Items'], 'Count' : response['Count']}
    return items

## Verify session
def session_is_valid(sessionid):
    valid = False
    try:
        response = sessions_table.get_item(
            Key={
            'sessionId': sessionid
            }
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        if 'Item' in response:
            item = response['Item']
            if item['TTL'] > int(time.time()):
                valid = True
    return valid

def lambda_handler(event, context):

    headers = {
        "access-control-allow-headers": 
           "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "access-control-allow-methods": 
           "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
        "access-control-allow-origin": "*"
    }
     
    if session_is_valid(event["queryStringParameters"]["sessionid"]):
        destination = event["pathParameters"]["destination"]
        items = get_emails(destination)
        if items != None:
           result = {"statusCode": 200, "body": json.dumps(items), "headers": headers }

    return result    

La funzione GetEmailFileFunction risponde al metodo GET della risorsa /{destination}/{messageId} dove il parametro {destination} è l’indirizzo della mailbox e {messageId} è l’identificativo univoco della mail. Anche questa funzione verifica il sessionID fornito nella richiesta (queryString).

Restituisce il contenuto della mail leggendolo dal bucket S3 e si occupa di segnare come “letta” il relativo record della tabella emails.

## part of GetEmailFileFunction, see Github repository for  
## the complete functions code.

dynamodb = boto3.resource("dynamodb")
emails_table = dynamodb.Table(os.environ['emails_table_name'])
s3 = boto3.client('s3')

## Get emails details, including s3 bucket and s3 bucket object key.
def get_email_file(destination, messageId):
    result = None
    try:
        response = emails_table.get_item(
            Key={
            'destination': destination,
            'messageId' : messageId
            }
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])
    else:
        if 'Item' in response:
            result = response['Item']
    return result

## Set item ad readed in DynamoDB table
def set_as_readed(destination, messageId):
    try:
        response = emails_table.update_item(
            Key={
            'destination': destination,
            'messageId' : messageId
            },
            UpdateExpression="SET isNew = :updated",                   
            ExpressionAttributeValues={':updated': 'false'}
        )
    except ClientError as e:
        logger.info('## DynamoDB Client Exception')
        logger.info(e.response['Error']['Message'])

def lambda_handler(event, context):

    headers = {
        "access-control-allow-headers": 
           "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "access-control-allow-methods": 
           "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
        "access-control-allow-origin": "*"
    }

    email_file = get_email_file(destination, messageId) 
    if email_file != None:
        data = s3.get_object(Bucket=email_file['bucketName'], Key=email_file['bucketObjectKey'])
        contents = data['Body'].read().decode('utf-8')
        headers.update({"content-type": "message/rfc822"})
        result = {
           "statusCode": 200,
            "headers": headers,
            "body":  contents 
        }
        if email_file['isNew'] == 'true':
           set_as_readed(destination, messageId)

    return result

Al fine di abilitare CORS è stato aggiunto un metodo OPTION per ciascuna risorsa gestita da API Gateway. In aggiunta, le funzioni lambda restituiscono degli header specifici.

Il template CloudFormation si occupa inoltre di creare la distribuzione delle API e il relativo stage in modo da ottenere un endpoint valido per la configurazione della nostra web application.

Installazione backend

Una volta terminata la configurazione del servizio SES per il proprio dominio come spiegato nella sezione Primi Passi, utilizzeremo un template di CloudFormation per realizzare l’intera infrastruttura di backend. Il template prevede la creazione del bucket S3, delle tabelle DynamoDB, di tutte le funzioni lambda e delle API. Prevede inoltre la creazione di tutti i ruoli IAM, i permessi e la configurazione dei vari servizi, incluso il topic SNS.

AWSTemplateFormatVersion: 2010-09-09
Description: Disposable Email

Parameters:
    DomainName:
        Description: Domain name to be used
        Type: String
    ReCaptchaPrivateKey:
        Description: ReCaptcha private key to validate mailbox creation requests  
        Type: String
        Default: ""
    AddressesTableName:
        Description: DynamoDB table to store valid addresses
        Type: String
        Default: "disposable_addresses_table"
    EmailsTableName:
        Description: DynamoDB table to store emails 
        Type: String
        Default: "disposable_emails_table"
    SessionsTableName:
        Description: DynamoDB table to store client sessions 
        Type: String
        Default: "disposable_sessions_table"  
    RuleSetName:
        Description: SES RuleSet name to be used 
        Type: String
        Default: "default-rule-set"
    MailboxTTL:
        Description: Life (seconds) of mailbox
        Type: String
        Default: "3600"
    SessionTTL:
        Description: Session expiration in seconds
        Type: String
        Default: "600"
    SourceBucket:
        Description: Lambda functions source S3 Bucket
        Type: String
        Default: "cfvn" 

I sorgenti delle funzioni lambda non sono presenti direttamente nel template ma in file python esterni. Durante la creazione dello stack, i file ZIP delle funzioni lambda devono essere ospitati in un bucket S3. E’ possibile specificare un bucket differente da quello “ufficiale” (specificato come parametro “SourceBucket“) se si intende modificarli prima della creazione dello stack.

Per realizzare l’intera infrastruttura di backend puoi utilizzare la console di CloudFormation oppure AWS CLI:

aws cloudformation create-stack --stack-name <name> --template-url <url> --parameters <parameters> --capabilities CAPABILITY_IAM
DescrizioneEsempio
<name> il nome che vuoi dare allo stackdisposable_stack
<url>url del templatehttps://cfvn.s3-eu-west-1.amazonaws.com/disposable.yml

<parameters> è invece l’elenco dei parametri da fornire al template. I due parametri principali e obbligatori sono DomainName e ReCaptchaPrivateKey.

ParameterKey=DomainName,ParameterValue=<your_domain> 
ParameterKey=ReCaptchaPrivateKey,ParameterValue=<your_private_captcha_key>

Una volta creato la stack, ci interessa conoscere l’endpoint da utilizzare per la configurazione della web application. Questo è fornito da CloudFormation come output. E’ possibile vederlo nella console di CloudFormation oppure direttamente da AWS CLI:

aws cloudformation describe-stacks --stack-name <name> --query Stacks[0].Outputs[0].OutputValue

Frontend

L’applicazione React / Material-UI è molto semplice: si compone di tre semplici componenti: LoginForm per consentire la creazione e l’accesso ad una mailbox, EmailList per la visualizzazione dell’elenco dei messaggi presenti e EmailViewer per la visualizzazione del contenuto di una mail.

Il componente principale App prevede nel proprio stato l’indirizzo email e l’id di sessione, entrambi inizialmente vuoti. Viene costruito il componente LoginForm che gestirà il processo di login.

<LoginForm changeAddress={this.changeAddress.bind(this)} 
           apiEndpoint={APIEndpoint}
           recaptcha_key={ReCaptcha_SiteKey}
           email_domain={email_domain}/>

Il componente LoginForm incorpora il componente reCAPTCHA e si occupa di chiamare l’API /create sull’evento onSubmit della form.

handleSubmit(event) {
        const recaptchaValue = this.recaptchaRef.current.getValue();
        if (recaptchaValue === '') {
           window.location.reload();
        } else { 
            console.log("Captcha value:", recaptchaValue);
        
          fetch(this.props.apiEndpoint + 'create?address=' 
             + encodeURI(this.state.address + this.props.email_domain)
             + '&captcha=' + recaptchaValue)
          .then(r =>  r.json().then(data => ({status: r.status, body: data})))
          .then(r => {
            console.log(r);
            console.log('Response from API: ' + r.body.message);
            if (r.status === 200) {
              this.props.changeAddress(this.state.address + this.props.email_domain, r.body.sessionid);  
            }
        })
        .catch(console.log);
        }
        event.preventDefault();
    }

A login avvenuta, si ottiene un sessionID. Il componente App si occupa di costruire il componente EmailList.

<EmailList address={this.state.address} 
           sessionid={this.state.sessionid}
           changeAddress={this.changeAddress.bind(this)} 
           apiEndpoint={APIEndpoint}/>

Il componente EmailList si occupa di chiamare l’API /{destination} per ottenere l’elenco delle email.

getList(force) {
        fetch(this.props.apiEndpoint + this.state.address +
              '?sessionid=' + encodeURI(this.props.sessionid))
        .then(response => {
            const statusCode = response.status;
            const data = response.json();
            return Promise.all([statusCode, data]);
          })
        .then(res => {
            console.log(res);
            if (res[0] === 400) {
                this.logout();
            } else {      
                res[1].Items.sort(function(a,b){
                    if (a.timestamp > b.timestamp) { return -1 } else { return 1 }
                });         
                if ((this.listIsChanged(res[1].Items) || force)) {
                    this.setState({ emails: res[1].Items });
                    if ((this.state.selectedId === '') && (res[1].Items.length > 0)) {
                        this.setState({ selectedId: res[1].Items[0].messageId });   
                    }
                }
            }
        })
        .catch(console.log)
    }

Lo stesso si occupa inoltre di creare un componente EmailViewer per la visualizzazione del messaggio selezionato.

<EmailViewer address={this.state.address} 
             messageId={this.state.selectedId} 
             sessionid={this.state.sessionid} 
             apiEndpoint={this.props.apiEndpoint}/> 

La visualizzazione del messaggio email è la parte più complessa dell’applicazione client. Ottenuto il contenuto dalla API /{destination}/{messageId} è necessario effettuare il parsing dello stesso.

A tale scopo è utilizzata la libreria emailjs-mime-parser che consente di accedere ai vari oggetti che costituiscono la mail. La funzione getMailContents si occupa di scegliere il body corretto da mostrare, dando precedenza ai contenuti HTML.

Per visualizzare il contenuto HTML si utilizza un “trucco” molto pericoloso:

<div className="body" dangerouslySetInnerHTML={{__html: this.state.messageData}} />

Installazione Frontend

Per realizzare la web application React, una volta clonato il repository, è necessario configurare alcuni parametri che sono presenti all’inizio di App.js:

//  - your APIEndpoint
const APIEndpoint = <your_API_endpoint>;

//  - your ReCaptcha Site Key 
const ReCaptcha_SiteKey = <your_reCAPTCHA_site_key>;
 
//  - your email domain
const email_domain = <your_domain>;

Usa npm start per testare l’applicazione e npm run build per prepare i file da posizionare sul tuo hosting preferito oppure, come ho fatto io, direttamente in un bucket S3 pubblico. Ricordati di abilitare il dominio “localhost” tra quelli consentiti da reCAPTCHA per poter testare localmente la tua applicazione.

Conclusione

In questo repository GitHub trovi sia i sorgenti dell’applicazione React che il template CloudFormation per la realizzazione del backend AWS.

Disposable-mail nasce come esempio di realizzazione di una soluzione cloud native. Ci sono sicuramente molti aspetti da perfezionare e sarebbe necessario renderla più sicura. Si tratta comunque di una buona base di partenza per la realizzazione della propria webmail “usa e getta”.

Ci siamo divertiti? Alla prossima!

Keys

Certificati SSL gratuiti con Certbot e AWS Lambda

5 min

Grazie al progetto Certbot e alla Electronic Frontier Foundation è possibile dotare il proprio sito web di un certificato SSL totalmente gratuito. Certbot è un tool a linea di comando che consente di richiedere un certificato SSL valido per il proprio dominio, a seguito di un processo di verifica della proprietà dello stesso. Il tool, grazie all’ausilio di diversi plug-in, si può inoltre occupare dell’installazione e dell’aggiornamento dello stesso sul proprio server web. In questo post vediamo come richiedere e rinnovare automaticamente i propri certificati SSL gratuiti con Certbot e AWS Lambda.

Perché gestire i certificati con AWS Lambda?

Diverse applicazioni web di cui mi occupo utilizzano CloudFront per la distribuzione dei contenuti, associato ad un bucket S3 di origine. Ho così deciso di realizzare una semplice Lambda function che si occupi di ottenere i certificati SSL con Certbot e di verificare periodicamente la data di scadenza degli stessi. Se necessario, questa provvede automaticamente al rinnovo ed all’importazione su AWS Certificate Manager del nuovo certificato.

Risultato? Mai più certificati SSL scaduti! L’automazione del processo è particolarmente importante tenendo conto della breve vita (90 giorni) dei certificati emessi da Let’s Encrypt CA.

Overview della soluzione

Al centro della soluzione c’è ovviamente la funzione Lambda, invocata periodicamente da un evento CloudWatch. La funzione gestisce un elenco di domini (specificato nella variabile d’ambiente DOMAINS_LIST – valori separati da virgola) e per ciascuno di essi:

  • viene verificata la presenza del certificato in ACM
  • se non presente, lo stesso viene richiesto tramite Certbot. Per completare correttamente il processo di verifica della proprietà del dominio, la funzione si occupa di copiare il token di validazione nel relativo bucket S3 che ospita le relative risorse statiche
  • per i certificati esistenti, viene verificata la data di scadenza e, qualora necessario, si procede al rinnovo

Al termine del processo, la configurazione, i certificati e le private key vengono memorizzate in un bucket S3 privato in modo che siano utilizzabili alla prossima esecuzione.

Utilizzo di Certbot in AWS Lambda

Certbot è scritto in Python ed è facilmente utilizzabile per automatizzare quindi i processi di richiesta, rinnovo e revoca dei certificati. Utilizzarlo però in ambiente AWS Lambda richiede uno step di preparazione aggiuntivo, in modo che tutti i pacchetti e le dipendenze necessarie siano correttamente installate. Questa fantastica guida spiega in maniera dettagliata come utilizzare un’istanza EC2 a tal scopo. Ho preparato il pacchetto contenente la versione 1.3.0 di Certbot che è disponibile nel repository relativo a questo post.

La configurazione di Certbot è collocata nella directory /tmp dell’istanza lambda e rimossa per ragioni di sicurezza al termine dell’esecuzione, in quanto include anche le private key dei certificati.

Per procedere alle operazioni di rinnovo è però necessario preservare la configurazione di Certbot. L’albero di directory contenente la configurazione è compresso in un file zip e copiato nel bucket S3 indicato dalla variabile d’ambiente CERTBOT_BUCKET. Al successivo avvio la configurazione viene ripristinata dal file zip nella directory /tmp dell’istanza.

Il lato oscuro dei Symlinks

Certbot verifica che il proprio albero di configurazione sia valido. Viene verificata, tra le altre cose, la presenza di link simbolici nella directory live relativa a ciascun dominio.

Il processo di backup e restore dell’albero di configurazione in formato zip rimuove tali link (sostituiti dai file effettivi). Per ripristinare la situazione iniziale, viene utilizzato questo metodo.

Superare la challenge di verifica con AWS S3

Per richiede un certificato è necessario superare una challenge, che dimostri la proprietà del relativo dominio. Poiché la Lambda function di questo progetto possa correttamente gestire tale challenge, è richiesto che:

  • esista un bucket S3 dello stesso nome del dominio
  • il bucket S3 sia già configurato come sorgente di una distribuzione CloudFront per il dominio oppure, in alternativa, che la funzionalità di static website hosting (HTTP) del bucket S3 sia – temporaneamente – attiva
  • la configurazione DNS per il dominio sia corretta
  • il ruolo IAM assegnato alla lambda function consenta le operazioni di PutObject e DeleteObject sul bucket S3

La challenge consiste nella creazione di uno specifico file contenente un token fornito da Certbot. Tale file deve essere posizionato temporaneamente sul sito web in modo che possa essere verificato dai server di Let’s Encrypt. Al termine del processo di verifica il file viene rimosso. Tali operazioni sono svolte da due script Python (auth-hook.py e cleanup-hook.py).

L’accesso al bucket S3 del dominio è richiesto solo per il superamento della challenge e l’ottenimento del primo certificato SSL. Non sarà più richiesto durante i successivi rinnovi.

Importare il certificato su ACM

Una volta ottenuto il certificato, la lambda function si occupa dell’importazione dello stesso in ACM. Qualora esista già un certificato per il medesimo dominio, questo viene sostituito specificando durante l’importazione il relativo ARN.

Nota importante: per essere utilizzato in CloudFront, è necessario importare il certificato nella region US East (N. Virginia). Per comodità ho realizzato l’intero stack in questa region AWS.

Schedulazione con CloudWatch

CloudWatch è utilizzato per eseguire la lambda function una volta al giorno. Tra i parametri di configurazione della lambda, è prevista una variabile d’ambiente (CERTS_RENEW_DAYS_BEFORE_EXPIRATION) che determina quanti giorni prima della scadenza provvedere al rinnovo del singolo certificato. La scadenza viene ottenuta da ACM. In questo modo l’esecuzione della funzione non comporta necessariamente il rinnovo e quindi può essere schedulata quotidianamente senza preoccupazioni.

Il rinnovo tramite CertBot è forzato, garantendo l’ottenimento di un nuovo certificato 30 (valore di default) giorni prima della scadenza.

Deployment tramite CloudFormation

Nel repository di questo progetto è disponibile il template CloudFormation per la creazione dello stack. Questo comprende la funzione, il relativo ruolo, l’evento CloudWatch e i relativi permessi.

Per effettuare il deployment, la prima operazione da svolgere dopo aver clonato il repository, è la build della funzione.

make lambda-build

Per creare lo stack è necessario indicare il bucket S3 da utilizzare per la memorizzazione della configurazione di Certbot.

make BUCKET=your_bucket_name create-stack 

Il bucket indicato viene utilizzato anche per memorizzare temporaneamente i sorgenti della funzione lambda per il deployment.

Una volta creato lo stack, è necessario impostare le variabili di ambiente DOMAINS_LIST con l’elenco dei domini da gestire separati da virgola e DOMAINS_EMAIL con l’indirizzo email da utilizzare durante la richiesta dei certificati. Per ciascun nuovo dominio è necessario fornire la corretta policy di accesso al relativo bucket S3 che ospita le risorse statiche.

Conclusioni

Ottenere certificati SSL gratuiti per i propri progetti è davvero utile; automatizzarne la gestione tramite questo progetto mi ha dato la possibilità di dimenticarmene e vivere felice.

Consiglio di effettuare i propri test sfruttando l’ambiente di staging messo a disposizione da Certbot. La variabile d’ambiente CERTBOT_ENV consente di definire se utilizzare l’endpoint di produzione (production) o quello di staging.

Attenzione alle quote ACM: il numero di certificati che è possibile importare in ACM è limitato nel tempo (quantità nell’ultimo anno). Durante i miei test mi sono scontrato con limite molto basso: 20! Come riportato in questo post è necessario contattare il supporto AWS per la rimozione.

Error: you have reached your limit of 20 certificates in the last year.

Ulteriori sviluppi? Molti! Il requisito di avere un bucket S3 con lo stesso nome del dominio può, per esempio, essere superato prevedendo una configurazione più avanzata, magari memorizzata in un DB. Sentiti libero di migliorare il codice sorgente originale e di farmelo sapere!

Ci siamo divertiti? Alla prossima!

Automation

Sviluppo su Ansible con Molecule, AWS e TravisCI

5 min

“Automation is awesome!” – Questo è uno degli slogan che leggo sempre più spesso su blog e articoli che si occupano di DevOps e CI/CD. In effetti credo di essere d’accordo: le possibilità che esistono oggi per automatizzare i processi di integrazione e deployment sono incredibili.

Supponiamo, per esempio, di dover automatizzare l’installazione di Apache su Ubuntu. Scriveremo un ruolo Ansible che svolga tale operazione e lo potremo poi riutilizzare come building block di un processo automatizzato più ampio, come il deployment di una soluzione.

Viene da chiedersi: come testare il ruolo durante il suo sviluppo e i successivi aggiornamenti? Avendo recentemente affrontato una simile challenge, ho avuto modo di realizzare una pipeline di CI utilizzando degli strumenti molto potenti (e gratuiti!). Vediamoli.

Ansible Role

Se non lo abbiamo ancora fatto, preoccupiamoci di installare Ansible.

Andremo a realizzare il nostro script my-httpd nel modo più semplice possibile: creiamo tutte le directory che descrivono normalmente i ruoli Ansible, nell’ambito di un progetto di esempio awesome-ci.

mkdir -p awesome-ci/roles/my-httpd
cd awesome-ci/roles/my-httpd
mkdir defaults files handlers meta templates tasks vars

Nella directory tasks creiamo il file main.yml con il codice Ansible per l’installazione di Apache. Nella directory root del progetto creiamo un playbook.yml che potrà utilizzare il nostro ruolo. Il contenuto dei due file sarà per esempio:

Ora che abbiamo scritto tutto il codice necessario per installare Apache con Ansible ci concentriamo sulla realizzazione della nostra pipeline, iniziando con i primi test.

Code linting

Cos’è il code linting? Wikipedia ci suggerisce essere “uno strumento che analizza il codice sorgente per contrassegnare errori di programmazione, bug, errori stilistici e costrutti sospetti”.

ansible-lint ci consente di effettuare questo tipo di verifica sul nostro playbook e ruolo. Per installarlo si usa PIP:

pip install ansible-lint

Per verificare quanto abbiamo scritto ci spostiamo nella directory root del progetto ed eseguiamo:

ansible-lint playbook.yml

ansible-lint verificherà il contenuto del playbook e dei ruoli utilizzati nei confronti di alcune regole, riportandoci eventuali errori e violazioni di best practices:

[201] Trailing whitespace
/home/awesome-ci/roles/my-httpd/tasks/main.yml:8
        name: httpd 

Ogni segnalazione riporta un codice identificativo (in questo caso 201). La relativa documentazione descrive cause e soluzioni. Nel mio fortunato caso l’unica segnalazione riportata riguarda uno semplice spazio di troppo alla fine della riga indicata. E’ possibile ignorare alcune regole specificandone i codici identificativi sulla command line.

ansible-lint playbook.yml -x 201

Ogni qualvolta andremo a modificare il nostro ruolo potremo utilizzare ansible-lint per verificare se abbiamo rispettato le opportune best practices. Non fermiamoci però qui! E’ arrivato il momento di verificare se il nostro ruolo fa effettivamente ciò di cui abbiamo bisogno.

Molecule

Molecule è uno strumento pensato per testare i propri ruoli Ansible, in diversi scenari e con diversi sistemi operativi, appoggiandosi a diversi provider di virtualizzazione e Cloud.

Per procedere alla sua installazione si usa PIP. Consiglio però di verificare la documentazione ufficiale per l’installazione dei requirements.

pip install molecule

Ad installazione terminata ci posizioniamo nella directory roles e ci occupiamo di ricreare il nostro ruolo con Molecule. Spostiamo poi il nostro file main.yml nel nuovo ruolo.

# Save old role
mv my-httpd my-httpd-orig

# Init new role using Molecule
molecule init role -r my-httpd -d docker

# Move task file to new role
cp my-httpd-orig/tasks/main.yml my-httpd/tasks/
rm -r my-httpd-orig 

Con il comando di inizializzazione del ruolo abbiamo indicato a Molecule di utilizzare docker come environment di test. Spostiamoci nella directory del ruolo e proviamo.

cd my-httpd
molecule test

Purtroppo vedremo subito che il nostro test fallisce durante la fase di linting. Molecule crea infatti un ruolo utilizzando ansible-galaxy, completo quindi di tutte le directory necessarie, META compresa. Quest’ultima andrebbe opportunamente corretta sostituendo le informazioni generiche riportate con quelle relative al nostro ruolo. In alternativa è possibile indicare ad ansible-lint di ignorare le relative regole. Andiamo quindi a modificare il file di configurazione di Molecule.

La directory molecule del nostro ruolo comprende una subdirectory per ciascuno scenario di test. Lo scenario che abbiamo creato è default. In questo folder troviamo il file molecule.yml che riporta la configurazione di ogni fase, compresa quella di linting. Aggiungiamo l’opzione per escludere determinate rules.

provisioner:
  name: ansible
  lint:
    name: ansible-lint
    options:
      x: [201, 701, 703]

Eseguiamo di nuovo il test: la fase di code linting viene superata con successo ma poi incappiamo in un altro errore.

fatal: [instance]: FAILED! => {"changed": false, "cmd": "apt-get update", "msg": "[Errno 2] No such file or directory", "rc": 2}

Il motivo? Non così immediatamente identificabile, ma analizzando un pò il file di configurazione di molecule che abbiamo precedentemente modificato, ci accorgiamo che l’immagine docker utilizzata per testare il ruolo è basata su CentOS. Decidiamo invece di usare Ubuntu 18.04, per il quale il comando “apt-get update” ha un senso.

platforms:
  - name: instance
    image: ubuntu:18.04

Lanciamo di nuovo il test e.. boom! Superato! Ci manca però ancora qualcosa: le operazioni indicate nel nostro ruolo vengono svolte senza errori ma non ci siamo effettivamente occupati di verificare che il servizio Apache2 sia installato ed attivo. Possiamo configurare Molecule in modo che lo verifichi per noi al termine dell’esecuzione del playbook, indipendentemente dalle azioni previste nel ruolo.

Dobbiamo modificare il file test_default.py posto nella directory tests di Molecule.

Eseguiamo di nuovo il test prestando attenzione all’azione “verify”.

--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/awesome-ci/roles/my-httpd/molecule/default/tests/...
    ============================= test session starts ==============================
    platform darwin -- Python 3.8.1, pytest-4.6.4, py-1.8.1, pluggy-0.13.1
    rootdir: /home/awesome-ci/roles/my-httpd/molecule/default
    plugins: testinfra-3.3.0
collected 2 items                                                              
    
    tests/test_default.py ..                                                 [100%]
    
    =========================== 2 passed in 3.14 seconds ===========================
Verifier completed successfully.

Ottimo! Ricapitoliamo: abbiamo configurato Molecule per verificare mediante ansible-lint il nostro ruolo. Successivamente lo stesso viene utilizzato in un playbook per installare e avviare Apache in un container docker basato su Ubuntu. Al termine viene inoltre verificato che Apache sia effettivamente installato e avviato. Il tutto in modo automatizzato.

E’ arrivato il momento di spostarci in Cloud. Perché non provare il nostro ruolo in uno scenario totalmente differente? Ad esempio su AWS EC2?

Molecule & AWS EC2

Per testare il nostro ruolo su AWS dobbiamo creare un nuovo scenario di Molecule. Dalla directory del ruolo inizializziamo il nuovo scenario aws-ec2 con:

molecule init scenario -d ec2 -r my-httpd -s aws-ec2

Come si può notare, questa volta usiamo ec2 come environment di test. Il file molecule.yml è ovviamente diverso dal precedente scenario, in quanto prevede le impostazioni specifiche per la piattaforma AWS.

platforms:
    - name: instance
      image: ami-0047b5df4f5c2a90e     # change according to your EC2
      instance_type: t2.micro
      vpc_subnet_id: subnet-9248d8c8   # change according to your VPC

L’immagine AMI e la subnet specificate nel file di configurazione devono essere modificate in accordo con la region che si intende utilizzare per il test. Per trovare la AMI Ubuntu corretta consiglio di utilizzare questo sito.

Valgono inoltre le configurazioni previste nello scenario docker: specificare le esclusioni nelle regole di code linting e i test a termine deployment per la verifica del servizio Apache.

Dobbiamo specificare region e credenziali AWS: lo si può fare impostando alcune variabili ambiente prima di avviare il test con Molecule.

export EC2_REGION=eu-west-2
export AWS_ACCESS_KEY_ID=YOUR_ACCESSKEY
export AWS_SECRET_ACCESS_KEY=YOUR_SECRETACCESSKEY 
molecule test

Successo! Abbiamo rapidamente creato un nuovo scenario e siamo in grado di testare il nostro ruolo anche su AWS. Se collegati alla console EC2, vedremo infatti che Molecule si occuperà di avviare una nuova virtual machine su cui effettuare i test. La stessa sarò terminata concluse le operazioni. Vediamo ora come creare una pipeline di CI che, ad ogni push sul nostro repository GitHub, provveda all’esecuzione di questi test con Molecule su AWS.

Travis CI

Una volta registrati su TravisCI è possibile configurare la nostra pipeline per il repository GitHub che ospita il nostro progetto di automazione. La configurazione avviene creando il file .travis.yml in root.

Come si vede nella sezione install, il file test-requirements.txt riporta l’elenco dei pacchetti da installare. Il suo contenuto è:

ansible-lint
molecule
boto 
boto3

Nella sezione script sono invece indicate le operazioni da svolgere: ansible-lint viene utilizzato preliminarmente per le verifiche di playbook e ruolo; viene poi utilizzato Molecule per effettuare il test del ruolo nello scenario AWS-EC2.

Manca qualcosa? Si, le credenziali AWS. Per condividerle in modo sicuro utilizziamo la CLI di Travis. Per prima cosa installiamola:

gem install travis

Procediamo poi a codificare le credenziali in modo sicuro, aggiungendole al file di configurazione di Travis. Le stesse saranno poi passate sotto forma di variabili di ambiente a Molecule durante l’esecuzione dei test.

travis login --pro

travis encrypt AWS_ACCESS_KEY_ID="<your_key>" --add env.global --pro
travis encrypt AWS_SECRET_ACCESS_KEY="<your_secret>" --add env.global --pro

Proviamo una build con TravisCI e … bingo! Abbiamo terminato.

Conclusioni

Abbiamo visto come l’utilizzo di questi strumenti consenta di sviluppare ruoli e playbook Ansible di qualità sottoponendoli costantemente ad una pipeline in grado di verificarne la correttezza, su ambienti e scenari completamente differenti.

Un progetto per il quale ho utilizzato gli stessi strumenti è disponibile su questo repository GitHub: si tratta di un playbook Ansible dedicato alla realizzazione di un cluster Docker Swarm.

Ci siamo divertiti? Alla prossima!

Questo è davvero uno switch molto smart!

4 min

Avete mai provato a chiedere ad Alexa di accendere le luci del salotto circondato da uno sciame di bambini urlanti? Difficile eh? Al contrario, a tarda serata, chiedereste ad Alexa di spegnere la luce del bagno che sistematicamente viene dimenticata accesa, correndo il rischio di svegliare i bambini? Proprio così: devi alzarti dal divano.

In queste e tante altri situazioni lo smart switch più adatto e sostenibile di sempre è un bel pezzo di cartone! Non ci credi? Te lo dimostro.

Il trucco non c’è

Qualche giorno fa mi sono chiesto come avrei potuto utilizzare le telecamere di sicurezza installate dentro e fuori la mia abitazione per renderla più smart.

Beh! Un sostituto silenzioso di Alexa farebbe comodo, mi sono detto.

Ho cercato quindi di trovare il modo di utilizzare le immagini delle telecamere per generare eventi, come l’accensione o lo spegnimento di una lampadina. L’idea che mi è venuta è questa: identificare un oggetto ben riconoscibile e leggere il testo posto al di sopra di esso, un cartello insomma! Un cartello che si possa utilizzare per dare comandi ai nostri dispositivi IoT.

Object detection

Ho utilizzato un Raspberry PI4 per analizzare il flusso video della mia telecamera e riconoscere gli oggetti grazie a CV2, il suo module DDN (Deep Neural Network) e il modello MobileNetSSD V2 Coco.

Andando a spulciare tra le classi di oggetti riconosciuti dal modello mi è subito saltato all’occhio un bel “bus”. Perfetto! Un oggetto facilmente riconoscibile e difficilmente presente nel mio salotto, giusto per non incorrere in falsi positivi. L’immagine che ho scelto per decorare il mio cartello è stata subito riconosciuta dal modello come bus. Ottimo!

Usando però l’immagine in un contesto diverso il risultato non è stato affatto positivo. La presenza di altri oggetti riconosciuti (persona e divano) e la dimensione ridotta del mio bus rispetto all’intera immagine ne ha pregiudicato il corretto riconoscimento.

Troviamo i contorni

Ci diamo per vinti? Ma nemmeno per idea! Il nostro cartello è un bel rettangolo. Perché non rilevare i rettangoli presenti nell’immagine, estrarne il contenuto e usarlo per l’object detection?

Andando ad analizzare i contorni e cercando quelli che approssimativamente hanno 4 lati, boom! Trovato!

Ora possiamo sottoporre al nostro modello di object detection la sola immagine contenuta nel rettangolo e finalmente lo stesso viene riconosciuto giustamente come bus.

Riconoscimento testi

Mi sono detto: il nostro bus è la “hot image” di attivazione del sistema, un pò come la parola “Alexa” è la “hot word” di attivazione quando pronunciata nelle vicinanze dei dispositivi Echo. A questo punto dobbiamo sottoporre l’immagine ad un sistema di riconoscimento dei testi per identificare il comando e agire poi di conseguenza.

Ho scelto una soluzione cloud, usando Amazon Rekognition. Quando un bus (o un truck o una car) vengono identificati nell’immagine, la stessa viene trasferita in un bucket S3 e sottoposta all’algoritmo di riconoscimento dei testi. Risultati? Ottimi.

Detected text:ALL ON
Confidence: 98.95%
Id: 0
Type:LINE

Finito!

Abbiamo terminato! Ora non ci resta che eseguire i comandi in funzione del testo restituito da Amazon Rekognition. Nel mio caso mi sono limitato ad accendere un paio di Led collegati direttamente al PI4 ma in questo caso il limite è solo la fantasia!

L’intero codice sorgente è qui.

Dietro le quinte

Alcune indicazioni per chi vuole cimentarsi nella realizzazione dello stesso progetto: la risoluzione e la posizione delle telecamera sono molto importanti ed è richiesto un pò di tuning per ottenere dei buoni risultati. Usando un Raspberry PI4 è meglio non esagerare con la risoluzione dell’immagine: è necessario trovare un compromesso tra una qualità sufficiente al riconoscimento del testo e che non richieda troppe risorse per la sua elaborazione.

Per installare OpenCV su Raspberry ho seguito le indicazioni trovate qui. La classe di cattura del video bufferless è stata presa da questo post. L’algoritmo di riconoscimento dei rettangoli da qui.

Ci siamo divertiti? Alla prossima!