Créer un raccourcisseur d'URL avec Bref
L’approche serverless est une approche tendance dans la conception d’applications. Son nom est trompeur, c’est une approche de conception d’application pour qu’elle s’exécute uniquement à la demande sans nécessité d’avoir un serveur en ligne tout le temps. On parle également de FaaS.
Par exemple, on peut concevoir une application pour envoyer un message Slack tous les jours ouvrés à 10 h sans nécessiter de serveur avec un cron configuré ou une application qui met à disposition une API pour générer une facture au format PDF sans avoir un serveur web allumé.
Chaque fournisseur Cloud met à disposition une infrastructure dans laquelle on déploie notre application à exécuter à la demande. Voici par exemple une liste des langages supportés par les principaux fournisseurs de services serverless :
Fournisseur Cloud | PHP | JavaScript | Java | Python | Ruby | C# | Go | PowerShell |
---|---|---|---|---|---|---|---|---|
AWS | Oui 1 | Oui | Oui | Oui | Oui | Oui | Oui | Oui |
GCP | Oui | Oui | Oui | Oui | Oui | Oui | Oui | Non |
Azure | Oui 2 | Oui | Oui | Oui | Oui 2 | Oui | Oui 2 | Oui |
Scaleway | Oui | Oui | Non | Oui | Non | Non | Oui | Non |
Cloudflare | Oui 3 | Oui | Non | Oui 3 | Non | Non | Non | Non |
L’intérêt est d’économiser sur les coûts des serveurs en exécutant l’application à la demande et sans devoir supporter la maintenance des serveurs. La facturation est liée uniquement au temps d’exécution de l’application. Il est donc important de faire en sorte que l’application s’exécute le plus rapidement possible.
Cette approche a un avantage dans la gestion de la montée en charge. Elle est supportée automatiquement par l’infrastructure mise à disposition par le fournisseur Cloud dans les limites définies par le fournisseur. Cela ne nécessite pas d’intervention de l’équipe technique pour répondre à une montée en charge ponctuelle. Cependant, il est possible de configurer comment réagir à une montée en charge.
Comme on a pu le voir dans le tableau, le support PHP par les fournisseurs Cloud est minime. Dans le cas d’AWS, qui représente à lui seul 34% du marché cloud au 3ᵉ trimestre 2022 d’après Synergy Research Group, il faut passer par Bref.
Bref est un paquet PHP qui permet de concevoir une application fonctionnant avec une approche serverless sur AWS. Il fournit également le nécessaire à l’exécution de code PHP natif sur AWS. Pour gérer la configuration et le déploiement, Bref s’appuie sur le Serverless framework.
Cette série d’articles va introduire l’utilisation de Bref avec un concept simple d’application : un raccourcisseur d’URL.
Concevoir un raccourcisseur d'URL
Un raccourcisseur d’URL consiste à permettre de communiquer une adresse URL longue sous une forme plus réduite. Son emploi est particulièrement utile sur les réseaux sociaux où la limite de caractères pour les messages peut être plus ou moins faible. Réduire le nombre de caractères pour partager un lien permet de proposer plus de contenu lors de la publication.
Prenons par exemple le lien de cet article pour illustrer l'intérêt.
- Lien original (76 caractères)
-
https://hephaist.io/blog/2023/03/01/creer-un-raccourcisseur-d-url-avec-bref/
- Lien raccourci (26 caractères)
-
https://link.test/FI7mvMOU
Grâce à sa forme raccourcie, il sera possible d'économiser 50 caractères dans le décompte total du message à mettre en ligne. C'est 2 fois le mot le plus long de la langue française.
Le projet de raccourcisseur d'URL va consister en 2 fonctionnalités :
- Rediriger le visiteur du lien raccourci vers l’adresse de destination
- Permettre d’enregistrer des liens raccourcis avec des identifiants à 8 caractères
Dans ce premier article, on va se focaliser sur la gestion de la redirection du visiteur vers l’adresse de destination.
Initialiser le projet
Pour commencer, on va d'abord installer Bref qui nous permettra d'exécuter notre fonction serverless avec AWS Lambda. On installe le package via Composer :
composer require bref/bref
Ensuite, on utilisera la commande d’initialisation de Bref afin de démarrer le projet. On
choisira l’option Event-driven function
pour pouvoir traiter directement les événements transmis par
AWS EventBridge.
vendor/bin/bref init
La commande initialisera alors les fichiers nécessaires à l'exécution du projet à l'image de ce qui pourrait être fait avec Symfony Flex.
Nous avons maintenant un fichier PHP index.php
contenant la fonction traitant l'événement
déclencheur et le fichier serverless.yml
pour configurer le déploiement avec
Serverless Framework.
index.php
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
return function ($event) {
return 'Hello ' . ($event['name'] ?? 'world');
};
serverless.yml
service: app
provider:
name: aws
region: eu-east-1
runtime: provided.al2
deploymentMethod: direct # fastest deployment method
plugins:
- ./vendor/bref/bref
functions:
hello:
handler: index.php
description: ''
layers:
- ${bref:layer.php-81}
# Exclude files from deployment
package:
patterns:
- '!tests/**'
us-east-1
. Il faut penser à modifier cela avant de déployer la fonction.On va renommer le service demo-bref-link-shortener
à la place de app
. C'est le nom qui sera utilisé dans toutes les
ressources créées lors du déploiement sur AWS : Bucket S3 de déploiement, Stack
CloudFormation, etc.
On est prêt à travailler sur notre raccourcisseur d'URL.
Mettre en place la redirection
Lorsqu’un visiteur va consulter un lien raccourci, c’est le service AWS API Gateway qui sera responsable de traiter la requête HTTP. Il sert de proxy aux autres services AWS.
Dans notre cas, il va notifier qu'un appel HTTP a eu lieu en diffusant un événement au service AWS EventBridge. C'est un service proposant un bus d’événement sur lequel les services AWS peuvent diffuser ou écouter des événements liés à des services AWS, des événements provenant de services tierce partie ou des événements personnalisés. Grâce à un système de règles, on peut notamment invoquer une fonction Lambda.
La fonction générée par la commande d’initialisation est pour l’instant agnostique de la source d’événements. Cependant, cela n’est pas le plus pratique. Heureusement, Bref propose les Typed handlers qui sont spécialisés par source d’événements.
Dans notre cas, pour les HTTP events, on va tout simplement implémenter la PSR-7 et la PSR-15. Au lieu de retourner une fonction anonyme, on va retourner une instance de classe anonyme implémentant les 2 recommandations.
<?php
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
return new class implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getQueryParams()['name'] ?? 'world';
return new Response(200, [], sprintf('Hello %s', $name));
}
};
Maintenant, on va modifier notre handler pour qu’il redirige l’utilisateur vers l’URL cible en fonction du lien
consulté. Pour récupérer le lien consulté, il faut passer par la requête pour récupérer les informations de l’URL grâce
à l’interface UriInterface
.
<?php
//...
return new class implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$path = $request->getUri()->getPath();
// ...
}
};
On utilisera dans un premier temps un fichier JSON pour stocker la liste des liens raccourcis existant et qui sera déployé avec le code de la fonction Lambda. Il prendra la forme suivante :
links.json
{
"/FI7mvMOU": "https://hephaist.io/blog/2023/03/01/creer-un-raccourcisseur-d-url-avec-bref/"
}
Nano ID
avec le package hidehalo/nanoid-php
.Pour lire le fichier JSON, on s’appuiera sur la librairie Psl pour effectuer l’ensemble des opérations de manière fiable et bénéficier d’un typage fort.
<?php
//...
return new class implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$path = $request->getUri()->getPath();
$fileContent = \file_get_contents('./links.json');
if (false === $fileContent) {
throw new \RuntimeException('The file "links.json" is missing.');
}
$registeredLinks = \Psl\Json\typed(
$fileContent,
\Psl\Type\non_empty_dict(
\Psl\Type\non_empty_string(),
\Psl\Type\non_empty_string()
)
);
$targetLocation = $registeredLinks[$path];
// ...
}
};
Maintenant, il faut rediriger l’utilisateur vers sa destination. Pour cela, on va retourner une réponse avec le code HTTP 302 et l’en-tête Location qui contiendra l’URL de destination.
<?php
// ...
return new class implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
//...
return new Response(302, ['Location' => $targetLocation]);
}
};
Pour tester le fonctionnement en local de notre fonction, on va utiliser la commande local
disponible avec
Bref pour simuler un événement. Pour cela, on va lui passer un fichier JSON contenant
l’événement HTTP API Gateway. Voici une version simplifiée contenant l’essentiel.
event.json
{
"version": "2.0",
"rawPath": "/FI7mvMOU",
"cookies": [],
"requestContext": {
"http": {
"method": "GET",
"path": "/FI7mvMOU",
"protocol": "HTTP/1.1"
}
},
"isBase64Encoded": false
}
Ensuite, on exécute notre fonction :
vendor/bin/bref local redirect --file=event.json
On récupère alors une réponse conforme au format 2.0 des réponses de fonction Lambda et contient notre redirection HTTP.
Nous avons maintenant un handler. Pour qu’il soit en capacité de traiter les événements, il faut configurer la fonction
Lambda pour indiquer qu’elle va écouter les événements HTTP d’API Gateway. Pour cela, on va modifier la configuration
dans le fichier serverless.yml
pour enregistrer les événements supportés :
service: demo-link-shortener
#...
functions:
redirect:
handler: index.php
description: 'Redirect user to target URL'
layers:
- ${bref:layer.php-81}
events:
- httpApi:
method: 'GET'
path: '/{id+}'
#...
La fonction écoutera désormais tous les événements HTTP avec la méthode GET et contenant un chemin d’URL non-vide.
Gérer le cas d'un lien inconnu
Nous avons une fonction qui permet de rediriger l’utilisateur vers sa destination. Cependant, celle-ci ne gère aujourd’hui pas le cas où le lien demandé n’existe pas.
On va modifier notre fonction Lambda pour diriger vers une page 404 si jamais le lien demandé ne fait pas partie de la liste des liens enregistrés.
index.php
<?php
//...
return new class implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
//...
$targetLocation = $registeredLinks[$path] ?? 'https://hephaist.io/404';
return new Response(302, ['Location' => $targetLocation]);
}
};
On testera notre fonction avec un nouvel événement utilisant un lien non référencé.
non-existing-link-event.json
{
"version": "2.0",
"rawPath": "/foobar",
"cookies": [],
"requestContext": {
"http": {
"method": "GET",
"path": "/foobar",
"protocol": "HTTP/1.1"
}
},
"isBase64Encoded": false
}
On lance la commande pour tester notre cas avec un lien non existant
vendor/bin/bref local redirect --file=non-existing-link-event.json
Désormais, on a un raccourcisseur d'URL complètement fonctionnel.
Déployer la fonction sur AWS
On va maintenant déployer notre fonction Lambda sur AWS avec
Serverless grâce à la commande deploy
.
serverless deploy
Et voilà, notre raccourcisseur d’URL est maintenant en ligne.
dev
. Il faut spécifier le stage dans le fichier de configuration serverless.yml
ou en option de la commande deploy
pour le changer.Conclusion
On a vu comment mettre en place un raccourcisseur d’URL avec une approche serverless grâce à Bref. Cela peut être rapidement mis en place sans trop d’effort.
En supposant 50 000 consultations de liens mensuels, cette approche ne coûtera que :
- 6 cts pour l’utilisation du service AWS API Gateway
- 6 cts pour l’utilisation du service AWS EventBridge
- 1 ct pour l’utilisation du service AWS Lambda
Cependant, il n’est pas pratique pour enregistrer de nouveaux liens de devoir modifier le code source de la fonction puis de déployer une nouvelle version. Il est possible avec d’autres services AWS de mettre en place la possibilité d’enregistrer de nouveaux liens.