Címkék: #Biztonság (Security) #Cache #Container #Docker #Image #Virtualizáció (Virtualization) #Volume
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/
É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.
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.
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:
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?
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…
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.
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!
És hogy én (mi) merre haladunk tovább...?
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.