This shows you the differences between two versions of the page.
cc:laboratoare:01 [2020/10/08 13:00] radu.ciobanu |
cc:laboratoare:01 [2022/10/10 09:09] (current) radu.ciobanu |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laboratorul 01 - Introducere în Docker ===== | + | ===== Laboratorul 01 - Docker ===== |
==== Introducere ==== | ==== Introducere ==== | ||
Line 5: | Line 5: | ||
Docker este o platformă de containere software, folosită pentru a împacheta și rula aplicații atât local, cât și pe sisteme Cloud, eliminând probleme de genul „pe calculatorul meu funcționează”. Docker poate fi deci privit ca un mediu care permite rularea containerelor pe orice platformă, bazat pe **//containerd//**. Ca beneficii, oferă compilare, testare, deployment, actualizare și recuperare în caz de eroare mai rapide față de modul standard de deployment al aplicațiilor. | Docker este o platformă de containere software, folosită pentru a împacheta și rula aplicații atât local, cât și pe sisteme Cloud, eliminând probleme de genul „pe calculatorul meu funcționează”. Docker poate fi deci privit ca un mediu care permite rularea containerelor pe orice platformă, bazat pe **//containerd//**. Ca beneficii, oferă compilare, testare, deployment, actualizare și recuperare în caz de eroare mai rapide față de modul standard de deployment al aplicațiilor. | ||
- | Docker oferă un mediu uniform de dezvoltare și producție, unde nu se mai pune problema compatibilității aplicațiilor cu sistemul de operare și nu mai există conflicte între versiunile de biblioteci/pachete de pe sistemul gazdă. Containerele sunt efemere, așa că stricarea sau închiderea unuia nu duce la căderea întregului sistemul. Ele ajută la asigurarea consistenței stricte între comportamentul în mediul de dezvoltare cu cel în mediul de producție. | + | Docker oferă un mediu uniform de dezvoltare și producție, unde nu se mai pune problema compatibilității aplicațiilor cu sistemul de operare și nu mai există conflicte între versiunile de biblioteci/pachete de pe sistemul gazdă. Containerele sunt efemere, așa că stricarea sau închiderea unuia nu duce la căderea întregului sistem. Ele ajută la asigurarea consistenței stricte între comportamentul în mediul de dezvoltare cu cel în mediul de producție. |
De asemenea, Docker oferă flexibilitate maximă. Dacă, într-un proiect de mari dimensiuni, avem nevoie de unelte software noi pentru că se schimbă anumite cerințe, le putem împacheta în containere și apoi să le legăm foarte ușor la sistem. Dacă avem nevoie de replicarea infrastructurii pe alt mediu, putem refolosi imginile de Docker salvate în registru (un fel de repository de containere). Dacă avem nevoie de actualizarea anumitor componente, Docker ne permite să rescriem imaginile, ceea ce înseamnă că se vor lansa, mereu, cele mai noi versiuni ale componentelor sub formă de containere. | De asemenea, Docker oferă flexibilitate maximă. Dacă, într-un proiect de mari dimensiuni, avem nevoie de unelte software noi pentru că se schimbă anumite cerințe, le putem împacheta în containere și apoi să le legăm foarte ușor la sistem. Dacă avem nevoie de replicarea infrastructurii pe alt mediu, putem refolosi imginile de Docker salvate în registru (un fel de repository de containere). Dacă avem nevoie de actualizarea anumitor componente, Docker ne permite să rescriem imaginile, ceea ce înseamnă că se vor lansa, mereu, cele mai noi versiuni ale componentelor sub formă de containere. | ||
Line 110: | Line 110: | ||
==== Rularea unui container ==== | ==== Rularea unui container ==== | ||
- | Am văzut mai sus cum putem rula un Hello World într-un container simplu, însă putem rula imagini mult mai complexe. Putem să ne creăm propria imagine (așa cum vom vedea în [[cc:laboratoare:02|laboratorul 2]]) sau putem descărca o imagine dintr-un registru, cum ar fi [[https://hub.docker.com|Docker Hub]] . Acesta conține imagini publice, care variază de la sisteme de operare (Ubuntu, Alpine, Amazon Linux, etc.) la limbaje de programare (Java, Ruby, Perl, Python, etc.), servere Web (NGINX, Apache), etc. | + | Am văzut mai sus cum putem rula un Hello World într-un container simplu, însă putem rula imagini mult mai complexe. Putem să ne creăm propria imagine sau putem descărca o imagine dintr-un registru, cum ar fi [[https://hub.docker.com|Docker Hub]] . Acesta conține imagini publice, care variază de la sisteme de operare (Ubuntu, Alpine, Amazon Linux, etc.) la limbaje de programare (Java, Ruby, Perl, Python, etc.), servere Web (NGINX, Apache), etc. |
Pentru acest laborator, vom rula Alpine Linux, care este o distribuție lightweight de Linux (dimensiunea sa este de 5 MB). Primul pas constă în descărcarea imaginii dintr-un registru Docker (în cazul nostru, Docker Hub): | Pentru acest laborator, vom rula Alpine Linux, care este o distribuție lightweight de Linux (dimensiunea sa este de 5 MB). Primul pas constă în descărcarea imaginii dintr-un registru Docker (în cazul nostru, Docker Hub): | ||
Line 271: | Line 271: | ||
</file> | </file> | ||
- | O imagine este definită de un fișier numit **//Dockerfile//**, care specifică ce se întâmplă în interiorului containerului pe care vrem să îl creăm, unde accesul la resurse (cum ar fi interfețele de rețea sau hard disk-urile) este virtualizat și izolat de restul sistemului. Prin intermediul acestui fișier, putem specifica mapări de porturi, fișiere care vor fi copiate în container când este pornit, etc. Un fișier Dockerfile se aseamana cu un Makefile, iar fiecare rând din el descrie un strat din imagine. Odată ce am definit un Dockerfile corect, aplicația noastră se va comporta totdeauna identic, indiferent în ce mediu este rulată. Un exemplu de Dockerfile pentru aplicația noastră este următorul: | + | O imagine este definită de un fișier numit **//Dockerfile//**, care specifică ce se întâmplă în interiorului containerului pe care vrem să îl creăm, unde accesul la resurse (cum ar fi interfețele de rețea sau hard disk-urile) este virtualizat și izolat de restul sistemului. Prin intermediul acestui fișier, putem specifica mapări de porturi, fișiere care vor fi copiate în container când este pornit, etc. Un fișier Dockerfile se aseamănă cu un Makefile, iar fiecare rând din el descrie un strat din imagine. Odată ce am definit un Dockerfile corect, aplicația noastră se va comporta totdeauna identic, indiferent în ce mediu este rulată. Un exemplu de Dockerfile pentru aplicația noastră este următorul: |
<file txt Dockerfile> | <file txt Dockerfile> | ||
Line 419: | Line 419: | ||
Accesând dintr-un browser Web adresa [[http://127.0.0.1:8888]], vom vedea aplicația Web pe care am creat-o. Flag-ul **//-p//** expune portul 5000 al aplicației și specifică o mapare între el și portul 8888 de pe mașina pe care rulăm. Dacă dorim să rulăm aplicația în modul detașat, o putem face folosind flag-ul **//-d//**. | Accesând dintr-un browser Web adresa [[http://127.0.0.1:8888]], vom vedea aplicația Web pe care am creat-o. Flag-ul **//-p//** expune portul 5000 al aplicației și specifică o mapare între el și portul 8888 de pe mașina pe care rulăm. Dacă dorim să rulăm aplicația în modul detașat, o putem face folosind flag-ul **//-d//**. | ||
+ | |||
+ | ==== Publicarea unei imagini într-un registru ==== | ||
+ | |||
+ | Mai devreme, am creat o imagine de Docker pe care am rulat-o local într-un container. Pentru a putea rula imaginea creată în orice alt sistem, este necesar să o publicăm, deci să o urcăm într-un registru pentru a putea să facem deploy de containere cu imaginea noastră în producție. Un registru este o colecție de repository-uri, iar un repository este o colecție de imagini (similar cu GitHub, cu diferența că, într-un registru Docker, codul este deja construit și se rețin modificările făcute în straturile imaginilor de Docker, nu în cod). Există numeroase registre pentru imagini Docker (Docker Hub, Gitlab Registry, etc.), iar la laborator vom folosi registrul public Docker, pentru că este gratuit și pre-configurat. | ||
+ | |||
+ | Pentru exemplificare, vom porni de la aplicația prezentată anterior, care afișează o poză aleatoare într-o pagină Web. Primul pas în publicarea unei imagini este crearea unui cont la [[https://hub.docker.com]]. Mai departe, logarea în registru de pe mașina locală se realizează prin următoarea comandă: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker login | ||
+ | |||
+ | Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. | ||
+ | Username: | ||
+ | Password: | ||
+ | Login Succeeded | ||
+ | </code> | ||
+ | |||
+ | Putem specifica numele de utilizator și parola direct în comandă, iar varianta generică a acesteia este (unde serverul implicit, dacă alegem să omitem acel parametru, este Docker Hub): | ||
+ | |||
+ | <code bash> | ||
+ | $ docker login [–u <UTILIZATOR> –p <PAROLĂ>] [SERVER] | ||
+ | </code> | ||
+ | |||
+ | Înainte de a publica imaginea în registru, ea trebuie tag-uită după formatul **//username/repository:tag//**. Tag-ul este opțional, dar este util pentru că denotă versiunea unei imagini Docker. Se folosește următoarea comandă pentru tag-uirea unei imagini (în exemplul de mai jos, unde vrem să tag-uim imaginea pe care am creat-o anterior, utilizatorul se numește **//raduioanciobanu//**, repository-ul este **//cloudcomputing//**, iar tag-ul este **//example//**): | ||
+ | |||
+ | <code bash> | ||
+ | $ docker tag testapp raduioanciobanu/cloudcomputing:example | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker images | ||
+ | REPOSITORY TAG IMAGE ID CREATED SIZE | ||
+ | testapp latest 74254b15e6ba About a minute ago 62.9MB | ||
+ | raduioanciobanu/cloudcomputing example 74254b15e6ba About a minute ago 62.9MB | ||
+ | alpine edge f96c4363411f 4 weeks ago 5.58MB | ||
+ | </code> | ||
+ | |||
+ | Odată tag-uită imaginea, ea poate fi publicată în registru: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker push raduioanciobanu/cloudcomputing:example | ||
+ | </code> | ||
+ | |||
+ | Din acest moment, imaginea va fi vizibilă pe [[https://hub.docker.com]], de unde poate fi descărcată și rulată pe orice mașină, server sau sistem Cloud: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker run -p 8888:5000 raduioanciobanu/cloudcomputing:example | ||
+ | |||
+ | Unable to find image 'raduioanciobanu/cloudcomputing:example' locally | ||
+ | example: Pulling from raduioanciobanu/cloudcomputing | ||
+ | cc5efb633992: Pull complete | ||
+ | cd0af7ebab8a: Pull complete | ||
+ | 41c55a3da379: Pull complete | ||
+ | a779b27637f8: Pull complete | ||
+ | dfaeccf28d0c: Pull complete | ||
+ | 805843c75452: Pull complete | ||
+ | Digest: sha256:25af18fb4ffa9bf439e90bd4baee9adf0ab1e2999a44aeaa216ebf0454201ce8 | ||
+ | Status: Downloaded newer image for raduioanciobanu/cloudcomputing:example | ||
+ | * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) | ||
+ | [...] | ||
+ | </code> | ||
+ | |||
+ | Alternativ, în loc să rulăm comanda de publicare a unei imagini de fiecare dată când modificăm ceva la codul sursă, putem să configurăm [[https://docs.docker.com/docker-hub/builds/|build-uri automate]] din contul de Docker Hub. Pașii necesari sunt descriși în continuare. | ||
+ | În primul rând, este necesară existența unui repository Docker Hub și a unui repository pe GitHub (Docker Hub funcționează și cu BitBucket, dar în acest exemplu ne vom concentra pe GitHub). Toate fișierele necesare creării unei imagini Docker (adică Dockerfile-ul și toate fișierele sursă și de configurare) trebuie să fie prezente în repository-ul GitHub. Mai departe, de pe pagina repository-ului de Docker Hub, se selectează tab-ul Builds și apoi opțiunea „Configure Automated Builds”, așa cum se poate observa în imaginea de mai jos. | ||
+ | |||
+ | {{:cc:laboratoare:dockerhub.png?direct&600|}} | ||
+ | |||
+ | În continuare, va fi necesară completarea unor informații despre repository-ul GitHub și opțiuni de testare automată înainte de build, după care trebuie specificate regulile de build. O regulă de build conține informații despre: tipul build-ului (bazat pe un branch sau pe un tag Git), sursa (numele branch-ului sau tag-ului de pe care se face build-ul), tag-ul care va fi asignat noii imagini Docker construite, numele și adresa fișierului Dockerfile în repository-ul GitHub, calea către sursele ce vor fi compilate, opțiuni de auto-build (dacă se va face build automat la fiecare push pe branch-ul sau cu tag-ul specificat), opțiuni de build caching (dacă se vor cache-ui fișiere la build în cazul unor repository-uri de dimensiuni mari). În exemplul de mai jos, atunci când are loc un push pe branch-ul **//master//**, se va crea automat o imagine Docker cu tag-ul **//latest//** folosindu-se fișierul Dockerfile aflat în rădăcina repository-ului de GitHub. | ||
+ | |||
+ | {{:cc:laboratoare:dockerhub2.png?direct&500|}} | ||
+ | |||
+ | În continuare, pe pagina de Builds de pe Docker Hub vor exista opțiuni pentru pornirea unui nou build, precum și informații despre build-urile precedente și statusurile lor. | ||
+ | |||
+ | ==== Networking ==== | ||
+ | |||
+ | Subsistemul de networking Docker este de tip pluggable și folosește drivere. Mai multe astfel de drivere există implicit, ele oferind funcționalitate de bază pentru componenta de rețea. Driverul de rețea implicit este **bridge**, și presupune crearea unui bridge software care permite containerelor conectate la aceeași rețea de acest tip să comunice între ele, oferind totodată izolare față de containerele care nu sunt conectate la această rețea bridge. Driverul de bridge Docker instalează automat reguli pe mașina gazdă astfel încât containerele de pe rețele bridge diferite nu pot comunica direct unele cu altele. Rețelele de tip bridge se aplică doar containerelor care rulează pe aceeași mașină Docker. | ||
+ | |||
+ | Pentru comunicație între containere care rulează pe mașini Docker diferite, se poate gestiona rutarea la nivel de sistem de operare, sau se poate folosi o rețea de tip **overlay**. Așa cum se va detalia în [[cc:laboratoare:02|laboratorul 2]], rețelele de tip overlay conectează mai multe mașini Docker și permit serviciilor dintr-un swarm să comunice între ele. Rețelele overlay se mai pot folosi și pentru a facilita comunicația între un serviciu swarm și un container de sine stătător, sau între două containere care rulează pe mașini Docker diferite. | ||
+ | |||
+ | Alte drivere de rețea Docker mai sunt **host** (pentru containere de sine stătătoare, eliminând izolarea de rețea dintre container și gazda Docker, folosindu-se astfel infrastructura de rețea a gazdei direct), **macvlan** (permite asignarea de adrese MAC unui container, făcându-l să apară ca un dispozitiv fizic pe rețea), sau **none**. | ||
+ | |||
+ | Containerele din aceeași rețea pot comunica fără să expună porturi, prin intermediul **named DNS**. Acest lucru înseamnă că putem să accesam un container nu prin IP, ci prin numele său. Pentru comunicarea cu lumea exterioară (gazda, containere din afara rețelei, etc.) trebuie expuse porturi. | ||
+ | |||
+ | Pentru a demonstra modul în care funcționează rețelele de tip bridge în Docker, întâi vom porni două containere ce vor rula Alpine. În mod implicit, orice container Docker nou-creat se va afla într-o rețea numită „bridge”, așa că, pentru a demonstra faptul că două containere care nu sunt în aceeași rețea nu pot comunica, va trebui întâi să le scoate din acea rețea. | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c1 -d -it alpine | ||
+ | |||
+ | f5a8653a325e8092151614d5a6a80b04b9410ea8b8a5fcfc4028f1ad33239ad9 | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c2 -d -it alpine | ||
+ | |||
+ | b063ad1ef7bd0ae82a7385582415e78938f7df531cef9eefc33e065af09cf92c | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network disconnect bridge c1 | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network disconnect bridge c2 | ||
+ | </code> | ||
+ | |||
+ | <note tip> | ||
+ | În comanda de **//docker run//** de mai sus, parametrul **//%%--%%name//** îi dă containerului un nume (sau alias) prin care îl putem referi mai ușor. | ||
+ | </note> | ||
+ | |||
+ | În acest moment, containerele **c1** și **c2** nu fac parte din nicio rețea. Mai departe, vom încerca să dăm **//ping//** dintr-un container în altul. | ||
+ | |||
+ | <code bash> | ||
+ | $ docker exec -it c1 ash | ||
+ | |||
+ | / # ifconfig | ||
+ | lo Link encap:Local Loopback | ||
+ | inet addr:127.0.0.1 Mask:255.0.0.0 | ||
+ | UP LOOPBACK RUNNING MTU:65536 Metric:1 | ||
+ | RX packets:0 errors:0 dropped:0 overruns:0 frame:0 | ||
+ | TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 | ||
+ | collisions:0 txqueuelen:1000 | ||
+ | RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) | ||
+ | |||
+ | / # ping c2 | ||
+ | ping: bad address 'c2' | ||
+ | |||
+ | / # exit | ||
+ | </code> | ||
+ | |||
+ | Se poate observa mai sus că **c1** nu are decât o adresă IP loopback și că nu poate accesa **c2**. De asemenea, ar fi interesant de menționat faptul că ne-am atașat la container folosind comanda **//docker exec//** rulând un shell (**//ash//** este shell-ul de pe Alpine). | ||
+ | |||
+ | Pentru a crea o rețea de tip bridge în Docker, putem folosi următoarea comandă: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network create -d bridge c1-c2-bridge | ||
+ | |||
+ | 8644b8accd2a14d10c9911c36635ca6b161449b3aa527db878a727ec1bf980d0 | ||
+ | </code> | ||
+ | |||
+ | Mai departe, putem vizualiza rețele existente astfel: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network ls | ||
+ | |||
+ | NETWORK ID NAME DRIVER SCOPE | ||
+ | ecd72738aa59 bridge bridge local | ||
+ | 8644b8accd2a c1-c2-bridge bridge local | ||
+ | 615363cafefa host host local | ||
+ | 1e3b8e49b20d none null local | ||
+ | </code> | ||
+ | |||
+ | Putem adăuga un container într-o rețea fie atunci când pornim containerul, fie atunci când el deja a fost pornit. Pentru cazul de mai sus, unde **c1** și **c2** erau deja pornite, le putem adăuga în rețea astfel: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network connect c1-c2-bridge c1 | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network connect c1-c2-bridge c2 | ||
+ | </code> | ||
+ | |||
+ | Dacă **c1** și **c2** nu ar fi fost deja pornite, le-am fi putut porni deja atașate la rețeaua **c1-c2-bridge** astfel: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c2 -d -it --network=c1-c2-bridge alpine | ||
+ | |||
+ | 67dde5da9b793de63903ac85ff46574da77f0031df9b49acf44d58062687729c | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c1 -d -it --network=c1-c2-bridge alpine | ||
+ | |||
+ | 4de3e000700f81d31e0458dbd034abe90dfce6b1b992d23d760a44f748c0de0d | ||
+ | </code> | ||
+ | |||
+ | Putem vedea containerele dintr-o rețea astfel: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network inspect c1-c2-bridge | ||
+ | |||
+ | [...] | ||
+ | "Containers": { | ||
+ | "b063ad1ef7bd0ae82a7385582415e78938f7df531cef9eefc33e065af09cf92c": { | ||
+ | "Name": "c2", | ||
+ | "EndpointID": "a76463662d110804205e9211537e541eb0de2646fa90e8760d3419a6dc7d32c7", | ||
+ | "MacAddress": "02:42:ac:12:00:03", | ||
+ | "IPv4Address": "172.18.0.3/16", | ||
+ | "IPv6Address": "" | ||
+ | }, | ||
+ | "f5a8653a325e8092151614d5a6a80b04b9410ea8b8a5fcfc4028f1ad33239ad9": { | ||
+ | "Name": "c1", | ||
+ | "EndpointID": "95d9061b47f73f9b4cc7a82111924804bdc73d0b496549dec834216ee58c64ed", | ||
+ | "MacAddress": "02:42:ac:12:00:02", | ||
+ | "IPv4Address": "172.18.0.2/16", | ||
+ | "IPv6Address": "" | ||
+ | } | ||
+ | } | ||
+ | [...] | ||
+ | </code> | ||
+ | |||
+ | În acest moment, cele două containere fac parte din aceeași rețea și pot comunica: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker exec -it c1 ash | ||
+ | |||
+ | / # ping -c2 c2 | ||
+ | PING c2 (172.18.0.3): 56 data bytes | ||
+ | 64 bytes from 172.18.0.3: seq=0 ttl=64 time=6.258 ms | ||
+ | 64 bytes from 172.18.0.3: seq=1 ttl=64 time=0.109 ms | ||
+ | |||
+ | --- c2 ping statistics --- | ||
+ | 2 packets transmitted, 2 packets received, 0% packet loss | ||
+ | round-trip min/avg/max = 0.109/3.183/6.258 ms | ||
+ | |||
+ | / # exit | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker exec -it c2 ash | ||
+ | |||
+ | / # ping -c2 c1 | ||
+ | PING c1 (172.18.0.2): 56 data bytes | ||
+ | 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.111 ms | ||
+ | 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.268 ms | ||
+ | |||
+ | --- c1 ping statistics --- | ||
+ | 2 packets transmitted, 2 packets received, 0% packet loss | ||
+ | round-trip min/avg/max = 0.111/0.189/0.268 ms | ||
+ | |||
+ | / # exit | ||
+ | </code> | ||
+ | ==== Volume și bind mounts ==== | ||
+ | |||
+ | În Docker, datele dintr-un container nu sunt persistate în exterior. Pentru a ilustra acest lucru, putem rula un container simplu de Alpine, în care creăm un fișier apoi ieșim. | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c1 -ti alpine sh | ||
+ | |||
+ | Unable to find image 'alpine:latest' locally | ||
+ | latest: Pulling from library/alpine | ||
+ | 88286f41530e: Pull complete | ||
+ | Digest: sha256:f006ecbb824d87947d0b51ab8488634bf69fe4094959d935c0c103f4820a417d | ||
+ | Status: Downloaded newer image for alpine:latest | ||
+ | |||
+ | / # mkdir /test && echo hello > /test/hello.txt | ||
+ | / # exit | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container ls -a | ||
+ | |||
+ | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | ||
+ | 97492cd1349b alpine "sh" 15 minutes ago Exited (0) 15 minutes ago c1 | ||
+ | </code> | ||
+ | |||
+ | Odată ce un container a fost rulat, chiar dacă execuția sa s-a oprit, layer-ele sale pot fi accesate până când containerul este șters cu comanda **//docker container rm//** (sau **//docker system prune//**). În mod implicit, Docker folosește [[https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt|OverlayFS]] sau [[https://www.thegeekstuff.com/2013/05/linux-aufs/|AUFS]] (ambele sisteme de fișiere de tip union) ca driver de storage pentru gestiunea imaginilor. Putem verifica acest lucru folosind **//docker info//**: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker info | grep -i storage | ||
+ | |||
+ | Storage Driver: overlay2 | ||
+ | </code> | ||
+ | |||
+ | Pentru fiecare layer dintr-un container Docker, se vor stoca în AUFS/OverlayFS informații despre cum arăta inițial și despre ce fișiere s-au modificat (au fost adăugate, șterse sau schimbate). Aceste informații se găsesc în **///var/lib/docker/aufs/diff//** (pentru AUFS) sau **///var/lib/docker/overlay2//** (pentru OverlayFS), unde există directoare pentru fiecare layer al fiecărui container care a fost rulat pe sistem fără să fi fost șters. Putem astfel să vedem din exteriorul containerului, după ce acesta a terminat de rulat, fișierul care a fost creat în interiorul containerului: | ||
+ | |||
+ | <code bash> | ||
+ | $ cd /var/lib/docker/overlay2/ | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ ls -latr | ||
+ | |||
+ | [...] | ||
+ | drwx------ 4 root root 4096 Oct 21 07:12 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc-init | ||
+ | drwx------ 4 root root 4096 Oct 21 07:12 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ ls 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/diff/ | ||
+ | |||
+ | root test | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ cat 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/diff/test/hello.txt | ||
+ | |||
+ | hello | ||
+ | </code> | ||
+ | |||
+ | Totuși, aceste date nu sunt persistente, ci sunt șterse împreuna cu layer-ul. Astfel, dacă se șterge containerul, datele vor fi pierdute: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container rm 97492cd1349b | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ ls 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc/ | ||
+ | |||
+ | ls: 5b3f2aeff7a90abd5c1a2eb50e5bbf9bde38983bda84728ab3788a12ea2399dc: No such file or directory | ||
+ | </code> | ||
+ | |||
+ | Pentru persistența datelor dintr-un container, în Docker se folosesc mecanisme de persistență numite **volume**, care sunt o mapare între fișierele din cadrul unui container și fișiere de pe sistemul gazdă sau NFS (Network File Storage). Volumele Docker au câteva proprietăți și beneficii: | ||
+ | |||
+ | * sunt ușor de salvat și migrat | ||
+ | * pot fi controlate și configurate cu comenzi CLI sau cu API-ul de Docker | ||
+ | * funcționează pe containere Linux și Windows | ||
+ | * pot fi partajate între containere | ||
+ | * prin driverele de volume, se pot stoca date persistente pe gazde remote sau pe provideri de cloud, se pot cripta datele, etc. | ||
+ | * conținutul unui volum nou poate fi pre-populat de un container | ||
+ | * utilizarea unui volum nu crește dimensiunea unui container care îl folosește, pentru că un volum există în afara ciclului de viață al containerului. | ||
+ | |||
+ | Volumele se mai numesc și „named volumes” și sunt gestionate de Docker. Există mai multe metode pentru a defini și utiliza un volum atunci când se rulează un singur container de Linux. Dacă se creează o imagine custom, atunci volumul se poate defini în fișierul Dockerfile, prin comanda **//VOLUME//**. Dacă se rulează, de exemplu, un container bazat pe o imagine existentă (cum ar fi Alpine în exemplul de mai devreme), atunci se poate defini un volum la runtime. În exemplul de mai jos, rulăm o imagine de Alpine în background care face **//ping//** într-un fișier localizat într-un volum **///test//**, pe care îl creăm folosind flag-ul **//-v//**: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c2 -d -v /test alpine sh -c 'ping 8.8.8.8 > /test/ping.txt' | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container ls | ||
+ | |||
+ | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | ||
+ | 59d0785188a6 alpine "sh -c 'ping 8.8.8..." About a minute ago Up About a minute c2 | ||
+ | </code> | ||
+ | |||
+ | În timp ce containerul rulează, putem să îl inspectăm și observăm că este legat de o componentă de tip **//Volume//** cu destinația **///test//**. Astfel, putem afla unde este localizat volumul. Dacă ne uităm în acel director, vom vedea fișierul în care se face **//ping//** din container: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container inspect -f "{{ json .Mounts }}" c2 | python -m json.tool | ||
+ | |||
+ | [ | ||
+ | { | ||
+ | "Destination": "/test", | ||
+ | "Driver": "local", | ||
+ | "Mode": "", | ||
+ | "Name": "2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288", | ||
+ | "Propagation": "", | ||
+ | "RW": true, | ||
+ | "Source": "/var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data", | ||
+ | "Type": "volume" | ||
+ | } | ||
+ | ] | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ ls /var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data | ||
+ | |||
+ | ping.txt | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ cat ping.txt | ||
+ | |||
+ | PING 8.8.8.8 (8.8.8.8): 56 data bytes | ||
+ | 64 bytes from 8.8.8.8: seq=0 ttl=38 time=58.619 ms | ||
+ | 64 bytes from 8.8.8.8: seq=1 ttl=38 time=58.498 ms | ||
+ | </code> | ||
+ | |||
+ | Dacă oprim și ștergem containerul, volumul va exista în continuare: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container stop c2 | ||
+ | |||
+ | c2 | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container rm c2 | ||
+ | |||
+ | c2 | ||
+ | </code> | ||
+ | |||
+ | <code bash> | ||
+ | $ ls /var/lib/docker/volumes/2afac5683222a3435549131a931a4c0628b775ecd3d79cb3fd597b3501418288/_data | ||
+ | |||
+ | ping.txt | ||
+ | </code> | ||
+ | |||
+ | O a treia metodă de a lucra cu volume în Docker este direct prin API-ul de volume, adică prin comenzi CLI de genul **//docker volume create//**, **//docker volume ls//**, etc. Dacă vrem să creăm volume pentru o stivă de servicii, acest lucru poate fi făcut în fișierul YAML folosit pentru Docker Compose, așa cum vom vedea în [[cc:laboratoare:02|laboratorul 2]]. | ||
+ | |||
+ | Pe lângă volume, mai există și noțiunea de **bind mounts**. Acestea sunt similare cu volumele, dar nu sunt gestionate de Docker, ci se pot afla oriunde în sistemul de fișiere al gazdei pe care rulăm containerele, și pot fi modificate extern de orice proces non-Docker. Diferența principală dintre un bind mount si un volum este că bind mount-ul este o cale fizică de pe mașina gazdă, în timp ce volumul este o entitate Docker care utilizează, în spate, un bind mount abstractizat. În imaginea de mai jos (preluată din [[https://docs.docker.com/storage/bind-mounts/|documentația oficială]]), se poate observa în mod grafic diferența dintre volume și bind mounts. | ||
+ | |||
+ | {{:cc:laboratoare:volumebind.png?direct&500|}} | ||
+ | |||
+ | Atunci când pornim un container prin comanda **//docker container run//**, atât argumentul **//-v//** (sau **//%%--%%volume//**), cât și **//%%--%%mount//** permit utilizarea de bind mounts și volume. Totuși, în cadrul serviciilor (așa cum vom vedea în [[cc:laboratoare:02|laboratorul 2]]), nu putem folosi decât **//%%--%%mount//**. Acesta este totuși considerat oricum mai expresiv, pentru că necesită specificarea efectivă tipului de legătura (volum sau bind mount). Astfel, exemplul anterior unde atașam un volum **///test//** containerului pe care îl rulam ar arăta în felul următor: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container run --name c2 -d --mount source=test,target=/test alpine sh -c 'ping 8.8.8.8 > /test/ping.txt' | ||
+ | </code> | ||
+ | |||
+ | Pentru a verifica efectul acestei comenzi, putem rula comanda de inspectare: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker container inspect -f "{{ json .Mounts }}" c2 | python -m json.tool | ||
+ | |||
+ | [ | ||
+ | { | ||
+ | "Destination": "/test", | ||
+ | "Driver": "local", | ||
+ | "Mode": "z", | ||
+ | "Name": "test", | ||
+ | "Propagation": "", | ||
+ | "RW": true, | ||
+ | "Source": "/var/lib/docker/volumes/test/_data", | ||
+ | "Type": "volume" | ||
+ | } | ||
+ | ] | ||
+ | </code> | ||
+ | |||
+ | ==== Docker pe Windows și pe MacOS ==== | ||
+ | |||
+ | Docker a fost creat nativ pentru Linux, utilizând componente de kernel specifice Linux, cum ar fi **//cgroups//** sau **//namespaces//**, folosite pentru a izola procese și alte componente ale sistemului de operare. Începând din 2016, el poate rula nativ și pe Windows, dar doar pentru versiunile Windows Server 2016 și Windows 10. De aceea, pentru a rula pe un sistem de operare desktop precum un Windows mai vechi sau MacOS, Docker necesită rularea virtualizată. | ||
+ | |||
+ | Pe Windows, se folosește izolare Hyper-V pentru a rula un kernel Linux cu un set minimal de componente suficiente pentru a executa Docker. Pe MacOS, Docker for Mac este o aplicație nativă care conține un hypervizor bazat pe [[https://github.com/machyve/xhyve|xhyve]] și o distribuție minimală de Linux, peste care rulează Docker. Astfel, se oferă o experiență mult mai apropiată de utilizarea Docker pe Linux, sistemul de operare pentru care a fost creat. | ||
+ | |||
+ | Ca un exemplu, pentru a avea acces în mașina virtuală de Docker pentru MacOS, se poate folosi **//screen//** (pentru a se termina sesiunea, se folosește combinația de taste **//Ctrl+a, k//**): | ||
+ | |||
+ | <code bash> | ||
+ | $ screen /Users/<UID>/Library/Containers/com.docker.docker/Data/vms/0/tty | ||
+ | |||
+ | linuxkit-025000000001:~# pwd | ||
+ | /root | ||
+ | </code> | ||
+ | |||
+ | O alternativă mai elegantă la comanda de **//screen//** de mai sus este utilizarea unei imagini speciale pentru accesul în mașina virtuală Docker: | ||
+ | |||
+ | <code bash> | ||
+ | $ docker run -it --privileged --pid=host justincormack/nsenter1 | ||
+ | </code> | ||
+ | |||
==== Comenzi utile ==== | ==== Comenzi utile ==== | ||
Line 426: | Line 854: | ||
</note> | </note> | ||
- | === Comenzi de sistem === | + | === Sistem === |
<code bash> | <code bash> | ||
Line 476: | Line 904: | ||
Diferența dintre comenzile **//exec//** și **//attach//** (care pot părea similare) este că, la **//attach//**, se asociază un terminal la container, ceea ce înseamnă că, dacă se iese din acel terminal, se iese cu totul și din container. | Diferența dintre comenzile **//exec//** și **//attach//** (care pot părea similare) este că, la **//attach//**, se asociază un terminal la container, ceea ce înseamnă că, dacă se iese din acel terminal, se iese cu totul și din container. | ||
</note> | </note> | ||
+ | |||
+ | === Lucru cu registre === | ||
+ | |||
+ | <code bash> | ||
+ | $ docker login [–u <UTILIZATOR> –p <PAROLĂ>] [SERVER] # loghează un utilizator într-un registru | ||
+ | $ docker tag <IMAGINE> <UTILIZATOR/REPOSITORY:TAG> # dă un tag unei imagini pentru upload în registru | ||
+ | $ docker push <UTILIZATOR/REPOSITORY:TAG> # uploadează o imagine în registru | ||
+ | </code> | ||
+ | |||
+ | === Creare și interacțiune cu rețele === | ||
+ | |||
+ | <code bash> | ||
+ | $ docker network create -d <DRIVER> <REȚEA> # creează o rețea cu un driver dat | ||
+ | $ docker network ls # afișează rețelele existente | ||
+ | $ docker network rm # șterge o rețea | ||
+ | $ docker network connect <REȚEA> <CONTAINER> # conectează un container la o rețea | ||
+ | $ docker network disconnect <REȚEA> <CONTAINER> # deconectează un container de la o rețea | ||
+ | $ docker network inspect <REȚEA> # afișează informații despre o rețea | ||
+ | $ docker container run --network=<REȚEA> <IMAGINE> # pornește un container într-o rețea | ||
+ | </code> | ||
+ | |||
+ | === Creare și interacțiune cu volume sau bind mounts === | ||
+ | |||
+ | <code bash> | ||
+ | $ docker volume create <VOLUM> # creează un volum | ||
+ | $ docker volume ls # afișează volumele existente | ||
+ | $ docker volume rm <VOLUM> # șterge un volum | ||
+ | $ docker container run -v <VOLUM> <IMAGINE> # rulează un container cu un volum atașat | ||
+ | $ docker container run -v <SURSĂ>:<DESTINAȚIE> <IMAGINE> # rulează un container cu un volum sau un bind mount atașat | ||
+ | $ docker container run --mount source=<SURSĂ>,target=<DESTINAȚIE> <IMAGINE> # rulează un container cu un volum sau bind mount atașat | ||
+ | </code> | ||
==== Exerciții ==== | ==== Exerciții ==== | ||
+ | |||
+ | === Comenzi de bază === | ||
- Aduceți în cache-ul local imaginea **//busybox//** din registrul oficial Docker. | - Aduceți în cache-ul local imaginea **//busybox//** din registrul oficial Docker. | ||
Line 484: | Line 945: | ||
- Rulați un container interactiv detașat (daemon) de **//busybox//**. Odată ce l-ați pornit, atașați-vă la el și dați comanda **//id//**, apoi ieșiți. | - Rulați un container interactiv detașat (daemon) de **//busybox//**. Odată ce l-ați pornit, atașați-vă la el și dați comanda **//id//**, apoi ieșiți. | ||
- Ștergeți toate containerele și imaginile create la punctele precedente. | - Ștergeți toate containerele și imaginile create la punctele precedente. | ||
- | - Pornind de la cele două fișiere din {{:cc:laboratoare:homework1.zip|această arhivă}}, scrieți un Dockerfile care va crea o imagine urmărind pașii de mai jos: | + | |
- | - se va porni de la cea mai recentă versiune a imaginii oficiale de NodeJS, adică //node:latest// | + | === Crearea unei imagini === |
- | - se va copia fișierul **//package.json//** din arhivă în directorul curent (**//.///**); acest fișier are rolul de a specifica dependențele aplicației NodeJS (de exemplu, framework-ul Express.js) | + | |
+ | <note tip>Pentru exercițiile următoare, veți porni de la {{:cc:laboratoare:homework1.zip|această arhivă}}, care conține o aplicație simplă NodeJS.</note> | ||
+ | |||
+ | - Pornind de la cele două fișiere din arhivă, scrieți un Dockerfile care va crea o imagine urmărind pașii de mai jos: | ||
+ | - se va porni de la cea mai recentă versiune a imaginii oficiale de NodeJS, adică **//node:14.13.0-stretch//** | ||
+ | - se va copia fișierul **//package.json//** din arhivă în directorul curent (./); acest fișier are rolul de a specifica dependențele aplicației NodeJS (de exemplu, framework-ul Express.js) | ||
- se va rula comanda **//npm install//** pentru a instala dependețele din fișierul de la pasul precedent | - se va rula comanda **//npm install//** pentru a instala dependețele din fișierul de la pasul precedent | ||
- | - se va copia sursa **//server.js//** în directorul de lucru **///usr/src/app///** | + | - se va copia sursa **//server.js//** în directorul de lucru **// /usr/src/app/ //** |
- se va expune portul 8080 | - se va expune portul 8080 | ||
- | - în final, se va menționa comanda de rulare a aplicației; astfel, se va rula fișierul **//"/usr/src/app/server.js"//** cu binarul **//node//**. | + | - în final, se va menționa comanda de rulare a aplicației; astfel, se va rula fișierul **///usr/src/app/server.js//** cu binarul **//node//**. |
- Folosiți Dockerfile-ul scris anterior pentru a crea o imagine numita **//nodejstest//**. | - Folosiți Dockerfile-ul scris anterior pentru a crea o imagine numita **//nodejstest//**. | ||
- Porniți un container care să ruleze imaginea **//nodejstest//** pe portul 12345 în modul detașat (daemon). Verificați că funcționează corect intrând pe [[http://127.0.0.1:12345]]. | - Porniți un container care să ruleze imaginea **//nodejstest//** pe portul 12345 în modul detașat (daemon). Verificați că funcționează corect intrând pe [[http://127.0.0.1:12345]]. | ||
+ | |||
+ | === Lucrul cu rețele, volume și bind mounts === | ||
+ | |||
+ | <note tip>Pentru exercițiile următoare, veți porni de la {{:cc:laboratoare:homework2.zip|această arhivă}}, care conține o aplicație NodeJS care realizează un API de adăugare de cărți într-o bibliotecă peste o bază de date PostgreSQL. Exercițiile de mai jos vă trec prin pașii necesari pentru a rula un container pentru o bază de date PostgreSQL și containerul cu aplicația în aceeași rețea, având persistență la oprirea containerelor.</note> | ||
+ | |||
+ | - Pe baza surselor și a fișierului Dockerfile din arhiva de mai sus, construiți o imagine cu numele (tag-ul) **//api-laborator-1-image//**. | ||
+ | - Creați o rețea bridge numită **//laborator1-db-network//**. | ||
+ | - Creați un volum numit **//laborator1-db-persistent-volume//**. | ||
+ | - Porniți în background un container pentru o bază de date cu următoarele caracteristici: | ||
+ | - se va atașa un bind mount care va face o mapare între fișierul **//init-db.sql//** de pe mașina locală (acesta va fi sursa la flag-ul de bind mount și se găsește în arhiva de laborator) și fișierul **///docker-entrypoint-initdb.d/init-db.sql//** din containerul care se va rula (acesta va fi destinația) | ||
+ | - se va atașa volumul **//laborator1-db-persistent-volume//** creat anterior (sursa) la calea **///var/lib/postgresql/data//** din containerul care se va rula (destinația) | ||
+ | - se va rula containerul în rețeaua **//laborator1-db-network//** creată anterior | ||
+ | - se vor specifica următoarele variabile de mediu (într-o comandă de **//docker run//**, acest se lucru se face astfel: **//docker run -e NUME=valoare//**): | ||
+ | - variabila **//POSTGRES_USER//** cu valoare **//admin//** | ||
+ | - variabila **//POSTGRES_PASSWORD//** cu valoarea **//admin//** | ||
+ | - variabila **//POSTGRES_DB//** cu valoarea **//books//** | ||
+ | - containerul rulat se va numi **//laborator1-db//** | ||
+ | - se va rula imaginea **//postgres//** din registrul oficial. | ||
+ | - Porniți în background un container cu imaginea **//api-laborator-1-image//** creată anterior, cu următoarele caracteristici: | ||
+ | - se va rula containerul în rețeaua **//laborator1-db-network//** creată anterior | ||
+ | - se vor specifica următoarele variabile de mediu: | ||
+ | - variabila **//PGUSER//** cu valoare **//admin//** | ||
+ | - variabila **//PGPASSWORD//** cu valoarea **//admin//** | ||
+ | - variabila **//PGDATABASE//** cu valoarea **//books//** | ||
+ | - variabila **//PGHOST//** cu valoarea **//laborator1-db//** | ||
+ | - variabila **//PGPORT//** cu valoarea **//5432//** | ||
+ | - containerul rulat se va numi **//laborator1-api//** | ||
+ | - containerul va expune portul 80 și îl va mapa la portul 5555 de pe mașina locală. | ||
+ | - Verificați că cele două containere rulează corect și au conectivitate: | ||
+ | - folosind [[https://www.postman.com|Postman]] sau orice altă aplicație similară, realizați cereri de GET și POST pe [[http://localhost:5555/api/books]] (pentru un tutorial de Postman, puteți intra [[https://learning.postman.com/docs/getting-started/sending-the-first-request/|aici]]) | ||
+ | - la cererile de POST, se așteaptă un body JSON cu formatul ''%%{"title":"titlu","author":"autor"}%%'' | ||
+ | - cererile de GET vor returna o listă de cărți adăugate prin cereri de POST. | ||
+ | - Verificați că volumul pe care l-ați adăugat păstrează persistența datelor: | ||
+ | - opriți și ștergeți cele două containere | ||
+ | - reporniți cele două containere cu aceleași comenzi ca anterior | ||
+ | - trimiteți o cerere de GET | ||
+ | - dacă ați configurat corect, veți primi o listă cu cărțile adăugate anterior. |