Hello Java from Docker
Partendo dal banale Hello World vedremo come integrare all’interno di un container Docker il download di un nostro progetto d’esempio da GitHub, compilarlo ed avviarlo.
Senza la presunzione di riassumerne tutto il funzionamento in poche righe, possiamo spendere due parole sul suo funzionamento. Docker permette la definizione, composizione e in qualche modo anche la gestione dei Container. Questa parola è molto usata nell’informatica e in questo caso possiamo associare a Container il significato di versione light di un sistema operativo dove poter far girare i nostri servizi, essendo sicuro che questo non vada in conflitto con altre cose. Il parallelo che si usa per capire il funzionamento dei Container è quello con le Virtual Machine, riepilogato nella seguente immagine
Quando vogliamo mettere le nostre applicazioni su una Virtual Machine questa ha un suo sistema operativo dedicato, quindi dovendo gestire diverse applicazioni su VM simili c’è uno spreco di risorse gestite. I Container sono un’astrazione che permette di gestire i processi isolati ma che condivisono il kernel del sistema operativo. Per questo motivo i Container sono più veloci e snelli rispetto alle classiche VM. Docker, sfruttando il concetto di Container, ci permette di definirne uno in un modo molto semplice e che può essere estremamente utile nelle fasi di sviluppo, test e anche produzione. L’architettura di Docker può essere rappresentata nel seguente diagramma dove abbiamo il Docker client che rappresenta il tool che utilizziamo da riga di comando. Questo si collega al demone Docker che a sua volta gestisce i Docker container.
Prima di tutto dobbiamo avere Docker installato sul nostro sistema, se così non fosse vi rimando alla guida ufficiale sul sito di Docker che vi spiega come installarlo nei diversi sistemi operativi. Per verificare che l’installazione sia stata effettuata correttamente possiamo lanciare il seguente comando
federico@work:~$ sudo docker run hello-world Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 78445dd45222: Pull complete Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: The Docker client contacted the Docker daemon. The Docker daemon pulled the "hello-world" image from the Docker Hub. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://cloud.docker.com/ For more examples and ideas, visit: https://docs.docker.com/engine/userguide/
In questo simpatico esempio di Hello World possiamo trovare già le informazioni di cosa è successo dietro alle quinte. Il client Docker ha comunicato al demone di far partire l’immagine hello-world. Localmente non avevamo questa immagine e quindi il demone si è collegato a Docker Hub, ha scaricato l’immagine localmente e ha creato un nuovo container a partire da questa immagine facendolo partire. Il container nella sua definizione interna ha un eseguibile che crea l’output appena riportato. Se volessimo creare anche noi una esempio simile a quello riportato il file possiamo creare la seguente immagine
FROM ubuntu:latest COPY hello.txt / RUN cat hello.txt
Con questi tre comandi all’interno del file Dockerfile, che è il file principale di configurazione per un’immagine Docker, stiamo dicendo le seguenti cose
1) Parti dall’immagine di ubuntu, ultima versione
2) Copia il file hello.txt nella root dell’immagine
3) Esegui il comando cat hello.txt
Passiamo quindi ad effettuare il build per verificarne il funzionamento.
federico@work:~$ sudo docker build -t hello-world-mio . Sending build context to Docker daemon 3.584 kB Step 1/3 : FROM ubuntu:latest ---> 0ef2e08ed3fa Step 2/3 : COPY readme.txt / ---> 7e7b8d9e9872 Removing intermediate container b64abf233e7e Step 3/3 : RUN cat readme.txt ---> Running in fe0e33c1c2c8 '##::::'##:'########:'##:::::::'##::::::::'#######:: ##:::: ##: ##.....:: ##::::::: ##:::::::'##.... ##: ##:::: ##: ##::::::: ##::::::: ##::::::: ##:::: ##: #########: ######::: ##::::::: ##::::::: ##:::: ##: ##.... ##: ##...:::: ##::::::: ##::::::: ##:::: ##: ##:::: ##: ##::::::: ##::::::: ##::::::: ##:::: ##: ##:::: ##: ########: ########: ########:. #######:: ..:::::..::........::........::........:::.......::: '##:::::'##::'#######::'########::'##:::::::'########:: ##:'##: ##:'##.... ##: ##.... ##: ##::::::: ##.... ##: ##: ##: ##: ##:::: ##: ##:::: ##: ##::::::: ##:::: ##: ##: ##: ##: ##:::: ##: ########:: ##::::::: ##:::: ##: ##: ##: ##: ##:::: ##: ##.. ##::: ##::::::: ##:::: ##: ##: ##: ##: ##:::: ##: ##::. ##:: ##::::::: ##:::: ##: . ###. ###::. #######:: ##:::. ##: ########: ########:: :...::...::::.......:::..:::::..::........::........::: ---> 1e1c45f7ff78 Removing intermediate container fe0e33c1c2c8 Successfully built 1e1c45f7ff78
Ogni comando riportato nel file di build crea un container intermedio dove viene eseguito il comando corrente. L’immagine finale verrà salvata in locale, come è possibile vedere dall’elenco delle immagini
federico@work:~$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE hello-world-mio latest 1e1c45f7ff78 7 minutes ago 130 MB
Hello World Java
Passiamo ora ad un Hello World più vicino ai nostri interessi. Come immagine base in questo caso utilizzeremo quella ufficiale java, che ovviamente ha al suo interno già installato Java. Il Dockerfile sarà quindi il seguente
FROM java:8 COPY HelloWorld.java / RUN javac HelloWorld.java RUN java HelloWorld
In questo caso possiamo vedere l’output del nostro Hello World semplicemente lanciando la build
federico@work:~/Docker/hello-world-java$ sudo docker build --no-cache=true -t hello-world-java . Sending build context to Docker daemon 3.072 kB Step 1/4 : FROM java:8 ---> d23bdf5b1b1b Step 2/4 : COPY HelloWorld.java / ---> 19a8fcd746db Removing intermediate container 03a2f1d4788e Step 3/4 : RUN javac HelloWorld.java ---> Running in 1e7cdba1287f ---> 66561106ada7 Removing intermediate container 1e7cdba1287f Step 4/4 : RUN java HelloWorld ---> Running in 454dc2efe378 Hello World da dentro un container ---> a5e078df59b4 Removing intermediate container 454dc2efe378 Successfully built a5e078df59b4
Ovviamente potevamo partire da un immagine base e installare Java da soli, ma avendo a disposizione delle immagini ufficiali rilasciate dai diversi produttori software talvolta può essere conveniente sfruttarle senza creare Dockerfile lunghi.
Hello World Maven
Dopo aver provato l’ebbrezza di compilare un Hello World dentro il nostro container dobbiamo provare a fare la build di un progetto Maven e lanciarlo. Prima di tutto dobbiamo definire un semplice progetto che ci permette di lanciare un webserver. L’unica classe di questo progetto è quella che trovate riportata di seguito
package com.javastaff; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; public class ExampleWebserver { public static void main(String[] args) throws Exception { HttpServer server = HttpServer.create( new InetSocketAddress(8080), 0); server.createContext("/", new MyHandler()); server.start(); } static class MyHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { StringBuilder responseBuilder=new StringBuilder(); responseBuilder .append("<html><head><title>ExampleWebserver</title></head><body>"); responseBuilder.append("<h3>ExampleWebserver</h3>") .append("Tu hai richiesto l'URL ") .append(t.getRequestURI()) .append(" ma io non so ancora come poterlo gestire :("); t.sendResponseHeaders(200, responseBuilder.toString().length()); OutputStream os = t.getResponseBody(); os.write(responseBuilder.toString().getBytes()); os.close(); } } }
Non soffermandosi troppo sull’utilità di questa classe, vediamo che lanciandola possiamo avere un inutile webserver sulla pagina 8080. Questa classe, insieme al progetto Maven che crea il relativo jar, è disponibile su github al repository https://github.com/fpaparoni/ExampleWebserver. Ora definiremo un Dockerfile che scaricherà il progetto da github, lo compilerà e lancerà
FROM maven:latest EXPOSE 8080 RUN apt-get install git RUN git clone https://github.com/fpaparoni/ExampleWebserver.git WORKDIR ExampleWebserver/ RUN mvn clean package WORKDIR target/ CMD ["java","-jar","example-webserver-1.0-SNAPSHOT.jar"]
L’immagine di partenza che utilizziamo è quella ufficiale rilasciata da Maven, che ovviamente ha installato Maven e Java. Successivamente con il comando EXPOSE mettiamo in ascolto la porta 8080 e scarichiamo il progetto dal repository git. Passiamo quindi alla directory appena creata con il comando WORKDIR e creiamo il JAR con il classico comando di Maven. Lanciamo quindi l’eseguibile Java dopo essere entrati nella directory target. Passiamo quindi al build della nostra nuova immagine
sudo docker build --no-cache=true -t hello-world-maven .
e al successivo run del nuovo container dove con il parametro -p8080:8080 stiamo mappando la porta 8080 del container con la porta 8080 del nostro sistema operativo
sudo docker run -p8080:8080 -t hello-world-maven
Collegandoci all’indirizzo localhost:8080 troveremo in ascolto il nostro webserver d’esempio lanciato dal container Docker. A questo punto dell’articolo vi sarebbe dovuto già venire in mente la domanda “Si ma ho lanciato questi container, ma poi come faccio a vederli? come faccio a fermarli??”. Con il comando docker ps -a possiamo vedere tutti i container presenti sul nostro sistema e in seguito possiamo stopparli con il comando docker stop <CONTAINER-ID> o addirittura cancellarli con il comando docker rm <CONTAINER-ID>.
Docker Compose e Spring Boot
Passiamo ora ad un esempio che permette di vedere qualcosa di più complesso del semplice hello world. L’applicazione che vogliamo far girare un’applicazione è realizzata con Spring Boot, un esempio di crud che trovate all’indirizzo https://github.com/fpaparoni/spring-boot-crud, che utilizza un database MySQL per memorizzare le informazioni. Abbiamo quindi bisogno di avviare diversi processi che dialogano tra di loro, quindi possiamo utilizzare Docker Compose, tool che serve per definire e lanciare applicazioni basate su molteplici container Docker. L’installazione di questo tool è semplicemente il download del binario per la vostra piattaforma, ma vi rimando comunque alla documentazione ufficiale.
Prima di addentrarci nella configurazione del file relativo a Docker Compose, dobbiamo definire le immagini relative al database e alla nostra applicazione. Partiamo con il database MySQL, dove rispetto all’immagine ufficiale andremo ad aggiungere uno script che verrà eseguito all’avvio dove verrà creato il database e la tabella che ci interessa
FROM mysql MAINTAINER Federico Paparoni ENV MYSQL_ROOT_PASSWORD=my-secret-pw ADD script.sql /docker-entrypoint-initdb.d EXPOSE 3306
e lanciamo la build
sudo docker build --no-cache=true -t custom-mysql .
Passiamo quindi all’applicazione Spring Boot. In questo caso il file di build scaricherà il progetto da git, lo compilerà, copierà un file di properties custom dove abbiamo inserito l’host MySQL a cui collegarsi ed infine avvierà l’applicazione in modalità debug, così potremo anche collegarci dal nostro IDE.
FROM maven:latest MAINTAINER Federico Paparoni EXPOSE 8080 EXPOSE 8000 RUN apt-get update RUN apt-get -y install git RUN git clone https://github.com/fpaparoni/spring-boot-crud.git WORKDIR spring-boot-crud/ ADD application.properties src/main/resources/application.properties RUN mvn clean package -DskipTests WORKDIR target/ CMD ["java","-Xdebug","-Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n","-jar","spring-boot-crud.jar"]
e anche per questa lanciamo la corrispondente build
sudo docker build --no-cache=true -t spring-boot-crud .
Ora che abbiamo definito queste due immagini passiamo alla definizione del file docker-compose.yml
version: '2.1' services: custom-mysql: image: custom-mysql:latest ports: - "3306:3306" healthcheck: test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] timeout: 20s retries: 10 spring-boot-crud: image: spring-boot-crud:latest ports: - "8080:8080" - "8000:8000" links: - custom-mysql depends_on: custom-mysql: condition: service_healthy
In questo file definiamo una lista di servizi da lanciare, il primo dei quali è MySQL con l’immagine custom-mysql che abbiamo definito precedentemente. Per questa immagine specifichiamo la porta da esporre e un healthcheck che viene utilizzato per capire se il database è raggiungibile e utilizzabile. C’è poi la definizione del servizio con l’applicazione crud. In questo caso le porte esposte sono due, una per l’applicazione e l’altra per raggiungerla con il debug remoto. Definiamo inoltre un link tra questa immagine e il servizio custom-mysql, in questo modo il servizio spring-boot-crud potrà accedere al servizio database. Facciamo quindi partire Docker Compose lanciando il seguente comando dalla directory dove è presente il file che riporta la precedente composizione di servizi
sudo docker-compose up
Come è possibile vedere dai log, partirà prima MySQL e successivamente l’applicazione in quando abbiamo messo come condizione che il servizio MySQL sia raggiungibile ed utilizzabile. Per raggiungere l’applicazione basta andare sulla porta 8080 dal nostro browser, mentre invece se vogliamo agganciare il debug remoto basta configurarlo come riportato ad esempio nella seguente immagine dove all’interno di Eclipse viene configurato un server remoto dove collegarsi
Conclusioni
Potremmo continuare con molti altri esempi, visto che ovviamente gli scenari per utilizzare Docker sono tantissimi. Di sicuro per approfondire l’argomento è interessante vedere sia l’utilizzo di Docker da plugin Maven (ne esistono diversi) e Docker Swarm che permette di gestire un cluster di macchine virtuali. Tutti gli esempi sono scaricabili dal progetto Github riportato di seguito

Looking for a right “about me”…
Commenti recenti