in Tutorial, Tutorial Docker, Tutorial Python

Progettare e distribuire applicazioni su Raspberry Pi con Docker – Organizzare container e dipendenze con docker-compose

Dopo avere visto come utilizzare i Dockerfile per generare container Docker, passiamo ora all’introduzione di un importante strumento per creare applicazioni multi-container: docker compose.

Applicazioni multi-container: Separation of Concerns

Il concetto di Separation of Concerns (traducibile in italiano come Separazione degli Interessi) consiste nell’individuare delle sotto sezioni di un’applicazione che eseguono dei compiti quanto più indipendenti tra loro. Una volta individuate, le sezioni possono essere implementate indipendentemente l’una dall’altra.

Il vantaggio guadagnato da questo approccio è quello di avere un’applicazione modulare, in cui ogni modulo può essere visto come una black box dagli altri. Ogni modulo può essere modificato indipendentemente dal resto dell’applicazione e comunica con l’esterno tramite delle API ben definite.

Un metodo per potere applicare questo concetto in un sistema reale è quello di individuare i moduli del sistema nella fase di progettazione ed implementarli come applicazioni in esecuzione su container indipendenti.

Le applicazioni possono poi comunicare tra di loro esponendo le proprie API su specifiche porte: in questo modo ogni applicazione può essere modificata, stoppata, riattivata in ogni momento senza andare a toccare funzionalità ad essa estranee.

docker-compose: introduzione ed installazione

Lo strumento di cui voglio discutere oggi tramite cui ottenere quanto descritto è docker compose, un tool per eseguire applicazioni multi-container modulari. Tramite la definizione di un file di configurazione in formato yaml, docker compose permette di specificare relazioni tra servizi esposti da container e le modalità di comunicazione tra di essi.

In pratica, ci da la possibilità di mettere insieme diverse applicazioni in esecuzione su diversi contenitori come se facessero parte di un unico sistema.

Per installarlo, tramite pip:

pip3 install docker-compose

Il tutto suppone che abbiate installato pip e Docker come indicato nei precedenti articoli (guida all’installazione su Windows 10 home  + WSL2).

Per installare pip3 su Raspbian/RaspberryPi OS:

sudo apt install python3-pip

Un’esempio: visualizzare via browser dati provenienti da sensori

Per dimostrare la potenza e la semplicità d’uso di docker compose, costruiamo un semplice sistema che acquisisce dati tramite MQTT da sensori, li salva in memoria e li espone tramite una semplice app web.

Consideriamo un sistema composto dai tre seguenti moduli:

  • Il broker MQTT Mosquitto, di cui è disponibile un’immagine ufficiale su Docker Hub;
  • Un applicativo che raccolga i dati inviati dai sensori e li possa esporre tramite HTTP;
  • Una webapp dove visualizzare i dati in aggiornamento per via grafica.

Grafico che riassume l’architettura in uso

Il progetto è organizzato in due sotto-cartelle contenenti i moduli del backend e della webapp, ognuno dei quali contiene a sua volta un Dockerfile con le istruzioni per costruire i container.

Il Dockerfile del modulo python è simile a quello visto nel precedente articolo; quello della webapp utilizza nginx per servire staticamente un file html:

FROM nginx
COPY . /usr/share/nginx/html 

copiando ogni file da servire nella cartella /usr/share/nginx/html.

L’app python consiste in una prima sezione in cui si definiscono delle funzioni callback da fare utilizzare al client MQTT, come avevamo già visto in passato su antima, quando effettua la connessione al broker o riceve messaggi sul topic topic/+:

 def on_connect(client, userdata, flags, rc):
    client.subscribe(conf["topic"])


def on_message(client, userdata, message):
    topic = message.topic.split("/")[-1]
    data_dict[topic] = message.payload.decode()

Un’altra definisce un endpoint flask per accedere ai dati ricevuti via MQTT: ricordo che anche questo argomento è stato trattato più in dettaglio sempre sul sito. I dati vengono salvati su un dict ed esposti tramite il metodo GET:

@app.route("/topics_data", methods=["GET"])
def get_data():
    return {"topics_data": data_dict}, 200 

La webapp è composta da un file html e da un’interfaccia Javascript che richiama le API dell’app python, aggiornando una tabella con le nuove letture dei sensori all’occorrenza.

 function fetch_topics_data() {
    fetch(endpoint)
    .then(response => response.json())
    .then(
        response => reload_table(response),
        error => alert(error)
    )
}

function reload_table(response) {
    if("topics_data" in response) {
        const topicData = response.topics_data;
        let tableCont = "";
        Object.entries(topicData).forEach(
            entry => tableCont += `<tr><td>${entry[0]}</td><td>${entry[1]}</td><td></td></tr>`
        );
        document.getElementById(tableId).innerHTML = tableCont;
    }
}

Unire le funzionalità: il file yaml

Una volta identificati ed implementati i moduli, possiamo passare alla fase in cui il tutto viene ad entrare in funzione. Descriviamo le interazioni tra i moduli usando un file di configurazione in formato yaml, in cui definiamo i dettagli di ogni container da generare.

Il file utilizzato in questo progetto d’esempio è il seguente:

version: "3"

services:
  broker:
    image: eclipse-mosquitto
    ports:
    - 1883:1883
    command:
      ["mosquitto", "-v"]
  backend:
    build: ./backend
    volumes:
    - ./backend/conf.json:/app/conf.json
    depends_on:
      - broker
    ports:
    - 8080:80
  webapp:
    build: ./webapp
    depends_on:
      - backend
    ports:
    - 5000:80

Commentiamo passo passo ogni sezione:

  • version è un campo che specifica quale versione del file compose stiamo andando ad utilizzare, differenti versioni supportano diverse funzionalità;
  • services  va a specificare quali contenitori andranno utilizzati come servizi del nostro sistema.

Nell’esempio sopra riportato sono state utilizzate due modalità di definizione di un servizio: una in cui si specifica direttamente un’immagine Docker reperibile tramite Docker Hub. L’altra in cui si specifica un percorso di build in cui è presente un Dockerfile a cui fare riferimento.

Nel primo caso abbiamo anche definito un comando da eseguire alla generazione del container tramite il campo command, che si comporta in maniera analoga al comando CMD di un Dockerfile.

I sotto-campi ports servono a definire dei mapping da porte esterne al container a interne, in maniera analoga a ciò che fa il flag -p  del comando docker run.

Due nuove funzionalità che non erano state trattate in precedenza sono le sguenti:

  • Il campo volumes che permette di descrivere dei volumi docker, ossia delle modalità per creare mapping di file e directory tra la macchina host ed il container;
  • Il campo depends_on che comunica a compose che un container dipende da un altro, e che quindi deve essere inizializzato in un secondo momento.

Il comando docker-compose up

A questo punto possiamo procedere con l’esecuzione del nostro progetto. Salviamo il file yaml con il nome docker-compose.yml, poniamoci nella cartella in cui lo abbiamo salvato, ed eseguiamo:

docker compose up –build -d

In questo modo, compose andrà a leggere il file di configurazione ed eseguirà quanto specificato, occupandosi di mettere in piedi l’architettura descritta.

Output dell’esecuzione del comando docker-compose up, in cui i container vengono inizializzati

Vista della dashboard di Docker Desktop dopo l’esecuzione di compose: il progetto viene visto come un unico sistema con le specifiche dei container iterni

Una volta eseguito il tutto, potremo interagire con la rete simulando dei sensori con mosquitto_pub e osservando i risultati tramite la webapp raggiungibile all’indirizzo http://localhost:5000.

Altri comandi utili utilizzabili tramite compose sono:

docker compose stop # per stoppare un insieme di container
docker compose start # per far ripartire un insieme di container
docker compose down # per rimuovere totalmente un insieme di container

Tabella esposta dalla webapp

Potete trovare il codice completo sulla pagina github di Antima.it al seguente indirizzo.

Vi consiglio vivamente di clonare la repo, cambiare o aggiungere container e sperimentare in prima persona con compose per apprezzarne la potenza e la comodità.

Per questo articolo è tutto, nel caso in cui ci fossero dubbi, curiosità o necessità di chiarimenti, non esitate a lasciare un commento nella sezione sottostante!

Scrivi un commento

Commento