Docker Stack e MongoDb Replica Set

di , in Docker,

Devo rinviare ancora il mio proposito anche se mi ero promesso di non scrivere più post dedicati a Docker Swarm (la mia intenzione era di scrivere qualcosa a riguardo di Kubernetes). Nei miei post precedenti avevo fatto numerosi esempi a riguardo di servizi resi disponibili grazie a Docker (Swarm) e come fosse facile poterli replicare velocemente anche su più macchine. Ma cosa avevano in comune tutti questi servizi di esempio che mostravo? Semplice, tutti avevano la particolarità di essere trasportabili! Una web app può essere replicata quante volte si vuole (lasciando a monte un balancer che distribuisca le richieste su tutte le app avviate) ma per quei servizi che non possono essere distribuiti così facilmente, cosa si può fare? Un database, per esempio, non può essere trasferito e replicato quanto si vuole per sopportare un carico sempre maggiore di richieste. La replica è nativamente supportata da ormai tutti i database, ma non è sufficiente creare più istanze su più macchine perché queste si colleghino e possano lavorare insieme per distribuirsi le richieste (come si sa, la replica in un database dev'essere configurato accuratamente). In questo post proverò a mostrare un esempio proprio con uno di questi casi.

Innanzitutto, per non complicare troppo, come servizio non replicabile non ho preso un vero e proprio database relazionale ma un database di tipo NOSQL: MongoDb. La scelta è caduta su di lui perché la sua configurazione in replica è abbastanza semplice (e anche perché ci ho avuto a che fare recentemente). Anche se questo post non vuole essere un corso sulla configurazione di MongoDb, ecco qui una veloce disamina sulla configurazione di un Replica Set con tre istanze di MongoDb (la stessa procedura può essere utilizzata anche per la più avanzata configurazione sharded cluster).

Ipotizzando di avere tre macchine con questi nomi: MongoDb1, MongoDb2 e MongoDb3. Come da documentazione per creare un Replica Set è necessario scegliere una macchina che farà da macchina Master. Questa sarà l'unica a cui si potranno inviare le richieste di scrittura. Le altre macchine saranno macchine secondarie e potranno essere utilizzate solo in lettura. Ogni scrittura inviata a Master invierà automaticamente gli aggiornamenti alle altre macchine collegate.

I comandi per creare il tutto sono semplici. Da terminale, entrato nella shell con il comando mongo sulla macchina MongoDb1, si deve inserire il comando:

rs.initiate()

Avendo la configurazione prima vista, si devono aggiungere le altre macchine coinvolte nel replica set:

rs.add({"host": "MongoDb2"})
rs.add({"host": "MongoDb3"})

Ora le tre macchina saranno configurate per il Replica Set dove la macchina MongoDb1 sarà Master e MongoDb2 e MongoDb3 saranno le macchine secondarie (ho semplificato tutto, ipotizzando che i firewall delle macchine siano disabilitati o che le porte corrette siano aperte). Con questa configurazione in caso una macchina per motivi tecnici o di software diventasse irraggiungibile, le macchine rimanenti continuerebbero a funzionare ed essere raggiungibili. In caso che la macchina irraggiungibile fosse una secondaria non succederebbe granché, ma se fosse la Master, automaticamente una macchina secondaria sarebbe eletta Master.

Dopo questa velocissima disamina è arrivato il momento di configurare Docker in modo che possa poter fare girare la situazione come quella appena vista: tre istanze di MongoDb di cui una Master e due Secondarie. Con Docker Swarm attivo (docker swarm init da terminale) utilizzando il comando stack, come già visto nel post precedente, con un file di configurazione si potrebbe scrivere:

version: '3.7'

services:
  mongo1:
    image: mongo:4.2.1
    networks:
      - mongo
    deploy:
      replicas: 1

  mongo2:
    image: mongo:4.2.1
    networks:
      - mongo
    deploy:
      replicas: 1

  mongo3:
    image: mongo:4.2.1
    networks:
      - mongo
    deploy:
      replicas: 1

networks:
  mongo:

Ovviamente le macchine devono essere configurate come visto prima. Inoltre voglio la sicurezza minima che pretende che queste macchina abbiano una autenticazione interna tra le macchine coinvolte nella configurazione del Replica Set, e l'autenticazione con il classico username e password per l'accesso ai dati in MongoDb. L'autenticazione interna può essere fatta in due modalità: con un file (keyfile) o con un certificato X.509. Io usero il primo metodo che si basa sulla memorizzazione su ogni macchina di un file contenente una qualsiasi stringa alfanumerica (solo le istanze di MongoDb con questo file potranno essere coinvolte nel Replica Set). Se avessi voluto usare il certificato X.509 la procedura sarebbe stata leggermente più lunga, ma imparato quanto farò con il Keyfile, l'utilizzo del certificato diventa facile.

version: '3.7'

services:
  mongo1:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /path/keyfiles
    environment:
      MONGO_INITDB_ROOT_USERNAME: username
      MONGO_INITDB_ROOT_PASSWORD: password_super_secret
    networks:
      - mongo
    deploy:
      replicas: 1

  mongo2:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /path/keyfiles
    environment:
      MONGO_INITDB_ROOT_USERNAME: username
      MONGO_INITDB_ROOT_PASSWORD: password_super_secret
    networks:
      - mongo
    deploy:
      replicas: 1

  mongo3:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /path/keyfiles
    environment:
      MONGO_INITDB_ROOT_USERNAME: username
      MONGO_INITDB_ROOT_PASSWORD: password_super_secret
    networks:
      - mongo
    deploy:
      replicas: 1

networks:
  mongo:

Nelle righe command ho specificato il nome della Replica Set e il path e il nome del file per il Keyfile. Il container di MongoDb accetta nelle environment variable l'username e la passoword per l'accesso. A questo punto ho tre istanze di MongoDb avviabili ma ancora non configurate per lavorare insieme; inoltre all'interno di questo file sono presenti in chiaro le credenziali con cui chiunque potrà accedere a MongoDb.

Per risolvere il problema Docker Swarm mette a disposizione i Secrets, che, in questo caso, mi risolveranno il problema delle credenziali e per l'inserimento del Keyfile. Solo se attivo lo Swarm in Docker si  potranno creare i secret con questa sintassi:

    echo "usermongo" | docker secret create MONGODB_USERNAME - 

Sarà creato un Secret con il nome MONGODB_USERNAME contenente la stringa usermongo. Questa stringa sarà gestita internamente da Docker e non sarà possibile leggerne il valore con nessun comando o opzione della cli di Docker. Il valore sarà possibile inserirlo direttamente nei container al momento del loro avvio da Docker Stack. Come già scritto sopra, l'inserimento in chiaro delle credenziali non è considerato sicuro, ma anche l'eventuale passaggio con le environment variable è una pratica da evitare. Con i Secret di Docker, il contenuto è iniettato nei container in file salvati nel path /run/secrets (memory file system). Il Secret inserito sopra inserire nel container Docker, nel path /run/secret, il nome del file MONGODB_USERNAME con il suo reale contenuto.

Faccio lo stesso con la password:

    echo "password_super_complex" | docker secret create MONGODB_PASSWORD -

Sopra avevo scritto che per l'autenticazione tra le macchine coinvolte nel replica set avrei usato il Keyfile. Da quanto visto finora, lo posso creare con i Secret:

    echo "example123456" | docker secret create MONGO_DB_KEYFILE -

In questo modo potrò passare il percorso di questo Secret al comando mongod:

    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE

Primo problema risolto. Spulciando tra le immagini ufficiali di MongoDb nell'hub di Docker, si trova che è stata inaserita anche la possibilità di passare dei parametri, tra cui username e password dell'admin, come path dei file dove saranno inserite queste informazioni. Nel dettaglio:

MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD

E anche questo problema è risolto. Il contenuto del file per la generazione dello stack sarà a questo punto:

  mongo1:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
      - source: MONGO_DB_KEYFILE
        uid: '999'
        gid: '999'
        mode: 0600
    deploy:
      replicas: 1

Nella sezione secrets viene definita la lista dei secret da iniettare nel container. Ci sono due formati possibili per la loro configurazione, con il primo metodo basta inserire il nome del Secret, nel secondo si possono definire anche i permessi:

      - source: MONGO_DB_KEYFILE
        uid: '999'
        gid: '999'
        mode: 0600

Ho dovuto inserire questa configurazione perché, come da documentazione, per ragione di sicurezza, il Keyfile dovrà avere questi permessi particolari. All'interno del file, sarà necessario inserire la configurazione dei Secret da utilizzare:

secrets:
  MONGODB_USERNAME:
    external: true
  MONGODB_PASSWORD:
    external: true
  MONGO_DB_KEYFILE:
    external: true

Per la web application ho inserito anche queste regole:

    deploy:
      mode: global
      update_config:
        parallelism: 1
        delay: 5s
        order: start-first
        failure_action: rollback
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 30s

Con global, un container per questa web application sarà avviato per ogni macchina presente nel cluster di Docker Swarm (come già scritto moltissime volte, è sufficiente inserire un constraints per limitare la sua propagazione solo alle macchine desiderate:

constraints: [engine.labels.web==true]

In update_config ho stabilito che in caso di aggiornamento questo sarà fatto una macchina alla volta con un ritardo di cinque secondi tra un container e l'altro. In order, che accetta come parametro start-first o stop-first, si può decidere se fare partire la nuova immagine prima di chiudere la vecchia, oppure chiudere la vecchia prima di avviare la nuova. Il faiulure_action ho inserito la stringa rollback: se c'è qualche problema all'avvio del container, dopo quanto definito nella sezione restart_policy, sarà caricata l'immagine precedente (se ce n'è una). In restart_policy vengono definite le opzioni per il numero di tentativi per il riavvio dell'immagine e il ritardo tra i tentativi... Maggiori info nella documentazione di docker, qui.

Ora le tre macchine con MongoDb sono pronte per essere connesse e configurate per il Replica Set. Tra i modi diponibili il più comodo è creare un container apposito che, una volta avviato, controlli che le macchine siano disponibili e l'istanza di mongodb avviata, quindi invii i comandi per la configurazione del Replica Set.

Per fare questo ho modificato per le mie esigenze uno script in questo modo:

#!/usr/bin/env bash
my_array=($(echo $MONGODB_CLUSTER_LIST))

if [ -z "$MONGODB_CLUSTER_LIST" ]; then
  echo "Nothing to do"
  exit 0
fi

for rs in "${my_array[@]}"
do
  echo "$rs..."
  while :
  do
    echo "check..."
    mongo --host $rs --eval 'db'
    if [ $? -eq 0 ]; then
      break
    fi
    sleep 2
  done
done

if [ -n "$MONGODB_USERNAME" ]; then
  username=$MONGODB_USERNAME
fi

if [ -n "$MONGODB_PASSWORD" ]; then
  password=$MONGODB_PASSWORD
fi

if [ -n "$MONGODB_USERNAME_FILE" ]; then
  username=$(cat $MONGODB_USERNAME_FILE)
fi

if [ -n "$MONGODB_PASSWORD_FILE" ]; then
  password=$(cat $MONGODB_PASSWORD_FILE)
fi

if [ -z "$username" ]; then
  echo "Missing username"
  exit 100
fi

if [ -z "$password" ]; then
  echo "Missing password"
  exit 100
fi

clusterlength=${#my_array[@]}
status=$(mongo -u "$username" -p "$password" --host ${my_array[0]} --quiet --eval 'rs.status().members.length')
if [ "$status" != "$clusterlength" ]; then
  echo "Creating replica set"
  mongo --host ${my_array[0]} -u "$username" -p "$password" --eval 'rs.initiate()';
  sleep 5
  for r in "${my_array[@]}"
  do
    if [ "$r" != "${my_array[0]}" ]; then
      echo "add '$r'"
      mongo --host ${my_array[0]} -u "$username" -p "$password" --eval 'rs.add( { host: "'$r'" })';
    fi
  done
  echo "Done"
else
  echo "Already done"
fi

Questo Bash Script accetta come parametro il nome delle macchine su cui gira l'istanza di MongoDb e le credenziali - come visto sopra, username e password vengono letti da file salvati nel file system, e i parametri passati sono solo i path di questi file. Nel primo ciclo viene controllato che le macchine siano presenti e sia avviato MongoDb; quindi sulla prima macchina viene avviata la procedura per la creazione del Replica Set al quale vengono collegate le macchine secondarie. Per creare un container con questo script ho creato questo Dockerfile:

    
FROM mongo:4.2.1
COPY init.sh /tmp/init.sh
RUN chmod +x /tmp/init.sh
LABEL maintainer="az"
CMD /tmp/init.sh

Chi conosce la sintassi del Compose file di Docker può chiedersi perché non ho utilizzato l'opzione depends_on per attendere l'avvio dei service ma ho dovuto inserire nello script qui sopra un ciclo per attendere che tutto fosse avviato. La risposta è semplice: con Docker Swarm l'opzione depends_on è ignorata così come altre opzioni.

Arrivato a queto punto è tutto pronto. Anzi, quasi tutto pronto perché manca  una web app che utilizzi MongoDb. Ho preso questo esempio dalla documentazione di Microsoft, ed ho fatto alcune modifiche. A parte modificare tutti i metodi in modo che le richieste a MongoDb utilizzino il paradigma della programmazione asincrona con async/await, la più importante è la possibilità di prendere i parametri, come le credenziali, dalle environment variable, ma questi parametri possono essere passati anche come path e nome file, così da poter usare i Secret di Docker:

            string mongodbServerList = helperService.GetEnvFileValue("MONGODB_SERVER_LIST", null);
            if (string.IsNullOrEmpty(mongodbServerList))
            {
                throw new ArgumentNullException("MONGODB_SERVER_LIST or MONGODB_SERVER_LIST_FILE missing");
            }

            string username = helperService.GetEnvFileValue("MONGODB_SERVER_USERNAME", null);
            if (string.IsNullOrEmpty(username))
            {
                throw new ArgumentNullException("MONGODB_SERVER_USERNAME or MONGODB_SERVER_USERNAME_FILE missing");
            }

            string password = helperService.GetEnvFileValue("MONGODB_SERVER_PASSWORD", null);
            if (string.IsNullOrEmpty(password))
            {
                throw new ArgumentNullException("MONGODB_SERVER_PASSWORD or MONGODB_SERVER_PASSWORD_FILE missing");
            }

            string replicaSet = helperService.GetEnvFileValue("MONGODB_REPLICA_SET", settings.ReplicaSet);
            string connectionString = $"mongodb://{username}:{password}@{mongodbServerList}/admin?replicaSet={replicaSet}&readPreference=secondaryPreferred";
            string databaseName = helperService.GetEnvFileValue("MONGODB_DATABASE_NAME", settings.DatabaseName);
            string booksCollectionName = helperService.GetEnvFileValue("MONGODB_BOOKS_COLLECTION_NAME", settings.BooksCollectionName);

            var client = new MongoClient(connectionString);
            var database = client.GetDatabase(databaseName);
            _books = database.GetCollection<Book>(booksCollectionName);

La funzione chiamata GetEnvFileValue è la seguente:

        public string GetEnvFileValue(string key, string defaultValue)
        {
            string returnValue = defaultValue;

            if (Environment.GetEnvironmentVariable(key+"_FILE") != null)
            {
                string filePath = Environment.GetEnvironmentVariable(key+"_FILE");
                if (File.Exists(filePath))
                {
                    returnValue = File.ReadAllText(filePath).Trim();
                }
            }
            else if (Environment.GetEnvironmentVariable(key) != null)
            {
                returnValue = Environment.GetEnvironmentVariable(key);
            }

            return returnValue;
        }

Viene controllato che il nome del parametro passato esista come environment variable sia nel formato diretto sia come path ("_FILE" alla fine del nome del parametro); in caso sia un file, viene lette il suo contenuto e ritornato come valore. Infine ho creato un Controller aggiuntivo dal nome Info, che visualizza i setting della web application e le environment variables (questo controller sarà utile anche per verificare che il tutto funzioni correttamente).

Alla fine creo il container in Docker per questa web application:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
WORKDIR /app

# copy csproj and restore as distinct layers
COPY MongoDbTest/*.csproj .
RUN dotnet restore

# copy everything else and build app
COPY MongoDbTest/. .
WORKDIR /app
RUN dotnet publish -c Release -o out


FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine AS runtime
WORKDIR /app
LABEL maintainer="az"
ENV ASPNETCORE_Environment=Production
ENV ASPNETCORE_URLS http://+:5000
EXPOSE 5000/tcp
RUN apk --no-cache add curl
COPY --from=build /app/out ./
HEALTHCHECK --start-period=5s --interval=10s --timeout=3s --retries=5 CMD curl -f http://localhost:5000/api/info || exit 1
ENTRYPOINT ["dotnet", "MongoDbTest.dll"]

Come da documentazione di Docker si possono usare anche più immagini e suddividere la creazione della nostra in più passaggi. Questo è comodo per limitare lo spazio dell'immagine finale: inizialmente si carica l'immagine sdk di Dotnet Core 3.1 che pesa più di 450MB e la si usa per la compilazione iniziale. Quindi si usa la più leggera immagine aspnet (meno di 140MB) per la creazione finale. Per fare in modo che Docker verifichi correttamente che il container parte correttamente (c'è differenza tra avviare una web application e la sua vera disponibilità, ma anche di questo ne ho parlato in passato), con HEALTHCHECK ho inserito un controllo con il comando curl che ritornerà un valore positivo solo quando la web application risponderà al comando).

Ok, finito. E' il momento di pubblicare lo stack seguente:

version: '3.7'

services:
  mongo1:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
      - source: MONGO_DB_KEYFILE
        uid: '999'
        gid: '999'
        mode: 0600
    deploy:
      replicas: 1

  mongo2:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
      - source: MONGO_DB_KEYFILE
        uid: '999'
        gid: '999'
        mode: 0600
    deploy:
      replicas: 1

  mongo3:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
      - source: MONGO_DB_KEYFILE
        uid: '999'
        gid: '999'
        mode: 0600
    deploy:
      replicas: 1

  mongoreplicaset:
    image: sbraer/mongoreplicaset:v1
    environment:
      MONGODB_CLUSTER_LIST: mongo1:27017 mongo2:27017 mongo3:27017
      MONGODB_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGODB_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
    deploy:
      restart_policy:
        condition: none

  mongo-express:
    image: mkucuk20/mongo-express
    ports:
      - 5002:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME_FILE: /run/secrets/MONGODB_USERNAME
      ME_CONFIG_MONGODB_ADMINPASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
      ME_CONFIG_MONGODB_SERVER: mongo1,mongo2,mongo3
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD

  webapp:
    image: sbraer/mongodbtest:v1
    ports:
      - 5000:5000
    environment:
      MONGODB_REPLICA_SET: replicaTest
      MONGODB_DATABASE_NAME: MyDatabase
      MONGODB_BOOKS_COLLECTION_NAME: MyTest
      MONGODB_SERVER_LIST: mongo1,mongo2,mongo3
      MONGODB_SERVER_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGODB_SERVER_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    secrets:
      - MONGODB_USERNAME
      - MONGODB_PASSWORD
    deploy:
      mode: global

  visualizer:
    image: dockersamples/visualizer
    ports:
      - 5001:8080
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]
    networks:
      - visualizer

networks:
  mongo:
  visualizer:
      
secrets:
  MONGODB_USERNAME:
    external: true
  MONGODB_PASSWORD:
    external: true
  MONGO_DB_KEYFILE:
    external: true

Oltre alle tre istanze di MongoDb e la web app, ho inserito anche altri due container, il primo è mongo-express, una web app per la gestione di MongoDb, il secondo è visualizer che mostra la struttura dei container all'interno del cluster di docker swarm. All'interno del file ho anche definito due network, mongo e visualizer, questo consente una sicurezza aggiuntiva perché i container che girano in due diverse network non possono vedersi.

Avvio il tutto:

< docker stack deploy -c docker-compose-single-machine.yml mongo
> Creating network mongo_mongo
> Creating service mongo_mongo2
> Creating service mongo_mongo3
> Creating service mongo_mongoreplicaset
> Creating service mongo_mongo-express
> Creating service mongo_webapp
> Creating service mongo_visualizer
> Creating service mongo_mongo1

Per controllare che tutto funzioni, uso il comando docker service ls:

Aspetto che tutti i REPLICAS siano 1/1 (tranne il service mongo_mongoreplicaset che è il container che configura il Replica Set e una volta concluso il suo lavoro si deve chiudere) si può controllare che tutto funzioni. Inizio dalla web application che risponde alla porta 5000:

Ora la API che richiedere i dati a Mongodb:

Inizialmente il database è vuoto e i dati qui sopra li ho inseriti da mongo-express che risponde alla porta 5002:

Alla porta 5001 dovrebbe rispondere il visualizer:

Ok, funziona tutto e i dati critici come le credenziali non sono passati in chiaro. Un occhio attento avrà notato però un piccolo problema: i dati utilizzati da MongoDb sono salvati all'interno del container stesso. La conseguenza è semplice: in caso di riavvio dello stack i dati saranno persi (riavvio, NON update). Qui ci sono numerose soluzioni. La più semplice e memorizzare i dati nel file system della macchina host. Nel file di configurazione dello stack è sufficiente inserire la definizione di volume apposita:

  mongo1:
    image: mongo:4.2.1
    command: mongod --replSet replicaTest --port 27017 --keyFile /run/secrets/MONGO_DB_KEYFILE
    environment:
      MONGO_INITDB_ROOT_USERNAME_FILE: /run/secrets/MONGODB_USERNAME
      MONGO_INITDB_ROOT_PASSWORD_FILE: /run/secrets/MONGODB_PASSWORD
    networks:
      - mongo
    volumes:
      - /mongoDbData/data1:/data/db
    secrets:
...

Nel post precedente avevo scritto del plugin Cloudstor. Questo permette la gestione sicura di un file system per i container di Docker (nel mio post dedicato a AWS). Se volessi utilizzare lo stack appena visto su AWS con Cloudstor, inizialmente dovrei creare la struttura adatta con CloudFormation. Per fare questo, se si sono letti i miei post precedenti, ho utilizzato Troposphere. Nello script in Python ho aggiunte tre virtual machine, una per ogni datacenter della zona scelta (questo per permettere che in caso anche di grossi problemi hardware il tutto continui a funzionare). Queste tre macchine avranno tre IP statici all'interno della rete interna di AWS, e avranno porte aperte per MongoDb (la 27017) ma solo all'interno di questa rete (anche avendo l'IP pubblico, lasciato libero per le mie verifiche, non sarà possibile accedere a MongoDb). Una volta inserito la configurazione in YML in CloudFormation, questo creerà una macchina Master per Docker, una macchina Web (alla quale sarà assegnata la label "web") dove inserirò le webapplication, e tre macchine per MongoDb (a queste macchine assegnerò le label mongo1, mongo2 e mongo3).

E' arrivato il momento della modifica nello script YML dello stack. In verità le modifiche sono poche, la più importante è nella sezione volumes:

volumes:
  data1:
    driver: cloudstor:aws
    driver_opts:
      backing: relocatable
      size: 1
      ebstype: gp2
  data2:
    driver: cloudstor:aws
    driver_opts:
      backing: relocatable
      size: 1
      ebstype: gp2
  data3:
    driver: cloudstor:aws
    driver_opts:
      backing: relocatable
      size: 1
      ebstype: gp2

Quindi per le tre configurazioni per MongoDb ho inserito la sezione volumes:

  mongo1:
    image: mongo:4.2.1
    ...
    volumes:
      - data1:/data/db
    ...
  mongo2:
    image: mongo:4.2.1
    ...
    volumes:
      - data2:/data/db
    ...
  mongo3:
    image: mongo:4.2.1
    ...
    volumes:
      - data3:/data/db
    ...

La prima sezione fa in modo che CloudStor crei tre dischi ESB in Aws. La definizione volumes nelle immagini, invece, collegheranno i dischi creati al container quando questo sarà avviato, e saranno accessibili al path /data/db, dove MongoDb salva i suoi file.

Entrato via remoto alla macchina Master di Docker Swarm in AWS (da non confondere con la macchina Master di MongoDb), con il comando prima visto sarà creato il tutto:

    docker stack deploy -c docker-compose-single-machine.yml mongo

Dopo qualche minuto, aprendo il visualizer con l'ip dalla macchina web, si vedranno questa volta le cinque macchine presenti nel cluster di Docker Swarm con i loro corretti container avviati al loro interno:

Ora, grazie a CloudStor posso spostare, distruggere e ricreare i container di MongoDb e non perderei i dati.

Fine. Spero che questa è l'ultima volta che parlo di Docker Swarm.

Ecco il link della web application con tutti i file trattati in questo post, e il link dello script in Python con Troposphere per la creazione del file per Aws CloudFormation (in un branch apposito). Questo è il link del container in Docker della web application e questo è il link per il container con lo script per la creazione del Replica Set in MongoDb.

Commenti

Visualizza/aggiungi commenti

Docker Stack e MongoDb Replica Set
| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese