Docker - 6. rész: Legjobb gyakorlatok és technikák: ellenőrzés, rétegezés, gyorsítótárazás

Attila | 2023. 01. 04. 18:21 | Olvasási idő: 4 perc

Címkék: #Biztonság (Security) #Cache #Container #Docker #Image #Virtualizáció (Virtualization) #Volume

Ebben a bejegyzésben megnézzük a legjobb gyakorlatokat, amelyeket érdemes alkalmazni a Docker és egyéb kapcsolódó elemei használata során. Ilyenek például a biztonsági ellenőrzés, az image rétegezés (layering), a réteg gyorsítótárazása (layer caching) és a többlépcsős építkezés folyamata. Végül egy kitekintést adok arra vonatkozóan, hogy merre induljon az, aki még a Docker-es virtualizációs világban szeretne kalandozni, de mi majd egy speciálisabb irányba fogunk haladni ezután.
docker-cache-layers

Docker Image építési legjobb gyakorlatok

Biztonsági ellenőrzés és vizsgálat

Amikor egy image-t építünk, akkor jó gyakorlat lehet, ha biztonsági szempontból is leellenőrizzük. Ehhez a docker scan parancsot tudjuk használni.

docker scan <docker-hub-os-felhasználóneved>/getting-started

A scan mindig egy frissített adatbázist használ a problémák felderítéséhez, amihez egy harmadik féltől származó szolgáltatást is igénybe vesz: Synk (https://snyk.io/docker/).

A Docker + Synk használatával úgynevezett "DevSecOps" folyamatot tudunk definiálni és automatizáltan futtatni. A DevSecOps = Development (fejlesztés) + Security (biztonság) + Operations (működtetés) szavakból áll össze. Így amikor egy alkalmazást fejlesztünk, akkor nem csak a fejlesztésre, tesztelésre és működtetés koncentrálhatunk, hanem a lépések végrehajtása során a biztonságra is kitüntetett figyelmet fordítunk.

A DevSecOps-ról bővebben itt olvashatunk:

De térjünk vissza a mi folyamatunkhoz a terminálban, amely az elején rákérdez, hogy használhatja-e ezt, válaszoljunk "y"-nal. Majd megkapjuk az eredményt:

A Docker Hub-os felhasználónevemet kitakartam a képről. De szerencsére a scannelés nem talált semmilyen biztonsági problémát a docker image-ünknél. Részletesebb módon is scannelhetünk, a teljes dokumentációt érdemes böngészni hozzá: https://docs.docker.com/engine/scan/

Image rétegezés (layering)

Érdemes tudni, hogy meg lehet nézni, hogy hogyan épül fel az image-ünk, hogyan épülnek egymásra a rétegek, mint ha egy vöröshagymánk lenne és az rétegről-rétegre "épülne fel".

docker image history <docker-hub-os-felhasználóneved>/getting-started

Az azonosítók és a dátumok nyilvánvalóan eltérőek lehetnek nálatok, de a rétegek láthatóak a "CREATED BY" oszlopban. A legelső réteg látható legalul, és amit utoljára hozzáadtunk, az látható legfelül a listában. Így rögtön láthatjuk, azt is a "SIZE" oszlopban, hogy melyik rétegnek mekkora volt a mérete és ha esetleg van egy nagyon nagy image-ünk, akkor láthatjuk, hogy melyik rétegek okozták a nagy méretnövekedést.

Réteg gyorsítótárazása (layer caching)

Most, hogy már ismerjük a rétegezést működés közben, meg kell tanulnunk egy fontos leckét azért, hogy csökkenteni tudjuk a konténer image építésének idejét.

A lecke: "Ha egy réteg megváltozik, minden alatta lévő réteget újra kell építeni."

Itt van a Dockerfile tartalma, amit a Node.js projektünkben létrehoztunk:

FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Ha most ezt összevetjük a legutóbbi képernyőképpel (history megjelenítésénél), akkor láthatjuk, hogy minden egyes parancs a Dockerfile-ban (legalább) egy új réteget fog eredményezni az image-ben.

Talán emlékszünk arra, amikor az image-ben megváltoztattunk valamit, akkor a yarn csomagkezelőnek újra telepíteni kellett a függőségeket… de biztosan létezik erre egy jobb (gyorsabb, hatékonyabb) módszer is. Hiszen elég nagy pazarlás lenne ugyanazokat a függőségeket mindig újra és újra letölteni, ha egy kicsit változtatunk az image-ünkön, ugye?

Hogy ezt orvosoljuk, meg kell változtatnunk a Dockerfile-unk tartalmát minimálisan. A Node-alapú alkalmazásokban a package.json fájl tartalmazza a függőségeit a projektnek. Szóval mi lenne, ha először erre a fájlra koncentrálnánk, majd telepítenénk a függőségeket és utána másolnánk át minden mást is…? Ekkor csak akkor kellene újra telepíteni a függőségeket, ha tényleg azok változtak meg, nem pedig valami egészen más fájl változása okozná ennek igényét.

Módosítsuk tehát a Dockerfile tartalmát eszerint:

FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]

A korábbi 3-4 sorok sorrendje és minimálisan a tartalma változott, az új verzió sorait külön kiemeltem itt (3-5 sorok).

Ezután érdemes (a git-es .gitignore-hoz hasonlóan) létrehozni a .dockerignore fájlt is, amelybe beletesszük a Node függőségeket tartalmazó node_modules mappa nevét.

Ennek hatására, amikor a COPY . . réteg érvénybe lép, akkor a node_modules mappát ki fogja hagyni a másolásból, tehát gyorsabban fel fog épülni az image-ünk.

Node-alapú projekteknél ez mindenképpen hasznos átszervezési lépés (további információk erről itt: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/), és Laravel esetében is érdemes ezt megtenni nem csak a node_modules mappával, hanem a vendor mappával is.

De visszatérve a mostani projektünkhöz, ha átszerveztük a Dockerfile-unkat a fentiek alapján és létrehoztuk a .dockerignore fájlt a node_modules tartalommal, akkor utána következhet a következő utasítás kiadása:

docker build -t <docker-hub-os-felhasználóneved>/getting-started .

(ne maradjon le a végéről a pont)

Itt láthatjuk a parancs futtatásának eredményét és azt, hogy a rétegek hogyan épültek újra az image-ünkben. Nálam legtovább a yarn install parancs futása tartott, ezek ugye a függőségeknek a telepítése, szóval időt és erőforrást spórolunk azzal, ha ezt a sok mappát a node_modules-ban nem kell utána ismét átmásolni, csak akkor, ha ténylegesen a függőségekben (package.json) történt változás. Érdekességként megnéztem, hogy az app mappám mérete 54,7 MB és ebből a node_modules mappa 50,4 MB-ot tesz ki, tehát ez a mappa jelentős részét teszi ki az egész projektnek.

Ha most megváltoztatok valamit az src/static/index.html fájlban, például az oldal címét (<title> tag-eken belül) átírom erre: "The Awesome Todo App", majd újra lefuttatom az előző image építő parancsot (docker build), akkor a következő eredményt kapom:

Azt mindenképpen láthatjuk, hogy egy szempillantás alatt lefutott: 0,3 másodperc alatt, az előző 22,6 másodperchez képest. Ez azért van, amit ki is emeltem: a 2-3-4 réteg építését a gyorsítótárból intézte el a docker, ez pedig óriási időnyereséget eredményezett a számunkra.

Többlépcsős építkezés (multi-stage builds)

Bár ebben a bemutatóban nem fogunk túlságosan belemenni, de a többlépcsős építkezés egy hihetetlenül hatékony eszköz, amely több lépcsőben segít nekünk egy image létrehozásában. Számos előnyt kínál, többek között:

  • A build-elési idő függőségeit elkülöníti a futási idejű függőségektől.
  • Csökkenti a teljes image méretét azáltal, hogy csak azt adja, amire az alkalmazásnak szüksége van a futtatáshoz.

Nézzünk erre egy React-os példát!

Amikor egy React alkalmazást építünk, szükségünk van Node-os környezetre ahhoz, hogy lefordítsuk a Javascript (tipikusan JSX), SASS stíluslapjainkat és más egyéb fájljainkat statikus HTML, CSS és JS fájlokba. Habár nem szerver oldali renderelést végzünk, szóval nincs is szükségünk Node környezetre az éles környezetbe való image felépítéshez, de miért is ne használhatnánk ezeknek a statikus erőforrásoknak a leszállításához egy statikus nginx (webszerver) konténert? Itt van egy példakód az iménti folyamat Dockerfile-ba ültetéséhez:

FROM node:18 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

Itt egy node:18 image-t használunk arra, hogy elvégezze az építést (maximalizáljuk a rétegek gyorsítótárazását), és aztán átmásoljuk a kimenetet az nginx konténerbe. Cool, nem igaz?

Megtanultuk, hogy hogyan lehet újra strukturálni az image-ek felépítését, így látványos időbeli javulást értünk el. Az image-ek scannelésével megnyugtató eredményt kaphatunk arról, hogy biztonsági szempontból megfelelőek. A többlépcsős építkezés pedig segít nekünk az image méretét lecsökkenteni és növelni a végső konténer biztonságosságát azáltal, hogy elkülöníti a build-idejű függőségeket a futási idejű függőségektől.

Mi jöhet még ezután?


Irányvonalak

Itt az eddigi sorozat során egyáltalán nem mentünk bele a részletekbe, sokkal többet érdemes még tanulni a konténerekről, de adok néhány tippet, hogy merre induljunk tovább…

1. Docker-es irányvonal: Container Orchestration

Konténerek együttes üzemeltetése nehéz feladat az éles környezetben. Valószínűleg nem szeretnél bejelentkezni egy gépbe és csak simán futtatni a docker run vagy a docker compos up parancsokat. Miért nem? Mert mi is fog történni, ha a konténer elhalálozik? Hogyan fogod skálázni a konténerek erőforrásait a gépek között? A "container orchestration" témaköre oldja meg ezeket a problémákat. Olyan eszközök, mint a Kubernetes (https://kubernetes.io/), Swarm (https://docs.docker.com/engine/swarm/), Nomad (https://www.nomadproject.io/) és az ECS (https://docs.docker.com/cloud/ecs-integration/) mind segítenek megoldani ezeket a problémákat, különböző módokon.

Az általános elképzelés az, hogy vannak "menedzserek", akik megkapják az elvárt állapotot. Ez az állapot lehet a következő például: "Két példányt akarok futtatni a webes alkalmazásomból, és a 80-as portot akarom megnyitni." A menedzserek ezután megnézik az összes gépet a fürtben (gépcsoportban), és delegálják a munkát a "dolgozó" csomópontoknak. A menedzserek figyelik a változásokat (például egy konténer kilépését), majd azon dolgoznak, hogy a tényleges állapot megfeleljen a elvárt állapotnak.

2. Docker-es irányvonal: Cloud Native Computing Foundation (CNCF) Projects

A CNCF egy gyártó-független otthona a különböző nyílt forráskódú projekteknek, beleértve a Kubernetes, Prometheus, Envoy, Linkerd, NATS és még sok más projektet! A felügyelt projekteket itt (https://www.cncf.io/projects/), a teljes CNCF Landscape-et pedig itt (https://landscape.cncf.io/) nézheted meg. Rengeteg olyan projekt van, amely segít megoldani a felügyeletet, naplózást, biztonságot, üzenetküldést és még sok minden mást is!

A mi legközelebbi irányvonalunk

És hogy én (mi) merre haladunk tovább...?

  • Laravel-es irányvonalat követve, először Docker alapokra építkezve készítünk új projektet.
  • Egy már meglévő Laravel projekt "docker-izálálását" is megnézzük.
  • Feltelepítjük az MS Azure felhőbe a docker-alapú Laravel projektet és csomagot (csomagokat), majd ott is működésre bírjuk és működtetjük, frissítjük.
  • ...
  • és persze az sem lehetetlen, hogy én fogom folytatni még az iménti Docker-es irányvonalakat is!

Kövessetek a Facebook-on, és ha tetszik a munkám, akkor támogassátok néhány euróval a blog és az oldal fennmaradását a "buymeacoffee" (kávé) ikon útmutatásait követve.