#6 Couchbase Server: integrare Couchbase al tuo backend serverless

Hai bisogno di utilizzare Couchbase come database nel tuo backend serverless? O vuoi semplicemente cimentarti nel fare un POC?
Scopri Couchbase Server e come usare AWS Lambda con MOLO17.

Introduzione a Couchbase Server

In questo articolo integreremo Couchbase in un backend serverless, utilizzando il framework serverless con Node.js, docker e AWS Lambda Layer, grazie a Couchbase Server.
Come progetto creeremo delle API REST CRUD che ci permetteranno di effettuare le varie operazioni sugli oggetti di una entità contatto.

Un contatto sarà formato da:

  • Nome – obbligatorio, con lunghezza massima di 64 caratteri
  • Cognome – obbligatorio, con lunghezza massima di 64 caratteri
  • Email – opzionale, con lunghezza massima di 64 caratteri e deve essere formattata correttamente

Il progetto di esempio è disponibile su GitHub.

SDK Couchbase

Per procedere utilizziamo la versione 2.6.11 dell’SDK Node.js. L’SDK non è scritto interamente in javascript, il core è infatti scritto in C. Questo è un punto critico del nostro progetto, in quanto AWS Lambda gira su una distribuzione Linux di AWS.

Io ad esempio, sto usando un MacBook Pro e se provassi ad utilizzare le mie dipendenze anche sulle stesse Lambda, queste ultime lancerebbero l’eccezione var/task/node_modules/couchbase/build/Release/couchbase_impl.node: invalid ELF header.
Per ovviare a questo problema dobbiamo installare l’SDK in un runtime simile a quello supportato dalle Lambda: su questo tema ci viene fortunatamente incontro docker!

Non preoccupatevi, perchè nella repository troverete tutti gli script e le utility necessarie per ridurre al minimo le vostre operazioni.

Potete infatti trovare il seguente Dockerfile.

FROM ideavate/amazonlinux-node:12
WORKDIR /couchbase
COPY package.json ./
COPY package-lock.json ./
VOLUME /couchbase/node_modules
CMD [ "npm", "install" ]

Qui non facciamo altro che utilizzare un’immagine base “amazonlinux” con Node.js 12. Copiamo package.json e package-lock.json e dichiariamo il volume per i “node_modules”. In seguito basta eseguire npm install all’avvio del container.

Una volta effettuata la build dell’immagine, possiamo far eseguire un docker run. Ricordiamoci di montare l’apposito volume specificando come sorgente locale la cartella “nodejs/node_modules” e come destinazione “/couchbase/node_modules”. In questo modo avremo tutte le dipendenze dell’SDK nella cartella “nodejs/node_modules” che verranno poi utilizzate per creare un Lambda layer.

NB: per praticità è possibile eseguire npm run-script build e npm run-script install-couchbase all’interno della cartella “layers/couchbase”. Si occuperanno rispettivamente di creare l’immagine e di lanciare il container montando il volume apposito.

Framework Serverless

Come anticipato,  utilizzeremo il framework serverless, che ci permette di definire, impacchettare e rilasciare le nostre Lambda in tutta velocità.

Una volta installato globalmente con npm install --global serverless, possiamo configurare le credenziali di accesso al nostro account AWS con il comando sls config credentials --provider aws --key access_key --secret secret_access_key: access_key e secret_access_key sono le credenziali programmatiche create appositamente dalla console.
Per generare le credenziali puoi seguire il seguente tutorial.

Tutorial – How to generate AWS credentials

Lambda Layer

Ora che abbiamo i nostri “node_modules” con l’SDK Couchbase pronto all’uso, passiamo alla creazione del nostro layer.

In primis, che cosa sono i Lambda Layer? Perché li usiamo in questo progetto?

AWS a fine 2018 ha annunciato questo nuovo servizio all’interno di AWS Lambda. Ciò permette di creare e rilasciare dei pacchetti in modo separato dalle Lambda. Ci da l’opportunità di condividere librerie, runtime custom o altre dipendenze tra diverse Lambda in diversi account e/o globalmente.

Nel nostro caso ci aiuta a limitare e unificare le operazioni necessarie all’installazione dell’SDK Couchbase, che come abbiamo visto non risulta così immediata.

Utilizzando il framework serverless, la definizione del Lambda Layer risulta veramente semplice. Basterà aggiungere al nostro serverless.yml.

[...]
layers:
  couchbase-node-sdk-2-6-11:
    path: couchbase
    compatibleRuntimes:
      - nodejs12.x
[...]

Per poter referenziare all’interno dei vari stack CloudFormation il nostro layer, aggiungiamo la seguente definizione:

[...]
resources:
  Outputs:
    CouchbaseNodeSdk2611LayerExport:
      Value:
        Ref: CouchbaseDashnodeDashsdkDash2Dash6Dash11LambdaLayer
      Export:
        Name: couchbase-node-sdk-2-6-11-layer

La definizione ci permetterà di utilizzare la variabile couchbase-node-sdk-2-6-11-layer per ottenere l’identificativo del layer anche in altri stack.

In precedenza abbiamo creato la cartella “nodejs” con all’interno i “node_modules”. Questa scelta non è affatto casuale. In questo modo le nostre funzioni potranno importare le dipendenze normalmente come se fossero installate localmente. AWS infatti estende automaticamente il path dei node_modules a “/opt/nodejs/node_modules“.

API CRUD

Passiamo quindi alla definizione delle nostre API CRUD:

  • POST /: per creare un nuovo contatto
  • GET /{contactId}: per recuperare un contatto
  • PUT /{contactId}: per aggiornare un contatto
  • DELETE /{contactId}: per cancellare un contatto

Utilizzo di Lambda Layer

Per recuperare il layer creato in precedenza usiamo la funzione import nel nostro serverless.yml come segue.

[...]
custom:
  couchbase-sdk-layer:
    Fn::ImportValue: couchbase-node-sdk-2-6-11-layer
[...]

Ora che abbiamo recuperato il layer, possiamo aggiungerlo ad una Lambda.

[...]
functions:
  create:
    handler: src/create.default
    layers:
      - ${self:custom.couchbase-sdk-layer}
    events:
[...]

Creazione di contatto in Couchbase Server

Per effettuare la creazione di un nuovo contatto, definiamo l’endpoint sul serverless.yml.

[...]
functions:
  create:
    handler: src/create.default
    layers:
      - ${self:custom.couchbase-sdk-layer}
    events:
      - http:
          method: post
          path: /
          cors: true
[...]

Quindi passiamo al codice vero e proprio, andando ad implementare la funzione create.

Nell’esempio vengono effettuate le seguenti operazioni:

  • Connessione al cluster Couchbase
  • Apertura del bucket
  • Validazione del payload
  • Inserimento del documento su Couchbase

Le operazioni di connessione al cluster e apertura del bucket vengono effettuate durante la fase di inizializzazione della funzione e non durante la sua esecuzione. Instaurare una connessione con Couchbase prevede diversi scambi di messaggi TCP. Di conseguenza eseguire tali operazioni ad ogni richiesta appesantirebbe e rallenterebbe la funzione stessa.

[...]
cluster.authenticate(CLUSTER_USERNAME, CLUSTER_PASSWORD);
const bucket = cluster.openBucket(BUCKET);
bucket.on("error", error => {
  console.log("Error from bucket:", JSON.stringify(error, null, 2));
});
[...]

Da notare che nell’implementazione della funzione la proprietà callbackWaitsForEmptyEventLoop del context viene impostata a false

[...]
const createHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;
[...]

Questo è necessario per restituire subito il risultato della funzione senza aspettare che eventuali operazioni dell’event loop di node vengano completate. Nel nostro caso alcune operazioni di connessione al bucket potrebbero mandare la funzione in timeout.

Vengono subito effettuate le validazioni del payload. Se il risultato è negativo, la funzione tornerà un 400 Bad Request indicando il campo non valido.

Una volta passate le validazioni, viene preparato il documento da storicizzare su Couchbase, aggiungendo la proprietà type Contract. Inoltre viene calcolato l’id del documento concatenando il tipo con il separatore :: e un UUID generato. Queste due operazioni sono una buona pratica di modellazione in Couchbase. Permettono sia di eseguire query specifiche per tipologia di documento, sia di migliorare la leggibilità di questi.

[...]const document = {
  id,
  name,
  surname,
  email,
  type: "Contact”};
const documentId = `contact::${id}`;
[...]

Infine per inserire il documento su Couchbase utilizziamo il metodo insert dell’SDK.

const documentInserted = await new Promise((resolve, reject) => {
  bucket.insert(documentId, document, (error, result) => {
    if (error) {
      reject(error);
    } else {
      resolve({ ...result, ...document });
    }
  });
});

Recupero di un contatto in Couchbase Server

Anche la funzione di recupero del contatto segue le stesse logiche della creazione. Troveremo quindi anche qui una definizione dell’endpoint, la connessione a Couchbase e al bucket e la disabilitazione dell’attesa dello svuotamento dell’event loop paritetici.

Per eseguire il recupero del documento utilizziamo il metodo get dell’SDK.

[...]const document = await new Promise((resolve, reject) => {
  bucket.get(documentId, (error, result) => {
    if (error) {
      reject(error);
    } else {
      resolve(result.value);
    }
  });
});[...]

Nel caso in cui il documento non esista, verrà restituito un errore con codice 13. Possiamo poi ritornare un errore 404 Not Found.

Aggiornamento di un contatto in Couchbase Server

Stesso iter per l’aggiornamento di contatto. Quindi anche qui: una definizione dell’endpoint, la connessione a Couchbase e al bucket, la disabilitazione dell’attesa dello svuotamento dell’event loop e la validazione del payload paritetici.

Per eseguire l’aggiornamento del documento utilizziamo il metodo upsert dell’SDK.

[...]const documentUpdated = await new Promise((resolve, reject) => {
  bucket.upsert(documentId, document, (error, result) => {
    if (error) {
      reject(error);
    } else {
      resolve({ ...result, ...document });
    }
  });
});[...]

Nel caso in cui il documento non esista ne verrà creato uno nuovo, in quanto il metodo upsert inserisce o aggiorna un documento.

Cancellare un contatto in Couchbase Server

Seguiremo infine sempre lo stesso procedimento anche per la funzione di cancellazione del contatto. Ripetiamo la definizione dell’endpoint, la connessione a Couchbase e al bucket e la disabilitazione dell’attesa dello svuotamento dell’event loop paritetici.

Per la cancellazione del documento utilizziamo il metodo remove dell’SDK.

[...]
const document = await new Promise((resolve, reject) => {
  bucket.remove(documentId, (error, result) => {
    if (error) {
      reject(error);
    } else {
      resolve({ contactId, ...result });
    }
  });
});
[...]

Come per il recupero, nel caso in cui il documento non esista, verrà restituito un errore con codice 13, verrà di conseguenza ritornato un errore 404 Not Found.

Conclusioni

In questo articolo abbiamo visto come integrare Couchbase Server in un backend serverless, semplificando le operazioni durante lo sviluppo utilizzando il framework serverless e AWS Lambda Layer.

Ti è piaciuto questo articolo?
Leggi anche gli altri articoli della serie Scopri Couchbase.

conosciamo solo un quinto di ciò che vive in profondità. tutto il resto è da esplorare.

coderspace sta arrivando.