Docker

Queste poche righe vogliono essere una sintesi pragmatica dei comandi da usare per operare correttamente nello sviluppo di applicazioni django/quasar di cui facciamo il deploy con Docker.

Non ha alcuna ambizione di essere esaustiva e probabilmente è poco utile fuori da Thux in quanto fa riferimento ad alcune utility interni.

Le premesse sono intese a rendere plausibili e facili da memorizzare i comandi, una presentazione più rigorosa del setup sarà curata in un webinar.

Deploy

../_images/schema-heptapod.png

L’ambiente in cui usiamo docker è molto semplice: usiamo docker-compose per descrivere la relazione fra i docker e per avviarli. Questo significa che, al momento, non stiamo usando né swarm né kubernetes per l’orchestrazione.

Al momento abbiamo le seguenti macchine docker:

docker1.thux.dev

per clienti senza macchina propria

docker-c.thux.dev

per tutti i servizi: hepapod, runner, jitsy-meet, npm, pypi… Heptapod ha anche il servizio registry.thux.dev, ovvero uno storage di immagini

acomea-dk

una macchina per acomea (per ora il nuovo cms)

onplay.it

una macchina per adrenalinik

Ogni macchina ha traefik, un web proxy che gira pure in docker e che gestisce i certificati https.

Setup

Usiamo un container per l’applicazione di backend ed un db postgres per il frontend ed un db ospitato da un sistema operativo tradizionale (non dockerizzato, ma pur sempre virtualizzato).

Nel caso di frontend separato il docker che ospita il frontend ha al suo interno un nginx a cui arriva tutto il traffico e fa eventualmente da proxy verso django. Il container django serve i file statici con uwsgi. I file di configurazione uwsgi.py e nginx.conf stanno nella radice dei rispettivi progetti assieme al file che contiene la ricetta per creare il docker Dockerfile.

Un estratto dei file del backend Django:

django/
├── apps
├── Dockerfile
├── setup.py
├── uwsgi
└── web

Un estratto dei file del frontend Vuejs/Quasar:

quasar/
├── Dockerfile
├── nginx.conf
├── quasar.conf.js
└── src

immagini

Le immagini che produciamo (direttamente al commit di mercurial tramite il meccanismo delle pipeline di gitlab) hanno dentro il codice python/js/css oltre all’interprete Python (uwsgi) ma non hanno dati e configurazioni «delicate» (password).

docker-compose.yml

Ogni applicazione è composta da una o più immagini che devono essere accessibili fra di loro. Ad esempio un cms è tipicamente solo una applicazione Django quindi un solo container, jeetsi-meet è composto da 4 servizi. Con zoooom potremmo decidere di avere 4 docker differenti per backend, vettori, supplier, customer (risulta più facile gestire gli aggiornamenti) o produrre una sola immagine per i 3 frontend.

In ogni caso la scelta viene scritta in un file docker-compose.yml che mostra in modo molto chiaro il layout deciso. In questo file vengono presentati i servizi (django, quasar, jeetsy, heptapod, registry….) che verranno eseguiti ed eventualmente il numero di container che devono eseguire quel servizio (caratteristica che avrà un senso solo per traffico molto elevato).

Questo file sta in un repository suo, indipendente dagli altri ma strettamente legato a questi:

opencapital-2020/
├── docker-compose.yml
├── local.py
├── logs
│   ├── django
│   │   ├── opencapital-api_exceptions.log
│   │   ├── opencapital-exceptions.log
│   │   └── opencapital.log
│   ├── nginx
│   └── uwsgi
│       ├── access.log
│       └── errors.log
├── media
└── README.rst

Alla fine, nel server di produzione il codice sta dentro il container in modo molto meno visibile ed accessibile di prima. Soprattutto non facilmente modificabile.

volumes

Oltre all’immagine nel momento in cui avviamo il docker dobbiamo anche fornire:

  • configurazione (es.: local.py)

  • dati (es.: media)

  • spazio per i log

Queste cose vengono collegate con un bind in modo tale che siano visibili e modificabili sia dal docker che da fuori:

services:
  django:
    image: registry.thux.dev/customers/open-capital/office2020.opencapital.it
    volumes:
      - ./media:/code/media
      - ./logs/django:/var/log/django
      - ./local.py:/code/web/settings/local.py
    networks:
      - django

Nell’estratto qui sopra si notano 3 volumi dove il : divide a sinistra la cartella nel server ospitante a destra il nome che avrà nel container.

Quindi concretamente: se vogliamo cambiare i valori del file local.py dovremo cambiarli nel file che sta nella cartella dove c’è docker-compose.yml e non nella solita posizione.

CI/CD

La produzione dell’immagine che eseguiamo in produzione viene fatta direttamente nel server heptapod da un «runner» (che è una container a parte) che segue le istruzioni scritte nel file .gitlab-ci.yml. Questa è chiamata una «pipeline» ed è ben visibile in heptapod.

Tendenzialmente non ci sarà necessità di creare file diversi e questo file sarà presente già nel cookiecutter.

Se il codice contiene errori è probabile che la pipeline fallisca ed è probabilmente poco opportuno fare merge di un branch che sia fallito. Di sicuro è impossibile fare il deploy di un branch che sia fallito.

Nota

tag delle immagini

Abbiamo scelto di mettere un tag alle immagini corrispondente al branch di cui fa parte. Nel caso di un topic il nome sarà ad esempio: topic-default-ci.

Operatività

Dal momento che c’è l’immagine pronta, il passaggio in produzione è semplicemente (suppongo di avere già fatto io la prima configurazione con il db):

docker-compose up -d

Quando si deve aggiornare l’immagine che però ha lo stesso nome (in quanto sullo stesso branch) si può lanciare:

docker-compose pull

Ho modificato il comando dj in modo che si renda conto che siamo in un contesto docker e faccia le cose correttamente, ovvero esegua il comando all’interno del docker anche se lo lanciate dal server ospitante. Ad esempio all’inizio:

dj migrate
dj createsuperuser

Se modificate il file di setting o volete spegnere il solo django:

dj reload
dj stop
dj start

Ogni altro comando funziona come vi aspettate…

Se per qualche motivo volete spegnere il docker:

docker-compose down

Requirements

L’installazione avviene in automatico all’interno della pipeline, quindi è vitale che il file requirements.txt sia correttamente compilato con tutte le dipendenze con versione.

È altresì importante che siano messe le più recenti compatibilmente con il sistema usato. In questo modo, soprattutto per le installazioni con file binari, si riesce ad evitare la compilazione mettendo le wheel.

Per ottenere un requirements.txt recente potete ad esempio eliminare ogni vincolo non necessario (es.: “Django=>2.2,<3” lasciatelo…) e reinstallare tutto. Ovviamente qeesto va bene in sviluppo, occorre essere molto più prudenti in produzione, particolarmente se non ci sono test.

Attenzioni

Rispetto a quanto succedeva prima, tenete presente che:

  • deve esistere requirements.txt

  • deve esistere manage.py

  • settings/__init__.py

    • deve permettere creazione link da variabile ENV

    • deve permettere creazioen dinaminca di SECRET_KEY

  • dj compilemessages non è necessario in quanto è già fatto all’interno del docker

  • dj collectstatic non è necessario

  • Il database sta di fatto su una macchina diversa, non viene raggiunto come localhost

  • se avete necessità di provare una immagine di un topic lanciate il comando così:

    TAG=topic-default-my-topic docker-compose up -d
    

Per dockerizzare un nostro progetto

È suff:

  • aggiungere Dockerfile (quasi sempre lo stesso usato ad esempio in opencapital o cookiecutter)

  • uwsgi.ini uguale per tutti i progetti

  • aggiungere settings/__init__.py (opencapital o cookiecutter)

  • .gitlab-ci.yml (opencapital o cookiecutter)

  • Zoooom è più complesso in quanto aveva bisogno di librerie aggiuntive, non escludo di semplificarlo.