J’ai testé Java Google Cloud Functions Alpha

J’ai testé Java Google Cloud Functions Alpha

J’ai testé les Java Google Cloud Functions en Alpha.

Jusqu’ici, les Cloud Functions de Google n’étaient implémentables qu’en NodeJs, Go ou Python. Mais Google est en train de préparer l’ouverture d’un runtime Java (8 et 11), que j’ai pu tester en alpha release privée (pour s’inscrire, c’est ici). Après inscription à l’alpha privée, vous aurez accès à un document de tutoriel pas à pas, et à un forum d’aide en ligne.

On peut écrire deux types de fonctions : les fonctions HTTP et les fonctions « tâche de fond » ou background (pour réagir à un événement Pub/Sub ou Cloud Storage par exemple). Ici je vais vous présenter uniquement les fonctions HTTP.

En pré-requis, il faut installer le SDK Google Cloud qui comprend la ligne de commande gcloud, puis s’authentifier à Google Cloud et installer les composants alpha :

gcloud auth login
gcloud components install alpha

Une fois ceci fait, vous voici prêt pour votre première fonction en Java !

Hello Google Function in Java

Un projet Cloud Function en Java est un projet Maven (ou Gradle) classique qui contient la librairie com.google.cloud.functions:functions-framework-api.

Voici ce qu’il faut mettre dans votre pom.xml.

   <dependency>
    <groupid>com.google.cloud.functions</groupid>
     <artifactid>functions-framework-api</artifactid>
     <version>1.0.0-alpha-2-rc3</version>
     <scope>provided</scope>
   </dependency>

Nous allons ensuite créer notre première fonction, celle-ci est une simple classe qui implémente l’interface com.google.cloud.functions.HttpFunction, cette interface contient une méthode service qui nous permet d’accéder à un objet HttpRequest et un objet HttpResponse. Nous allons utiliser le writer de le l’objet response pour écrire le traditionnel Hello World!.

Voici ce que ça donne, rien de compliqué. Pour plus de facilité j’ai mis cette classe dans le package par défaut, mais libre à vous de la placer où bon vous semble.

import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;

public class Example implements HttpFunction {
    @Override
    public void service(HttpRequest request, HttpResponse response)
            throws IOException {
        var writer = response.getWriter();
        writer.write("Hello world!");
    }
}

Nous allons ensuite déployer notre fonction. Comme celle-ci est une simple classe sans dépendance, on peut directement la déployer sans nécessité de packaging préalable via Maven.

Le déploiement se fait, à la racine du projet, via la commande suivante :

gcloud alpha functions deploy helloworld --entry-point Example --runtime java11  \
        --trigger-http

Cette ligne de commande va déployer une fonction HTTP nommée helloworld, en utilisant le runtime java11, dans le projet GCP en cours, et utilisant le point d’entrée Example qui est notre fonction précédemment créée.

La commande prend pas mal de temps à s’exécuter, voici son résultat :

Deploying function (may take a while - up to 2 minutes)...done.                                                                                                                                            
availableMemoryMb: 256
entryPoint: Example
httpsTrigger:
  url: https://us-central1-methodical-mesh-238712.cloudfunctions.net/helloworld
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/methodical-mesh-238712/locations/us-central1/functions/helloworld
runtime: java11
serviceAccountEmail: ...
sourceUploadUrl: ...
GoogleAccessId=...
status: ACTIVE
timeout: 60s
updateTime: '2020-05-06T08:50:23.833Z'
versionId: '2'

Pour appeler notre fonction, il faut utiliser la valeur de httpsTrigger.url, ici https://us-central1-methodical-mesh-238712.cloudfunctions.net/helloworld

curl https://us-central1-methodical-mesh-238712.cloudfunctions.net/helloworld
> Hello world!

Et voilà, notre première fonction ! Facile non ?

Un exemple un peu plus complexe

Bon, une fonction sans librairie c’est pas super pratique, surtout si on veut réaliser un endpoint REST. A minima il nous faudrait une librairie JSON.

Essayons donc un exemple plus complexe. Comme précédemment, nous allons créer un projet Maven standard avec la dépendance com.google.cloud.functions:functions-framework-api.

Puis, on va ajouter la librairie Gson pour pouvoir gérer de la représentation JSON :

   <dependency>
       <groupid>com.google.code.gson</groupid>
       <artifactid>gson</artifactid>
       <version>2.8.5</version>
   </dependency>

Ensuite, on va créer une fonction FruitRestFunction qui va implémenter un POST pour créer un Fruit, et un GET pour récupérer la liste des Fruits ou un seul, via le paramètre name. Le tout sérialisé en JSON.

Voici ce que ça donne (croyez-le ou pas mais j’ai écrit ça en une fois sans faute et j’en suis fière 😉 ).

public class FruitRestFunction implements HttpFunction {
    private Gson gson = new Gson();
    private Map fruits = new HashMap<>();

    @Override
    public void service(HttpRequest request, HttpResponse response)
            throws Exception {
        String data = null;
        if(request.getMethod().equalsIgnoreCase("POST")){
            //create a fruit
            Fruit fruit = gson.fromJson(request.getReader(), Fruit.class);
            fruits.put(fruit.name, fruit);
            data = "{\"status\" : \"created\"}";
        }
        else if (request.getMethod().equalsIgnoreCase("GET")){
            if(request.getQueryParameters().containsKey("name")){
                //get a fruit
                Fruit fruit = fruits.get(request.getQueryParameters().get("name").get(0));
                data = gson.toJson(fruit);
            }
            else {
                //get all fruits
                data = gson.toJson(fruits.values());
            }
        }

        //write to the response
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(data);
    }
}

Et pour la classe Fruit tout simplement :

public class Fruit {
    public String name;
    public String color;
    public String description;
}

Même si cet exemple contient une librairie de plus, et deux classes dans deux fichiers différents, on peut quand même le déployer directement sans devoir le packager. Et ça, on peut l’avouer, c’est quand même top !

Si vous voulez le packager comme un JAR, il faudrait alors réaliser un JAR de type uber jar (ou fat jar) en utilisant le plugin maven shade, copier le JAR dans un répertoire, et utiliser l’option de ligne de commande --source=directory.

Mais ici, on peut déployer tout simplement via cette commande (toujours à la racine de votre projet) :

gcloud alpha functions deploy fruit-rest \
        --entry-point fr.loicmathieu.gcp.function.FruitRestFunction \
        --runtime java11 --trigger-htt

Et voici un petit exemple d’utilisation via curl :

curl -X POST -d '{"name":"apple", "color":"green", "description":"Delicious"}' \
        https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-rest
> {"status" : "created"}

curl -X POST -d '{"name":"banana", "color":"yellow", "description":"Yummy"}' \
        https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-rest
> {"status" : "created"}

curl https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-rest
> [{"name":"banana","color":"yellow","description":"Yummy"},{"name":"apple","color":"green","description":"Delicious"}]

curl https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-rest?name=banana
> {"name":"banana","color":"yellow","description":"Yummy"}

Attention : si Google déploie une nouvelle instance de votre fonction pendant vos tests, comme les Fruits sont stockés dans une HashMap, celle-ci sera vide. Il vous faudra donc recommencer vos tests …

Micronaut

La future version de Micronaut a un support pour les Cloud Functions de GCP. Celui-ci permet d’utiliser toutes les fonctionnalités de Micronaut (dont l’injection de dépendances) pour écrire vos fonctions.

Pour démarrer mon projet Micronaut, j’ai utilisé Micronaut Launch. Comme il créé un pom.xml un peu complexe, le mieux est d’aller voir directement dans l’exemple disponible sur GitHub : fruit-micronaut.

Nous allons reprendre l’exemple précédent, et le ré-écrire via un Controller Micronaut.

Tout d’abord, externalisons la gestion des fruits dans un service :

@Singleton
public class FruitService {
    private Map fruits = new HashMap<>();

    public Collection<Fruit> list(){
        return fruits.values();
    }

    public void add(Fruit fruit){
        fruits.put(fruit.name, fruit);
    }

    public Fruit get(String name){
        return fruits.get(name);
    }
}

Puis, écrivons un Controller standard Micronaut, qui sera utilisé par notre fonction pour répondre à chaque requête HTTP :

@Controller("/")
public class FruitController {
    @Inject
    private FruitService fruitService;

    @Get(produces = MediaType.APPLICATION_JSON)
    public Collection<Fruit> list() {
        return fruitService.list();
    }

    @Get("/{name}")
    @Produces(MediaType.APPLICATION_JSON)
    public Fruit get(String name) {
        return fruitService.get(name);
    }

    @Post(processes = MediaType.APPLICATION_JSON)
    @Status(HttpStatus.CREATED)
    public ResponseStatus savePet(@Body Fruit fruit) {
        fruitService.add(fruit);
        return new ResponseStatus("created");
    }

   public static class ResponseStatus {
        public String status;

       public ResponseStatus(String status) {
           this.status = status;
       }
   }
}

Pour déployer notre fonction, il faut d’abord la packager via Maven, puis la copier dans un répertoire dans lequel le JAR doit être le seul fichier.

mvn clean package

mkdir deployment

cp target/fruit-micronaut-0.1.jar deployment/

gcloud alpha functions deploy fruit-micronaut \
        --entry-point io.micronaut.gcp.function.http.HttpFunction \
        --runtime java11 --trigger-http --source deployment/ --memory 512MB

Notez ici qu’il faut allouer 512m pour que la fonction fonctionne.
Le support ne semble pas encore parfait (ou je n’ai pas bien suivi les instructions), car on voit dans les logs que Micronaut ouvre le port 8080 ce qui ne devrait pas être le cas.

Mais il faut avouer que le fait que le packaging par défaut fonctionne, et qu’on puisse écrire des fonctions HTTP aussi simplement qu’un simple Controller, est un vrai plus. Le support est bien pensé et fonctionne sans trop de soucis.

Voici ensuite un exemple d’utilisation avec curl :

curl -X POST -d '{"name":"apple", "color":"green", "description":"Delicious"}' \
        -H "Content-Type: application/json" \
        https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-micronaut
> {"status" : "created"}

curl -X POST -d '{"name":"banana", "color":"yellow", "description":"Yummy"}' \
        -H "Content-Type: application/json" \
        https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-micronaut
> {"status" : "created"}

curl https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-micronaut
> [{"name":"banana","color":"yellow","description":"Yummy"},{"name":"apple","color":"green","description":"Delicious"}]

curl https://us-central1-methodical-mesh-238712.cloudfunctions.net/fruit-micronaut?name=banana
> {"name":"banana","color":"yellow","description":"Yummy"}

Attention : si Google déploie une nouvelle instance de votre fonction pendant vos tests, comme les Fruits sont stockés dans une HashMap, celle-ci sera vide. Il vous faudra donc recommencer vos tests …

Pour aller plus loin avec Micronaut : le guide des fonctions HTTP et l’exemple officiel.

Comment ça marche

Il n’y a pas beaucoup de documentation expliquant comment ça marche. En regardant les logs on comprend qu’une fonction, c’est un déploiement dans AppEngine.

Si vous déployez une fonction sans la packager au préalable, la ligne de commande va d’abord la packager en utilisant Maven de manière transparente (je l’ai vu car j’avais un test qui ne passait pas et j’ai eu un joli message d’erreur Maven), avant de l’envoyer à Google AppEngine. Si vous buildez votre fonction, la ligne de commande va se contenter de prendre le JAR buildé et de l’envoyer à AppEngine.

Par défaut, notre fonction a 256m de mémoire allouées, et ses logs sont disponibles directement depuis la console GCP.

Après, comme c’est du serverless, on ne gère ni le nombre de fonctions, ni quand une autre est déployée. Même en testant seul via curl, j’ai eu des re-déploiement de fonction (ou re-démarrage). Comme toujours, attention donc au cold start !!!

Conclusion

Le support de Java dans Google Cloud Function était attendu de longue date, AWS et Azure supportant déjà Java.

L’implémentation est assez propre, une simple interface qui donne accès aux requêtes et réponses HTTP et c’est tout ! J’aime ça 😉

Au niveau outillage, le build automatique de votre projet Maven est vraiment pratique, après tout, une fonction est censée être simple, et ça peut donc suffire pour pas mal d’utilisation. Pour des projets plus complexes, un simple uber JAR créé avec le plugin shade suffit. On apprécie de n’avoir rien de spécifique à faire pour builder nos fonctions.

Le support de Java 8 et 11 est un plus, ce sont les deux versions les plus utilisées. Par comparaison, Azure ne supporte toujours par Java 11 au jour d’écriture de cet article !

Google ré-utilise AppEngine pour faire tourner nos fonctions, et ça c’est bien car AppEngine est un outil stable, et qui fonctionne très bien (certains diront que c’est le premier outil serverless … il est effectivement très vieux).

Vous pouvez retrouver tous les exemples de cet article dans mon repository cloud-function-tests.

P.S. – Ceux qui me connaissent savent que je contribue régulièrement à Quarkus et se poseront donc la question, et Quarkus ? Quarkus est en train de refactorer son support des fonctions via Funqy, une nouvelle librairie qui permettra de développer les fonctions de la même manière pour AWS et Azure (les deux Clouds supportés par Quarkus pour le moment). J’ai commencé à travailler sur un support pour Google Cloud Function via Funqy … stay tuned 😉

Un remerciement tout spécial à Logan pour sa relecture et la correction des nombreuses fautes d’orthographe 😉

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.