Partage de données entre plusieurs serveurs via AWS S3

Publié: 2022-03-10
Résumé rapide ↬ Lors de la création d'un formulaire en plusieurs étapes dans lequel un fichier est téléchargé et manipulé, si l'application s'exécute sur plusieurs serveurs derrière un équilibreur de charge, alors nous devons nous assurer que le fichier est disponible tout au long de l'exécution du processus , pour le serveur qui gère le processus à chaque étape. Dans cet article, nous allons résoudre ce problème en créant un référentiel accessible à tous les serveurs où télécharger les fichiers, basé sur AWS S3.

Lors de la fourniture de certaines fonctionnalités pour le traitement d'un fichier téléchargé par l'utilisateur, le fichier doit être disponible pour le processus tout au long de l'exécution. Une simple opération de téléchargement et de sauvegarde ne présente aucun problème. Cependant, si en plus le fichier doit être manipulé avant d'être enregistré, et que l'application s'exécute sur plusieurs serveurs derrière un équilibreur de charge, alors nous devons nous assurer que le fichier est disponible sur le serveur qui exécute le processus à chaque fois.

Par exemple, une fonctionnalité "Télécharger votre avatar utilisateur" en plusieurs étapes peut nécessiter que l'utilisateur télécharge un avatar à l'étape 1, le recadre à l'étape 2 et enfin l'enregistre à l'étape 3. Une fois le fichier téléchargé sur un serveur à l'étape 1, le fichier doit être disponible sur le serveur qui gère la requête pour les étapes 2 et 3, qui peut ou non être le même pour l'étape 1.

Une approche naïve consisterait à copier le fichier téléchargé à l'étape 1 sur tous les autres serveurs, afin que le fichier soit disponible sur chacun d'eux. Cependant, cette approche est non seulement extrêmement complexe mais également irréalisable : par exemple, si le site fonctionne sur des centaines de serveurs, dans plusieurs régions, cela ne peut pas être réalisé.

Une solution possible consiste à activer les "sessions persistantes" sur l'équilibreur de charge, qui attribuera toujours le même serveur pour une session donnée. Ensuite, les étapes 1, 2 et 3 seront gérées par le même serveur, et le fichier téléchargé sur ce serveur à l'étape 1 sera toujours là pour les étapes 2 et 3. Cependant, les sessions persistantes ne sont pas entièrement fiables : si entre les étapes 1 et 2 ce serveur est tombé en panne, alors l'équilibreur de charge devra affecter un serveur différent, perturbant la fonctionnalité et l'expérience utilisateur. De même, toujours affecter le même serveur pour une session peut, dans des circonstances particulières, entraîner des temps de réponse plus lents d'un serveur surchargé.

Une solution plus appropriée consiste à conserver une copie du fichier sur un référentiel accessible à tous les serveurs. Ensuite, une fois le fichier téléchargé sur le serveur à l'étape 1, ce serveur le téléchargera dans le référentiel (ou, alternativement, le fichier pourrait être téléchargé dans le référentiel directement à partir du client, en contournant le serveur) ; l'étape 2 de gestion du serveur téléchargera le fichier depuis le référentiel, le manipulera et le téléchargera à nouveau ; et enfin, l'étape 3 de gestion du serveur le téléchargera à partir du référentiel et l'enregistrera.

Plus après saut! Continuez à lire ci-dessous ↓

Dans cet article, je vais décrire cette dernière solution, basée sur une application WordPress stockant des fichiers sur Amazon Web Services (AWS) Simple Storage Service (S3) (une solution cloud de stockage d'objets pour stocker et récupérer des données), fonctionnant via le SDK AWS.

Note 1 : Pour une fonctionnalité simple telle que le recadrage des avatars, une autre solution serait de contourner complètement le serveur et de l'implémenter directement dans le cloud via les fonctions Lambda. Mais comme cet article concerne la connexion d'une application s'exécutant sur le serveur avec AWS S3, nous n'envisageons pas cette solution.

Remarque 2 : Pour utiliser AWS S3 (ou tout autre service AWS), nous aurons besoin d'un compte utilisateur. Amazon propose ici un niveau gratuit pendant 1 an, ce qui est suffisant pour expérimenter leurs services.

Remarque 3 : Il existe des plugins tiers pour télécharger des fichiers de WordPress vers S3. L'un de ces plugins est WP Media Offload (la version allégée est disponible ici), qui offre une fonctionnalité intéressante : il transfère de manière transparente les fichiers téléchargés dans la médiathèque vers un compartiment S3, ce qui permet de découpler le contenu du site (comme tout ce qui se trouve sous /wp-content/uploads) à partir du code de l'application. En découplant le contenu et le code, nous sommes en mesure de déployer notre application WordPress à l'aide de Git (sinon nous ne pouvons pas car le contenu téléchargé par l'utilisateur n'est pas hébergé sur le référentiel Git), et d'héberger l'application sur plusieurs serveurs (sinon, chaque serveur devrait conserver une copie de tout le contenu téléchargé par l'utilisateur.)

Créer le seau

Lors de la création du compartiment, nous devons tenir compte du nom du compartiment : chaque nom de compartiment doit être unique au monde sur le réseau AWS, donc même si nous aimerions appeler notre compartiment quelque chose de simple comme "avatars", ce nom peut déjà être pris , alors nous pouvons choisir quelque chose de plus distinctif comme "avatars-nom-de-mon-entreprise".

Nous devrons également sélectionner la région où le bucket est basé (la région est l'emplacement physique où se trouve le centre de données, avec des emplacements partout dans le monde.)

La région doit être la même que celle où notre application est déployée, afin que l'accès à S3 lors de l'exécution du processus soit rapide. Sinon, l'utilisateur peut devoir attendre des secondes supplémentaires avant de télécharger/télécharger une image vers/depuis un emplacement distant.

Remarque : il est logique d'utiliser S3 comme solution de stockage d'objets dans le cloud uniquement si nous utilisons également le service d'Amazon pour les serveurs virtuels sur le cloud, EC2, pour exécuter l'application. Si, au lieu de cela, nous comptons sur une autre société pour héberger l'application, telle que Microsoft Azure ou DigitalOcean, nous devrions également utiliser leurs services de stockage d'objets dans le cloud. Dans le cas contraire, notre site subira un surcoût dû aux données voyageant entre les réseaux de différentes entreprises.

Dans les captures d'écran ci-dessous, nous verrons comment créer le compartiment où télécharger les avatars des utilisateurs pour le recadrage. Nous nous dirigeons d'abord vers le tableau de bord S3 et cliquons sur "Créer un compartiment":

Tableau de bord S3
Tableau de bord S3, montrant tous nos compartiments existants. ( Grand aperçu )

Ensuite, nous tapons le nom du bucket (dans ce cas, "avatars-smashing") et choisissons la région ("EU (Francfort)") :

Créer un écran de compartiment
Création d'un compartiment via S3. ( Grand aperçu )

Seuls le nom du bucket et la région sont obligatoires. Pour les étapes suivantes, nous pouvons conserver les options par défaut, nous cliquons donc sur "Suivant" jusqu'à ce que nous cliquions enfin sur "Créer un seau", et avec cela, nous aurons créé le seau.

Configuration des autorisations de l'utilisateur

Lors de la connexion à AWS via le SDK, nous devrons saisir nos informations d'identification d'utilisateur (une paire d'ID de clé d'accès et de clé d'accès secrète), pour valider que nous avons accès aux services et objets demandés. Les autorisations des utilisateurs peuvent être très générales (un rôle "administrateur" peut tout faire) ou très granulaires, accordant simplement l'autorisation aux opérations spécifiques nécessaires et rien d'autre.

En règle générale, plus les autorisations accordées sont spécifiques, mieux c'est, afin d'éviter les problèmes de sécurité . Lors de la création du nouvel utilisateur, nous devrons créer une politique, qui est un simple document JSON listant les autorisations à accorder à l'utilisateur. Dans notre cas, nos autorisations d'utilisateur accorderont l'accès à S3, pour le bucket "avatars-smashing", pour les opérations de "Put" (pour télécharger un objet), "Get" (pour télécharger un objet) et "List" ( pour répertorier tous les objets du bucket), ce qui entraîne la règle suivante :

 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:Put*", "s3:Get*", "s3:List*" ], "Resource": [ "arn:aws:s3:::avatars-smashing", "arn:aws:s3:::avatars-smashing/*" ] } ] }

Dans les captures d'écran ci-dessous, nous pouvons voir comment ajouter des autorisations utilisateur. Il faut aller sur le tableau de bord Identity and Access Management (IAM) :

Tableau de bord IAM
Tableau de bord IAM, répertoriant tous les utilisateurs que nous avons créés. ( Grand aperçu )

Dans le tableau de bord, on clique sur « Utilisateurs » et tout de suite après sur « Ajouter un utilisateur ». Dans la page Ajouter un utilisateur, nous choisissons un nom d'utilisateur ("crop-avatars") et cochons "Accès programmatique" comme type d'accès, qui fournira l'ID de clé d'accès et la clé d'accès secrète pour se connecter via le SDK :

Ajouter une page utilisateur
Ajout d'un nouvel utilisateur. ( Grand aperçu )

Nous cliquons ensuite sur le bouton « Suivant : Autorisations », cliquez sur « Attacher directement les politiques existantes » et cliquez sur « Créer une politique ». Cela ouvrira un nouvel onglet dans le navigateur, avec la page Créer une politique. On clique sur l'onglet JSON, et on rentre le code JSON de la politique définie ci-dessus :

Créer une page de stratégie
Création d'une stratégie accordant les opérations 'Get', 'Post' et 'List' sur le compartiment 'avatars-smashing'. ( Grand aperçu )

Nous cliquons ensuite sur Review policy, lui donnons un nom ("CropAvatars"), et enfin cliquons sur Create policy. Une fois la politique créée, nous revenons à l'onglet précédent, sélectionnons la politique CropAvatars (nous devrons peut-être actualiser la liste des politiques pour la voir), cliquez sur Suivant : Réviser, et enfin sur Créer un utilisateur. Une fois cela fait, nous pouvons enfin télécharger l'ID de clé d'accès et la clé d'accès secrète (veuillez noter que ces informations d'identification sont disponibles pour ce moment unique ; si nous ne les copions pas ou ne les téléchargeons pas maintenant, nous devrons créer une nouvelle paire ):

Page de réussite de la création de l'utilisateur
Une fois l'utilisateur créé, un temps unique nous est proposé pour télécharger les informations d'identification. ( Grand aperçu )

Connexion à AWS via le SDK

Le SDK est disponible dans une myriade de langues. Pour une application WordPress, nous avons besoin du SDK pour PHP qui peut être téléchargé à partir d'ici, et les instructions sur la façon de l'installer sont ici.

Une fois le compartiment créé, les informations d'identification de l'utilisateur prêtes et le SDK installé, nous pouvons commencer à télécharger des fichiers sur S3.

Téléchargement et téléchargement de fichiers

Pour plus de commodité, nous définissons les informations d'identification de l'utilisateur et la région sous forme de constantes dans le fichier wp-config.php :

 define ('AWS_ACCESS_KEY_ID', '...'); // Your access key id define ('AWS_SECRET_ACCESS_KEY', '...'); // Your secret access key define ('AWS_REGION', 'eu-central-1'); // Region where the bucket is located. This is the region id for "EU (Frankfurt)"

Dans notre cas, nous implémentons la fonctionnalité d'avatar de recadrage, pour laquelle les avatars seront stockés sur le seau "avatars-smashing". Cependant, dans notre application, nous pouvons avoir plusieurs autres compartiments pour d'autres fonctionnalités, nécessitant d'exécuter les mêmes opérations de téléchargement, de téléchargement et de liste des fichiers. Par conséquent, nous implémentons les méthodes communes sur une classe abstraite AWS_S3 et nous obtenons les entrées, telles que le nom du compartiment défini via la fonction get_bucket , dans les classes enfants d'implémentation.

 // Load the SDK and import the AWS objects require 'vendor/autoload.php'; use Aws\S3\S3Client; use Aws\Exception\AwsException; // Definition of an abstract class abstract class AWS_S3 { protected function get_bucket() { // The bucket name will be implemented by the child class return ''; } }

La classe S3Client expose l'API pour interagir avec S3. Nous l'instancions uniquement lorsque cela est nécessaire (via une initialisation différée) et enregistrons une référence à celle-ci sous $this->s3Client afin de continuer à utiliser la même instance :

 abstract class AWS_S3 { // Continued from above... protected $s3Client; protected function get_s3_client() { // Lazy initialization if (!$this->s3Client) { // Create an S3Client. Provide the credentials and region as defined through constants in wp-config.php $this->s3Client = new S3Client([ 'version' => '2006-03-01', 'region' => AWS_REGION, 'credentials' => [ 'key' => AWS_ACCESS_KEY_ID, 'secret' => AWS_SECRET_ACCESS_KEY, ], ]); } return $this->s3Client; } }

Lorsque nous traitons avec $file dans notre application, cette variable contient le chemin absolu vers le fichier sur le disque (par exemple /var/app/current/wp-content/uploads/users/654/leo.jpg ), mais lors du téléchargement du file vers S3, nous ne devons pas stocker l'objet sous le même chemin. En particulier, nous devons supprimer le bit initial concernant les informations système ( /var/app/current ) pour des raisons de sécurité, et éventuellement nous pouvons supprimer le bit /wp-content (puisque tous les fichiers sont stockés dans ce dossier, il s'agit d'informations redondantes ), en ne gardant que le chemin relatif vers le fichier ( /uploads/users/654/leo.jpg ). De manière pratique, cela peut être réalisé en supprimant tout ce qui suit WP_CONTENT_DIR du chemin absolu. Les fonctions get_file et get_file_relative_path ci-dessous basculent entre les chemins de fichier absolu et relatif :

 abstract class AWS_S3 { // Continued from above... function get_file_relative_path($file) { return substr($file, strlen(WP_CONTENT_DIR)); } function get_file($file_relative_path) { return WP_CONTENT_DIR.$file_relative_path; } }

Lors du téléchargement d'un objet sur S3, nous pouvons établir qui a accès à l'objet et le type d'accès, via les autorisations de la liste de contrôle d'accès (ACL). Les options les plus courantes sont de garder le fichier privé (ACL => « private ») et de le rendre accessible en lecture sur Internet (ACL => « public-read »). Comme nous devrons demander le fichier directement à S3 pour le montrer à l'utilisateur, nous avons besoin d'ACL => "public-read":

 abstract class AWS_S3 { // Continued from above... protected function get_acl() { return 'public-read'; } }

Enfin, nous implémentons les méthodes pour télécharger un objet et télécharger un objet depuis le compartiment S3 :

 abstract class AWS_S3 { // Continued from above... function upload($file) { $s3Client = $this->get_s3_client(); // Upload a file object to S3 $s3Client->putObject([ 'ACL' => $this->get_acl(), 'Bucket' => $this->get_bucket(), 'Key' => $this->get_file_relative_path($file), 'SourceFile' => $file, ]); } function download($file) { $s3Client = $this->get_s3_client(); // Download a file object from S3 $s3Client->getObject([ 'Bucket' => $this->get_bucket(), 'Key' => $this->get_file_relative_path($file), 'SaveAs' => $file, ]); } }

Ensuite, dans la classe enfant d'implémentation, nous définissons le nom du bucket :

 class AvatarCropper_AWS_S3 extends AWS_S3 { protected function get_bucket() { return 'avatars-smashing'; } }

Enfin, nous instancions simplement la classe pour télécharger les avatars ou les télécharger depuis S3. De plus, lors du passage des étapes 1 à 2 et 2 à 3, nous devons communiquer la valeur de $file . Nous pouvons le faire en soumettant un champ "file_relative_path" avec la valeur du chemin relatif de $file via une opération POST (nous ne transmettons pas le chemin absolu pour des raisons de sécurité : pas besoin d'inclure le "/var/www/current ” informations pour les étrangers à voir):

 // Step 1: after the file was uploaded to the server, upload it to S3. Here, $file is known $avatarcropper = new AvatarCropper_AWS_S3(); $avatarcropper->upload($file); // Get the file path, and send it to the next step in the POST $file_relative_path = $avatarcropper->get_file_relative_path($file); // ... // -------------------------------------------------- // Step 2: get the $file from the request and download it, manipulate it, and upload it again $avatarcropper = new AvatarCropper_AWS_S3(); $file_relative_path = $_POST['file_relative_path']; $file = $avatarcropper->get_file($file_relative_path); $avatarcropper->download($file); // Do manipulation of the file // ... // Upload the file again to S3 $avatarcropper->upload($file); // -------------------------------------------------- // Step 3: get the $file from the request and download it, and then save it $avatarcropper = new AvatarCropper_AWS_S3(); $file_relative_path = $_REQUEST['file_relative_path']; $file = $avatarcropper->get_file($file_relative_path); $avatarcropper->download($file); // Save it, whatever that means // ...

Affichage du fichier directement depuis S3

Si on veut afficher l'état intermédiaire du fichier après manipulation à l'étape 2 (par exemple l'avatar de l'utilisateur après recadrage), alors il faut référencer le fichier directement depuis S3 ; l'URL n'a pas pu pointer vers le fichier sur le serveur puisque, encore une fois, nous ne savons pas quel serveur traitera cette requête.

Ci-dessous, nous ajoutons la fonction get_file_url($file) qui obtient l'URL de ce fichier dans S3. Si vous utilisez cette fonction, assurez-vous que l'ACL des fichiers téléchargés est "en lecture publique", sinon elle ne sera pas accessible à l'utilisateur.

 abstract class AWS_S3 { // Continue from above... protected function get_bucket_url() { $region = $this->get_region(); // North Virginia region is simply "s3", the others require the region explicitly $prefix = $region == 'us-east-1' ? 's3' : 's3-'.$region; // Use the same scheme as the current request $scheme = is_ssl() ? 'https' : 'http'; // Using the bucket name in path scheme return $scheme.'://'.$prefix.'.amazonaws.com/'.$this->get_bucket(); } function get_file_url($file) { return $this->get_bucket_url().$this->get_file_relative_path($file); } }

Ensuite, on peut simplement récupérer l'url du fichier sur S3 et imprimer l'image :

 printf( "<img src='%s'>", $avatarcropper->get_file_url($file) );

Liste des fichiers

Si, dans notre application, nous voulons permettre à l'utilisateur de voir tous les avatars précédemment téléchargés, nous pouvons le faire. Pour cela, nous introduisons la fonction get_file_urls qui répertorie l'URL de tous les fichiers stockés sous un certain chemin (en termes S3, cela s'appelle un préfixe) :

 abstract class AWS_S3 { // Continue from above... function get_file_urls($prefix) { $s3Client = $this->get_s3_client(); $result = $s3Client->listObjects(array( 'Bucket' => $this->get_bucket(), 'Prefix' => $prefix )); $file_urls = array(); if(isset($result['Contents']) && count($result['Contents']) > 0 ) { foreach ($result['Contents'] as $obj) { // Check that Key is a full file path and not just a "directory" if ($obj['Key'] != $prefix) { $file_urls[] = $this->get_bucket_url().$obj['Key']; } } } return $file_urls; } }

Ensuite, si nous stockons chaque avatar sous le chemin "/users/${user_id}/", en passant ce préfixe nous obtiendrons la liste de tous les fichiers :

 $user_id = get_current_user_id(); $prefix = "/users/${user_id}/"; foreach ($avatarcropper->get_file_urls($prefix) as $file_url) { printf( "<img src='%s'>", $file_url ); }

Conclusion

Dans cet article, nous avons exploré comment utiliser une solution de stockage d'objets cloud pour agir comme un référentiel commun pour stocker les fichiers d'une application déployée sur plusieurs serveurs. Pour la solution, nous nous sommes concentrés sur AWS S3 et avons ensuite montré les étapes nécessaires à intégrer dans l'application : création du compartiment, configuration des autorisations utilisateur, téléchargement et installation du SDK. Enfin, nous avons expliqué comment éviter les pièges de sécurité dans l'application et avons vu des exemples de code montrant comment effectuer les opérations les plus élémentaires sur S3 : télécharger, télécharger et répertorier les fichiers, ce qui nécessitait à peine quelques lignes de code chacun. La simplicité de la solution montre que l'intégration de services cloud dans l'application n'est pas difficile, et cela peut également être réalisé par des développeurs qui n'ont pas beaucoup d'expérience avec le cloud.