Une API REST sans serveur

Les sources de l’idée

En regardant la doc de Micronaut je suis tombé sur un paragraphe qui m’a fait rêver : https://micronaut-projects.github.io/micronaut-aws/latest/guide/#apiProxy

Je me suis toujours demandé comment faire un backend d’application facile à configurer mais également pouvant être scalé facilement en cas de charge. J’ai l’impression d’avoir enfin trouvé une solution, voyons voir ce que ça vaut.

On va donc tenter de créer une API REST qu’on va faire tourner sur AWS Lambda qu’on va cacher derrière AWS API Gateway.

Pour ceux que ça intéresse les sources du projet sont disponibles sur Gitlab : https://gitlab.com/gautier-levert/micronaut-lambda-rest/

Créer le projet Micronaut

On commence donc gentiment à créer un projet Micronaut à l’aide de leur CLI, comme expliqué dans la documentation.

Premier petit problème, la feature décrite dans la doc n’existe pas. Il y a déjà une issue GitHub sur le sujet. Pas grave, on crée le projet quand même et on verra plus tard.

1
mn create-app test-app --build gradle --jdk 11 --lang kotlin --features logback,rxjava3

On ouvre notre projet dans notre IDE préféré et on va voir ce qu’on peut faire.

Le handler AWS Lambda

Comme expliqué plus haut la feature n’existe pas mais les modules existent bel et bien et il est possible de les ajouter au dépendances du projet.

1
2
3

    implementation("io.micronaut.aws:micronaut-function-aws-api-proxy")

La magie opère dans la classe : StreamLambdaHandler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

class StreamLambdaHandler : RequestStreamHandler {

    private val log = getLogger<StreamLambdaHandler>()

    private var handler = try {
        MicronautLambdaContainerHandler(
                Micronaut.build()
                        .packages("test.app")
        )
    } catch (e: ContainerInitializationException) {
        // if we fail here. We re-throw the exception to force another cold start
        log.error("", e)
        throw RuntimeException("Could not initialize Micronaut", e)
    }

    @Throws(IOException::class)
    override fun handleRequest(inputStream: InputStream, outputStream: OutputStream, context: Context) {
        handler.proxyStream(inputStream, outputStream, context)
    }
}

On voit ici que cette classe, instancie une application Micronaut au complet et va nous permettre d’exécuter des requêtes au sein d’un handler lambda !

La configuration AWS

Maintenant il nous reste à configurer une fonction AWS Lambda et y déployer notre projet, on change de documentation et on se retrouve du côté de aws-serverless-java-container.

Ici on a besoin de SAM. C’est un peu l’équivalent de serverless que vous avez déjà pu rencontrer si vous jouez un peu avec AWS Lambda ou Google Cloud Functions habituellement.

Je pars du principe qu’il est installé et configuré avec vox identifiants AWS (je vous renvoie vers la documentation officielle au besoin).

Là les ennuis commencent, le build de sam génère une archive trop grosse et les exemples de configuration ne fonctionnent pas…

Mais en creusant un peu j’ai fini pas comprendre le problème : https://github.com/awslabs/aws-lambda-builders/issues/138

J’ai finalement dû retirer les plugins Gradle shadow et application qui rentrait en conflit avec la procédure de build de SAM.

Pour le reste du fichier template, je me suis beaucoup inspiré de celui fourni dans le projet de démo : https://github.com/awslabs/aws-serverless-java-container/tree/master/samples/micronaut/pet-store

Je me suis principalement attaché à remplacer le runtime pour passer d’une compilation GraalVM à un projet java classique.

Voilà au final ce que ça donne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  test-app

  Sample SAM Template for test-app  

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Api:
    EndpointConfiguration: REGIONAL

Resources:
  HelloWorldApiService:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./
      Handler: test.app.StreamLambdaHandler::handleRequest
      Runtime: java11
      MemorySize: 256
      Policies: AWSLambdaBasicExecutionRole
      Tracing: Active
      Timeout: 15
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: any

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  MicronautServiceApi:
    Description: URL for application
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod'
    Export:
      Name: MicronautServiceApi

Le déploiement

Voilà tout est en place, je compile, j’envoie et je laisse la magie opérer :

1
2
sam build
sam deploy

et ça marche !

J’appelle l’URL avec Postman et je reçois correctement le très attendu "Hello World".

Le problème : environ 6 secondes de délai. Je m’y attendais mais je m’attendais à un peu mieux… Heureusement les appels suivants ont des délais beaucoup plus raisonnables : 44ms, 56ms, 71ms, etc. Mais à chaque fois que le timeout de lambda arrive à terme et que le projet doit redémarrer, BOUM 6 secondes.

Du coup c’est mort ?

Non, le projet fonctionne très bien dans son ensemble et même si le premier chargement est long, on y gagne quand même pas mal de chose :

  • un cout d’exécution ridicule : étant donné que le serveur tourne uniquement quand il exécute des requêtes.
  • une montée en charge facile : par défaut AWS laisse tourner plus de 1000 instances simultanées.
  • une descente de charge facile : les instances se coupent toutes seules.
  • une disponibilité digne des plus grands : plus de coupure durant le déploiement, l’infrastructure d’Amazon pour gérer…

Par contre il ne faut pas oublier que tout ça ne fonctionne que si le reste de votre infrastructure (et en particulier la base de données) tient également la charge.

Donc si des requêtes de plusieurs secondes de temps en temps ne vous font pas peur, pourquoi pas.

Il ne faut pas oublier non plus que ce projet n’est qu’un simple Hello World, il est sûr que des projets plus gros vont avoir un temps de démarrage bien plus grand.

Pour que tout ça reste viable dans un environnement de production, il faudra tout d’abord limité la taille des projets et des dépendances, mais certainement aussi se pencher du côté de GraalVM pour compiler le projet en code natif.

Load Comments?