Docker e Java

Costruire un’immagine Docker per microservizi Java non sempre è così immediato, soprattutto dovendo gestire a mano il Dockerfile. Per fortuna, ci vengono in aiuto diversi plugin. Fra questi, uno dei più completi e semplici è fabric8io/docker-maven-plugin. Tra l’altro, questo plugin è lo stesso che Andrea ha utilizzato in un post precedente quando ci ha fatto vedere come risolvere i problemi di memoria di Java 8 in combinazione con Docker.

Sicuramente è un’ottima base di partenza ed è molto più semplice che dover gestire il processo con un DockerFile. Tuttavia, questo plugin ha una limitazione importante. Ogni volta che aggiorniamo la nostra applicazione Java, un nuovo jar con tutti i file dell’applicativo viene inviato all’interno di un layer al Docker registry. Potrebbe sembrare un dettaglio, e per certi versi lo è, ma proviamo a pensare quanto spazio e tempo viene impiegato per la creazione dell’immagine, l’upload, il download negli ambienti di test, la conservazione nel registry e il download in ogni istanza di produzione. Il tutto moltiplicato per il numero di nuove versioni e il numero di microservizi di un progetto.

In Google si sono chiesti se è semplificare la creazione di un’immagine e al tempo stesso renderla più efficiente evitando che lo sviluppatore debba conoscere troppi dettagli di Docker. La risposta a questa domanda è Jib (Java Image Builder), rilasciato quest’estate.

Primi passi con Jib

Usare Jib con il demone Docker attivo è estremamente semplice. In questo post sono partito da una semplicissima applicazione web con Vert.x; il codice di esempio si trova su https://github.com/coding-jam/java-docker.

Per usare Jib basta aggiungere il plugin alla configurazione di Maven specificando l’unico parametro obbligatorio, il nome dell’immagine, ed eseguire mvn clean compile jib:dockerBuild .

<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>0.10.1</version> <configuration> <to> <image>codingjam/${project.name}:${project.version}</image> </to> </configuration> </plugin>

Esaminiamo i passi eseguiti da Jib.

L’immagine di base utilizzata di default è grc.io/distroless/java , ma è possibile utilizzare qualsiasi immagine specificando il parametro from . Su questo default torneremo brevemente in fondo al post. Jib costruisce un’immagine composta da tre layer, il prmo è per le dipendenze, il secondo è per le risorse e il terzo infine contiene le classi. Non viene generato un jar. L’idea alla base di Jib è che le dipendenze cambino raramente, le risorse un po’ più spesso e infine le classi cambino frequentemente. In questo modo, è molto probabile che aggiornando l’applicazione, solo l’ultimo layer venga modificato, risparmiando spazio e tempo. Jib è abbastanza furbo da individuare l’entrypoint del servizio, tuttavia possono essere specificati i parametri mainClass o entrypoint . In questo caso l’immagine viene pubblicata usando il demone Docker locale. Andando a cercare l’immagine sul registry, si noterà una cosa curiosa, l’immagine risulta creata 48 anni fa. Anche su questo aspetto ritorneremo in seguito.

L’idea dietro Jib

Cosa differenzia Jib dalle altre soluzioni analoghe? Possiamo considerere almeno tre aspetti.

Jib è una soluzione completamente Java; utilizza Maven o Gradle ma a parte questo non necessita di Docker, della Docker CLI o del demone. In fin dei conti, un’immagine Docker è composta da un manifest, una configurazione e una directory di layer, dove ogni layer a sua volta è un tarball accompagnato da metadati e da un’indicatore di versione. Per verificare questa affermazione con l’immagine appena creata, possiamo dare il comando docker save 5e737eccfa15 > vertx-docker-jib-jar-1.0.tar e visualizzare il contenuto.

Jib costruisce le immagini nativamente, senza aver bisogno di Docker installato. Per far questo basta sostituire il comando jib:dockerBuild con jib:build . A questo punto però dovrete anche specificare il registry su cui fare il push. Di default viene usato DockerHub. Per gli esempi di questo post, invece ho preferito usare un registry senza autenticazione installato su di una macchina Vagrant locale, come specificato qui.

<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>0.10.1</version> <configuration> <to> <image>localhost:5000/codingjam/${project.name}:${project.version}</image> </to> <allowInsecureRegistries>true</allowInsecureRegistries> </configuration> </plugin>

Abbiamo già incrociato il secondo punto chiave. Jib vuole favorire la velocità e per far questo suddivide l’applicazione in almeno tre layer: dipendenze, risorse e codice. In questo modo solo quello che è cambiato viene effettivamente ridistribuito.

Per dimostrare questo aspetto, ho creato la versione 1.1 dove ho modificato il index.html e una versione 1.2 dove invece ho cambiato un file Java. Ho pubblicato queste versioni assieme a quella iniziale sul registry della macchina virtuale.

Ora, dopo aver cancellato il registry locale, sono pronto a fare un pull di tutte e tre le immagini e verificare quanto promesso da Jib.

Come si vede dall’immagine, con l’ultimo pull, ho scaricato soltanto un layer, quello che contiene la classe modificata. Possiamo intuire che questo porti ad un beneficio notevole quando la dimensione del progetto inizi a crescere.

Purtroppo non ho fatto dei test specifici per questo post, ma voglio riportare alcuni numeri forniti dagli sviluppatori del progetto sul tempo necessario per costruire un’immagine Docker.

https://speakerdeck.com/coollog/build-containers-faster-with-jib-a-google-image-build-tool-for-java-applications

Infine, l’ultimo aspetto di Jib consiste nella ricerca dell’immutabilità dell’immagine. Lo stesso codice produrrà sempre la stessa immagine, indipendentemente dalla macchina utilizzata per produrla. Questo è il motivo per cui dai file che vengono inclusi nell’immagine vengono rimossi timestamp, utenti e gruppi. Di conseguenza l’immagine viene marcata con il primo gennaio 1970 e risulta creata 48 anni fa.

Un’ultima parola sull’immagine di base

Come detto prima, il default per Jib è grc.io/distroless/java , un’immagine basata su openjdk8 che occupa 119 Megabyte, un po’ di più dell’equivalente realizzata con Alpine. Tra le due però c’è una differenza profonda. L’immagine con Alpine è di fatto un kernel Linux con BusyBox, musl (al post di gclib) il port di alcune librerie e il package manager apk-tools. In pratica una distribuzione completa, anche se molto piccola. L’immagine “distroless” è invece una Debian Stretch, OpenJdk e.. null’altro. Non è presente un package manager e non è presente neppure la shell, che può essere abilitata solo usando il tag debug .

L’obiettivo di questa immagine (e di tutte le altre distroless declinate secondo il linguaggio) non è quello di essere minima ma piuttosto minimale. A scapito delle dimensioni, viene eliminato tutto quello che non serve per l’esecuzione del servizio e che costituisce un potenziale punto d’attacco come il package manager e la shell.

Conclusioni

In questo post abbiamo visto Jib, un tool proposto da Google per la creazioni di immagini Docker per servizi scritti in Java. La filosofia degli sviluppatori di questo progetto è molto chiara, realizzare uno strumento veloce e facile da usare che permetta di passare dal sorgente all’immagine nascondendo i passaggi intermedi. Continuerò a seguire questo plugin e a tenervi aggiornati sulle sue evoluzioni. Nel caso in cui l’abbiate già adottato, o intendiate usarlo in futuro, sarei curioso di conoscere i vostri pareri nei commenti qui sotto o su Slack.

Per approfondire