PHP & Serverless avec Bref - part 2

PHP & Serverless avec Bref - part 2


Cet article est la suite d'un premier article qui fait l'introduction du Serverless. Dans cette deuxième partie, nous allons d'abord voir ce que sont les layers dans AWS Lambda et comment les implémenter. Ensuite, nous verrons l'utilisation du framework Bref.

AWS Lambda

Fonctionnement

Un environnement AWS Lambda inclut :

  • L'environnement d'exécution (runtime) du langage choisi (Java, Go, PowerShell, Node.js, C#, Python, Ruby par défaut)
  • L'implémentation de Lambda runtime API, c'est à dire le cycle de vie de l'exécution de l'environnement et l'invocation des fonctions Serverless

Le cycle de vie d'un environnement d'exécution Lambda est composé d'une phase d'initialisation et de plusieurs (autant que nécessaire) phases d'invocation.

La phase d'initialisation représente le temps entre le moment où l'environnement démarre le runtime et le moment où le code d'une fonction est exécuté. Cette phase n'est exécutée qu'une seule fois durant le cycle de vie de l'environnement.

Après l'initialisation, l'environnement d'exécution se met dans la phase d'invocation et va sans cesse vérifier et exécuter des tâches, et ce jusqu'à ce que l'environnement s'éteigne.

Depuis novembre 2018, il est possible de déclarer ses propres environnements d'exécution pour des fonctions Lambda, mais aussi d'y incorporer des composants réutilisables sous forme de Layers.

On peut implémenter un runtime dans n'importe quel langage. Un runtime est un programme qui exécute le handler d'une fonction Lambda lorsqu'elle est appelée. Un runtime peut être inclus dans le package de déploiement d'une fonction sous forme d'un fichier exécutable nommé bootstrap (nous verrons un exemple plus bas dans l'article).

Layers

Une fonction Lambda peut être configurée pour télécharger du code et du contenu additionnel sous forme d'un layer. Un layer est une archive ZIP qui contient des librairies, un runtime personnalisé ou d'autres dépendances.

Si vous avez déjà écrit des fonctions serverless en Node.js, vous savez qu'il faut packager tout le dossier node_modules pour chacune des fonctions (puisqu'elles sont déployées de façon indépendante les unes des autres). Ceci ralentit le process de déploiement et rend les builds lents.

Mais désormais il est possible de publier le dossier node_modules sous forme d'un Layer partagé et réutilisable pour toutes nos fonctions. Cela veut dire que l'on pourrait avoir un layer pour notre runtime custom, un autre layer qui contient nos dépendances et configurer nos fonctions pour utiliser ces 2 layers. Notez qu'une fonction a une limite de 5 layers.

Exemple

Fonction PHP

Prenons la fonction suivante simple comme exemple :

// src/profession.php function occupation() { $jobs = [ 'Fireman', 'Astronaut', 'Super hero', 'Pilot', 'Professional cook', 'Artist', ]; return ['occupation' => $jobs[array_rand($jobs)]]; }

Layer PHP

Je vais créer un dossier layers/php dans mon application et je vais y placer mon layer. Pour créer un runtime custom, nous avons besoin d'un fichier bootstrap qui contiendra la logique de notre runtime responsable d'appeler nos fonctions.

Nous avons également besoin d'un exécutable PHP capable d’interpréter notre code. Je vais créer un dossier bin dans le dossier de mon layer pour y placer mon binaire php. Pour générer un binaire, je vous recommande de regarder cet article.

Lorsqu'on déploie un layer, il est placé dons le dossier /opt dans les containers. Ainsi, mon fichier bootstrap pourrait ressembler à ceci :

#!/bin/sh #go into the source directory cd $LAMBDA_TASK_ROOT #execute the runtime /opt/bin/php /opt/runtime.php

Voici un exemple de runtime.php inspiré de l'article sur le blog AWS. Nous allons utiliser Guzzle pour faire les appels réseau, par conséquent je vais d'abord exécuter la commande suivante :

composer require guzzlehttp/guzzle
<?php // Invoke Composer's autoloader to use Guzzle require $_ENV['LAMBDA_TASK_ROOT'] . '/vendor/autoload.php'; // Request processing loop => barring unrecoverable failure, this loop runs until the environment shuts down do { // Ask the runtime API for a request to handle $request = getNextRequest(); // Obtain the function name from the _HANDLER environment variable and ensure the function's code is available list($handlerFile, $handlerFunction) = explode(".", $_ENV['_HANDLER']); require_once $_ENV['LAMBDA_TASK_ROOT'] . '/src/' . $handlerFile . '.php'; // Execute the desired function and obtain the response $response = $handlerFunction($request['payload']); // Submit the response back to the runtime API sendResponse($request['invocationId'], $response); } while (true); function getNextRequest() { $client = new \GuzzleHttp\Client(); $response = $client->get(sprintf( 'http://%s/2018-06-01/runtime/invocation/next', $_ENV['AWS_LAMBDA_RUNTIME_API'] )); return [ 'invocationId' => $response->getHeader('Lambda-Runtime-Aws-Request-Id')[0], 'payload' => json_decode((string) $response->getBody(), true), ]; } function sendResponse($invocationId, $response) { $client = new \GuzzleHttp\Client(); $client->post( sprintf( 'http://%s/2018-06-01/runtime/invocation/%s/response', $_ENV['AWS_LAMBDA_RUNTIME_API'], $invocationId ), ['body' => $response] ); }

Pour résumer, nous avons actuellement la structure de fichiers suivante :

layers/
    php/
        bin/
            php #binary file
        bootstrap
        runtime.php
src/
    profession.php
vendor/
    guzzlehttp/

Déploiement

Je vais utiliser le framework serverless pour le déploiement de mon layer et de ma fonction :

# serverless.yml service: php-serverless provider: name: aws runtime: provided region: eu-west-3 memorySize: 512 layers: php: path: layers/php functions: occupation: handler: profession.occupation layers: - {Ref: PhpLambdaLayer}

Comme on peut constater, dans ma fonction occupation, le handler contient le nom de mon fichier profession.php et la méthode occupation. C'est comme ça que je l'ai configuré dans le runtime.php :

//... list($handlerFile, $handlerFunction) = explode(".", $_ENV['_HANDLER']); require_once $_ENV['LAMBDA_TASK_ROOT'] . '/src/' . $handlerFile . '.php'; $response = $handlerFunction($request['payload']);

C'est donc à nous de bien configurer la façon dont on nomme les handlers et la façon de les exécuter dans le runtime.

Le nom de notre layer PhpLambdaLayer correspond à sa référence CloudFormation. Vous pouvez lire les détails ici.

Pour déployer la fonction et le layer, exécutons la commande suivante :

$ sls deploy Serverless: Packaging service... #... Serverless: Stack update finished... Service Information service: php-serverless stage: dev region: eu-west-3 stack: php-serverless-dev resources: 7 api keys: None endpoints: None functions: occupation: php-serverless-dev-occupation layers: php: arn:aws:lambda:eu-west-3:087017887086:layer:php:1

Enfin, appelons la fonction occupation :

$ sls invoke -f occupation -l { "occupation": "Fireman" } -------------------------------------------------------------------- START RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608 Version: $LATEST END RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608 REPORT RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608 Duration: 38.15 ms Billed Duration: 300 ms Memory Size: 512 MB Max Memory Used: 59 MB Init Duration: 191.10 ms

Récapitulatif

Nous venons donc de réaliser un exemple fonctionnel avec un layer capable d'exécuter du code PHP.

Maintenant, imaginez que vous avez une grande application, disons une API REST en Symfony, que vous voudriez déployer sur AWS Lambda. Il faudrait développer un runtime beaucoup plus poussé capable de s'intégrer avec le front controller de Symfony, et pourquoi pas aussi avec la console. Il faudrait également modifier le layer PHP pour ajouter toutes les librairies dont nous aurions besoin et de recompiler le binaire PHP.

Heureusement pour nous, une solution open source existe pour gérer tout cela : Bref.

Bref

Bref est un package Composer open source qui nous permet de déployer des applications PHP sur AWS Lambda. Il est développé par Matthieu Napoli.

Bref fournit :

  • la documentation
  • les runtimes PHP pour AWS Lambda
  • des outils de déploiement
  • l'intégration avec des frameworks PHP

Je vous propose de déployer une application Symfony sur AWS Lambda en utilisant Bref.

Application Symfony

Pour créer mon application,

$ composer create-project symfony/skeleton sf-serverless-example

Ensuite, modifions le controller par défaut comme ceci (pour reprendre le même exemple que plus haut) :

namespace App\Controller; use Symfony\Component\HttpFoundation\JsonResponse; class DefaultController { public function index() { $jobs = [ 'Fireman', 'Astronaut', 'Super hero', 'Pilot', 'Professional cook', 'Artist', ]; return new JsonResponse([ 'occupation' => $jobs[array_rand($jobs)], ]); } }

Ajoutons maintenant la librairie Bref :

$ composer require bref/bref

Enfin, configurons le déploiement avec serverless :

# serverless.yml service: php-serverless-sf-bref provider: name: aws region: eu-west-3 runtime: provided environment: # Symfony environment variables APP_ENV: prod plugins: - ./vendor/bref/bref functions: website: handler: public/index.php timeout: 28 # API Gateway has a timeout of 29 seconds layers: - ${bref:layer.php-74-fpm} events: - http: 'ANY /' - http: 'ANY /{proxy+}' console: handler: bin/console timeout: 120 # in seconds layers: - ${bref:layer.php-74} # PHP - ${bref:layer.console} # The "console" layer

La liste des layers mis à disposition par Bref peut être consultée ici. Je vous recommande également de lire la documentation de Bref, elle est très claire et fournit plein d'exemples dont vous pourriez avoir besoin.

Il ne faut pas oublier qu'avec la plupart des fournisseurs Cloud, le filesystem est disponible de lecture seulement. Ainsi, nous devons changer l'endroit où sont stockés les fichers de logs et cache :

public function getLogDir() { // When on the lambda only /tmp is writeable if (getenv('LAMBDA_TASK_ROOT') !== false) { return '/tmp/log/'; } return parent::getLogDir(); } public function getCacheDir() { // When on the lambda only /tmp is writeable if (getenv('LAMBDA_TASK_ROOT') !== false) { return '/tmp/cache/'.$this->environment; } return parent::getCacheDir(); }

Dernière étape, le déploiement :

$ sls deploy Serverless: Packaging service... Service Information service: php-serverless-sf-bref stage: dev region: eu-west-3 stack: php-serverless-sf-bref-dev resources: 15 api keys: None endpoints: ANY - https://maeck9uwyf.execute-api.eu-west-3.amazonaws.com/dev ANY - https://maeck9uwyf.execute-api.eu-west-3.amazonaws.com/dev/{proxy+} functions: website: php-serverless-sf-bref-dev-website console: php-serverless-sf-bref-dev-console layers: None

L'URL à laquelle mon application est accessible est indiquée dans les endpoints. Voici donc le résultat :

Nous avons terminé ! Nous venons de déployer une application Symfony sur AWS Lambda en utilisant Bref. Comme vous avez vu, c'est assez simple au final...

Maintenant vous pouvez déployer vos applications PHP sur des infrastructures serverless :)

Auteur(s)

Marie Minasyan

Marie Minasyan

Astronaute Raccoon @ ElevenLabs_🚀 De retour dans la Galaxie.

Voir le profil

Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !

Notre équipe d'experts répond à toutes vos questions.

Nous contacter

Découvrez nos autres contenus dans le même thème

Astronaute revenant de mission

Retour sur le Forum PHP 2024

Découvrez un résumé concis des conférences qui nous ont le plus marqué lors du Forum PHP 2024 !