La conférence AWS re:Invent s’est déroulée il y a à peine deux semaines, et j’ai déjà hâte d’y retourner l’année prochaine ! Ce fut une semaine passionnante avec de nombreuses annonces excitantes de la part d’Amazon. J’y ai non seulement appris beaucoup de choses, mais je m’y suis également vraiment amusée.

Passons maintenant à l’un de mes sujets favoris de cette édition de la conférence re:Invent : le service AWS Lambda et la nouvelle tendance, l’architecture sans serveur. Par « sans serveur », on veut dire qu’aucun déploiement, infrastructure ou serveur spécifique n’est nécessaire pour exécuter votre code (qui est intégré en tant que fonction Lambda) et interagir avec les services AWS existants en réponse aux événements AWS natifs. C’est magique ! Sans parler du fait qu’AWS Lambda supporte le code rédigé en JavaScript, Java et Python et que ces fonctions n’ont pas d’état. Cela signifie qu’elles peuvent s’ajuster rapidement et que plusieurs copies de la même fonction peuvent être exécutées en parallèle.

Voici un exemple montrant la puissance d’AWS Lambda et les choses intéressantes que vous pouvez faire en combinant ce service et Nuxeo Platform. Un déploiement classique serait un serveur Nuxeo fonctionnant sur une instance EC2 configurée avec Amazon S3 en tant que gestionnaire de données binaires.

Dans cette configuration, un document est créé dans Nuxeo Platform et le blob associé (s’il y en a un) est stocké dans S3. Par défaut, si vous uploadez un fichier, il est d’abord uploadé vers Nuxeo Platform pour que la plateforme l’uploade ensuite sur S3. Mais en utilisant le service AWS Lambda, vous pouvez uploader votre fichier directement sur S3 et le document pointant vers ce bloc sera automatiquement créé dans Nuxeo Platform.

Cas d’utilisation

Imaginez le cas où vous venez juste de commencer à utiliser Nuxeo Platform et que vous voulez réaliser un import en masse afin d’uploader les documents que vous aviez dans votre ancien système.

Allons y étape par étape. Comme il s’agit d’une preuve de concept et que je me concentre sur la façon de procéder, je vais utiliser les configurations par défaut, aucun chiffrage dans S3, pas d’upload en plusieurs parties et une authentification basique avec un utilisateur par défaut pour créer les documents dans Nuxeo Platform.

Voici mon instance actuelle (la marketplace S3BinaryManager est installée et configurée) :

Nuxeo and S3

Configure the bucket

Par défaut, il y a plusieurs événements de bucket S3 qui sont notifiés lorsque des objets sont créés, modifiés ou supprimés d’un bucket. L’une des choses que je préfère avec Lambda, c’est que les fonctionnalités sont nativement intégrées à ces notifications pour qu’on puisse avoir une fonctionnalité Lambda configurée pour exécuter une action en fonction d’un événement. Et la seule chose que cette fonctionnalité a besoin de faire, c’est de créer le document dans Nuxeo Platform.

Voilà ce que nous voulons : une fonctionnalité Lambda qui reçoit une notification lorsqu’un objet est uploadé dans notre bucket et qui invoque une action de création d’un document dans Nuxeo Platform qui pointe vers ce blob existant.

Lambda Function

Créer la fonctionnalité Lambda

Dans la console AWS, nous créons une nouvelle fonctionnalité Lambda à partir d’un blueprint Node.js existant (un blueprint est un modèle de fonctionnalité proposé par Amazon et le blueprint existant dispose déjà d’une fonctionnalité modèle Amazon S3). Le seul élément important à prendre en compte est que la fonctionnalité doit être dans la même région AWS que le bucket S3 et l’instance EC2.

Voici les étapes. Allez dans AWS Lambda et créez une nouvelle fonctionnalité.

  1. Dans Select blueprint, choisissez le modèle s3-get-object.
  2. Dans Configure event sources, S3 est déjà pré-sélectionné. Choisissez votre bucket (dans mon cas il s’appelle ‘mariana’) et sélectionnez Object Created (All) pour le type d’événement (Event type).
  3. Dans Configure function, la durée d’exécution est déjà définie sur node.js. Vous pouvez laisser la mémoire à 128MB et augmenter le Timeout à 5 secs. Pour le rôle IAM, choisissez le rôle existant lambda_s3_exec_role (puisque ce rôle dispose de toutes les permissions nécessaires pour les actions AWS liées à cette fonctionnalité).

Et c’est à peu près tout ! Il ne vous reste plus qu’à ajouter notre code personnalisé pour créer le document dans Nuxeo Platform. Comme vous pouvez le voir dans le code existant, certains journaux sont déjà activés. Vous pouvez consulter le résultat de ces journaux dans CloudWatch. Il vous suffit d’aller dans le sous-onglet Monitoring (où vous avez accès à tout un tas de statistiques bien pratiques, telles que le compteur d’invocation, la durée, etc.) et de cliquer sur View logs dans le lien CloudWatch. Vous devriez obtenir le résultat suivant :

Create a Lambda function

Save and test Lambda function

Ajouter du code personnalisé pour créer le document dans Nuxeo Platform

Nuxeo Custom Operation :

C’est la partie un peu plus délicate. Nous ne pouvons pas simplement invoquer une opération Create.Document ou FileManager.Import car elles attendent toutes les deux le fichier en tant que paramètre. Nous devons donc écrire une opération personnalisée qui crée le document pointant vers le blob existant. Le S3BinaryManager a besoin du résumé du fichier (l’algorithme par défaut est MD5) et celui-ci doit être l’un des paramètres attendus par l’opération, avec le titre, le type de contenu et la longueur du blob.

Nous devons donc faire attention et utiliser le résumé en tant que clé de l’objet lors de l’upload vers S3. Il faut également se souvenir de transférer son nom de fichier.

Voici le code :

@Operation(id = CreateDocumentFromS3Blob.ID, category = Constants.CAT_DOCUMENT, label = "Create", description = "")
public class CreateDocumentFromS3Blob {

    public static final String ID = "CreateDocumentFromS3Blob";

    @Context
    protected CoreSession session;

    @Param(name = "filename")
    protected String filename;

    @Param(name = "mimeType")
    protected String mimeType;

    @Param(name = "digest")
    protected String digest;

    @Param(name = "length")
    protected Long length;

    @OperationMethod(collector = DocumentModelCollector.class)
    public DocumentModel run(DocumentModel doc) throws Exception {
        if (filename == null) {
            filename = "Untitled";
        }
        DocumentModel newDoc = session.createDocumentModel(doc.getPathAsString(), filename, "File");
        newDoc = session.createDocument(newDoc);
        StorageBlob sb = new StorageBlob(new LazyBinary(digest, Framework.getLocalService(RepositoryManager.class).getDefaultRepositoryName(),
                (CachingBinaryManager) Framework.getLocalService(BinaryManagerService.class).getBinaryManager(                     Framework.getLocalService(RepositoryManager.class).getDefaultRepositoryName())), filename,mimeType, null, digest, length);
        newDoc.setPropertyValue("file:content", sb);
        newDoc.setPropertyValue("dc:title", filename);
        return session.saveDocument(newDoc);
   }
}

La partie intéressante de ce code est que nous créeons un LazyBinary avec le résumé dont nous disposons et que nous le définissons en tant que propriété file:content.

Ajouter du code personnalisé dans la fonctionnalité Lambda :

Partons maintenant du principe que cette opération personnalisée est déployée sur notre Nuxeo Platform qui tourne sur l’instance EC2. Il faut ajouter le code personnalisé pour l’invoquer à partir de notre fonctionnalité Lambda.

Pour la démonstration, nous allons juste utiliser Administrator/Administrator pour l’utilisateur qui invoque cette opération et créer le document dans son espace de travail personnel (la saisie de l’opération est codée en dur dans l’ID de ce document).

Comme mentionné plus haut, le S3 BinaryManager attend le résumé du blob à utiliser en tant que clé de l’objet dans le bucket. Il nous faut donc uploader l’objet à l’aide de cette clé. Et comme nous avons également besoin du titre du document, nous pouvons utiliser une métadonnée personnalisée S3 pour le transférer.

La fonctionnalité Lambda :

var aws = require('aws-sdk');
var s3 = new aws.S3({
    apiVersion : '2006-03-01'
});
var http = require('http');
var crypto = require('crypto');

var options = {
host : '52.26.252.66',
port : '8080',
method : 'POST',
path : '/nuxeo/site/automation/CreateDocumentFromS3Blob',
headers : {
'Accept' : 'application/json',
'Content-Type' : 'application/json+nxrequest'
},
auth : 'Administrator:Administrator'
};

exports.handler = function(event, context) {
    console.log('Received event:', JSON.stringify(event, null, 2));

    var bucket = event.Records[0].s3.bucket.name;
    var key = event.Records[0].s3.object.key;

    var params = {
    Bucket : bucket,
    Key : key
    };

    s3.getObject(params, function(err, data) {
        if (err) {
            console.log(err);
            var message = "Error getting object " + key + " from bucket " + bucket + ". Make sure they exist and your bucket is in the same region as this function.";
            console.log(message);
            context.fail(message);
        } else {

            //Nuxeo expects the key to be the digest of the file
            // var digest = crypto.createHash('md5').update(data.Body).digest("hex");
            var title = data.Metadata.title !== undefined ? data.Metadata.title : key;
            //console.log('title :', data.Metadata.title);

            //the input is the id of the parent document
            var postData = JSON.stringify({
            "input" : "f04453f9-de1c-4a8d-9956-add074069813",
            "params" : {
            "filename" : title,
            "mimeType" : data.ContentType,
            "digest" : key,
            "length" : data.ContentLength
            }
            });

            var req = http.request(options, function(res) {
                res.on('data', function(response) {
                    console.log('Nuxeo response:' + response);
                    context.succeed('succeed');
                });

                res.on('end', function(response) {
                    context.succeed('end');
                });

            });
            req.write(postData);
            req.end();
        }
    });
};

Et c’est tout !

Le résultat en action :

1. À partir de la ligne de commande, j’uploade ma place pour le concert des Foo Fighters (excellent concert d’ailleurs :) !) dans mon bucket S3 en passant son titre en tant que métadonnée personnalisée :

Marianas-MacBook-Pro:opt mariana$ md5 /Users/mariana/Downloads/FooFigthers.pdf
MD5 (/Users/mariana/Downloads/FooFigthers.pdf) = 1a29c592b09ee7725415efa354907426
Marianas-MacBook-Pro:opt mariana$ aws s3api put-object --bucket mariana --key 1a29c592b09ee7725415efa354907426 --body /Users/mariana/Downloads/FooFigthers.pdf --content-type application/pdf --metadata title=FooFighters.pdf

La réponse est :

{
    "ETag": "\"1a29c592b09ee7725415efa354907426\""
}

2. Ma fonctionnalité Lambda createDocInNuxeo a été automatiquement invoquée :

createDocInNuxeo Lambda function was automatically invoked

3. Et on peut maintenant voir le document dans Nuxeo Platform :

Document seen in the Nuxeo Platform

Le fichier principal est FooFighters.pdf et je peux le télécharger pour voir que le fichier est bien celui que j’avais uploadé.

Et c’est tout ! Vous pouvez trouver le code source ici (un plug-in contient le code de l’opération ainsi que le code de la fonctionnalité Lambda).

Lisez tout sur Nuxeo et AWS dans ce guide