Docker
Multipass
Ansible i SSH client
Octavi Fornés <ofornes@albirar.cat>
Els entorns d’execució de les aplicacions plantejen problemes, tant a sistemes com a desenvolupament.
Alguns d’aquests problemes es poden resoldre amb contenidors.
Quins problemes?
Proves d’integració
Entorns d’execucions diferents
Automatització de la construcció, proves, QA i documentació (CI)
Preparació d’entorns d’execució
Rendiment de les aplicacions
Control de fallades d’aplicacions
Balanceig i encaminament
Desplegament (CD)
Disposar d’entorns d’execució reproduïbles de manera senzilla
Evitar embrutar el host amb les dependències i requeriments de cada aplicació
Amb els contenidors apareix un nou paradigma en la gestió del software:
| DEVOPS Desenvolupament + Operacions |
La cooperació entre desenvolupament i sistemes (operacions) és més estreta.
El desenvolupador pot expressar amb arxius els requeriments del context d’execució.
Operacions pot adaptar els requeriments al sistema instal·lat
Eines necessàries al host per a poder fer les pràctiques:
Docker
Multipass
Ansible i SSH client
node
maven
java jdk 1.8
Opcionalment, per a formatar el retorn d’algunes comandes:
jq
Instruccions a https://docs.docker.com/engine/install/ubuntu/
Convé eliminar la versió del sistema i instal·lar amb el repositori apt de docker
Instal·lar:
docker-ce
docker-ce-cli
containerd.io
docker-compose-plugin
Instal·lar tant ssh com ansible per a operar amb les màquines virtuals:
sudo apt-get install -y ansible openssh-clientNomés s’ha d’instal·lar:
sudo snap install multipassNode, maven i jdk
sudo snap install node --stable
sudo apt install openjdk-8-jdk mavenI jq:
sudo apt-get install jqnpm s’instal·la amb el paquet node |
Laboratori de clúster Docker Swarm amb Multipass

Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
Crear una clau SSH específica per al laboratori
Crear màquines virtuals (create-multipass-cluster.sh)
Actualitzar paquets (update-packages-cluster.sh)
Afegir software docker (swarm-add-software.sh)
Inicialitzar cluster swarm (swarm-init-cluster.sh)
Seleccionar el context del laboratori (swarm-sandbox)
ssh-keygen -f workshop_id_rsa -t rsa| No indiqueu password, per a fer més senzilles les operacions |
./create-multipass-cluster.sh -k workshop_id_rsa| Heu d’indicar la clau SSH creada abans per tal que sigui utilitzada en la configuració del client SSH per a accedir als nodes |
./update-packages-cluster.sh| Aquesta operació i les següents es fan amb Ansible |
| Això ho podeu fer en qualsevol moment, per a actualitzar el software dels nodes |
./swarm-add-software.sh/swarm-init-cluster.shEl nom per defecte del context docker serà swarm-sandbox, podeu canviar-lo amb el paràmetre -n |
Per a que tots els nodes puguin accedir al contingut o dades de l’aplicació necessiteu «volums»
En entorns CLOUD, els volums són unitats compartides de xarxa
Al laboratori utilitzarem NFS
ssh sw-controller
mkdir -p /opt/stack/volumes
echo "/opt/stack/volumes sw-node1(rw,sync,no_root_squash,no_subtree_check)
> /opt/stack/volumes sw-node2(rw,sync,no_root_squash,no_subtree_check)
> /opt/stack/volumes localhost(rw,sync,no_root_squash,no_subtree_check)" \
| sudo tee -a /etc/exports
sudo systemctl restart nfs-kernel-server
sudo exportfs -a/etc/hosts del host: 1Per a accedir a les aplicacions i eines del cluster heu de modificar l’arxiu /etc/hosts per a adreçar els noms a la IP del manager del cluster
multipass ls
Name State IPv4 Image
sw-controller Running 192.168.122.117 Ubuntu 20.04 LTS (1)
172.18.0.1
172.17.0.1
sw-node1 Running 192.168.122.74 Ubuntu 20.04 LTS
172.18.0.1
172.17.0.1
sw-node2 Running 192.168.122.51 Ubuntu 20.04 LTS
172.17.0.1
172.18.0.1| 1 | Aquesta és la IP del manager |
/etc/hosts del host :2portainer.workshop.cat
swarmpit.workshop.cat
traefik.workshop.cat
router-traefik
www.svelte.cat
svelte.cat
www.servei-rest.cat
servei-rest.cat
www.proxy-rest.cat
proxy-rest.cat
Afegiu la IP associada amb els noms:
echo "192.168.122.117 portainer.workshop.cat \
swarmpit.workshop.cat traefik.workshop.cat \
router-traefik www.svelte.cat svelte.cat \
www.servei-rest.cat servei-rest.cat \
www.proxy-rest.cat proxy-rest.cat" \
| sudo tee -a /etc/hostsVerifiqueu:
nslookup portainer.workshop.cat
Server: 127.0.0.53
Address: 127.0.0.53#53
Name: portainer.workshop.cat
Address: 192.168.122.117Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern

Navegueu a http://portainer.workshop.cat
Indiqueu el nom de l’usuari
Indiqueu la password i repetiu-la
Piqueu a crear
| Si marqueu aquesta casella, s’enviaran dades d’ús a portainer! |

Navegueu a http://swarmpit.workshop.cat
Indiqueu l’usuari i la password per a crear l’usuari d’administració
| Jo he utilitzat els mateixos que per a portainer |
Portainer
Entorns
Dashboard → Cluster
Mostrar només tasques vives
Stacks
Un stack (swarmpit)
Tasques
Estat d’una tasca
Swarmpit
Dashboard
Stacks
Un stack (swarmpit)
Un servei
Una tasca
Nodes
Un node
Traefik
Dashboard
Routers
Un router
Serveis
Un servei
Middlewares
Middleware de traefik
Ara ja està preparat el laboratori i heu vist les eines del cluster
Ara podeu començar a fer les proves dels objectius esmentats al principi
…
Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
Docker utilitza el que s’anomena Context per a operar
Un context és una comunicació amb un dimoni docker que permet operar en el seu entorn (normalment local)
S’ha creat un context docker per a operar amb el manager del cluster de la preparació
Podeu revisar els contexts dockers del host amb:
docker context lsHeu d’activar el del laboratori per a operar:
docker context use nom_contextdocker stack ls (1)
docker service ls (2)
docker stack deploy \
--compose-file file stack-name (3)
docker stack services stack-name (4)
docker stack ps stack-name (5)
docker stack rm stack-name (6)
docker service rm nom_servei (7)| 1 | Veure stacks desplegats |
| 2 | Veure serveis |
| 3 | Desplegar aplicacions |
| 4 | Veure els serveis d’un stack |
| 5 | Veure les tasques d’un stack |
| 6 | Eliminar tots els serveis d’un stack |
| 7 | O només un dels serveis |
Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
Heu d’utilitzar arxius docker-compose per a configurar les aplicacions.
Ha d’incloure tot allò que necessiti per a operar, tret d’altres serveis.
Per exemple: base de dades
Es despleguen amb:
docker stack deploy --compose-file arxiu_compose nom_stack_serveisNormalment requereix d’un volum amb el contingut de l’aplicació (jars, html, css, etc.) o dades (base de dades, mq, etc)
Al directori workshop/webapp hi ha una app web feta amb vitejs i svelte (html+css+js)
És contingut estàtic i només requereix NGINX (o Apache)
Requereix un volum compartit
S’executarà a un «node worker»
npm install
npm run buildEl contingut de l’aplicació és al directori "dist"
Heu de crear un volum compartit amb el contingut de l’aplicació:
Executeu les següents comandes:
ssh sw-controller mkdir -p /opt/stack/volumes/svelte-web
scp -r dist/* sw-controller:/opt/stack/volumes/svelte-webDesplegueu:
docker stack deploy --compose-file docker-compose.yml webappVerifiqueu el servei:
docker stack services webappPodeu cercar el node en que s’executa:
docker stack ps webapp --format '{{ .Node }}\t{{ .CurrentState }}'
sw-node1 Running 6 minutesS’executa al node sw-node1 i està en execució
Es requereix un volum compartit per a poder disposar l’aplicació i que sigui a l’abast de qualsevol node
La imatge utilitzada «nginx:1.23» disposa de tot el que necessitem per a «aixecar» una web estàtica
Permet que us centreu en l’aplicació, amb la facilitat de poder desplegar-la on sigui (al laboratori, a un cloud, a un altre cluster…)
Si s’actualitza la web, només heu de copiar el contingut nou i ja està
Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
Els clusters han de mantenir vives les aplicacions encara que caigui un o més nodes
A més, han d’assegurar que la "salut" de l’aplicació és bona, és a dir, és operativa i en ple rendiment (healthy)
Sinó, s’ha de reinstanciar o, fins i tot, crear més d’una instància
Per a provocar una «caiguda» de l’aplicació, aturarem el node on s’està executant:
docker stack ps webapp --format '{{ .Node }}\t{{ .CurrentState }}'
sw-node1 Running 12 minutes (1)
multipass stop sw-node1 (2)| 1 | Indica que s’executa al node 1 |
| 2 | Amb multipass atureu el node 1 |
Fins que veieu que s’instancia al node sw-node2
docker stack ps webapp --format '{{ .Node }}\t{{ .CurrentState }}'
sw-node1 Running 12 minutes
...
docker stack ps webapp --format '{{ .Node }}\t{{ .CurrentState }}'
sw-node1 Shutdown 1 minutes
sw-node2 Running 1 minutesProveu de reiniciar el node «sw-node1» i deactivar el «sw-node2»
multipass start sw-node1 (1)
docker node update --availability drain sw-node2 (2)
docker stack ps webapp --format '{{ .Node }}\t{{ .CurrentState }}'| 1 | Amb multipass tornem a engegar el node 1 |
| 2 | Amb això, indiquem al cluster no utilitzi el node 2 per a instanciar aplicacions |
Veureu que els serveis de sw-node2 han estat desplaçats al node 1
El sistema de docker per a refer-se d’una caiguda accidental d’un node garanteix la continuitat de l’execució de les aplicacions.
De manera natural, sense haver d’afegir altres aplicacions de monitoratge, tria de nous nodes i reinici, docker swarm garanteix l’execució de les aplicacions del cluster.
Docker swarm disposa d’un sistema per a verificar la salut d’una aplicació. S’hi indica a l’arxiu docker-compose:
healthcheck: (1)
test: ["CMD", "curl", "-s", "--max-time", "20", "http://localhost:8080/api/v1.1.0/return-test"] (2)
interval: 1m (3)
timeout: 30s (3)
retries: 1 (3)
start_period: 30s (3)| 1 | healthcheck és el descriptor de verificació de l’aplicació |
| 2 | test indica la comanda a executar (dins del contenidor del servei) |
| 3 | interval, timeout, retries i start_periode permeten parametritzar el comportament de docker swarm en la verificació i reinici de l’aplicació |
A workshop/spring/servei-rest hi ha una aplicació per a fer aquest experiment.
L’heu de construir amb maven:
mvn clean packageI l’heu de copiar a un volum:
ssh sw-controller mkdir -p /opt/stack/volumes/servei-rest
scp target/servei-rest-1.1.0-SNAPSHOT.jar sw-controller:/opt/stack/volumes/servei-rest/I ara desplegar
docker stack deploy --compose-file src/docker/docker-compose.yml srvL’aplicació es configura per a respondre a la URL
curl -s http://servei-rest.cat/api/v1.1.0/return-test | jq .Ha de retornar alguna cosa semblant a:
{
"hostIp": "10.0.1.4",
"tsInit": "20-01-2023 20:46:45.36",
"randomId": "226c229b-48a9-4941-99eb-5bc9dcaf4af1"
}L’endoint return-info permet verificar els paràmetres de resposta de l’endpoint return-test:
curl -s http://servei-rest.cat/api/v1.1.0/return-info | jq .
{
"httpCodeToReturn": 200,
"msDelay": 0
}msDelay indica el temps que s’espera l’app abans de respondre. Si val 0 no hi ha espera, un nombre major indica els mSeg que s’espera abans de respondre.
Es canvia amb post. Proveu a indicar 5000 (5 segons)
curl -vvv -s -X POST --data '{"httpCodeToReturn": 200,"msDelay": 5000}"' \
-H 'Content-Type: application/json' \
http://servei-rest.cat/api/v1.1.0/return-info | jq .Torneu a provar amb curl
Indiqueu un paràmetre de 30.000 mil·lisegons (30 segons).
I verifiqueu que passa al cluster (trigarà un parell de minuts)
docker stack ps srv --format '{{ .Node }}\t{{ .CurrentState }}'
sw-node2 Starting 44 seconds ago
sw-node2 Failed 49 seconds agoComplementa el sistema de caiguda de nodes de manera que ofereix un control més acurat i més específic de l’estat de salut de l’aplicació.
Es pot implementar un sistema més elaborat, que permeti reiniciar una aplicació segons paràmetres interns (nombre de tasques, consum de memòria, etc.)
La nova instància no manté el valor de 30.000 ja que només s’emmagatzema en memòria
Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
Heu de distingir dos tipus:
Intern (entre serveis del cluster)
Extern (crides des de fora del custer cap a serveis)
El primer és fonamental en entorns multi-servei en que cada servei pot col·laborar amb d’altres
El segon és important per a poder establir entorns fiables i eficients
«swarm» disposa d’un sistema de balanceig de càrrega extern
Utilitza el que s’anomena mesh de docker
Aquest sistema encamina les peticions a un port de qualsevol node al servei que publica aquest port
Si una aplicació publica el port 8080, i al desplegar-se s’instancia al node 1, qualsevol petició a aquest port en qualsevol node (manager, node1 o node2) serà atesa per l’app
Si l’aplicació està escalada, el mesh repartirà les peticions de manera seqüencial, a cada instancia de l’aplicació
A workshop/spring/proxy-rest hi ha una aplicació que permetrà de fer les proves
Procediu com amb l’anterior servei-rest:
mvn clean package
ssh sw-controller mkdir -p /opt/stack/volumes/proxy-rest
scp target/proxy-rest-1.0.0-SNAPSHOT.jar sw-controller:/opt/stack/volumes/proxy-rest/Per a desplegar heu d’utilitzar l’arxiu src/docker/docker-compose-alone.yml que publica el port 8080
Desplegueu i escaleu:
docker stack deploy --compose-file src/docker/docker-compose-alone.yml proxy
docker service scale proxy_proxy-rest=3Ara verifiqueu on s’hi troba desplegat:
docker service ps proxy_proxy-rest --format '{{ .Node }}'
sw-node2
sw-node1
sw-node1Una instància al node 2 i dues al node 1
Ara podeu fer la prova de balanceig. La informació retornada per l’endoint indica la IP on s’està executant:
curl -s http://sw-controller:8080/api/v1.0.0/info-host | jq .Proveu repetides vegades i, també, canvieu el host pel dels altres nodes:
sw-node1
sw-node2
Com veieu, no només fa balanceig de càrrega, sinó que a més, respon a qualsevol dels nodes del cluster
Aquest sistema és util, en part. Funciona correctament però cada aplicació ha d’estar associada amb un port específic.
Això complica la gestió del desplegament i de l’encaminament extern (nom de domini a aplicació)
Hi ha altres opcions?
L’alternativa a mesh és un encaminador intern
Hi ha diverses alternatives, i la primera que ve al cap és la natural: HAProxy
El problema de HAProxy és que requereix d’una configuració que s’ha de mantenir manualment.
Traefik, però, permet configurar les regles de cada aplicació al mateix arxiu docker-compose de desplegament.
Ara elimineu l’stack proxy que hem utilitzat per a les proves mesh
Desplegueu i escaleu la mateixa aplicació, però amb l’arxiu src/docker/docker-compose.yml:
docker stack rm proxy
docker stack deploy --compose-file src/docker/docker-compose.yml proxy
docker service scale proxy_proxy-rest=3A diferència del sistema mesh, en aquest cas necessitem operar amb el nom del domini assignat a aquesta aplicació: http://proxy-rest.cat
Traefik ha estat configurat en el moment del desplegament de l’aplicació, ja que ha detectat que el servei desplegat disposa de etiquetes que l’afecten: traefik.http…
labels:
traefik.enable: "true"
traefik.http.services.proxy_rest.loadbalancer.server.port: 8080
traefik.http.routers.proxy_rest.rule: Host(`proxy-rest.cat`) || Host(`www.proxy-rest.cat`)
traefik.http.routers.proxy_rest.middlewares: proxy_rest_redirect
traefik.http.middlewares.proxy_rest_redirect.redirectregex.regex: '/www.proxy-rest.cat/(.*)'
traefik.http.middlewares.proxy_rest_redirect.redirectregex.replacement: '/proxy-rest.cat/$${1}'
traefik.http.middlewares.proxy_rest_redirect.redirectregex.permanent: "true"
traefik.http.routers.proxy_rest.entrypoints: webAra proveu:
curl -s -L http://www.proxy-rest.cat/api/v1.0.0/info-host | jq .Proveu repetides vegades i veureu que Traefik balanceja les peticions a les diferents instàncies de l’aplicació (ips diferents)
Ara no podeu cridar als nodes directament, sinó que es fa a través de Traefik (sw-manager:80)
Aquest sistema és molt més interessant que mesh, ja que permet resoldre tres problemes:
Encaminament
Balanceig
Proxy
Traefik disposa d’un munt d’opcions, filtres, TLS, etc.
El balanceig de càrrega intern és el que es produexi entre els serveis a un mateix cluster
Per exemple, una aplicació que internament crida a una altra per REST
L’aplicació proveïdora ha de poder escalar-se i ha de ser transparent a l’aplicació client
És imprescindible en entorns de serveis col·laboratius
Docker swarm proporciona una manera de trobar els serveis amb el nom i retornar la IP corresponent
Si el servei està escalat, a cada crida per a resoldre el nom retornarà una de les IPs de les instàncies
L’aplicació client ha de demanar la IP en cada crida, per tal que es resolgui amb balanceig
| Aquestes crides no passen pel Traefik, són directes al port que utilitzi el servei proveïdor |
L’aplicació desplegada proxy té aquesta funció.
L’endpoint /api/v1.0.0/service-info fa una crida a l’aplicació servei-rest per a obtenir informació, l’adjunta a la pròpia del proxy i la retorna
Està feta amb Java i, per desgràcia, implementa una caché d’IPs eterna.
Així, doncs, no es fa el balanceig de manera transparent.
No serveix, tret que s’implementi un sistema que faci el balanceig
Escaleu servei rest a 3 i crideu a proxy per a verificar:
docker service scale srv_servei-rest=3
curl -s http://proxy-rest/api/v1.0.0/service-info | jq .servei.hostIpRepetiu la crida curl varies vegades, veureu que sempre retorna la mateixa IP
Això és conseqüència del disseny de Java, que un cop resol el nom d’un «host» no torna a demanar més la IP
Tingueu en compte aquest funcionament
Heu d’activar el Java Security Manager i desactivar la cache
O utilitzar sistemes de descobriment de serveis (Eureka, Consul, etc)
O una llibreria que no utilitzi la classe «java.net.InetAddress»
He desenvolupat una replica de proxy en Typescript. La trobareu a workshop/ts-proxy.
El procediment per a desplegar, com a la resta d’aplicacions és el següent:
docker stack rm proxy
npm install
npm run build
ssh sw-controller mkdir -p /opt/stack/volumes/ts-proxy/
scp -r build/* sw-controller:/opt/stack/volumes/ts-proxy/
scp package.json sw-controller:/opt/stack/volumes/ts-proxy/
docker stack deploy --compose-file docker-compose.yml proxycurl -s http://proxy-rest.cat/api/v1.0.0/info-host | jq .
curl -s http://proxy-rest.cat/api/v1.0.0/service-info | jq .servei.hostIpLes IPs retornades per la segona crida no sempre funcionen com esperem, ja que Typescript implementa la seva pròpia cache.
Però en qualsevol cas, ofereix alguna mena de balanceig, molt més que en el cas anterior.
El balanceig de càrrega intern és possible sempre que l’aplicació client no mantingut cache de les IPs resoltes.
L’objectiu és construir una aplicació que faci la crida REST però que no es preocupi d’implementar cap mena de balanceig, sinó estem inserint problemes de sistema a l’aplicació.
El balanceig intern és un element fonamental per tal de poder aprofitar les característiques del cluster per a mantenir un sistema resistent i eficient
Pràctiques amb eines: Portainer, Swarmpit, Traefik
Control remot de les instàncies
Desplegament d’aplicacions
Aplicacions a prova de fallades
Balanceig de càrrega segons la demanda
Adreçament de peticions o encaminament (routing), extern i intern
La resolució de noms és indispensable en qualsevol sistema d’aplicacions interconnectat.
Docker proporciona resolutor de noms interns.
A més, hem de poder resoldre noms externs que s’encaminin a l’aplicació desplegada al cluster
Docker resol el nom del servei o el nom del host indicat al compose amb la Ip o Ips de cada instància
No és necessari publicar ports, s’utilitza el port que gestiona el servei (amb spring el 8080, amb nodejs el 6060, amb nginx el 80)
Per a adreçar una petició internament, hem de construir la URL: http://nom_del_servei:port_del_servei/…;
El nom del servei és l’indicat a l’arxiu docker-compose:
version: '3.9'
services:
ts-proxy: (1)
image: node:19.3-slim
hostname: proxy-rest.cat (2)| 1 | El nom del servei és ts-proxy |
| 2 | Alternativament podem donar un nom de host per a la instància i és àlies del nom del servei |
El sistema mesh de Docker resol les peticions a les IPs del cluster (qualsevol node) segons el port de la petició
No és pràctic ja que requereix d’una gestió de ports i la relació amb els serveis desplegats
El servei Traefik resol el problema
Ha de publicar els ports que hagi de respondre (80, 443, etc)
Es pot adreçar la petició a qualsevol dels nodes del cluster (associació per port)
S’ha de configurar el servidor de noms (o arxiu «hosts») amb l’associació nom_host → qualsevol dels nodes del cluster
Traefik resol la petició contra el servei i retorna la resposta (proxy)
Es pot complementar amb un balancejador extern que asseguri resposta tot verificant les IPs de tots els nodes del cluster. Si cau el node assignat a la resolució, crida a un altre
Octavi Fornés <ofornes@albirar.cat>