MongoDB e Docker – Come creare e configurare un replica set

docker e mongo
La creazione di un replica set in MongoDB richiede diversi passaggi che devono essere eseguiti in modo accurato. Sfruttando le potenzialità di Docker è possibile automatizzare tutto il processo. Andremo a scoprire passo passo come configurare i vari componenti del nostro progetto.

Share

Reading time: 7 minutes

L’installazione e la configurazione di un database richiede molte volte ore di lavoro. Trovare la giusta configurazione affinchè non vengano compromessi altri servizi e allo stesso tempo garantire un alto livello di efficienza e sicurezza non è un compito sempre facile. Quando poi si parla di configurare un replica set di MongoDB, il lavoro può risultare ancora più arduo e pieno di insidie. Inoltre, se avviene un guasto o anche solo un blackout dei server, il riavvio automatico dei database potrebbe riservare qualche spiacevole sorpresa.

Come abbiamo visto negli articoli Introduzione a Docker e Docker compose – come orchestrare diversi container, l’utilizzo di Docker e Docker Compose ci permette di creare un ambiente virtuale altamente affidabile sia per lo sviluppo che per la produzione. In questo articolo analizzeremo come è possibile creare un’installazione di MongoDB configurato in replica set sfruttando il Docker Compose. 

Configurazione dell’ambiente di lavoro

Prima di iniziare è opportuno verificare di avere tutto il software necessario installato. In particolare, bisogna aver installato sulla macchina sia Docker che Docker Compose. Trovate tutte le informazioni su come installare e configurare in modo opportuno il vostro pc nell’articolo Introduzione a Docker. Non è invece necessario avere MongoDB installato. Le istanze di MongoDB verranno, infatti, create all’interno del Docker.

Andiamo a creare una cartella per il nostro tutorial che chiameremo mongo_example. All’interno di questa cartella creiamo un file chiamato docker-compose.yml che avrà il seguente contenuto.

version: '3'

services:
    mongodb1:
        image: mongo:4
        restart: always
        container_name: mongodb1
        volumes:
        - mongodata1:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all"]

    mongodb2:
        image: mongo:4
        restart: always
        container_name: mongodb2
        volumes:
        - mongodata2:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all"]
      
    mongodb3:
        image: mongo:4
        restart: always
        container_name: mongodb3
        volumes:
        - mongodata3:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all" ]
        
    

volumes:
    mongodata1:
    mongodata2:
    mongodata3: 

Nel docker file abbiamo così definito 3 servizi basati ciascuno sull’ultima versione dell’immagine di MongoDB. Ciascun servizio ha un nome e un volume dedicato per il salvataggio dei dati. Per far parlare i vari servizi tra di loro abbiamo abilitato la porta di default usata da MongoDB mediante l’opzione expose. E’ possibile anche mappare la porta di ciascun container su una porta dell’host mediante l’opzione ports.

Attenzione

Le porte mappate sull'host dai vari servizi devono essere obbligatoriamente tutte diverse. Inoltre, se sulla macchina esiste già un'installazione di MongoDB che utilizza la porta di default (27017), bisognerà scegliere attentamente la mappatura. Nel caso ci sia un conflitto il servizio non verrà avviato.

Per seguire le varie istanze di MongoDB in modalità replica set utilizziamo l’opzione entrypoint. In questo modo specifichiamo il comando che deve essere eseguito ogni volta che il container viene avviato. In particolare, abbiamo aggiunto le opzioni per  definire il nome del replica set, rsmongo, e la possibilità di accettare richieste da qualsiasi indirizzo ip (opzione –bind_ip_all). 

A questo punto il nostro ambiente è quasi pronto. Manca solo la configurazione del replica set. Per farlo però dobbiamo eseguire i servizi. Per far ciò ci basterà semplicemente lanciare il seguente comando.

$ docker-compose up 

E’ possibile anche eseguire il tutto in detach mode mediante l’opzione -d. Il consiglio però per capire come un replica set lavora dietro le quinte è di leggersi il lungo output che ciascun container stamperà. Nel caso invece i servizi sono stati lanciati in detach mode potete verificare che sia attivi mediante il comando.

$ docker ps 

In questo caso dovreste vedere un output simile a questo.

PORTS               NAMES
0e5fa683450d        mongo:4             "/usr/bin/mongod --r…"   8 seconds ago       Up 3 seconds        27017/tcp           mongodb3
8a2568914450        mongo:4             "/usr/bin/mongod --r…"   8 seconds ago       Up 3 seconds        27017/tcp           mongodb2
7ad6132bb37d        mongo:4             "/usr/bin/mongod --r…"   8 seconds ago       Up 4 seconds        27017/tcp           mongodb1 

Configurazione del replica set

Le istanze di MongoDB sono ora funzionanti e configurate per appartenere al replica set rsmongo. Tuttavia, il replica set non è ancora configurato e quindi nessun nodo è stato eletto primario. Per configurare il replica set è necessario aprire la shell di un’istanza di MongoDB. Per far ciò possiamo usare il seguente comando.

$ docker-compose exec mongodb1 mongo 

In questo modo ci verrà aperta la shell di mongo dell’istanza mongodb1. La scelta dell’istanza è arbitraria. All’interno della shell andremo a fornire la configurazione del replica set. Poichè la shell di MongoDB è basata su javascript, possiamo definire una variabile di configurazione che poi verrà passata al comando rs.initiate(). Andiamo quindi a definire una variabile rsconf come segue.

rsconf = {
   _id : "rsmongo",
   members: [
       {
           "_id": 0,
           "host": "mongodb1:27017",
           "priority": 4
       },
       {
           "_id": 1,
           "host": "mongodb2:27017",
           "priority": 2
       },
       {
           "_id": 2,
           "host": "mongodb3:27017",
           "priority": 1
       }
   ]
}
 

Come si può notare l’_id del documento è il nome del replica set, rsmongo, mentre il vettore members contiene la descrizione di ciascun nodo che apparterrà al replica set. Ogni nodo, rappresentato da un embedded document, sarà caratterizzato da un _id pari a un numero e dall’host. Per l’host, essendo all’interno di un servizio Docker, utilizziamo il nome del container seguito dalla porta su cui è in ascolto il servizio. Questo perché non è possibile a priori sapere l’indirizzo ip assegnato a ciascun container. Sarà compito del docker indirizzare opportunamente il traffico. Abbiamo inserito anche una proprietà priority per ciascun membro del replica set. Nonostante non sia necessario, questa informazione ci permetterà di influenzare l’elezione del primario. Infatti, avendo dato priorità maggiore al nodo mongodb1, siamo sicuri, che a meno di problemi su quel servizio, esso verrà sempre eletto come primario.

Per inizializzare il replica set è necessario solamente passare questa variabile al comando rs.initiate() come mostrato di seguito.

> rs.initiate(rsconf); 

A questo punto vedremo che il prompt dei comandi cambia inserendo il nome del replica set seguito dalla tipologia del nodo (PRIMARY o SECONDARY). Se siamo collegati al nodo con priorità massima, all’inizio vedremo che sarà etichettato SECONDARY. Questo non ci deve stupire. Infatti, sono necessari alcuni secondi prima che l’elezione del primario venga effettuata. Semplicemente premendo il tasto “invio” dopo un pò vedremo che questo nodo sarà diventato PRIMARY.

Per verificare la configurazione del replica set appena creato possiamo utilizzare il seguente comando.

rsmongo:PRIMARY> rs.conf() 

L’output che otterremmo sarà simile a quello riportato di seguito.

{"_id" : "rsmongo",
	"version" : 1,
	"term" : 1,
	"protocolVersion" : NumberLong(1),
	"writeConcernMajorityJournalDefault" : true,
	"members" : [
		{
			"_id" : 0,
			"host" : "mongodb1:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 4,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 1,
			"host" : "mongodb2:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 2,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		},
		{
			"_id" : 2,
			"host" : "mongodb3:27017",
			"arbiterOnly" : false,
			"buildIndexes" : true,
			"hidden" : false,
			"priority" : 1,
			"tags" : {
				
			},
			"slaveDelay" : NumberLong(0),
			"votes" : 1
		}
	],
	"settings" : {
		"chainingAllowed" : true,
		"heartbeatIntervalMillis" : 2000,
		"heartbeatTimeoutSecs" : 10,
		"electionTimeoutMillis" : 10000,
		"catchUpTimeoutMillis" : -1,
		"catchUpTakeoverDelayMillis" : 30000,
		"getLastErrorModes" : {
			
		},
		"getLastErrorDefaults" : {
			"w" : 1,
			"wtimeout" : 0
		},
		"replicaSetId" : ObjectId("60180880996f5407158e79e8")
	}
}
 

Come si può notare MongoDB riporterà tutte le informazioni relative alla configurazione del replica set. I membri presentano alcuni opzioni aggiuntive rispetto a quelle fornite mediante la configurazione definita in precedenza. I valori che vedete sono quelli di default che possono essere cambiati in fase di configurazione. Stesso discorso vale per tutti i parametri riportati all’interno dell’attributo settings.

Se invece si vuole vedere lo stato del replica set, ossia le informazioni contenute nei pacchetti heartbeat inviati dagli altri membri del set di replica e ricevute da un nodo, possiamo digitare il seguente comando.

rsmongo:PRIMARY> rs.conf() 

L’output riporterà una lunga lista di informazioni. Per comprendere il significato di ciascuna voce vi rimandiamo alla documentazione ufficiale.

A questo punto il replica set è funzionante e pronto per essere utilizzato. Ricordatevi, però, che tutte le operazioni di scrittura devono essere effettuate sul primario, mentre per leggere da un secondario bisogna abilitare le letture dal secondario oppure digitare il comando

rsmongodb:SECONDARY> rs.secondaryOk() 

Approfondimento: automatizzare la configurazione del replica set

Come abbiamo visto in precedenza è possibile creare un replica set di MongoDB mediante il Docker Compose. Tuttavia, nell’esempio riportato, è stato necessario collegarsi ad un’istanza di MongoDB ed eseguire i comandi per l’inizializzazione del replica set.

Questa procedura manuale, che deve essere fatta nel momento di creazione dei servizi, riduce i vantaggi di avere un’architettura basata su Docker. Infatti, ogni volta che il progetto dovrà essere installato su una macchina dovremo ripeterla introducendo possibili errori. Come è possibile automatizzare anche questo aspetto? Vediamolo insieme!

Per prima cosa dobbiamo creare un nuovo servizio che avrà il compito di configurare il replica set. Creiamo quindi una cartella chiamata mongo-setup e al suo interno andiamo a definire un Docker file. 

Il Dockerfile si baserà sull’immagine di mongo per avere il client con cui connettersi alle altre istanze del replica set. Inoltre, copierà all’interno del container il file con i comandi della shell di MongoDB per la configurazione del replica set (chiamato mongo-setup.js) e uno script bash (mongo-setup.sh) per inoltrare i comandi ad un’istanza mongo. 

Poichè il comando di configurazione deve essere eseguito quando almeno un’istanza di MongoDB è pronta, utilizzeremo lo script wait-for-it. Esistono altri tool per sincronizzare l’esecuzione dei vari container. Potete trovare alcuni suggerimenti nella documentazione di docker.

Il comando lanciato all’avvio del container sarà quindi wait-for-it con parametro un container di MongoDB e infine lo script bash per l’inizializzazione. Di seguito è riportato il Dockerfile.

FROM mongo:4
RUN mkdir /config
WORKDIR /config
COPY wait-for-it.sh .
COPY mongo-setup.js .
COPY mongo-setup.sh .
RUN chmod +x /config/wait-for-it.sh
RUN chmod +x /config/mongo-setup.sh
CMD [ "bash", "-c", "/config/wait-for-it.sh mongodb1:27017 -- /config/mongo-setup.sh"]

 

Il file di configurazione mongo-setup.js conterrà la variabile rsconf vista precedentemente oltre all’istruzione rs.initiate(rsconf).

Lo script bash, invece, controllerà se il replica set è già stato inizializzato mediante il controllo di esistenza di un file apposito.  In caso sia necessaria l’inizializzazione effettuerà la connessione all’istanza di MongoDB passando lo script mongo-setup.js. Infine, creerà il file per indicare che l’inizializzazione è avvenuta. Di seguito riportiamo il codice completo.

#!/usr/bin/env bash

if [ ! -f /data/mongo-init.flag ]; then
    echo "Init replicaset"
    mongo mongodb://mongodb1:27017 mongo-setup.js
    touch /data/mongo-init.flag
else
    echo "Replicaset already initialized"
fi
 

Infine il docker-compose file. Rispetto a quello visto in precedenza basterà aggiungere il nuovo servizio e il volume associato per mantenere traccia dello stato di inizializzazione del replica set. Di seguito riportiamo il contenuto del file.

services:
    mongodb1:
        image: mongo:4
        restart: always
        container_name: mongodb1
        volumes:
        - mongodata1:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]

    mongodb2:
        image: mongo:4
        restart: always
        container_name: mongodb2
        volumes:
        - mongodata2:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
      
    mongodb3:
        image: mongo:4
        restart: always
        container_name: mongodb3
        volumes:
        - mongodata3:/data/db
        expose:
        - "27017"
        entrypoint: [ "/usr/bin/mongod", "--replSet", "rsmongo", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1" ]
    
    mongosetup:
        image: "mongo-setup"
        build: "./mongo-setup"
        container_name: "mongosetup"
        depends_on:
            - mongodb1
        volumes:
            - mongostatus:/data/
    

volumes:
    mongodata1:
    mongodata2:
    mongodata3:
    mongostatus:
 

Tutto il progetto è disponibile su github.

Letture consigliate

More To Explore

Intelligenza artificiale

AI: le migliori tecniche di prompt per sfruttare i LLM

Le tecniche di prompt sono alla base dell’uso dei LLM. Esistono diversi studi e linee guide per ottenere i migliori risultati da questi modelli. Analizziamo alcuni di essi per estrarre i principi fondamentali che ci permetteranno di ottenere le risposte desiderate in base al nostro compito.

Intelligenza artificiale

AI: creare un chatbot con i propri dati

ChatGPT ci permette di avere un assistente virtuale a nostra completa disposizione. Ha però una grande limitazione: non conosce i nostri dati privati. Come possiamo costruirci un nostro assistente virtuale, o meglio un chabot, che usi i nostri dati e che non ci richieda investimenti di denaro? Scopriamo come costruirne uno usando LLM open, ambienti computazionali gratuiti come Colab e librerie Python per gestire file PDF e creare interfacce web semplici ed intuitive.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Design with MongoDB

Design with MongoDB!!!

Buy the new book that will help you to use MongoDB correctly for your applications. Available now on Amazon!