@VinceOPS

Nest Serverless : Firebase Functions

Your Cat in the Cloud

Nest est mon framework Node.js préféré et aussi mon framework de travail. Je l’ai présenté sur ce blog l’année dernière et l’article est toujours d’actualité si tu veux y jeter un œil. Je l’utilise très souvent dans mes projets Node.js : Rest API, serveur GraphQL, micro-services, CLI, scripts, etc.

Récemment, c’est avec Nest que j’ai décidé de développer quelques Functions. Dans cet article, je vais rappeler ce que sont les Functions puis présenter une façon simple d’y intégrer Nest. En l’occurrence, avec Firebase Functions (Firebase avec des Cloud Functions).


FaaS : Functions As A Service

Les Functions permettent de déployer et d’exécuter du code sur un serveur qui ne nous appartient pas. Zéro minute consacrée à la configuration, la sécurisation et l’entretien des machines. Tout ce qui est demandé au développeur, c’est d’écrire son code en respectant quelques règles (langage, structure, temps d’exécution…), puis de le déployer via une CLI, ou à la main.

💡 Chez Amazon, le service s’appelle AWS Lambda. Chez Google, c’est Cloud Functions, Microsoft a les Azur Functions. Les CloudFlare Workers utilisent directement V8 (pas Node.js), mais l’import de modules NPM est possible et WebAssembly est supporté 🎉.

L’exécution dudit code peut ensuite être déclenchée par un appel HTTP et/ou par un évènement : un fichier déposé sur AWS S3, un utilisateur inscrit dans Firebase Authentication, etc.

Le FaaS offre de nombreux avantages :

  • Un déploiement et une disponibilité quasi instantannés du code
  • Pas de serveur à louer, configurer, maintenir
  • Auto-scaling : la montée en charge (comme la baisse…) est assurée par le service
  • Une tarification à la carte dépendante du nombre d’exécutions, de leur durée, de la mémoire vive allouée

Et présente quelques limitations par nature et/ou fournisseur :

  • Plafond de ressources, d’exécutions simultanées, de durée d’exécution
  • Passé un délai d’inactivité, un délai de démarrage (cold start) est à prévoir à la prochaine exécution

Dessine-moi une Function

On installe firebase-tools pour utiliser la CLI de Firebase, puis on crée le projet :

npm i -g firebase-tools
mkdir my-cat-in-the-cloud && cd my-cat-in-the-cloud
firebase login
firebase projects:list # vérifie la bonne authentification

firebase init functions # crée un projet ou utilise un existant
* Please select an option: Create a new project
* What language would you like to use to write Cloud Functions? TypeScript # 💖
* Do you want to use TSLint to catch probable bugs and enforce style? No # ESLint ftw
* Do you want to install dependencies with npm now? No # on va s'en occuper nous-même

La CLI génère firebase.json et, le plus intéressant, ./functions. Si on étudie rapidement ce dernier avant de l’effacer (#yolo), on voit dans package.json que sont inclus : firebase-admin, firebase-functions (dependencies) et typescript, firebase-functions-test (dev dependencies). Quant à src/index.ts :

import * as functions from 'firebase-functions';

// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript

export const helloWorld = functions.https.onRequest((request, response) => {
  response.send('Hello from Firebase!');
});

Il expose seulement une function helloWorld qui peut être exécutée localement de deux manières :

$ yarn start
✔  functions: Emulator started at http://localhost:5000
i  functions: Loaded functions: helloWorld
firebase > helloWorld()
Sent request to function.
firebase >
RESPONSE RECEIVED FROM FUNCTION: 200, Hello from Firebase!

Dans ce code, pas de framework Web particulier : seulement une fonction onRequest qui prend en paramètre un callback de type Requête-Réponse.

💡 Le fichier firebase.json situé au même niveau que le répertoire ./functions permet la configuration (et le déploiement) des différents services de Firebase (Hosting, Functions, Storage, Firestore…).


Release the Kraken Cat

Comme annoncé plus haut, on va supprimer ./functions ! … Et créer un projet Nest à la place.

all-the-things

rm -rf functions
npm i -g @nestjs/cli # si vous ne l'avez pas déjà installé
nest new api && cd api
yarn && yarn add firebase-functions firebase-admin

Puisque le projet Nest a été nommé api, on change la propriété "source" des "functions" dans firebase.json :

{
  "functions": {
    "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build",
    "source": "api"
  }
}

Le fichier api/src/main.ts doit exposer une Function, que l’on nomme très originalement api :

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as express from 'express';
import * as functions from 'firebase-functions';

import { AppModule } from './app.module';

const startNestApplication = async (expressApp: Express.Application) => {
  const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
  return app.init();
};

const server = express();
startNestApplication(server).catch(err => console.error('Nest did not start', err));

export const api = functions.region('europe-west1').https.onRequest(server);

💡 Notez la configuration de la région avec europe-west1, afin de diminuer la latence entre mes utilisateurs et ma Function, une fois celle-ci déployée.

Ensuite, la CLI de Firebase doit connaître le point d’entrée de notre Function. Il faut ajouter une propriété "main" ayant pour valeur "dist/main.js" dans le package.json généré par @nestjs/cli.

Reste à compiler puis à exécuter !

$ yarn build && firebase serve --only functions
✔  functions: Emulator started at http://localhost:5000
i  functions: Watching "/home/vince/blog/my-cat-in-the-cloud/api" for Cloud Functions...
>  [Nest] 20843 - [NestFactory] Starting Nest application...
✔  functions[api]: http function initialized (http://localhost:5000/my-cat-in-the-cloud/europe-west1/api).
# [...]
>  [Nest] 20903 - [NestApplication] Nest application successfully started +2ms

On vérifie que l’API répond à nos requêtes :

$ curl http://localhost:5000/my-cat-in-the-cloud/europe-west1/api
Hello World! # Ça fonctionne 🎉

Et c’est gagné !

c'est gagné

Pour déployer la Function, il faut indiquer à Firebase quelle version du runtime (Node.js) utiliser via package.json, et utiliser la commande deploy :

  "engines": {
    "node": "10"
  }
$ firebase deploy --only functions
=== Deploying to 'my-cat-in-the-cloud'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@0.0.1 prebuild /home/vince/blog/my-cat-in-the-cloud/api
> rimraf dist


> functions@0.0.1 build /home/vince/blog/my-cat-in-the-cloud/api
> nest build

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing api directory for uploading...
i  functions: packaged api (197.62 KB) for uploading
✔  functions: api folder uploaded successfully
i  functions: creating Node.js 10 (Beta) function api(europe-west1)...
✔  functions[api(europe-west1)]: Successful create operation.
Function URL (api): https://europe-west1-my-cat-in-the-cloud.cloudfunctions.net/api

✔  Deploy complete!

💡 La Console de Firebase offre des metrics et du logging pour chaque Function déployée.

Partant de là, on peut imaginer :

  • Créer de nouveaux controllers/endpoints (e.g. @Post('api/users'), @Get('healthcheck')…)
  • Présenter une documentation Swagger (e.g. api/<route_choise_pour_le_document_Swagger>)
  • Ajouter un endpoint pour GraphQL (e.g. api/graphql)

👮 Néanmoins, l’utilisation d’Express et de Nest entraîne nécessairement un surcoût en terme temps de lancement, de temps d’exécution et de mémoire consommée. Je ne recommande pas de le faire en production sous prétexte que “c’est possible”. L’article Web Frameworks Implication on Serverless Cold Start Performance in NodeJS traîte bien le sujet, bien qu’il soit concentré sur le Cold start. Pour un fonctionnement optimal, il est recommandé de créer plusieurs Functions très spécialisées (et avec un minimum d’overhead), plutôt qu’une seule offrant une API complète.

À vos use cases 😬 !

Credits à @Jeffdelaney23 pour l’exposition de Nest en tant que Function
Credits à Sam pour le pixel-art de Nuage ☁

#nest#serverless#firebase
VinceOPS

Retrouvez-moi sur Twitter 🤷
@VinceOPS