Créer un raccourcisseur d'URL avec Bref - Partie 2

Clément Michelet 17 minutes de lecture
Bureau avec un ordinateur portable et un téléphone posé dessus ainsi qu'un moniteur derrière l'ordinateur
Crédits: Christopher Gower @ Unsplash

Dans la précédente partie, on a pu voir comment initialiser une application serverless avec Bref. On a également mis en place notre première fonction qui s’occupe de rediriger le visiteur vers l’adresse de destination.

Créer un raccourcisseur d'URL avec Bref - Partie 1

Pour pouvoir enregistrer de nouveaux liens raccourcis, il faut modifier le fichier contenant la liste des liens et mettre en ligne la nouvelle version de notre fonction. Ce n’est pas le plus pratique dans un usage courant.

Dans cet article, on va voir comment permettre d’enregistrer de nouveaux liens en mettant en place une API d’enregistrement de nouveaux liens.

Permettre la modification de la liste des liens

L’objectif est de faire en sorte que l’on puisse enregistrer de nouveaux liens raccourcis de la manière la plus simple possible. Pour cela, on va faire évoluer notre raccourcisseur d’URL afin de lui ajouter une API d’enregistrement de liens raccourcis.

Cette API demande une ou plusieurs adresses à raccourcir et fournira en retour les URL raccourcis respectives. Comme cela, il sera possible d’enregistrer facilement des liens avec un client d’API, en créant une interface d’administration ou tout simplement avec un appel cURL.

Pour y parvenir, on va :

  1. créer un emplacement de stockage pour le fichier de lien en dehors de la fonction de redirection
  2. modifier la fonction de redirection des visiteurs afin de lire le fichier depuis le nouvel emplacement de stockage
  3. créer une base de données pour stocker les liens enregistrés
  4. créer une fonction pour l’API pour enregistrer le lien dans une base de données
  5. créer une fonction pour mettre à jour le fichier de liens lors de l’enregistrement d’un lien en base de données

Stocker la liste de liens en dehors de la fonction

Le fichier link.json est pour l’instant embarqué avec le code source de la fonction. Pour le mettre à jour, il faut déployer une nouvelle version de la fonction. On va modifier la fonction afin de lire le fichier depuis un emplacement externe à la fonction Lambda.

On va utiliser le service Amazon S3 qui sert à stocker des fichiers. Il permet également de garder des anciennes versions de fichiers sans nécessiter un outil de gestion de versions comme git.

Il est possible de monter un dossier partagé entre chaque exécution de la fonction Lambda grâce au service Amazon EFS.

Dans un premier temps, on va déployer un bucket S3 pour l’utiliser comme emplacement de stockage. Grâce à Serverless framework et à Lift, on va pouvoir créer automatiquement ce bucket lors du déploiement de notre application. Il est possible de créer un bucket S3 en utilisant directement la configuration via CloudFormation mais cela est plus compliqué et verbeux.

On va d’abord installer le plugin Lift pour pouvoir l’utiliser dans notre application :

npm install -g serverless-lift

Ensuite, on va modifier notre configuration serverless.yml afin d’activer le plugin et de déclarer un emplacement de stockage.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml
+++ b/serverless.yml
@@ -7,7 +7,8 @@
     deploymentMethod: direct # fastest deployment method

 plugins:
-    - ./vendor/bref/bref
+  - serverless-lift
+  - ./vendor/bref/bref

 functions:
     redirect:
@@ -20,6 +21,10 @@
                   method: 'GET'
                   path: '/{id+}'

+constructs:
+  published-links:
+    type: storage
+
 # Exclude files from deployment
 package:
     patterns:

Le bucket sera créé la prochaine fois qu’on lancera la commande serverless deploy.

Sortie dans la console de la commande serverless deploy avec la configuration d'un bucket S3

Par défaut, toutes les fonctions de cette application auront un accès en lecture et en écriture au bucket S3. Néanmoins, il serait préférable de configurer les fonctions avec des permissions plus granulaires.
Pour cet article, on gardera ce comportement par simplicité.

Maintenant que nous avons un bucket disponible, nous pouvons maintenant l’utiliser dans notre fonction afin de lire le fichier de liens. On supposera que le fichier a été déposé sur S3 et donc qu’il est forcément disponible.

On va avoir besoin d’accéder à notre bucket S3 depuis notre fonction pour récupérer le contenu du fichier de liens. Pour cela, on utilisera le SDK AWS officiel :

composer require aws/aws-sdk-php

On va mettre à jour le code dans notre fonction qui lit le fichier depuis le système local afin de lire le fichier depuis S3. On va instancier un client S3 pour pouvoir consulter le bucket et on va l'enregistrer en tant que Stream Wrapper pour pouvoir utiliser s3:// en tant que protocole d’accès aux fichiers du bucket lors de l’utilisation des fonctions natives PHP de lecture de fichiers.

index.php

diff --git a/index.php b/index.php
--- a/index.php
+++ b/index.php
@@ -4,6 +4,7 @@

 require __DIR__ . '/vendor/autoload.php';

+use Aws\S3\S3Client;
 use Nyholm\Psr7\Response;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
@@ -12,13 +13,21 @@
 use function Psl\Type\non_empty_dict;
 use function Psl\Type\non_empty_string;

-return new class implements RequestHandlerInterface
+return new class($_ENV['BUCKET_NAME']) implements RequestHandlerInterface
 {
+    public function __construct(private readonly string $bucketName) {}
+
     public function handle(ServerRequestInterface $request): ResponseInterface
     {
         $path = $request->getUri()->getPath();

-        $fileContent = file_get_contents('./links.json');
+        $client = new S3Client([
+            'region' => 'eu-west-3',
+            'version' => '2006-03-01'
+        ]);
+        $client->registerStreamWrapper();
+
+        $fileContent = file_get_contents(sprintf('s3://%s/links.json', $this->bucketName));
         if (false === $fileContent) {
             throw new RuntimeException('The file "links.json" is missing.');
         }

Et voilà, on lit désormais le fichier depuis S3 avec peu de modifications. Le nom du bucket est passé en argument de notre fonction. Cela permet de configurer simplement le bucket de destination notamment lorsque l’on déploie notre fonction sur différents stage.

AsyncAWS apporte plus de fonctionnalités que le SDK officiel comme l’exécution asynchrone ou la séparation des services en différents packages.

Il faut maintenant passer le nom de notre bucket créé avec Lift à notre fonction en modifiant la configuration.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml
+++ b/serverless.yml
@@ -16,6 +16,8 @@
         description: 'Redirect user to target URL'
         layers:
             - ${bref:layer.php-81}
+        environment:
+          BUCKET_NAME: ${construct:published-links.bucketName}
         events:
             - httpApi:
                   method: 'GET'

Il n’y a plus qu’à déployer la nouvelle version de notre fonction avec la commande serverless deploy.

Et voilà, les visiteurs sont toujours redirigés comme avant mais le fichier de liens est désormais disponible en dehors de notre fonction.

Enregistrer de nouveaux liens

On va mettre en place une API pour permettre d’enregistrer un ou plusieurs liens. L’API attendra une requête POST avec un corps de requête au format JSON contenant la liste des liens.

[
  "https://hephaist.io/blog/2023/03/03/creer-un-raccourcisseur-d-url-avec-bref-partie-2/",
  "https://hephaist.io/mentions-legales/"
]

L’API retournera un code HTTP 200 pour indiquer que la requête a été traité et un objet JSON avec les liens raccourcis respectifs dans le corps de réponse.

{
  "https://hephaist.io/blog/2023/03/03/creer-un-raccourcisseur-d-url-avec-bref-partie-2/": "https://link.test/K0tbl20D",
  "https://hephaist.io/mentions-legales/": "https://link.test/ECuA1hjC"
}

On va d’abord mettre en place une nouvelle fonction Lambda nommée entrypoint pour recevoir la requête HTTP et qui retournera une réponse avec le code HTTP 200 et un objet JSON vide en corps de réponse.

entrypoint.php

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.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
    {
        return new Response(
            status: 200,
            headers: ['Content-Type' => 'application/json'],
            body: \Psl\Json\encode(value: [], flags: \JSON_FORCE_OBJECT)
        );
    }
};

Ensuite, on va enregistrer notre fonction dans notre application pour que la fonction soit appelée lorsqu’une requête HTTP est effectuée sur /links avec la méthode POST.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml
+++ b/serverless.yml
@@ -11,6 +11,16 @@
   - ./vendor/bref/bref

 functions:
+    entrypoint:
+        handler: entrypoint.php
+        description: 'API to register one or more links'
+        layers:
+            - ${bref:layer.php-81}
+        events:
+            - httpApi:
+                method: 'POST'
+                path: '/links'
+
     redirect:
         handler: index.php
         description: 'Redirect user to target URL'
A ce stade, l’API est utilisable par toute personne ayant l'URL pour enregistrer les liens.
Il est possible de restreindre l’accès en configurant une autorisation basée sur le service AWS IAM ou une autorisation personnalisée.

On va utiliser à nouveau la librairie PSL afin de traiter la requête pour transformer le tableau JSON en liste typée avant de valider que tous les liens sont des URL valides.

entrypoint.php

diff --git a/entrypoint.php b/entrypoint.php
--- a/entrypoint.php    
+++ b/entrypoint.php    
@@ -5,18 +5,53 @@
 require __DIR__ . '/vendor/autoload.php';

 use Nyholm\Psr7\Response;
+use Psl\Json\Exception\DecodeException;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use function Psl\Json\encode;
+use function Psl\Json\typed;
+use function Psl\Type\non_empty_string;
+use function Psl\Type\non_empty_vec;

 return new class implements RequestHandlerInterface
 {
     public function handle(ServerRequestInterface $request): ResponseInterface
     {
+        $errors = [];
+        try {
+            $links = typed(
+                $request->getBody()->getContents(),
+                non_empty_vec(
+                    non_empty_string()
+                )
+            );
+        } catch (DecodeException $error) {
+            return new Response(
+                status: 400,
+                headers: ['Content-Type' => 'application/json'],
+                body: encode(['errors' => [$error->getMessage()]])
+            );
+        }
+
+        foreach($links as $link) {
+            if (false === filter_var($link, FILTER_VALIDATE_URL)) {
+                $errors[] = sprintf('Link "%s" is not a valid URL.', $link);
+            }
+        }
+
+        if (!empty($errors)) {
+            return new Response(
+                status: 400,
+                headers: ['Content-Type' => 'application/json'],
+                body: encode(['errors' => $errors])
+            );
+        }
+
         return new Response(
             status: 200,
             headers: ['Content-Type' => 'application/json'],
-            body: \Psl\Json\encode(value: [], flags: \JSON_FORCE_OBJECT)
+            body: encode(value: [], flags: JSON_FORCE_OBJECT)
         );
     }
 };

Maintenant que l’on est assuré de recevoir une requête HTTP avec uniquement des liens valides, il est temps d’enregistrer nos nouveaux liens raccourcis. On va stocker les liens dans une base de données afin de pouvoir y inclure plus d’informations, par exemple, si le lien est désactivé, le nombre de consultations, etc.

Pour se faire, on va utiliser le service AWS DynamoDB qui permet d’avoir une base de données NoSQL à bas coût. On va utiliser à nouveau le plugin Lift pour créer notre base de données que l’on va nommer links.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml    
+++ b/serverless.yml    
@@ -14,6 +14,8 @@
     entrypoint:
         handler: entrypoint.php
         description: 'API to register one or more links'
+        environment:
+          TABLE_NAME: ${construct:links.tableName}
         layers:
             - ${bref:layer.php-81}
         events:
@@ -36,6 +38,8 @@
 constructs:
   published-links:
     type: storage
+  links:
+    type: database/dynamodb-single-table

 # Exclude files from deployment
 package:

Notre base de données a été pré-configurée par Lift notamment avec 2 attributs :

PK
C’est la clé d’identification d’un enregistrement. On générera un UUID v5 à partir de l’adresse de destination afin d’obtenir un identifiant prédictible.
SK
Cette clé permettra de définir comment trier nos enregistrements. On va utiliser le lien de destination.

Pour générer un lien, il est nécessaire de définir quel est le domaine utilisé pour construire le lien. Grâce à une variable d’environnement déclarée dans le fichier de configuration serverless.yml, on va passer à notre fonction le domaine à utiliser.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml    
+++ b/serverless.yml    
@@ -16,6 +16,7 @@
         description: 'API to register one or more links'
         environment:
           TABLE_NAME: ${construct:links.tableName}
+          DOMAIN_NAME: 'https://link.test'
         layers:
             - ${bref:layer.php-81}
         events:

Afin de générer un UUID v5, on va utiliser le package symfony/uid. Pour générer le lien raccourci, on utilisera le package hidehalo/nanoid-php.

composer require symfony/uid hidehalo/nanoid-php

Pour effectuer l’enregistrement de nos liens dans DynamoDB, on va utiliser à nouveau le SDK AWS. Il est possible d’écrire un enregistrement à la fois avec la méthode putItem ou plusieurs enregistrements avec batchWriteItem. C’est cette dernière méthode que l’on va utiliser.

entrypoint.php

diff --git a/entrypoint.php b/entrypoint.php
--- a/entrypoint.php
+++ b/entrypoint.php
@@ -4,18 +4,28 @@

 require __DIR__ . '/vendor/autoload.php';

+use Aws\DynamoDb\DynamoDbClient;
+use Hidehalo\Nanoid\Client;
 use Nyholm\Psr7\Response;
 use Psl\Json\Exception\DecodeException;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use Symfony\Component\Uid\Uuid;
 use function Psl\Json\encode;
 use function Psl\Json\typed;
 use function Psl\Type\non_empty_string;
 use function Psl\Type\non_empty_vec;

-return new class implements RequestHandlerInterface
+return new class($_ENV['TABLE_NAME'], $_ENV['DOMAIN_NAME']) implements RequestHandlerInterface
 {
+    private const NS_ID = '7c1a90e3-de46-48cb-811b-06d42dde7524';
+
+    public function __construct(
+        private readonly string $linksTableName,
+        private readonly string $domainName
+    ) {}
+
     public function handle(ServerRequestInterface $request): ResponseInterface
     {
         $errors = [];
@@ -48,6 +58,41 @@
             );
         }

+        $dbClient = new DynamoDbClient(['region' => 'eu-west-3', 'version' => '2012-08-10']);
+        $shortIdGenerator = new Client(size: 8);
+
+        $putItemRequests = [];
+        foreach ($links as $target) {
+            // Générer le lien raccourci
+            $source = sprintf('%s/%s', $this->domainName, $shortIdGenerator->generateId());
+
+            // Générer un UUID prédictible à partir du lien de destination
+            $linkId = Uuid::v5(namespace: Uuid::fromString(self::NS_ID), name: $target);
+
+            // Préparer la requête d'enregistrement dans DynamoDB
+            $putItemRequests[] = [
+                'PutRequest' => [
+                    'Item' => [
+                        // Champs pré-configurés par Lift
+                        'PK' => ['S' => $linkId], // Clé primaire
+                        'SK' => ['S' => $target], // Clé de tri
+
+                        // Attribut de l'enregistrement
+                        'linkId' => ['S' => $linkId],
+                        'target' => ['S' => $target],
+                        'source' => ['S' => $source]
+                    ]
+                ]
+            ];
+        }
+
+        // Écrire les enregistrements dans DynamoDB
+        $dbClient->batchWriteItem([
+            'RequestItems' => [
+                $this->linksTableName => $putItemRequests
+            ]
+        ]);
+
         return new Response(
             status: 200,
             headers: ['Content-Type' => 'application/json'],

Maintenant que nos liens sont enregistrés en base de données, il faut retourner les liens raccourcis à l’utilisateur dans la réponse de l’API.

entrypoint.php

diff --git a/entrypoint.php b/entrypoint.php
--- a/entrypoint.php
+++ b/entrypoint.php
@@ -61,6 +61,7 @@
         $dbClient = new DynamoDbClient(['region' => 'eu-west-3', 'version' => '2012-08-10']);
         $shortIdGenerator = new Client(size: 8);

+        $generatedLinks = [];
         $putItemRequests = [];
         foreach ($links as $target) {
             // Générer le lien raccourci
@@ -84,6 +85,8 @@
                     ]
                 ]
             ];
+
+            $generatedLinks[$target] = $source;
         }

         // Écrire les enregistrements dans DynamoDB
@@ -96,7 +99,7 @@
         return new Response(
             status: 200,
             headers: ['Content-Type' => 'application/json'],
-            body: encode(value: [], flags: JSON_FORCE_OBJECT)
+            body: encode(value: $generatedLinks, flags: JSON_FORCE_OBJECT)
         );
     }
 };

On peut déployer notre nouvelle API avec sa base de données avec la commande serverless deploy.

Sortie dans la console de la commande serverless deploy avec la configuration d'une base DynamoDB

On a maintenant une API pour enregistrer des liens dans une base de données.

Si un utilisateur demande à enregistrer un grand nombre de liens, la fonction Lambda mettra beaucoup de temps à s’exécuter ce qui augmentera le coût d'exécution.
En séparant la responsabilité de traiter la requête HTTP d’enregistrement de liens et d’exécuter l’enregistrement en base de données, il sera possible de paralléliser le traitement des enregistrements par l’utilisation d’un bus de commande.

Mettre à jour le fichier de liens

A ce stade, on enregistre de nouveaux liens en base de données. Cependant, ils ne sont pas enregistrés dans notre fichier JSON sur S3.

On pourrait mettre à jour la liste des liens sur S3 en même temps qu’on les enregistre en base de données. Toutefois, notre API peut être appelée par plusieurs utilisateurs en simultanés ce qui peut provoquer des problèmes d’exécutions concurrentes.

Pour résoudre cette problématique, nous allons :

  1. lever un événement pour notifier que l’on a enregistré un lien
  2. créer une fonction update-short-links réagissant à l’événement pour mettre à jour le fichier JSON
  3. configurer l'exécution de cette nouvelle fonction pour qu’elle ne puisse s’exécuter qu’une seule fois en simultané

AWS met à disposition le service Amazon EventBridge qui permet de créer des bus de messages pouvant être exploités par des services AWS ou par n’importe quelle application.

On va modifier notre fonction d’enregistrement de liens afin de lever un événement personnalisé dans le bus par défaut du service EventBridge. Pour construire cet événement, le service demande au minimum 3 informations :

detail
C’est un objet JSON pouvant contenir les informations relatives à l’événement. On y mettra le lien raccourci et le lien cible.
detail-type
C’est un identifiant du type d’événement. On utilisera LinkWasRegistered.
source
C’est un identifiant de la source émettrice de l’événement. On utilisera demo-link-shortener.entrypoint.
Il existe d’autres propriétés facultatives qui sont prédéfinies pour construire un événement à diffuser avec EventBridge.

Toujours grâce au SDK AWS, on va envoyer un événement à EventBridge pour chacun des liens enregistrés. On enverra les événements en lot même s'il est possible également de les envoyer unitairement.

entrypoint.php

diff --git a/entrypoint.php b/entrypoint.php
--- a/entrypoint.php
+++ b/entrypoint.php
@@ -5,6 +5,7 @@
 require __DIR__ . '/vendor/autoload.php';

 use Aws\DynamoDb\DynamoDbClient;
+use Aws\EventBridge\EventBridgeClient;
 use Hidehalo\Nanoid\Client;
 use Nyholm\Psr7\Response;
 use Psl\Json\Exception\DecodeException;
@@ -63,6 +64,7 @@

         $generatedLinks = [];
         $putItemRequests = [];
+        $events = [];
         foreach ($links as $target) {
             // Générer le lien raccourci
             $source = sprintf('%s/%s', $this->domainName, $shortIdGenerator->generateId());
@@ -86,6 +88,14 @@
                 ]
             ];

+            $events[] = [
+                'source' => 'demo-link-shortener.entrypoint',
+                'detail-type' => 'LinkWasRegistered',
+                'detail' => [
+                    'source' => $source,
+                    'target' => $target
+                ]
+            ];
             $generatedLinks[$target] = $source;
         }

@@ -96,6 +106,9 @@
             ]
         ]);

+        $eventBridgeClient = new EventBridgeClient(['region' => 'eu-west-3', 'version' => '2015-10-07']);
+        $eventBridgeClient->putEvents(['Entries' => $events]);
+
         return new Response(
             status: 200,
             headers: ['Content-Type' => 'application/json'],

Pour que notre fonction puisse publier l’événement dans le bus EventBridge, nous allons devoir ajouter une permission pour l’autoriser à effectuer l’action. Grâce à Serverless framework, on peut la déclarer dans le fichier de configuration.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml    
+++ b/serverless.yml    
@@ -5,6 +5,12 @@
     region: eu-west-3
     runtime: provided.al2
     deploymentMethod: direct # fastest deployment method
+    iam:
+      role:
+        statements:
+          - Effect: Allow
+            Action: events:PutEvents
+            Resource: '*'

 plugins:
   - serverless-lift
Les permissions IAM présentées ne sont pas configurées sur une ressource précise AWS. Il est important de suivre les bonnes pratiques AWS et de configurer les permissions par ressources.

La prochaine étape est de mettre en place la fonction update-short-links qui va traiter l'événement d’enregistrement pour mettre à jour le lien. Bref nous met à disposition une classe EventBridgeHandler pour faciliter le traitement des événements EventBridge.

update-short-links.php

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Bref\Context\Context;
use Bref\Event\EventBridge\EventBridgeEvent;
use Bref\Event\EventBridge\EventBridgeHandler;

return new class extends EventBridgeHandler
{
    public function handleEventBridge(EventBridgeEvent $event, Context $context): void
    {
        ['source' => $source, 'target' => $target] = $event->getDetail();
    }
};

Comme pour la fonction de redirection, on va avoir besoin de lire le fichier sur S3 avec le SDK AWS mais aussi de le remplacer par la version mise à jour avec le nouveau lien. On utilisera la fonction parse_url pour récupérer uniquement le chemin de l’URL.

update-short-links.php

diff --git a/update-short-links.php b/update-short-links.php
--- a/update-short-links.php
+++ b/update-short-links.php
@@ -7,11 +7,36 @@
 use Bref\Context\Context;
 use Bref\Event\EventBridge\EventBridgeEvent;
 use Bref\Event\EventBridge\EventBridgeHandler;
+use function Psl\Json\encode;
+use function Psl\Type\dict;

-return new class extends EventBridgeHandler
+return new class($_ENV['BUCKET_NAME']) extends EventBridgeHandler
 {
+    public function __construct(private readonly string $bucketName) {}
+
     public function handleEventBridge(EventBridgeEvent $event, Context $context): void
     {
         ['source' => $source, 'target' => $target] = $event->getDetail();
+
+        $client = new S3Client(['region' => 'eu-west-3', 'version' => '2006-03-01']);
+        $client->registerStreamWrapper();
+
+        $filePath = sprintf('s3://%s/links.json', $this->bucketName);
+        $fileContent = file_get_contents($filePath);
+        if (false === $fileContent) {
+            $fileContent = '{}'; // Le fichier n'existe pas encore, on initialise à vide notre liste de liens.
+        }
+
+        $registeredLinks = typed(
+            $fileContent,
+            dict(
+                non_empty_string(),
+                non_empty_string()
+            )
+        );
+
+        $registeredLinks[parse_url($source, PHP_URL_PATH)] = $target;
+
+        file_put_contents($filePath, encode(value: $registeredLinks, flags: JSON_FORCE_OBJECT));
     }
 };

Notre fonction est prête à recevoir les événements. Il faut maintenant indiquer qu’elle écoute l’événement qu’on a précédemment créé. Pour cela, il faut déclarer la fonction dans le fichier de configuration serverless.yml et indiquer qu’elle écoute les événements dans EventBridge et notamment celui qu’on envoie. Pour limiter le nombre d’exécutions simultanées, on va définir l’option reservedConcurrency à 1 afin qu’il n’y ait pas d’exécution concurrente de la fonction qui met à jour le fichier sur S3.

serverless.yml

diff --git a/serverless.yml b/serverless.yml
--- a/serverless.yml
+++ b/serverless.yml
@@ -30,6 +30,19 @@
                 method: 'POST'
                 path: '/links'

+    update-short-links:
+        handler: update-short-links.php
+        description: 'Update the list of redirected short links'
+        layers:
+            - ${bref:layer.php-81}
+        environment:
+            BUCKET_NAME: ${construct:published-links.bucketName}
+        reservedConcurrency: 1
+        events:
+            - eventBridge:
+                  pattern:
+                      detail-type: ['LinkWasRegistered']
+
     redirect:
         handler: index.php
         description: 'Redirect user to target URL'

Il ne reste qu’à déployer notre fonction avec la commande serverless deploy afin que le fichier soit mis à jour lorsque l’on enregistrera un nouveau lien.

Sortie dans la console de la commande serverless deploy avec la nouvelle fonction

Conclusion

Notre raccourcisseur d’URL est maintenant complètement fonctionnel en permettant l’enregistrement de liens raccourcis et de gérer la redirection vers l’adresse de destination.

En se basant sur les services AWS disponibles et Bref, on a un raccourcisseur d’URL avec les coûts suivants pour environ 50 000 appels mensuels :

Service AWS Coût mensuel Coût annuel
DynamoDB 0,13 $ 1,56 $
S3 0,07 $ 0,84 $
EventBridge 0,06 $ 0,72 $
API Gateway 0,06 $ 0,72 $
Route 53 0,52 $ 6,24 $
Lambda 0,00 $ 0,00 $
Total 0,84 $ 10,11 $

À titre de comparaison, il faut compter environ 8 à 10 $ / mois avec un abonnement à un service en ligne de raccourcisseurs d'URL. En concevant son propre raccourcisseur, on économise sur le coût et on reste maître de l'utilisation des données.

AWS propose un certain nombre de services avec lesquels on aurait pu avoir une approche différente. On pourrait par exemple utiliser API Gateway ou S3/Cloudfront en mode hébergement de site statique pour réaliser la redirection. De même, au lieu de lire le fichier depuis S3, on aurait pu faire en sorte de compiler une nouvelle version de la fonction en embarquant la dernière version du fichier de liens disponible afin de n’accéder à S3 qu’au moment de la compilation.

Avec ces 2 articles, vous avez désormais les clés pour concevoir un raccourcisseur d'URL et également d'y ajouter par exemple une API pour désactiver des liens ou mettre en place le suivi du nombre de visites d’un lien raccourci.

L'ensemble du code source est disponible sur GitHub.