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 Mapfruits = 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 Mapfruits = 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 😉