Blog

Il mio viaggio nel mondo serverless

Lettura 23 minuti

Quante volte avete sentito parlare di Serverless? Io davvero tante, troppe forse, quindi ho deciso di intraprendere questo viaggio alla scoperta di questa nuova tecnologia.

Preparo quindi lo zaino, gli scarponi, corda, imbraghi e parto..

Intro

Fuori è una giornata uggiosa, autunnale, il clima giusto per restare in casa con una tazza di caffè e il notebook. Click sul mio fido compagno Firefox, da dove iniziare? Prima di tutto devo scegliere quale provider cloud usare.. 5 secondi per pensare.. AWS ovviamente.

Le prime ricerche mi portano a centinaia e centinaia di articoli di blog, davvero tanto tanto materiale. Cerco di fare un riassunto così mi autoconvinco di aver capito:

IaaS (Infrastructure as a Service)

Avete presente i server? Quei computer strani, in stanze buie, ecco, non vanno più di moda.. perchè? Sono difficili da gestire, devi usare mille accortezze, gruppi di continuità nel caso vada via la luce, la temperatura giusta, assolutamente niente polvere, porte tagliafuoco, sistemi antincendio con argon etc..etc.. quindi per garantire un servizio stabile senza venirne matti e spendere un sacco di soldi ecco che servizi come AWS, Google Cloud, Azure e molti altri “affittano” parte dei loro data center, a te rimane solo da configurare l’infrastruttura con qualche click e preoccuparti solo della parte software.

SaaS (Software as a Service)

Dopo questo grandioso passo avanti arrivano altri problemi (e ovviamente a nessuno piacciono), devi tenere aggiornato il sistema operativo installato nei tuoi server virtuali, installare le patch di sicurezza, configurare uno scaling delle risorse computazionali, ridondare l’infrastruttura in modo da garantire un’alta affidabilità del servizio e tutto quanto, quindi cosa si sono inventati i cloud provider per venire in contro? Gestire alcuni dei servizi come MySQL, Elasticsearch, Redis, RabbitMQ e molti altri, servendoti direttamente l’accesso senza che tu ti debba occupare di installare e aggiornare tali servizi, ovviamente la configurazione (anche se un po’ limitata) è sempre possibile.

FaaS (Function as a Service)

Dopo questo ultimo passo avanti rimane ancora il problema delle macchine virtuali che faranno girare il tuo software (sempre lo stesso problema riportato sopra) quindi qual è il prossimo step? Il Serverless! Ovvero piattaforme gestite dove l’unica cosa di cui ti devi preoccupare è il tuo codice, nient’altro, è il provider cloud che si occuperà automaticamente di scalare, aggiornare i sistemi operativi che ospita il tuo codice e garantirne l’affidabilità.

Server-less non vuol dire che non ci sono i server ma che non li gestite voi :)

Framework

Dopo lo studio iniziale fatto ieri, oggi è il giorno del test pratico! Ci sono molte guide che spiegano come creare una semplice API usando i servizi di AWS, però i passaggi di attivazione dei servizi vengono fatti direttamente dalla console web, per quanto sia comodo non permette di riprodurre facilmente gli step di configurazione e le modifiche non sono tracciate. AWS mette a disposizione un servizio chiamato CloudFormation con cui, descrivendo l’infrastruttura con un file JSON o YAML puoi fare il provisioning di tutte le risorse che ti servono in un colpo solo, gestire gli aggiornamenti e rollback in caso di errore.

Certo che scrivere un enorme file JSON o YAML non è comodissimo, quindi ho cercato quali framework AWS mette a disposizione:

  • SAM un wrapper di CloudFormation con notevoli funzionalità di test in locale che emula i servizi cloud.

  • Mobile CLI un progetto abbastanza vecchio, in fase di dismissione

  • Amplify il nuovo e fiammante framework per applicazioni serverless

Sinceramente ho trovato un po’ di confusione su cosa usare ma il consiglio è: SAM per servizi API, Mobile CLI da evitare, Amplify per applicazioni più complesse dotate anche di frontend. Ho puntato quindi su quest’ultimo, per provare l’esperienza completa e vedere le novità.

Amplify

Molto molto facile da usare, forse anche troppo, basta scaricare la cli

npm install -g @aws-amplify/cli

configurarla seguendo gli step guidati

amplify configure

entrare nel cartella principale della vostra applicazione web e aggiungere Amplify come dipendenza

npm install --save aws-amplify

e come ultimo step inizializzare lo stack base di CloudFormation dove verranno create giusto un paio di regole di accesso e poco altro

amplify init

Ora arriva la parte davvero divertente, aggiungere i servizi di cui la nostra applicazione ha bisogno, la CLI di Amplify userà sempre degli step guidati per farteli configurare. Facendo una panoramica di cosa si può aggiungere vediamo:

  • analytics sistema di analisi del comportamento dell’utente e metriche varie
  • api per esporre API e aggiungere la logica “server”-side
  • auth autenticazione, gestione degli utenti, login, registrazione, conferma della mail, reset delle password..
  • function semplici funzioni con logica “server”-side
  • hosting i servizi che serviranno la tua applicazione ai client
  • storage
    servizi per il salvataggio, upload e download di file
  • notifications notifiche push

Wow! C’è praticamente tutto e si può aggiungere con un semplice comando

amplify add analytics

e una volta configurato il servizio selezionato fare il deploy delle modifiche

amplify push

Veramente bello, alcuni servizi li conoscevo già come Cognito per la gestione delle utenze, S3 in combinazione con CloudFront per servire il frontend, Lambda per la logica dell’applicazione, API Gateway per esporre le API e infine DynamoDB per salvare e persistire i dati.

Amplify è davvero un gran strumento, soprattutto per le integrazioni con React, AngularJS e Vue.js che ti permettono di collegare i servizi con rapidità usando il framework che più ti piace. Ci sono voluti 15 minuti per avere una semplice applicazione (sì, la solita vecchia TODO list) up&running, però se non ci si sporca le mani infilandole nel motore non mi sento del tutto soddisfatto.

L’integrazione

Autenticazione

Comincio allora ad integrare la mia applicazione scritta in Vue.js partendo dall’autenticazione. La configurazione di Cognito viene semplificata davvero tanto:

amplify add auth

1 minuto ed è tutto pronto per gestire le utenze.

Mi sono stupito di quante funzionalità offra Cognito: conferma della mail e/o numero di telefono attraverso un codice (via SMS per il telefono), login con Google, Facebook, Amazon e OpenID, fino a servirti su un piatto d’argento un server OAuth2 completo. Creare questo, da zero, con un server PHP o NodeJS almeno 12 giornate di lavoro sarebbero servite, pensare di averci messo 1 minuto è pazzesco.

Dati

Veniamo ora alla base dati, avevo già usato in passato DynamoDB per alcune integrazioni, ma mai in combinazione con AppSync, un servizio abbastanza recente (non ha più di 1 anno e mezzo) che aiuta a gestire l’accesso ai tuoi dati usando endpoint GraphQL. È il caso di dirlo: all’ultima moda!

Con Amplify è stato davvero semplice, bisogna certo avere un po’ di esperienza con GraphQL.

amplify add api

la risposta di questo comando parla chiaro:

? Please select from one of the below mentioned services (Use arrow keys)
❯ GraphQL
 REST

non solo endpoint GraphQL ma anche REST! un passo avanti per scegliere il metodo di autenticazione

? Please select from one of the below mentioned services GraphQL
? Provide API name: myNotesApi
? Choose an authorization type for the API (Use arrow keys)
❯ API key
 Amazon Cognito User Pool

si possono usare sia delle chiavi statiche (generate durante questo processo) oppure andare ad utilizzare il sistema di autenticazione creato prima con Cognito, ovviamente scelgo quest’ultima opzione.

La doccia fredda

Oggi ho iniziato a scrivere i modelli per la mia applicazione. Una delle funzionalità più comode di Amplify è la generazione automatica dei resolver di GraphQL, addirittura con dei trasformatori per poter specificare il livello di accesso. Questa ultima parte è vitale per limitare l’accesso degli utenti solo ai loro dati e non all’intera tabella di DynamoDB.

Qui è sorto il primo problema, ho cercato di seguire fedelmente la documentazione ma niente, controllando i resolvers generati non vedevo differenza utilizzando oppure no il trasformatore @auth. Ho perso parecchio tempo su questo finchè, arreso, ho creato una issue sul respository di Github per chiedere supporto, mi hanno risposto veramente in fretta e si è scoperto che effettivamente non stava proprio funzionando… va bene, cose che possono succedere.

DynamoDB

Durante il debug ho scoperto che gli indici delle tabelle di DynamoDB venivano creati con il Throughput Capacity per Reads and Writes a 5, senza possibilità di essere modificato.

Per chi non lo sapesse, questo valore è uno dei parametri che viene utilizzato per calcolare il costo del servizio di DynamoDB: il numero di accessi in lettura o scrittura al secondo (non così semplice ma non mi addentro del calcolo). Per non scendere troppo nei dettagli, questo database non relazionale risponde per ogni query alla massima velocità possibile indifferentemente dal quantitativo di dati e non c’è limite al numero di righe (paghi per il peso in GB della tabella). La parte negativa è un motore di query un po’ ostico e bisogna creare con attenzione gli indici della tabella, vitali per ordinamento e query.

Una scelta difficile

Tornando a noi, non mi aspettavo quindi che per qualsiasi test si pagasse da 13$ in sù di fattura, un po’ tantino visto che con meno di 1$ ho 2 progetti di IoT, una action di GoogleHome, il mio sito web (con due ambienti) e uso S3 per fare un backup criptato di alcuni file importanti. Con giusto 56 modelli arrivavo a cifre spropositate, con un tabella di DynamoDB pronta per la produzione quando ancora l’applicazione non era nata. Ho quindi aperto un’altra issue, questa volta non ancora presa in considerazione.

Ho quindi eliminato il progetto e mi sono perso, avevo trovato uno strumento molto facile da usare e potente però è ancora troppo giovane per poter essere usato. Essendo così semplice non permette neanche una modifica così profonda di alcune sue parti se non con diversi workaround.

Seguono diverse sere passate a valutare alternative.

Qualche giorno dopo, da capo

Nel corso delle giornate ho collezionato diverse alternative ad Amplify, difficile trovarne perchè non ci sono altre CLI che con un paio di comandi fai TUTTO.

La mia scelta è ricaduta quindi su Serverless Framework, un framework molto potente e modulare con una grande community dietro che sviluppa centinaia di plugin. L’ho già usato in precedenza per alcuni progetti di automazione di infrastrutture cloud ed API e ne sono sempre stato soddisfatto.

Organizzare bene la struttura del progetto

La gestione delle risorse usa direttamente i template di CloudFormation, questo garantisce di essere sempre aggiornata ed utilizza le funzionalità servite da AWS senza doversi riportare le modifiche all’interno del framework (un esempio, Terraform usa la sua sintassi, la sua integrazione ai servizi, non riesce a stare al passo con gli aggiornamenti e ho trovato sempre decine di bug).

Quando ci sono molte risorse da aggiungere fuori da quelle gestite da Serverless la guida ufficiale spiega come crearle all’interno del file, non fate l’errore di mettere tutto all’interno di un solo file di 2000 righe difficile da mantenere, meglio spezzarlo in più file

resources:
 - ${file(resources/cognito.yml)}
 - ${file(resources/userRole.yml)}
 - ${file(resources/queues.yml)}
 - ${file(resources/tables/todos.yml)}
 - ${file(resources/tables/projects.yml)}
 - ${file(resources/tables/categories.yml)}
 - ${file(resources/outputs.yml)}

e poi all’interno di questi file specificare le risorse

Resources:
 AccountTable:
   Type: AWS::DynamoDB::Table
   Properties:
     TableName: "${self:service}-${self:provider.stage}-${self:custom.todosTableName}"
     AttributeDefinitions:
       -
         AttributeName: userId
         AttributeType: S
       -
         AttributeName: id
         AttributeType: S
     KeySchema:
       -
         AttributeName: userId
         KeyType: HASH
       -
         AttributeName: id
         KeyType: RANGE
     ProvisionedThroughput:
       ReadCapacityUnits: 1
       WriteCapacityUnits: 1
     StreamSpecification:
       StreamViewType: "NEW_AND_OLD_IMAGES"

in questo modo il codice sarà molto più organizzato e i file più piccoli e mantenibili.

Usare un file separato per le configurazioni

Il discorso precedente vale anche per le configurazioni del progetto, meglio specificare un file separato in modo da poterlo cambiare a seconda dell’ambiente o progetto senza usare centinaia di variabili d’ambiente

service: MyService
custom: ${file(./config.yml)}

provider:
 name: aws
 runtime: nodejs8.10
 stage: ${opt:stage,self:custom.env}
 region: ${self:custom.region}

il file di configurazione diventa molto più facile da visualizzare

env: "Stage"
region: "eu-west-1"

todosTableName: "Todos"

Semplificare

Attirato da Amplify ad usare GraphQL ho subito iniziato ad usare serverless-graphql, facile da usare e con un po’ di organizzazione e riuso dei resolvers sono arrivato ad un buon punto. Mi sono accorto però che stavo scrivendo davvero tanto codice, troppo, ciò voleva dire mantenerlo, testarlo, ottimizzarlo e portava via tanto tempo nel creare una semplice funzionalità CRUD per i modelli.

API?

Mi è sorto un dubbio: mi servono davvero delle API in graphQL? Certo sono davvero comode, espressive e facili da tenere aggiornate ma perchè? Più nello specifico: mi servono delle API da consumare? Le API vengono usate principalmente per accedere alla base dati, aggiungere della logica server-side (che l’utente non può manipolare) e rendere fruibile il servizio a servizi terzi.

Accedere ai dati

DynamoDB ha una sua funzionalità build-in per limitare l’accesso a righe, colonne ed operazioni in base all’utente, ovvero un utente può vedere solo determinati attributi dell’oggetto salvato (le “colonne”), accedere solamente agli oggetti che lui ha creato (le “righe”) e leggere alcune tabelle senza la possibilità di modificarle, tutto questo identificando l’utente tramite l’autenticazione di Cognito. Cosa vuol dire? Che ha un sistema di sicurezza per l’accesso CRUD già servito, senza dover mettersi a fare questi controlli via codice, è già fatto! È AWS a garantirti questa sicurezza, pensi di essere TU sviluppatore a fare meglio del team di 100 sviluppatori Amazon? Potresti, ma nel 99% dei casi starai tralasciando qualche vulnerabilità.

Si può dire tutto sull’ecosistema AWS a seconda dei pareri, che sia scomodo, costoso, ostico, complicato ma non si può dire che non sia sicuro, quindi ok, delego a loro questa parte.

Nota a margine: una volta autenticato un utente con Cognito può accedere a qualsiasi risorsa AWS limitandone l’accesso e operazioni tramite le IAM Role, non tutte le risorse supportano una granularità come DynamoDB ma già solo per l’upload di file il servizio S3 può farlo (pensate all’avatar dell’utente per esempio, comodo!).

Logica server-side

Mi serve? Cosa sto facendo che devo nascondere all’utente? Se già so che lui accederà solo alle sue righe, al massimo, sta spaccando la formattazione dei suoi dati, beh, non potrà visualizzarli correttamente.

Sicuro prima o poi ci sarà la necessità di chiamare servizi terzi per aggiungere qualche funzionalità, non c’è problema, con Serverless Framework è davvero semplicissimo creare funzioni Lambda dove poter fare tutto quello che si vuole, gli eventi che possono scatenare questa logica custom sono infiniti: dalla creazione o modifica di oggetto in DynamoDB all’upload di un file su S3, perfino tutto il flusso di autenticazione/registrazione/conferma degli utenti Cognito, non serve altro.

Servizio fruibile da terzi

Serve già? Abbiamo già dei partner che chiedono di poter sfruttare i nostri servizi? se la risposta è sì, è possibile creare tutto il comparto API che si vuole con Serverless (qui la guida) creando solo gli endpoint che servono, altrimenti si può saltare benissimo questa parte.

API Gateway è un servizio di AWS che permette di organizzare endpoint di API e manipolare parametri

Quindi, API?

No, non mi servono per il momento. Ho voluto dare un’altra possibilità ad Amplify e uso direttamente il client per accedere a DynamoDB e usare le risorse AWS che mi servono. Molto pulito e davvero semplice da usare.

In ogni caso per l’autenticazione all’endpoint delle API si può usare sia direttamente Cognito (utile nel caso vengano chiamate dalla stessa applicazione che serve il login) delle chiavi fisse e un access key generato dall’endpoint OAuth2 servito da Cognito. Questa ultima opzione rende molto professionale l’endpoint utilizzando la sicurezza di un accesso OAuth2 e permette di configurare degli scope custom per regolare l’accesso alle risorse.

Il frontend

Messo in piedi il comparto backend è venuto il momento di cominciare a creare la parte di frontend. Non ho avuto molti dubbi sulla tecnologia da usare, avendo abbastanza esperienza con Vue.js ho deciso di scegliere questo framework.

Grazie alla Vue CLI 3 è stato immediato lo startup del progetto

vue create myapp-frontend

e seguendo la procedura guidata ho scelto una configurazione molto semplice e poi salvata come preset. La configurazione da inserire nel file .vuerc nella vostra home dell’utente è

{
 "packageManager": "npm",
 "useTaobaoRegistry": false,
 "presets": {
   "vue-router-vuex": {
     "useConfigFiles": true,
     "plugins": {
       "@vue/cli-plugin-babel": {},
       "@vue/cli-plugin-pwa": {},
       "@vue/cli-plugin-eslint": {
         "config": "base",
         "lintOn": [
           "commit"
         ]
       }
     },
     "router": true,
     "routerHistoryMode": true,
     "vuex": true,
     "cssPreprocessor": "stylus"
   },
 }
}

una cosa salta all’occhio, perché stylus? È stata una scelta guidata dalla libreria di componenti che ho voluto utilizzare: Vuetify. L’ho già usato per qualche progetto e mi sono sempre trovato bene, lo trovo molto completo per il quantitativo di componenti e, cosa molto importante, davvero molto personalizzabile.

Anche qui, grazie alle Vue CLI è bastato un comando per aggiungere questa libreria insieme ad alcune configurazioni di esempio

vue add vuetify

Amplify JS

Avendo Amplify una gestione “à la carte” ho installato direttamente quello che mi serviva come l’autenticazione

npm install @aws-amplify/auth --save

Vuex

Ho voluto utilizzare il modulo Vuex per un semplice motivo organizzativo in modo da poter gestire l’accesso ai dati in DynamoDB attraverso lo state di Vuex. Lo consiglio vivamente anche perchè se un giorno si volesse cambiare totalmente l’accesso ai dati basterebbe modificare i moduli dello state creati per DynamoDB senza dover toccare il resto dell’applicativo.

Utilizzando la gestione dei moduli di Vuex ho creato moduli separati per l’autenticazione, l’accesso ai dati e la gestione delle credenziali per le risorse di AWS. Come detto in precedenza ho utilizzato lo stesso Amplify JS per un fattore di comodità, non è necessario avere il progetto backend per essere utilizzato, si può collegare a risorse create anche con altri framework oppure già esistenti.

Ho creato quindi un modulo Vuex per gestire l’accesso alla libreria di Amplify e configurarla

import Amplify, { Auth } from 'aws-amplify';
import AWS from 'aws-sdk';

export default function AmplifyStore(configuration) {

 Amplify.configure({
   Auth: {
     identityPoolId: configuration.IdentityPoolId,
     region: configuration.Region,
     userPoolId: configuration.UserPoolId,
     userPoolWebClientId: configuration.ClientId,
     mandatorySignIn: true
   },
   Analytics: {
     disabled: true,
   }
 });

 return {
   namespaced: true,
   state: {
     auth: Auth,
     aws: AWS,
     configuration,
   },
 };
}

Amplify di suo non espone il client di DynamoDB quindi ho dovuto autenticare l’SDK di AWS, quindi in un modulo Vuex separato ho gestito questi client esterni ad Amplify

import AWS from 'aws-sdk';

export default {
 loadClients({ commit, rootState }) {
   return new Promise((resolve, reject) => {
     rootState.amplify.auth.currentCredentials()
       .then(credentials => {
         const docClient = new AWS.DynamoDB.DocumentClient({
           apiVersion: '2012-08-10',
           credentials: rootState.amplify.auth.essentialCredentials(credentials)
         });
         commit('setDynamoDBClient', docClient);

         // more clients can be initialized here

         resolve();
       })
       .catch(error => {
         reject(error)
       });
   });
 },
};

Ora comodamente posso gestire l’accesso ai dati con specifici moduli Vue separati

// ...
   async getTodo({ commit, state, rootState }, todoId) {
       const response = await rootState.aws.dynamoDB.get({ //rootState.aws is the AWS clients module
           TableName: 'Prod-Todos',
           Key: {
             id: todoId,
             userId: rootState.auth.user.id  //rootState.auth is the Amplify auth wrapper
           },
       }).promise();

       if (response.Item) {
           commit('pushTodo', response.Item);
       }

       return response.Item; // return raw Todo object
   },
// ...

Webpack

Ho trovato però un punto dolente, come dipendenza vi è ovviamente aws-sdk: l’SDK di AWS per l’accesso alle sue risorse. Questo pacchetto non ha una gestione modulare delle API quindi installandolo vengono aggiunti 35Mb nel node_modules, 2Mb compressi in gzip che ovviamente dovrete servire al client browser.

Eseguendo la compilazione dell’applicativo si nota subito il problema: webpack vendor chunk

parliamo di un file di 4Mb di asset, inaccettabile per avere un frontend leggero e veloce anche sfruttando una CDN come CloudFront, infatti la compilazione ci segnala che può impiegare 4.5s con una rete 3G veloce.

Andando ad analizzare cosa c’è in quel file di chunk si nota subito quanto incide l’SDK di AWS (insieme anche l’SDK di PayPal) webpack vendor chunk analisi Ho fatto alcuni tentativi separando i moduli però il risultato era lo stesso, la soluzione che ho adottato è stato configurare Webpack per spezzare i file in molti e più piccoli chunk in modo da sfruttare l’HTTP2 di CloudFront per servire rapidamente i file di assets.

Ho aggiunto la configurazione splitChunks nel file di configurazione vue.config.js

module.exports = {
 lintOnSave: false,
 productionSourceMap: false,
 configureWebpack: {
   optimization: {
     splitChunks: {
       minSize: 10000,
       maxSize: 250000,
     }
   }
 }
}

Lanciando di nuovo la compilazione il risultato è stato questo: webpack vendor chunk in file mutipli molti più file con dimensioni piccolissime quindi molto più rapidi da scaricare in parallelo.

Deploy

Al momento ho sviluppato l’applicazione testandola in locale grazie alla funzionalità della Vue CLI di servire, compilare sul momento i file modificati e ricaricare la pagina del browser quando necessario. Lo step successivo è quello di rendere fruibile l’applicazione a tutto il mondo, dal momento che è un sito statico capita a fagiolo per l’ecosistema Serverless.

Pipeline

Il servizio di AWS in questione è S3, semplice da utilizzare e con grandi prestazioni, ciò che serve è una pipeline che venga scatenata al commit nel repository GIT con il compito di compilare e copiare il risultato all’interno del bucket S3 scelto per ospitare l’applicazione.

Essendo un utilizzatore di Gitlab posso facilmente creare questa procedura aggiungendo al progetto il file .gitlab-ci.yml con questa configurazione:

# Stages

stages:
 - build
 - deploy

# Configurations

variables:
 AWS_DEFAULT_REGION: 'eu-west-1'
 AWS_BUCKET: 's3://my-bucket-name/'

# Build

build:
 stage: build
 image: node:8.10.0
 cache:
   key: "$CI_JOB_STAGE-stage"
   paths:
     - node_modules/
 only:
   - master
 artifacts:
   expire_in: 1 week
   paths:
   - dist/
 script:
   - npm install
   - npm run build

# Deploy

deploy:
 stage: deploy
 image: fstab/aws-cli
 only:
   - master
 dependencies:
   - build
 environment:
   name: staging
   url: https://my-bucket-name.s3-website-eu-west-1.amazonaws.com/
 script:
   - aws s3 sync --acl public-read --delete ./dist $AWS_BUCKET

Si può notare che gli unici step di build sono l’installazione di moduli npm

npm install

e successivamente il lancio dello script npm per la compilazione

npm run build

semplicemente quello che già stiamo facendo in locale.

La pipeline verrà scatenata automaticamente ad ogni commit sul ramo master, come specificato all’interno di ogni step

only:
 - master

Sito web statico su S3

Adesso che abbiamo i file compilati all’interno del nostro bucket S3 è necessario abilitare questa funzionalità. Per fare questo basta andare nella console AWS, selezionare il servizio S3, selezionare il nostro bucket, selezionare la scheda Properties e abilitare la funzionalità Static website hosting spuntando Use this bucket to host a website.

A questo punto viene richiesto di selezionare il documento di indice (nel nostro caso index.html) e quello di errore, essendo tutto il routing gestito da Vue router possiamo utilizzare direttamente index.html oppure nel caso si voglia stilare una pagina di errore personalizzata cambiarla con il documento necessario.

Navigando ora l’URL <bucket-name>.s3-website-<AWS-region>.amazonaws.com potremo vedere la nostra applicazione perfettamente funzionante. Si può però notare che se navighiamo un path specifico senza passare prima dalla root del sito risponderà la pagina di errore (se personalizzata) oppure l’applicativo con la pagina corretta ma con un codice di errore HTTP (403). Questo perché all’interno del bucket non c’è la cartella richiesta dal path.

Per ovviare a questo problema torniamo nella configurazione Static website hosting questa volta aggiungendo nella textarea Edit Redirection Rules questo

<RoutingRules>
 <RoutingRule>
   <Condition>
     <HttpErrorCodeReturnedEquals>403</HttpErrorCodeReturnedEquals>
   </Condition>
   <Redirect>
     <HostName>my-bucket-name.s3-website-eu-west-1.amazonaws.com</HostName>
     <ReplaceKeyPrefixWith>#!/</ReplaceKeyPrefixWith>
   </Redirect>
 </RoutingRule>
</RoutingRules>

in questo modo il path verrà riscritto utilizzando l’hashtag come prefisso e verrà elaborato dal Vue router. Vedremo adesso l’applicazione funzionare a dovere.

Ottimizzazione del sito statico

Il prezzo del servizio di S3 è basato su quanto spazio utilizziamo e quante richieste di accesso facciamo ai nostri file, riguardo al primo parametro non c’è problema, è molto leggera un’applicazione web statica, riguardo il secondo parametro può essere un problema.

Per ottimizzare questo costo e migliorare la velocità del sito è meglio utilizzare CloudFront per servire il contenuto tramite le Edge location, ovvero server sparsi per il mondo dove il contenuto verrà replicato, questo consente agli utenti di scaricare i contenuti dal server più vicino a loro. Riguardo al costo il prezzo di CloudFront si basa sul traffico che viene servito ed è inferiore e meglio ottimizzato rispetto a pagare per le richieste, essendo il sito web molto leggero si vedrà la differenza nella fatturazione.

Andiamo quindi a creare una distribuzione web CloudFront dalla console di AWS, tra le mille impostazioni mi soffermo sulla prima: Origin Domain Name. Qui è possibile selezionare il nostro bucket dal menù a tendina, bisogna fare attenzione a questo aspetto, collegandolo in questo modo non funzionerà il sistema di routing di S3 quindi nel caso navigassi un path come /my-page CloudFront non sarà in grado di andare a prendere il file /my-page/index.html come faceva S3.

Nel nostro caso non è un problema perchè abbiamo un solo index.html all’interno della root del sito quindi possiamo benissimo usare questo metodo e andiamo ad impostare Default Root Object come index.html (questo vale solo per il path /). E per il problema del routing? La soluzione più semplice è modificare la configurazione all’interno della tab Error Pages della nostra distribuzione CloudFront (sarà visibile dopo la creazione) per gli errori di 403 e 404 index.html. Non è una soluzione pulita, personalmente in questo caso preferisco utilizzare come soluzione quella di impostare come origin HTTP direttamente l’endpoint del sito web statico servito da S3: <bucket name>.s3-website-<region>.amazonaws.com).

Preparativi pre-lancio

A questo punto della configurazione sono soddisfatto del risultato, l’applicazione funziona bene, il tempo di caricamento dell’applicativo rimane sotto il secondo, l’accesso ai dati in DynamoDB è sicuro e rapidissimo (60/70ms) e cosa più importante di tutte:

fino a questo punto ho speso 0$

essendo pochi gli accessi sono sempre rimasto nel free tier di AWS che ti consente, per un anno, di utilizzare gratuitamente i servizi con delle soglie veramente molto alte, l’applicazione potrebbe girare in versione beta con pochi utenti con il costo sempre a 0. Alla fine di questo anno di prova le soglie gratuite si abbassano ma comunque, nella fase di test e staging, è davvero difficile raggiunge 2$ di fatturazione.

Cosa manca però prima di poter aprire il servizio al mondo?

Consumo delle risorse

L’architettura Serverless dà un gran vantaggio: lo scaling automatico delle risorse. Questo vuol dire che se il vostro servizio funziona con 10 utenti, funzionerà anche con 1.000 e con 1.000.000 di utenti senza problemi, basta avere un bel platfond configurato per la tua carta di credito. CloudFront non ha problemi se il carico di utenti aumenta, di Cognito, essendo un servizio gestito, non ce ne preoccupiamo, DynamoDB invece può essere configurato sia con Throughput Capacity fisso sia in modalità autoscaling dove da solo aumenterà e diminuirà questo valore.

La prima preoccupazione che mi è venuta in mente è stata: quindi non ho controllo su questo scaling? serverless consumo di risorse

La prima cosa che si può fare per controllare lo scaling e il consumo di risorse è impostare un Throughput Capacity di DynamoDB fisso in modo che nel caso il traffico aumenti più del previsto le richieste vengano messe in throttle, ovvero ignorate. L’SDK di AWS in maniera automatica ritenterà la richiesta causando un rallentamento del frontend, forse più accettabile che dover pagare delle fatture pesantissime di AWS senza avere ancora i fondi necessari per coprirle.

Metriche, metriche e metriche

La cosa più importante è tenere sotto controllo le metriche di CloudWatch per servizi critici come Lambda, DynamoDB, API Gateway, CloudFront e aggiungere degli allarmi per venire allertati via mail nel caso qualcosa non stia funzionando correttamente.

È possibile creare delle metriche di CloudWatch personalizzate basate sui log applicativi salvati in CloudWatch Logs, questo è molto comodo per avere una visione migliore di specifiche funzioni dell’applicazione. In automatico tutti i log delle funzioni Lambda vengono salvati in stream di CloudWatch Logs. Una funzionalità molto carina di CloudWatch è quella di poter creare delle dashboard personalizzate dove inserire i grafici, cambiare i colori, aggiungere degli appunti e tanto altro. Attenzione che ogni dashboard creata costerà 3$ al mese, quindi andateci cauti.

Profilazione e analisi

Nel caso l’applicazione lavori molto sulle funzioni Lambda per rispondere alle API, elaborare messaggi nelle code SQS, reagire agli eventi di DynamoDB è bene tenere monitorata questa parte il più possibile. Bisogna ricordare la regola principale: tante funzioni e rapide. È meglio eseguire molte funzioni della durata di pochi millisecondi che poche della durata di alcuni minuti, questo farebbe lievitare molto velocemente la fattura di AWS.

Ho fatto un esperimento attivando X-Ray (un servizio di analisi e debug) alle mie funzioni lambda. Usando il framework Serverless è stato davvero semplice, ho installato il plugin serverless-plugin-tracing con npm

npm install --save-dev serverless-plugin-tracing

e poi aggiunto al progetto

plugins:
 - serverless-plugin-tracing

Utilizzando all’interno delle Lambda l’SDK di AWS, quest’ultima va instrumentalizzata modificando l’inclusione del modulo in

const XRay = require('aws-xray-sdk');
const AWS = XRay.captureAWS(require('aws-sdk'));

senza apportare altre modifiche al codice, in un attimo, mi sono ritrovato tutta l’analisi dell’applicazione in maniera grafica e ben ordinata all’interno della console di AWS.

Questo è un esempio del risultato: aws x-ray

Allarmi sulla fatturazione

Non cominciate a usare nessun servizio di AWS prima di aver configurato gli allarmi sulla fatturazione, la trovo una funzionalità vitale quella di essere avvisati in caso i costi cominciano a lievitare. Potete trovare la guida ufficiale di AWS che spiega come fare qui.

Concludendo

Ho impiegato almeno un paio di settimane non lavorando con continuità, ovviamente ci sono stati molti cambi di rotta ed esperimenti. Spero quindi di avervi messo sulla strada giusta nel caso vogliate provare questa esperienza per qualche progetto minore in modo da prenderci la mano. Sicuramente vale la pena provarci, sia che abbiate oppure no tutte le conoscenze architetturali, questi servizi Serverless servono anche per sopperire al gap di conoscenza, in alcuni ambiti, di gestione server e delegare a provider cloud questa parte.

Buon divertimento!

Fabio Gollinucci