Accelerare la distribuzione del software: un tutorial su Docker Swarm
Pubblicato: 2022-03-11A meno che tu non abbia vissuto all'interno di un container, probabilmente hai sentito parlare di container. L'industria si è spostata nettamente dalle infrastrutture persistenti a quelle effimere e i container sono nel mezzo di quella mossa. Il motivo è abbastanza semplice: sebbene i container aiutino sicuramente i team di sviluppo a iniziare a funzionare rapidamente, hanno ancora più potenziale per cambiare completamente il volto delle operazioni.
Ma che aspetto ha esattamente? Cosa succede quando sei pronto per fare il salto di qualità eseguendo i container in locale o manualmente su alcuni server? In un mondo ideale, vuoi semplicemente lanciare la tua applicazione su un cluster di server e dire "eseguilo!"
Bene, per fortuna, ecco dove siamo oggi.
In questo articolo, esploreremo cos'è Docker Swarm, insieme ad alcune delle fantastiche funzionalità che ha da offrire. Quindi daremo un'occhiata a come appare effettivamente l'utilizzo della modalità Sciame e la distribuzione in uno sciame, e concluderemo con alcuni esempi di come sono le operazioni quotidiane con uno sciame schierato. Una conoscenza di base di Docker e dei container è decisamente consigliata, ma puoi prima dare un'occhiata a questo eccellente post sul blog se non conosci i container.
Cos'è Docker Swarm?
Prima di immergerci nella creazione e distribuzione nel nostro primo sciame, è utile avere un'idea di cosa sia Docker Swarm. Docker stesso esiste da anni e la maggior parte delle persone oggi lo considera un runtime di container. In realtà, però, Docker è composto da molti pezzi diversi, che lavorano tutti insieme. Ad esempio, quella parte di runtime del contenitore è gestita da due componenti più piccoli, chiamati runC e containerd. Man mano che Docker si è evoluto e restituito alla comunità, hanno scoperto che la creazione di questi componenti più piccoli è il modo migliore per crescere e aggiungere rapidamente funzionalità. In quanto tale, ora abbiamo SwarmKit e la modalità Swarm, che è integrata direttamente in Docker.
Docker Swarm è un motore di orchestrazione di container. Ad alto livello, richiede più Docker Engine in esecuzione su host diversi e ti consente di usarli insieme. L'utilizzo è semplice: dichiara le tue applicazioni come stack di servizi e lascia che Docker gestisca il resto. I servizi possono essere qualsiasi cosa, dalle istanze dell'applicazione ai database o alle utilità come Redis o RabbitMQ. Questo dovrebbe suonare familiare se hai mai lavorato con docker-compose in fase di sviluppo, poiché è esattamente lo stesso concetto. In effetti, una dichiarazione dello stack è letteralmente solo un file docker-compose.yml
con la sintassi della versione 3.1. Ciò significa che puoi utilizzare una configurazione di composizione simile (e in molti casi identica) per lo sviluppo e la distribuzione dello swarm, ma qui sto andando un po' più avanti di me stesso. Cosa succede quando hai istanze di Docker in modalità Swarm?
Non cadere dalla zattera
Abbiamo due tipi di nodi (server) nel mondo Swarm: manager e lavoratori. È importante tenere a mente che i manager sono anche lavoratori, hanno solo la responsabilità aggiuntiva di mantenere le cose in funzione. Ogni sciame inizia con un nodo manager designato come leader. Da lì, è solo questione di eseguire un comando per aggiungere in modo sicuro nodi allo sciame.
Swarm è altamente disponibile grazie alla sua implementazione dell'algoritmo Raft. Non entrerò nei dettagli su Raft perché c'è già un ottimo tutorial su come funziona, ma ecco l'idea generale: il nodo leader controlla costantemente con i suoi colleghi nodi manager e sincronizza i loro stati. Affinché un cambiamento di stato sia "accettato", i nodi manager raggiungono molto il consenso, cosa che accade quando la maggior parte dei nodi riconosce il cambiamento di stato.
Il bello di questo è che i nodi manager possono cadere sporadicamente senza compromettere il consenso dello sciame. Se un cambiamento di stato raggiunge il consenso, sappiamo che esisterà sicuramente sulla maggior parte dei nodi manager e persisterà anche se l'attuale leader fallisce.
Diciamo che abbiamo tre nodi manager chiamati A, B e C. Ovviamente, A è il nostro leader senza paura. Ebbene, un giorno un errore di rete transitorio mette A offline, lasciando B e C da soli. Non avendo notizie di A da molto tempo (poche centinaia di millisecondi), B e C aspettano un periodo di tempo generato casualmente prima di candidarsi all'elezione e avvisare l'altro. Naturalmente, in questo caso, sarà eletto il primo a candidarsi. In questo esempio, B diventa il nuovo leader e viene ripristinato il quorum. Ma poi, colpo di scena: cosa succede quando A torna online? Penserà che è ancora il leader, giusto? Ogni elezione ha un termine associato, quindi A è stato effettivamente eletto nel mandato 1. Non appena A torna online e inizia a ordinare B e C in giro, gli farà sapere gentilmente che B è il leader del mandato 2 e A si dimetterà.
Questo stesso processo funziona su scala molto più ampia, ovviamente. Puoi avere molti più di tre nodi manager. Aggiungo però un'altra rapida nota. Ogni sciame può subire solo un numero specifico di perdite di manager. Uno sciame di n nodi manager può perdere (n-1)/2
manager senza perdere il quorum. Ciò significa che per uno sciame di tre manager puoi perderne uno, per cinque puoi perderne due, ecc. La ragione alla base di ciò torna all'idea del consenso della maggioranza, ed è sicuramente qualcosa da tenere a mente mentre vai alla produzione.
Pianificazione e riconciliazione delle attività
Finora, abbiamo stabilito che i nostri manager sono davvero bravi a rimanere sincronizzati. Grande! Ma cosa stanno facendo effettivamente? Ricordi come ho detto che distribuisci uno stack di servizi su Swarm? Quando dichiari i tuoi servizi, fornisci a Swarm informazioni importanti su come desideri effettivamente che i tuoi servizi vengano eseguiti. Ciò include parametri come il numero di repliche desiderate per ciascun servizio, il modo in cui le repliche devono essere distribuite, se devono essere eseguite solo su determinati nodi e altro ancora.
Una volta distribuito un servizio, è compito dei gestori garantire che tutti i requisiti di distribuzione impostati continuino a essere soddisfatti. Supponiamo di distribuire un servizio Nginx e di specificare che dovrebbero esserci tre repliche. I gestori vedranno che nessun container è in esecuzione e distribuiranno uniformemente i tre container tra i nodi disponibili.
La cosa ancora più interessante, tuttavia, è che se un container dovesse fallire (o un intero nodo dovesse andare offline), lo Swarm creerà automaticamente container sui nodi rimanenti per compensare la differenza. Se dici di volere tre contenitori in esecuzione, avrai tre contenitori in esecuzione, mentre Swarm gestisce tutti i dettagli più importanti. Inoltre, e questo è un grande vantaggio, aumentare o diminuire è facile come dare a Swarm una nuova impostazione di replica.
Scoperta del servizio e bilanciamento del carico
Voglio sottolineare un dettaglio importante ma sottile di quell'ultimo esempio: se Swarm sta avviando in modo intelligente contenitori su nodi di sua scelta, non sappiamo necessariamente dove verranno eseguiti quei contenitori. All'inizio può sembrare spaventoso, ma in realtà è una delle funzionalità più potenti di Swarm.
Continuando lo stesso esempio di Nginx, immagina di aver detto a Docker che quei contenitori dovrebbero esporre la porta 80. Se punti il tuo browser a un nodo che esegue quel contenitore sulla porta 80, vedrai il contenuto di quel contenitore. Non c'è nessuna sorpresa lì. Ciò che può sorprendere, tuttavia, è che se invii la tua richiesta a un nodo che non esegue quel container, vedrai comunque lo stesso contenuto! Cosa sta succedendo qui?
Swarm sta effettivamente utilizzando una rete di ingresso per inviare la tua richiesta a un nodo disponibile che esegue quel contenitore e allo stesso tempo lo sta bilanciando. Quindi, se fai tre richieste allo stesso nodo, probabilmente colpirai i tre diversi contenitori. Finché conosci l'IP di un singolo nodo nello sciame, puoi accedere a qualsiasi cosa in esecuzione al suo interno. Al contrario, questo ti consente di puntare un sistema di bilanciamento del carico (come un ELB) a tutti i nodi nello sciame senza doversi preoccupare di cosa è in esecuzione e dove.
Non si ferma alle connessioni esterne. I servizi in esecuzione sullo stesso stack dispongono di una rete overlay che consente loro di comunicare tra loro. Invece di codificare gli indirizzi IP nel codice, puoi semplicemente utilizzare il nome del servizio come nome host a cui desideri connetterti. Ad esempio, se la tua app deve comunicare con un servizio Redis denominato "redis", può semplicemente utilizzare "redis" come nome host e Swarm indirizzerà la richiesta al contenitore appropriato. E poiché questo funziona perfettamente nello sviluppo con docker-compose e in produzione con Docker Swarm, è una cosa in meno di cui preoccuparsi quando si distribuisce l'app.
Aggiornamenti continui
Se sei nelle operazioni, probabilmente hai avuto un attacco di panico quando un aggiornamento di produzione va terribilmente storto. Potrebbe essere un cattivo aggiornamento del codice o anche solo un errore di configurazione, ma improvvisamente la produzione è inattiva! Le probabilità sono che al capo non importerà in nessun caso. Sapranno solo che è colpa tua. Bene, non preoccuparti, anche Swarm ti dà le spalle.
Quando si aggiorna un servizio, è possibile definire quanti contenitori devono essere aggiornati alla volta e cosa dovrebbe accadere se i nuovi contenitori iniziano a non funzionare. Dopo una certa soglia, Swarm può interrompere l'aggiornamento o (a partire da Docker 17.04) ripristinare i contenitori all'immagine e alle impostazioni precedenti. Non preoccuparti di dover portare un caffè al tuo capo domani mattina.
Sicurezza
Ultimo, ma non meno importante, Docker Swarm è dotato di ottime funzionalità di sicurezza pronte all'uso. Quando un nodo si unisce allo sciame, utilizza un token che non solo verifica se stesso, ma verifica anche che si stia unendo allo sciame che pensi che sia. Da quel momento in poi, tutte le comunicazioni tra i nodi avvengono utilizzando la crittografia TLS reciproca. Questa crittografia viene fornita e gestita automaticamente da Swarm, quindi non devi mai preoccuparti del rinnovo dei certificati e di altri problemi tipici della sicurezza. E ovviamente, se vuoi forzare una rotazione dei tasti, c'è un comando per quello.
L'ultima versione di Docker Swarm include anche la gestione dei segreti incorporata. Ciò ti consente di distribuire in modo sicuro segreti come chiavi e password ai servizi che ne hanno bisogno e solo ai servizi che ne hanno bisogno. Quando fornisci un servizio con un segreto, i contenitori per quel servizio avranno un file speciale montato nel loro file system che include il valore del segreto. Inutile dire che questo è molto più sicuro rispetto all'utilizzo di variabili di ambiente, che erano l'approccio tradizionale.
Immergersi nello sciame
Se sei come me, non vedi l'ora di entrare e prendere tutte queste funzionalità per un giro! Quindi, senza ulteriori indugi, tuffiamoci!
Esempio di app Docker Swarm
Ho creato un'app Flask molto rudimentale per dimostrare la potenza e la facilità dell'utilizzo di Docker Swarm. L'app Web mostra semplicemente una pagina che ti dice quale contenitore ha servito la tua richiesta, quante richieste totali sono state servite e qual è la password del database "segreta".
È suddiviso in tre servizi: l'attuale app Flask, un proxy inverso Nginx e un keystore Redis. Ad ogni richiesta, l'app incrementa la chiave num_requests
in Redis, quindi indipendentemente dall'istanza Flask che stai colpendo, vedrai riflesso il numero corretto di richieste.
Tutto il codice sorgente è disponibile su GitHub se vuoi "controllare" cosa sta succedendo.
Gioca con Docker!
Sentiti libero di usare i tuoi server mentre segui questo tutorial, ma ti consiglio vivamente di usare play-with-docker.com se vuoi semplicemente entrare. È un sito gestito da alcuni sviluppatori Docker che ti consente di avviare diversi in rete nodi con Docker preinstallato. Verranno spenti dopo quattro ore, ma è abbastanza per questo esempio!
Creazione di uno sciame
Va bene, ci siamo! Vai avanti e crea tre istanze in PWD (play-with-docker) o avvia tre server nel tuo servizio VPS (virtual private server) preferito e installa il motore Docker su tutti loro. Tieni presente che puoi sempre creare un'immagine e riutilizzarla quando aggiungi nodi in futuro. Non c'è alcuna differenza dal punto di vista software tra un nodo manager e un nodo di lavoro, quindi non è necessario mantenere due immagini diverse.

Stai ancora girando? Non preoccuparti, aspetterò. Ok, ora creeremo il nostro primo nodo manager e leader. Nella prima istanza, inizializza uno sciame:
docker swarm init --advertise-addr <node ip here>
Sostituisci <node_ip_here>
con l'indirizzo IP del tuo nodo. Su PWD, l'indirizzo IP viene visualizzato in alto e, se stai utilizzando il tuo VPS, sentiti libero di utilizzare l'indirizzo IP privato del tuo server purché sia accessibile dagli altri nodi della tua rete.
Ora hai uno sciame! È uno sciame piuttosto noioso, però, dal momento che ha solo un nodo. Andiamo avanti e aggiungiamo gli altri nodi. Noterai che quando eseguivi init
, veniva visualizzato un lungo messaggio che spiegava come utilizzare il token di unione. Non lo useremo perché renderebbe gli altri nodi lavoratori e vogliamo che siano manager. Otteniamo il token di join per un manager eseguendo questo sul primo nodo:
docker swarm join-token manager
Copia il comando risultante ed eseguilo sul secondo e terzo nodo. Ecco, uno sciame di tre nodi! Verifichiamo che tutti i nostri nodi esistano davvero. Il comando docker node ls
elencherà tutti i nodi nel nostro sciame. Dovresti vedere qualcosa del genere:
$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS su1bgh1uxqsikf1129tjhg5r8 * node1 Ready Active Leader t1tgnq38wb0cehsry2pdku10h node3 Ready Active Reachable wxie5wf65akdug7sfr9uuleui node2 Ready Active Reachable
Nota come il nostro primo nodo ha un asterisco accanto all'ID. Questo ci sta semplicemente dicendo che è il nodo a cui siamo attualmente connessi. Possiamo anche vedere che questo nodo è attualmente il Leader e gli altri nodi sono Raggiungibili se gli succedesse qualcosa.
Prenditi un momento per apprezzare quanto sia stato facile e distribuiamo la nostra prima app!
Spediscilo!
Proprio in questo momento, il team di sviluppo aziendale ha promesso a un cliente che la sua nuova app sarebbe stata distribuita e pronta entro un'ora! Tipico, lo so. Ma non temere, non avremo bisogno di così tanto tempo, dal momento che è stato creato utilizzando Docker! Gli sviluppatori sono stati così gentili da prestarci il loro file docker-compose
:
version: '3.1' services: web: image: lsapan/docker-swarm-demo-web command: gunicorn --bind 0.0.0.0:5000 wsgi:app deploy: replicas: 2 secrets: - db_password nginx: image: lsapan/docker-swarm-demo-nginx ports: - 8000:80 deploy: mode: global redis: image: redis deploy: replicas: 1 secrets: db_password: external: true
Lo analizzeremo tra un momento, ma non c'è ancora tempo per quello. Facciamolo schierare! Vai avanti e crea un file sul tuo primo nodo chiamato docker-compose.yml
e popolalo con la configurazione sopra. Puoi farlo facilmente con echo "<pasted contents here>" > docker-compose.yml
.
Normalmente, potremmo semplicemente distribuirlo, ma la nostra configurazione menziona che usiamo un segreto chiamato db_password
, quindi creiamo rapidamente quel segreto:
echo "supersecretpassword" | docker secret create db_password -
Grande! Ora tutto ciò che dobbiamo fare è dire a Docker di utilizzare la nostra configurazione:
docker stack deploy -c docker-compose.yml demo
Quando esegui questo comando, vedrai Docker che crea i tre servizi che abbiamo definito: web
, nginx
e redis
. Tuttavia, poiché abbiamo chiamato il nostro stack demo, i nostri servizi sono effettivamente denominati demo_web
, demo_nginx
e demo_redis
. Possiamo esaminare i nostri servizi in esecuzione eseguendo il comando docker service ls
, che dovrebbe mostrare qualcosa del genere:
$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS cih6u1t88vx7 demo_web replicated 2/2 lsapan/docker-swarm-demo-web:latest u0p1gd6tykvu demo_nginx global 3/3 lsapan/docker-swarm-demo-nginx:latest *:8000->80/ tcp wa1gz80ker2g demo_redis replicated 1/1 redis:latest
Ecco! Docker ha scaricato le nostre immagini nei nodi appropriati e ha creato contenitori per i nostri servizi. Se le tue repliche non sono ancora a piena capacità, attendi un momento e ricontrolla. È probabile che Docker stia ancora scaricando le immagini.
Vedere per credere
Tuttavia, non credere alla mia parola (o alla parola di Docker). Proviamo a connetterci alla nostra app. La nostra configurazione del servizio dice a Docker di esporre NGINX sulla porta 8000. Se sei su PWD, dovrebbe esserci un collegamento blu nella parte superiore della pagina che dice "8000". PWD ha effettivamente rilevato automaticamente che abbiamo un servizio in esecuzione su quella porta! Fai clic su di esso e ti indirizzerà al nodo selezionato sulla porta 8000. Se hai eseguito il rollover dei tuoi server, vai semplicemente a uno degli IP dei tuoi server sulla porta 8000.
Verrai accolto da uno schermo dal design accattivante che ti fornirà alcune informazioni di base:
Prendi nota di quale contenitore ha servito la tua richiesta e quindi aggiorna la pagina. È probabile che sia cambiato. Ma perché? Bene, abbiamo detto a Docker di creare due repliche della nostra app Flask e sta distribuendo le richieste a entrambe le istanze. Ti è capitato di urtare l'altro container la seconda volta. Noterai anche che il numero di richieste è aumentato perché entrambi i contenitori Flask stanno comunicando con la singola istanza Redis che abbiamo specificato.
Sentiti libero di provare a colpire la porta 8000 da qualsiasi nodo. Sarai comunque indirizzato correttamente all'app.
Demistificare la magia
A questo punto, tutto funziona e, si spera, hai trovato il processo indolore! Diamo un'occhiata più da vicino a quel file docker-compose.yml
e vediamo cosa abbiamo effettivamente detto a Docker. Ad alto livello, vediamo che abbiamo definito tre servizi: web
, nginx
e redis
. Proprio come un normale file di composizione, abbiamo fornito a Docker un'immagine da utilizzare per ogni servizio, oltre a un comando da eseguire. Nel caso di nginx
, abbiamo anche specificato che la porta 8000 sull'host deve essere mappata alla porta 80 nel contenitore. Tutto questo è finora la sintassi di composizione standard.
Le novità qui sono le chiavi deploy e secrets. Queste chiavi vengono ignorate da docker-compose
, quindi non influiranno sull'ambiente di sviluppo, ma vengono utilizzate da docker stack
. Diamo un'occhiata al servizio web. Abbastanza semplice, stiamo dicendo a Docker che vorremmo eseguire due repliche della nostra app Flask. Stiamo inoltre informando Docker che il servizio Web richiede il segreto db_password
. Questo è ciò che garantisce che il contenitore abbia un file denominato /run/secrets/db_password
contenente il valore del segreto.
Passando a Nginx, possiamo vedere che la modalità di distribuzione è impostata su global
. Il valore predefinito (che è implicitamente utilizzato in web) è replicated
, il che significa che specificheremo quante repliche vogliamo. Quando specifichiamo global
, dice a Docker che ogni nodo nello swarm dovrebbe eseguire esattamente un'istanza del servizio. Esegui di nuovo docker service ls
, noterai che nginx
ha tre repliche, una per ogni nodo nel nostro sciame.
Infine, abbiamo incaricato Docker di eseguire una singola istanza di Redis da qualche parte nello sciame. Non importa dove, poiché i nostri contenitori Web vengono instradati automaticamente ad esso quando richiedono l'host Redis.
Usando Swarm giorno per giorno
Congratulazioni per aver distribuito la tua prima app su uno sciame Docker! Prendiamoci un momento per rivedere alcuni comandi comuni che utilizzerai.
Ispezionare il tuo sciame
Hai bisogno di controllare i tuoi servizi? Prova docker service ls
e docker service ps <service name>
. Il primo mostra una panoramica di alto livello di ciascun servizio e il secondo fornisce informazioni su ogni contenitore in esecuzione per il servizio specificato. Quello è particolarmente utile quando vuoi vedere quali nodi stanno eseguendo un servizio.
Aggiornamenti continui
E quando sei pronto per aggiornare un'app? Bene, la cosa interessante della docker stack deploy
è che applicherà effettivamente gli aggiornamenti anche a uno stack esistente. Supponiamo che tu abbia inviato una nuova immagine Docker al tuo repository. In realtà puoi semplicemente eseguire lo stesso comando di distribuzione che hai usato la prima volta e il tuo sciame scaricherà e distribuirà la nuova immagine.
Naturalmente, potresti non voler sempre aggiornare tutti i servizi nel tuo stack. Possiamo eseguire aggiornamenti anche a livello di servizio. Supponiamo di aver aggiornato di recente l'immagine per il mio servizio web. Posso emettere questo comando per aggiornare tutti i miei contenitori web:
docker service update \ --image lsapan/docker-swarm-demo-web:latest \ demo_web
Un ulteriore vantaggio di quel comando è che applicherà un aggiornamento in sequenza se hai specificato che dovrebbe nella configurazione originale. E anche se non l'hai fatto, puoi passare i flag per l'aggiornamento che gli indicheranno di eseguire un aggiornamento continuo in questo modo:
docker service update \ --image lsapan/docker-swarm-demo-web:latest \ --update-parallelism 1 --update-delay 30s \ demo_web
Ciò aggiornerà un contenitore alla volta, aspettando 30 secondi tra gli aggiornamenti.
Ridimensionamento dei servizi verso l'alto o verso il basso
Avere due contenitori web è fantastico, ma sai cosa è meglio? Averne dieci! Aumentare e ridurre i servizi in uno sciame è facile come:
docker service scale demo_web=10
Esegui quel comando e controlla l'output del docker service ps demo_web
. Vedrai che ora abbiamo dieci container e otto di loro sono stati avviati solo un momento fa. Se sei interessato, puoi anche tornare all'applicazione Web e aggiornare la pagina alcune volte per vedere che ora stai ottenendo più dei due ID contenitore originali.
Rimozione di stack e servizi
I tuoi stack e i tuoi servizi sono distribuiti e ridimensionati, fantastico! Ma ora vuoi portarli offline. Questo può essere fatto con il rispettivo comando rm
. Per rimuovere il nostro stack demo, esegui il comando:
docker stack rm demo
Oppure, se preferisci rimuovere un singolo servizio, usa semplicemente:
docker service rm demo_web
Nodi drenanti
Ricordi quando abbiamo eseguito prima il docker node ls
per controllare i nodi nel nostro sciame? Ha fornito una serie di informazioni su ciascun nodo, inclusa la sua disponibilità . Per impostazione predefinita, i nodi sono Active , il che significa che sono un gioco leale per eseguire i container. Tuttavia, a volte potrebbe essere necessario portare un nodo temporaneamente offline per eseguire la manutenzione. Certo, potresti semplicemente spegnerlo e lo sciame si riprenderà, ma è bello dare un po' di preavviso a Moby (la balena portuale).
È qui che entra in gioco il drenaggio dei nodi. Quando contrassegni un nodo come Drain , Docker Swarm delegherà tutti i contenitori in esecuzione su di esso ad altri nodi e non avvierà alcun contenitore sul nodo finché non ne cambierai la disponibilità su Active .
Diciamo che vogliamo svuotare node1
. Possiamo eseguire:
docker node update --availability drain node1
Facile! Quando sei pronto per rimetterlo in funzione:
docker node update --availability active node1
Avvolgendo
Come abbiamo visto, Docker, insieme alla modalità Swarm, ci consente di distribuire le applicazioni in modo più efficiente e affidabile che mai. Vale la pena ricordare che Docker Swarm non è affatto l'unico motore di orchestrazione di container disponibile. In effetti, è uno dei più giovani. Kubernetes è in circolazione da più tempo ed è sicuramente utilizzato in più applicazioni di produzione. Detto questo, Swarm è quello sviluppato ufficialmente da Docker e stanno lavorando per aggiungere ancora più funzionalità ogni giorno. Indipendentemente da quale scegli di utilizzare, continua a contenere!