Déployer un jeu à boire sur le Edge avec Nuxt-hub
En fait le titre de ce billet aurait dû être :
Comment utiliser Cloudflare D1 et Cloudflare KV avec nuxt-hub pour déployer un jeu à boire sur Cloudflare worker ?
Mais ça faisait un peu long. Et pour beaucoup de gens, ce titre n'aurait eu aucun sens.
Bon, en même temps, le titre actuel n'a pas plus de sens. Mais, jeu à boire, ça va attirer quelques lecteurs.
L'origin story
Ici c'est le moment où je vous explique pourquoi j'ai créé cette application. Vous pouvez aller directement à la partie technique mais, si comme moi, vous aimez comprendre à quoi sert la technique, vous pouvez rester le temps de quelques lignes supplémentaires :)
On dit souvent que les deux problèmes les plus difficiles en informatique sont :
- le nommage des variables
- l'invalidation de cache
Mais, si je peux me permettre, on devrait mettre à jour cette citation.
En 2024, trouver un nom de domaine libre relève de l'exploit. Tout est pris, même des noms qui n'ont ni queue ni tête.
Alors, ça ne veut pas dire que tout a déjà été créé. Beaucoup de ces noms de domaines ne sont même pas utilisés. C'est aussi, et surtout, la faute des squatteurs de domaines.
Bref, c'est un enfer, parce que de très nombreuses personnes se sont dit que la meilleure façon de s'enrichir sur internet c'était d'acheter des noms de domaines au hasard pour espèrer les revendre plus tard.
Je peux pas y faire grand chose, donc autant en rire. J'en ai donc fait un jeu à boire.
Le coin de la technique
Quitte à faire une application vite faite, autant apprendre quelque chose au passage. J'ai donc décidé d'utiliser des fonctionnalités expérimentales de Cloudflare, notamment D1.
Pour ne rien gâcher, j'ai choisi d'utiliser Nuxt-Hub qui donne une couche d'abstraction pour manipuler D1 et le key value store de Cloudflare depuis une application Nuxt.
Les CDN et le Edge
Traditionnellement, un utilisateur qui consulte un site web, va déclencher des appels d'APIs vers un serveur. Ce serveur peut parfois être très éloigné de lui/elle. Et dans ce cas, c'est long.
On appelle ça la latence.
Ici l'appel au site web est rapide. Mais l'appel et la réponse à l'API sont lent. Car l'API est loin de l'utilisateur.
Parce que si vous consultez un site depuis le Japon, mais que pour chaque information affichée sur la page web, il faut aller demander à une base de données présente en France, ça fait quelques kilometres à parcourir.
On estime que chaque aller/retour entre la France et le Japon va faire environ entre 200 et 300ms. S'il y a beaucoup d'appels d'APIs, ça peut rapidement faire très mal et créer une expérience de navigation pas très agréable.
![votre sensation devant l'exécution de la page](/images/drinking-game-nuxt-hub/flash.webp)
Bon, vous pourriez me dire que 300ms ça reste acceptable et c'est assez vrai. Mais ici, je n'ai même pas inclus le temps de traitement, et le temps d'affichage. Sans parler que, il suffit que le réseau ait des soucis ou que l'application web soit un peu mal foutue avec de nombreux appels séquentiels pour que l'expérience se dégrade terriblement.
C'est pour ça que depuis très longtemps, on utilise des CDN pour les ressources statiques : images, scripts js, fonts etc...
Un CDN c'est un réseau de distribution de contenu (content distribution network).
C'est un ensemble de serveur réparti dans le monde qui contient chacun une copie de la ressource à distribuer.
Donc si l'utilisateur qui regarde une image est au Japon, le CDN va lui servir l'image qui est présente sur un serveur au Japon pour optimiser le chargement.
![l'image est prise sur le serveur le plus proche](/images/drinking-game-nuxt-hub/cdn.jpg)
Mais dès qu'on parle de serveurs applicatifs, c'est plus complexe.
Un serveur applicatif va réaliser des traitements. Pendant très longtemps ce type de serveur était donc à un seul endroit, par exemple en France et l'utilisateur au Japon devait accepter d'attendre que la donnée revienne depuis la France.
Sur le principe des CDN, certains, comme Cloudflare ont commencé à proposer de déporter les traitements et les données sur le Edge (la périphérie en francais mais personne n'utilise ce mot).
Ca a un nom, ça s'appelle le Edge computing.
(Le principe est loin d'être nouveau. Dans le domaine de la data ça fait plusieurs années qu'on parle de Data Locality.
DnsDrink et le Edge
Justement dnsdrink est une application Nuxt, déployé sur le Edge.
Ça utilise Cloudflare et plus exactement des workers cloudflare.
Les workers, ce sont des "serveurs", mais pas dans la forme traditionnelle qu'on entend.
Un worker peut exécuter du code sur le point de présence le plus proche de l'utilisateur final pour éviter la latence. C'est ça, le fameux Edge computing.
Ok, mais il reste le souci des bases de données ?
La base D1 sur le Edge
C'est ici qu'interviennent des fonctionnalités de Cloudflare :
- D1, une base de données SQLite dans le Edge
- les key value store
- R2 pour de l'object storage
Le principal souci pour qu'une base de données puisse être distribuée sur le Edge, c'est la cohérence des données.
En lecture seule, c'est facile. On peut répliquer les données partout dans le monde et les serveurs Edge peuvent servir les données locales. Enfin, facile, c'est vite dit. On est bien content de le déléguer à Cloudflare.
En lecture, un utilisateur au Japon de dnsdrink aura le même temps de latence qu'un utilisateur en France pour ces accès à la base de données.
Les écritures, elles, sont "eventually consistent", c'est-à-dire que la donnée est répliquée partout dans le monde mais pas instantanément. C'est un compromis entre la cohérence et la performance.
Si vous voulez comprendre le mécanisme exact, je vous invite à lire leur article de blog à ce sujet.
Sachez que c'est pas magique :
- L'écriture est donc un peu plus lente, mais au bénéfice d'une lecture rapide.
- à un instant t, vous pouvez avoir des données différentes selon le point de présence où vous vous trouvez.
Nuxt-Hub
Nuxt-Hub c'est deux choses :
- une plateforme de déploiement qui permet de piloter la création d'application sur cloudflare, avec la création des bases de données et l'association avec nos applications (le binding)
- un ensemble d'api qui permet d'utiliser les fonctionnalités de cloudflare, D1, R1 et R2 et de simplifier le développement local.
Et c'est l'ensemble de toute ces fonctionnalités que j'ai voulu utiliser avec dnsdrink.
DNSDrink
L'application en elle-même n'est pas très complexe. Vous pouvez consulter son code source sur Github.
La configuration de Nuxt-Hub
Regardons la configuration de nuxt
export default defineNuxtConfig({
modules: ['@nuxthub/core', '@nuxtjs/tailwindcss', '@nuxt/image'],
hub: {
database: true,
kv: true,
blob: true,
cache: true,
},
nitro: {
scheduledTasks: {
'* * * * *': ['schema'],
'0 0 1 * *': ['purge'],
},
experimental: {
tasks: true
}
}
})
On note l'utilisation du module @nuxthub/core
, l'activation des fonctionnalités dans hub
et des tâches nitro.
Les tâches nitro
Les tâches nitro ne sont pas spécifiques à Nuxt-Hub et, pour être honnête, je n'ai pas réussi à les faire fonctionner sur Cloudflare. Elles fonctionnent uniquement sur mon poste local. Sur le principe, il s'agit de planifier des tâches qui s'effectuent à intervalles réguliers, comme par exemple le cleaning de la base de données.
Je n'ai pas spécialement creusé parce que cette application n'a aucune importance particulière pour moi. Mais voici à quoi ressemble une tâche :
export default defineTask({
meta: {
name: 'purge',
description: 'Purge old quota entries from previous month',
},
async run({ payload, context }) {
const db = hubDatabase()
await db.prepare('DELETE FROM quota WHERE strftime(\'%Y-%m\', created_at) < strftime(\'%Y-%m\', \'now\')').run()
return { result: 'Success' }
},
})
Les appels à KV et D1
Dans le fichier index.post.ts vous pouvez voir les exemples d'appels.
async function checkQuotaExceeded() {
const db = hubDatabase()
// check if the number of requests exceeds the quota for this current month
const quota = 9500
const count = await db.prepare('SELECT COUNT(*) as count FROM quota WHERE strftime(\'%Y-%m\', created_at) = strftime(\'%Y-%m\', \'now\')').first('count') as number
if (count >= quota) {
consola.warn('Quota exceeded')
return true
}
return false
}
....
// check if result is already in cache
const responseFromCache = await hubKV().get(domain)
if (responseFromCache) {
return responseFromCache
}
....
// store result in cache
await hubKV().set(domain, summary, { expirationTtl: 60 * 60 * 24 * 30 }) // 30 days
My 2 cents
L'expérience de dev est plutôt agréable. En local, vous avez accès à une base SQLite et un mini key value store donc tout fonctionne de façon très fluide.
Le déploiement sur CF est également très simple.
Je n'ai pas réussi à faire fonctionner les tasks mais je suppose qu'en cherchant un peu, j'aurais pu y parvenir.
Pour des applications full stack sur Cloudflare, c'est une plutôt bonne option. C'est certainement moins abouti en termes de features et d'administration que du Supabase mais ça reste très clean et ça pourrait devenir un concurrent sérieux dans le futur (?).
Pour ma part je ne vais pas abandonner ma stack de prédilection avec du kotlin parce que je reste plus productif. Mais il faut bien comprendre que cette architecture est très peu couteuse. DnsDrink me coute 0. Donc c'est une option sérieuse. Selon mes besoins, je réutiliserais cette stack sans souci. Et peut-être qu'un jour ma stack pourra tourner dans le Edge ?