web-dev-qa-db-fra.com

aws api gateway & lambda: plusieurs terminaux / fonctions par rapport à un seul terminal

J'ai une api AWS qui assure le proxy des fonctions lamba. J'utilise actuellement différents terminaux avec des fonctions lambda distinctes:

api.com/getData --> getData
api.com/addData --> addData
api.com/signUp --> signUp

Le processus de gestion de tous les points de terminaison et de toutes les fonctions devient fastidieux. Existe-t-il un inconvénient lorsque j'utilise un seul point de terminaison pour une fonction lambda qui décide quoi faire en fonction de la chaîne de requête?

api.com/exec&func=getData --> exec --> if(params.func === 'getData') { ... }
36
Chris

Il est parfaitement valide de mapper plusieurs méthodes sur une seule fonction lambda et de nombreuses personnes l'utilisent aujourd'hui plutôt que de créer une ressource de passerelle api et une fonction lambda pour chaque méthode discrète.

Vous pouvez envisager de soumettre toutes les demandes à une fonction unique. Consultez la documentation suivante sur la création d’une passerelle API => Intégration du proxy Lambda: http://docs.aws.Amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple- proxy.html

Leur exemple est excellent ici. Une demande comme celle-ci:

POST /testStage/hello/world?name=me HTTP/1.1
Host: gy415nuibc.execute-api.us-east-1.amazonaws.com
Content-Type: application/json
headerName: headerValue

{
    "a": 1
}

L'envoi des données d'événement suivantes à votre fonction AWS Lambda se terminera ainsi:

{
  "message": "Hello me!",
  "input": {
    "resource": "/{proxy+}",
    "path": "/hello/world",
    "httpMethod": "POST",
    "headers": {
      "Accept": "*/*",
      "Accept-Encoding": "gzip, deflate",
      "cache-control": "no-cache",
      "CloudFront-Forwarded-Proto": "https",
      "CloudFront-Is-Desktop-Viewer": "true",
      "CloudFront-Is-Mobile-Viewer": "false",
      "CloudFront-Is-SmartTV-Viewer": "false",
      "CloudFront-Is-Tablet-Viewer": "false",
      "CloudFront-Viewer-Country": "US",
      "Content-Type": "application/json",
      "headerName": "headerValue",
      "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
      "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
      "User-Agent": "PostmanRuntime/2.4.5",
      "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
      "X-Forwarded-For": "54.240.196.186, 54.182.214.83",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    },
    "queryStringParameters": {
      "name": "me"
    },
    "pathParameters": {
      "proxy": "hello/world"
    },
    "stageVariables": {
      "stageVariableName": "stageVariableValue"
    },
    "requestContext": {
      "accountId": "12345678912",
      "resourceId": "roq9wj",
      "stage": "testStage",
      "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33",
      "identity": {
        "cognitoIdentityPoolId": null,
        "accountId": null,
        "cognitoIdentityId": null,
        "caller": null,
        "apiKey": null,
        "sourceIp": "192.168.196.186",
        "cognitoAuthenticationType": null,
        "cognitoAuthenticationProvider": null,
        "userArn": null,
        "userAgent": "PostmanRuntime/2.4.5",
        "user": null
      },
      "resourcePath": "/{proxy+}",
      "httpMethod": "POST",
      "apiId": "gy415nuibc"
    },
    "body": "{\r\n\t\"a\": 1\r\n}",
    "isBase64Encoded": false
  }
}

Vous avez maintenant accès à tous les en-têtes, paramètres d'URL, corps, etc., que vous pouvez utiliser pour gérer les demandes différemment dans une seule fonction Lambda (en mettant en œuvre votre propre routage).

En tant qu'opinion, cette approche présente des avantages et des inconvénients. Nombre d'entre eux dépendent de votre cas d'utilisation spécifique:

  • Déploiement : si chaque fonction lambda est discrète, vous pouvez les déployer indépendamment, ce qui pourrait réduire le risque de modification du code (stratégie de microservices). Inversement, vous pouvez constater que le besoin de déployer des fonctions séparément ajoute à la complexité et à la lourdeur.
  • Description de l'auto : l'interface de la passerelle API rend extrêmement intuitive la visualisation de la disposition de vos points de terminaison RESTful - les noms et les verbes sont tous visibles en un coup d'œil. La mise en œuvre de votre propre routage pourrait se faire au détriment de cette visibilité.
  • Dimensionnement lambda et limites : Si vous utilisez un proxy complet, vous devrez alors choisir une taille d'instance, un délai d'expiration, etc. qui s'adaptera à tous. vos points d'extrémité RESTful. Si vous créez des fonctions discrètes, vous pourrez alors choisir plus soigneusement l'empreinte mémoire, le délai d'attente, le comportement de mise à mort, etc. qui répondent le mieux aux besoins de l'appel spécifique.
38
Dave Maple

j'ai mis au point 5 ~ 6 microservices avec la passerelle Lambda-API et passé plusieurs tentatives et échecs.

en bref, de mon expérience, il est préférable de déléguer tous les appels d'API à lambda avec un seul mappage générique APIGateway, tel que

/api/{+proxy} -> Lambda

si vous avez déjà utilisé des frameworks tels que grape , vous savez que lors de la création d'API, des fonctionnalités telles que
"middleware"
"gestion globale des exceptions"
"routage en cascade"
"validation des paramètres"

sont vraiment cruciales. Au fur et à mesure que votre API grandit, il est presque impossible de gérer toutes les routes avec le mappage API Gateway. De plus, API Gateway ne prend en charge aucune de ces fonctionnalités.

de plus, ce n'est pas vraiment pratique de diviser lambda pour chaque terminal pour le développement ou le déploiement.

de votre exemple,

api.com/getData --> getData  
api.com/addData --> addData  
api.com/signUp --> signUp  

imaginez que vous ayez des données ORM, une logique d’authentification de l’utilisateur, un fichier de vue commune (tel que data.erb). Alors, comment allez-vous partager cela?

vous pourriez peut briser comme,

api/auth/{+proxy} -> AuthServiceLambda  
api/data/{+proxy} -> DataServiceLambda  

mais pas comme "par point final". vous pouvez peut-être consulter le concept de microservice et les meilleures pratiques pour diviser le service

pour les structures web telles que les fonctionnalités, la caisse this nous avons juste construit une structure web pour lambda, car j'en avais besoin dans mon entreprise.

13
Kurt Lee

J'aurais commenté d'ajouter quelques points à Dave Maple bonne réponse, mais je n'ai pas encore assez de points de réputation, alors je vais ajouter les commentaires ici.

J'ai commencé à suivre le chemin de plusieurs points de terminaison pointant vers une fonction Lambda pouvant traiter chaque point de terminaison en accédant à la propriété 'resource' de l'événement. Après l'avoir essayé, je les ai maintenant séparés en fonctions distinctes pour les raisons suggérées par Dave, plus:

  • Je trouve plus facile de parcourir les journaux et les moniteurs lorsque les fonctions sont séparées.
  • Une des nuances que je n'avais pas comprises au début, c'est que vous pouvez avoir une base de code et déployer exactement le même code que plusieurs fonctions Lambda. Cela vous permet de bénéficier des avantages de la séparation des fonctions et d'une approche consolidée dans votre base de code.
  • Vous pouvez utiliser l'AWS CLI pour automatiser des tâches sur plusieurs fonctions afin de réduire/éliminer les inconvénients de la gestion de fonctions distinctes. Par exemple, j'ai un script qui met à jour 10 fonctions avec le même code.
8
Inspector6

Autant que je sache, AWS n'autorise qu'un seul gestionnaire par fonction Lambda. C'est pourquoi j'ai créé un petit mécanisme de "routage" avec Java Generics (pour des contrôles de type plus stricts au moment de la compilation). Dans l'exemple suivant, vous pouvez appeler plusieurs méthodes et transmettre un objet différent Lambda et retour via un gestionnaire Lambda:

Classe Lambda avec gestionnaire:

public class GenericLambda implements RequestHandler<LambdaRequest<?>, LambdaResponse<?>> {

@Override
public LambdaResponse<?> handleRequest(LambdaRequest<?> lambdaRequest, Context context) {

    switch (lambdaRequest.getMethod()) {
    case WARMUP:
        context.getLogger().log("Warmup");  
        LambdaResponse<String> lambdaResponseWarmup = new LambdaResponse<String>();
        lambdaResponseWarmup.setResponseStatus(LambdaResponse.ResponseStatus.IN_PROGRESS);
        return lambdaResponseWarmup;
    case CREATE:
        User user = (User)lambdaRequest.getData();
        context.getLogger().log("insert user with name: " + user.getName());  //insert user in db
        LambdaResponse<String> lambdaResponseCreate = new LambdaResponse<String>();
        lambdaResponseCreate.setResponseStatus(LambdaResponse.ResponseStatus.COMPLETE);
        return lambdaResponseCreate;
    case READ:
        context.getLogger().log("read user with id: " + (Integer)lambdaRequest.getData());
        user = new User(); //create user object for test, instead of read from db
        user.setName("name");
        LambdaResponse<User> lambdaResponseRead = new LambdaResponse<User>();
        lambdaResponseRead.setData(user);
        lambdaResponseRead.setResponseStatus(LambdaResponse.ResponseStatus.COMPLETE);
        return lambdaResponseRead;
    default:
        LambdaResponse<String> lambdaResponseIgnore = new LambdaResponse<String>();
        lambdaResponseIgnore.setResponseStatus(LambdaResponse.ResponseStatus.IGNORED);
        return lambdaResponseIgnore;    
    }
}
}

Classe LambdaRequest:

public class LambdaRequest<T> {
private Method method;
private T data;
private int languageID; 

public static enum Method {
    WARMUP, CREATE, READ, UPDATE, DELETE 
}

public LambdaRequest(){
}

public Method getMethod() {
    return method;
}
public void setMethod(Method create) {
    this.method = create;
}
public T getData() {
    return data;
}
public void setData(T data) {
    this.data = data;
}
public int getLanguageID() {
    return languageID;
}
public void setLanguageID(int languageID) {
    this.languageID = languageID;
}
}

Classe LambdaResponse:

public class LambdaResponse<T> {

private ResponseStatus responseStatus;
private T data;
private String errorMessage;

public LambdaResponse(){
}

public static enum ResponseStatus {
    IGNORED, IN_PROGRESS, COMPLETE, ERROR, COMPLETE_DUPLICATE
}

public ResponseStatus getResponseStatus() {
    return responseStatus;
}

public void setResponseStatus(ResponseStatus responseStatus) {
    this.responseStatus = responseStatus;
}

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

public String getErrorMessage() {
    return errorMessage;
}

public void setErrorMessage(String errorMessage) {
    this.errorMessage = errorMessage;
}

}

Exemple de classe d'utilisateurs POJO:

public class User {
private String name;

public User() {
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
}

Méthode de test JUnit:

    @Test
public void GenericLambda() {
    GenericLambda handler = new GenericLambda();
    Context ctx = createContext();

    //test WARMUP
    LambdaRequest<String> lambdaRequestWarmup = new LambdaRequest<String>();
    lambdaRequestWarmup.setMethod(LambdaRequest.Method.WARMUP);
    LambdaResponse<String> lambdaResponseWarmup = (LambdaResponse<String>) handler.handleRequest(lambdaRequestWarmup, ctx);

    //test READ user
    LambdaRequest<Integer> lambdaRequestRead = new LambdaRequest<Integer>();
    lambdaRequestRead.setData(1); //db id
    lambdaRequestRead.setMethod(LambdaRequest.Method.READ);
    LambdaResponse<User> lambdaResponseRead = (LambdaResponse<User>) handler.handleRequest(lambdaRequestRead, ctx);
    }

ps .: si vous avez problèmes de désérialisation (LinkedTreeMap ne peut pas être converti en ...) en vous fonction Lambda (car uf the Generics/Gson ), utilisez l'énoncé suivant:

YourObject yourObject = (YourObject)convertLambdaRequestData2Object(lambdaRequest, YourObject.class);

Méthode:

private <T> Object convertLambdaRequestData2Object(LambdaRequest<?> lambdaRequest, Class<T> clazz) {

    Gson gson = new Gson();
    String json = gson.toJson(lambdaRequest.getData());
    return gson.fromJson(json, clazz);
}
2
Jörg

À mon avis, le choix d’une API simple ou multiple dépend des considérations suivantes:

  1. Sécurité: Je pense que c'est le plus grand défi d'avoir une structure d'API unique. Il peut être possible d'avoir un profil de sécurité différent pour différentes parties de l'exigence

  2. Pensez au modèle de microservice du point de vue commercial: le but essentiel de toute API doit être de servir certaines requêtes, il doit donc être bien compris et facile à utiliser. Les API associées doivent donc être combinées. Par exemple, si vous avez un client mobile et que 10 bases doivent être extraites et extraites de la base de données, il est logique de disposer de 10 points de terminaison dans une seule API. Mais cela devrait être raisonnable et doit être considéré dans le contexte de la conception de la solution globale. Par exemple, si vous concevez un produit de paie, vous pouvez penser à disposer de modules distincts pour la gestion des congés et la gestion des détails de l'utilisateur. Même si elles sont souvent utilisées par un seul client, elles doivent quand même être des API différentes, car leur signification commerciale est différente.

  3. Réutilisabilité: S'applique à la fois à la réutilisabilité du code et des fonctionnalités. La réutilisabilité du code est un problème plus facile à résoudre, c’est-à-dire créer des modules communs pour les besoins partagés et les construire comme bibliothèques. La réutilisation des fonctionnalités est plus difficile à résoudre. Dans mon esprit, la plupart des cas peuvent être résolus en modifiant la conception des points de terminaison/des fonctions, car si vous avez besoin de dupliquer des fonctionnalités, votre conception initiale ne sera pas suffisamment détaillée.

Je viens de trouver un lien dans un autre SO post qui résume mieux

0
Ayan Guha