Créer un raccourcisseur d'URL avec Bref

Clément Michelet 15 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

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
1 Support AWS non officiel grâce à Bref comme Runtime
2 Support Azure avec le déploiement de Custom handler pour exécuter le code
3 Support Cloudflare par la compilation du code du langage vers le langage JavaScript

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.

Pour garder l’article simple, on supposera que le compte AWS est déjà configuré et que la configuration pour faire en sorte que le domaine redirige vers API Gateway est déjà en place.

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.

Sortie dans la console de la commande d'initialisation de Bref

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/**'
Par défaut, les fonctions Lambda sont déployées dans la région 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.

Schema de l'invocation d'une fonction Lambda à partir d'une requête HTTP d'un navigateur. La requête HTTP interroge le service API Gateway qui envoie un événement HTTP à EventBridge qui lui-même invoque la fonction Lambda pour obtenir une réponse HTTP.

L’événement HTTP issu d'API Gateway contient un grand nombre d’informations dont le chemin de l’URL.

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/"
}
Les identifiants de lien seront générés au format 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]);
    }
};
Le code HTTP de redirection ne doit pas être le code 301 car les navigateurs mettent en cache l’adresse de destination. De plus, les moteurs de recherches remplaceront le lien raccourci par son adresse de destination lors de l’indexation avec ce code.
Cela peut être problématique lorsque l’on veut tracer les consultations de liens raccourcis.

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

Sortie dans la console de la commande Bref local

On récupère alors une réponse conforme au format 2.0 des réponses de fonction Lambda et contient notre redirection HTTP.

Il existe un serveur local pour simuler API Gateway et les événements 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

Sortie dans la console de la commande Bref local pour un lien non existant

Désormais, on a un raccourcisseur d'URL complètement fonctionnel.

L'ensemble du code source est disponible sur GitHub.

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

Sortie dans la console de la commande serverless deploy

Et voilà, notre raccourcisseur d’URL est maintenant en ligne.

Par défaut, les fonctions Lambda sont déployées avec le stage 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
AWS propose une offre gratuite nommée AWS Free Tier.

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.

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