Il mio viaggio nel mondo serverless
Lettura 23 minutiQuante 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 1⁄2 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 5⁄6 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:
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) 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: 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?
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:
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!
Articolo scritto da
☝ Ti piace quello che facciamo? Unisciti a noi!