Manapság a virtualizációról a legtöbb üzemeltetőnek a VMWare, HyperV, KVM/Qemu, Virtualbox, Xen és hasonlók jutnak eszébe. Ezek arra a feladatra születtek hogy egy fizikai hardveren képesek legyünk több virtuális gépet futtatni párhuzamosan, egymástól biztonságosan elszeparálva. Azért hogy a virtuális gépekben futó (vendég) operációs rendszereket a lehető legkisebb mértékben kelljen módosítani, ezek a hypervisor-ok igyekszenek a lehető legjobban emulálni egy valós fizikai hardvert, annak minden problémájával együtt.
Aztán jött az LXC és a Docker, ezek a container technológia segítségével próbálnak egy operációs rendszer példányon belül többé-kevésbé biztonságosan elszeparálni egymástól alkalmazásokat. A biztonsági szintjük jobb mint ha simán az operációs rendszeren futtatnánk az alkalmazásokat egymás mellett, de nincs olyan erős mint ha külön virtuális gépeket használnánk.
Következő lépésként jött a Kubernetes és vele együtt további container runtime-ok. A Kubernetes lehetővé tette hogy container-ek tömegeit menedzseljük automatizáltan.
Mire ide eljutottunk, a technológiai stack elég bonyolulttá vált:
- van a fizikai hardver
- ezen fut egy host operációs rendszer és egy hypervisor ami a virtualizációt biztosítja
- a következő réteg egy virtuális gép
- amiben fut egy újabb operációs rendszer
- majd ezen az operációs rendszeren egy container runtime
- a container-ben egy újabb, container-izált operációs rendszer (kernel nélkül)
- végül a container-izált operációs rendszerben maga az alkalmazás
Ha jól számolom ez hét réteg, félelmetes komplexitás néha triviálisan egyszerű alkalmazások futtatására. Némelyik réteg feleslegesen bonyolult, bőséges felületet adva a potenciális támadásoknak.
Jött az első megoldási kísérlet, a Kubernetes on bare metal: hagyjuk ki az első virtualizációs réteget, így marad:
- a fizikai hardver
- a host operációs rendszer
- azon egy container runtime
- a container-ben egy újabb, container-izált operációs rendszer (kernel nélkül)
- végül a container-izált operációs rendszerben maga az alkalmazás
Ez már csak öt réteg, valamivel jobb a helyzet, de feláldoztunk kicsit a biztonságból: ezen az architektúrán nem tudunk egymástól biztonságosan elszeparálva kiszolgálni több különböző ügyfelet.
Az AWS irányából jött a következő lépés: csináljunk egy teljesen leegyszerűsített virtualizációs technológiát, ami nem akar egy komplex valós hardvert emulálni, csak annyit abból ami egy container runtime futtatásához szükséges. Ez lett a Firecracker, illetve a rá épülő AWS Fargate szolgáltatás. Ez jelentősen leegyszerűsíti a hypervisor funkcionalitását, erőforrás igényét, a potenciális támadási felület méretét miközben továbbra is biztosítja az erős biztonsági elkülönítést a vendégek között.
A rétegezés ebben az esetben:
- van a fizikai hardver
- ezen fut egy host operációs rendszer és a Firecracker / KVM hypervisor
- a Firecracker-re épülő container runtime
- a Firecracker által biztosított microVM-ben egy újabb, container-izált operációs rendszer
- végül a container-izált operációs rendszerben maga az alkalmazás
Egy másik hasonló megoldás a kata containers irányából érkezett, ők eredetileg a Qemu-t használták virtualizációs rétegként, de ma már a Firecracker hypervisor-ral is együtt tudnak működni.
A következő probléma: a container-izált operációs rendszer fölösleges komplexitása. Kezdetben a container image-ek a normális operációs rendszer image-ekből indultak ki: Debian, Ubuntu, stb. Ezeket próbálták a lehető legkisebbre lecsupaszítani, de gyakran még így is több száz MB-os alap image-ek készültek. A dinamikus programozási nyelvek, mint pl. a PHP vagy Python esetén a runtime függőségek telepítése villámgyorsan fel tudja hízlalni az image méretét. Ezzel szemben az olyan statikus programozási nyelvek esetén mint a Go vagy a Rust nagyon kicsi a runtime függőség. A fordítás eredménye egyetlen futtatható bináris állomány, aminek általában csak a C runtime library (libc) és esetleg néhány shared library kell, mint a libssl / libtls. Static target-re fordítással ezek függőségek is bevihetőek a futtatható binárisba.
A container image mérete tovább csökkenthető az Alpine Linux segítségével ami a GNU libc helyett a musl libc-t használja és a sok különálló unix bináris helyett a busybox-ot.
Ha nincs szükség a busybox eszközökre, akkor lehet indulni from scratch, amikor az image lényegében csak a futtatandó állományt és a hozzá szükséges konfigurációkat tartalmazza. Ilyenkor persze a debuggolás nem lesz egyszerű mert a container-ben még egy shell-t sem tudunk futtatni.
Ha tovább megyünk rájövünk hogy van még egy fölösleges réteg: a vendég operációs rendszer kernele. A Linux és más többfelhasználós operációs rendszerek azzal a céllal születtek hogy egy fizikai vagy virtuális gépen egymástól viszonylag biztonságosan elszeparálva több felhasználó tudjon párhuzamosan dolgozni. Emiatt a kernel és a user space egymástól szigorúan elválasztott. Egy container esetén ahol a kernelen egyetlen alkalmazás fog futni, erre nem lenne szükség.
Erre a problémára születtek az unikernel megoldások. Van amelyik arra törekszik hogy a linuxos alkalmazások a lehető legkisebb változtatással futtathatóak legyenek, mint pl. az OSv. Mások egyszerűen csak egy library-t adnak, ami önmagában helyettesíti a kernel-t az alkalmazás számára, ilyen pl. a RustyHermit. Az unikernel alapú alkalmazások közvetlenül a hardveren vagy a virtuális gépen futtathóak, nincs szükségük operációs rendszerre.
Ha egy Hermit alapú alkalmazást futtatunk Firecracker alapon, akkor a stack-ünk így egyszerűsödik:
- van a fizikai hardver
- ezen fut egy host operációs rendszer és a Firecracker / KVM hypervisor
- a Firecracker által biztosított microVM-ben az unikernellel kombinált alkalmazás
Így már az eredeti hét helyett csak három rétegünk maradt, a teljes értékű virtualizációs réteget egy karcsúsított Firecracker-re cseréltük, három operációs rendszer környezet helyett csak egyre van szükségünk.
Az unikernel megoldások egyelőre még kísérleti stádiumban vannak. Nem igazán lehet még őket Kubernetes segítségével menedzselni végképp nem lehet magát a Kubernetes menedzsment réteget unikernel alapon futtatni.
Mindenesetre ebben az irányban is vannak próbálkozások. Az Unikraft például azzal a megoldással kísérletezik hogy az elkészült unikernel alapú alkalmazást docker kompatibilis OCI image-ekbe csomagolja, a szokásos runc runtime-ot pedig egy saját runu runtime-ra cseréli, ami valójában nem container-eket hanem virtuális gépeket hoz létre, bennük az unikernel alapú alkalmazással.
Egyelőre még kérdéses hogy valóban az unikernel jelenti-e a jövőt, sokan úgy gondolják túl nagy váltás lenne túl kevés előnyért, de az Unikraft próbálkozásai is mutatják hogy lehet benne fantázia, különösen ott ahol iszonyatos mennyiségű container-t kell futtatni és már a kicsivel jobb teljesítmény is dollár ezrekben vagy milliókban mérhető megtakarítást jelenthet.