Moje pierwsze spotkanie z kontenerami w linuksowym wydaniu było mocno "obarczone" wcześniejszymi doświadczeniami z wirtualizacją. Kontenery to był kolejny, po parawirtualizacji, krok w kierunku uczynienia wirtualizacji jeszcze "lżejszą" i mniej zasobożerną! Tak do tego podchodziłem. Gdy koledzy eksperymentowali z duetem vagrant + virtualbox ja skłaniałem się bardziej do eksperymentowania z Lxc, przy tym niespecjalnie interesowało mnie to, jak dokładnie Lxc działa - było to po prostu "środowisko wirtualne". Chwila "oświecenia" nadeszła podczas linuksowej jesieni w 2016 roku. Uczestniczyłem tam w warsztatach dotyczących Linux Namespaces - to było bardzo interesujące! Po konferencji temat jednak odłożyłem. Gdzieś w 2017 przeglądałem nagrania z DockerConf 2015 a wśród nich nagranie o tytule: Cgroups, namespaces, and beyond: what are containers made from? - to przypomniało mi o Linuksowej Jesieni i o jednym z moich po-jesiennych postanowień: przyjrzeć się bliżej Namespacom!
Linux Namespaces (NS) to produkt XXI wieku. Pierwszy namespace pojawił się około 2001 czy 2003 roku (wikipedia pewnie wie dokładnie :)), wszystkie aktualnie zdefiniowane NS są obecne w Linuksie od wersji 3.8 (czyli już od jakiegoś czasu). Nie będę tutaj kopiował informacji z Wikipedii (link powyżej - polecam tę stronę uwadze każdego), dla mnie NS to głównie mechanizm, dzięki któremu realizowana jest idea kontenerów w systemach GNU/Linux.
Jak w tym kontekście wyglądają narzędzia typu Lxc czy Docker? Jeżeli przyjrzymy się ich działaniu bliżej, okaże się, że można, w pewnym stopniu, uważać je za wysokopoziomowe narzędzia, które upraszczają zarządzanie NS poprzez definiowanie struktur, nazywanych kontenerami. To oczywiście dość uproszczony obraz, ale coś w tym jest :) Po omówieniu NS pokażę jak "podłączyć" się do kontenera Lxc, bądź Docker-owego, bez użycia Lxc/Docker klienta, a przy wykorzystaniu jedynie narzędzi z userspace służących do zarządzania NS. Do tego (uproszczonego) spojrzenia na Lxc i Dockera (jako wysokopoziomowych narzędzi do zarządzania NS) wrócę jeszcze pod koniec tego artykułu, w tym momencie tylko krótko zaznaczę kilka rzeczy:
- Jestem entuzjastą Dockera! Prezentowane spojrzenie na kontenery od strony NS nie jest podyktowane chęcią pokazania, iż "można inaczej" - absolutnie nie! To spojrzenie "pod podszewkę" to okazja do rozmowy o niskopoziomowych mechanizmach, nikt (na pewno nie ja) nie chce rezygnować z wysokopoziomowych narzędzi! (Niskopoziomowe mechanizmy oznaczają duuuużo problemów!)
- NS to mechanizmy zaimplementowane w Linuksie, a "kontenery" to idea realizowana (w Linuksie) przy wykorzystaniu NS - to sprawia, że wersje Dockera znane z systemów GNU/Linux, w systemach Windows czy MacOS muszą działać w maszynach wirtualnych (Virtualbox, HyberV, czy HyperKit) - muszą, ponieważ Docker i Lxc wymagają Linuksa, by móc funkcjonować (samo "skompilowanie" Dockera pod inny system nic tu nie da!)
- W związku z punktem 2 nasze kontenery nie są (teoretycznie) uzależnione od konkretnego dostawcy wysokopoziomowych rozwiązań (Docker, Lxc) - z czasem pewnie będzie przybywać implementacji i ciekawych projektów związanych z zarządzaniem kontenerami rozumianymi jako grupy namespaców – patrz np. rkt - dlatego ta technologia wydaje się rokować tak dobrze :) (o pewnym, ciekawym z mojego punktu widzenia, futurystycznym scenariuszu napiszę kilka słów w podsumowaniu tego artykułu).
Ok, pora na konkrety!
Aktualna lista NS zawiera siedem pozycji. W dalszej części artykułu omówię 4 z nich dokładniej, a o trzech (cgroups, userId, ipc) tylko wspomnę. Strona podręcznika systemowego (man) podaje zmienną konfiguracyjną dla każdego z NS, odpowiedzialną za włączenie danego NS w kodzie wynikowym Linuksa. Oznacza to, że NS (wszystkie, bądź część z nich) nie będą dostępne w Linuksach > 3.8, jeżeli nie zostały świadomie dodane w procesie kompilacji, choć w domyślnych, dystrybucyjnych kompilacjach są przeważnie dodawane (przynajmniej w takich dystrybucjach jak Debian czy Centos, aczkolwiek warto wspomnieć, że w przypadku Centosa mamy dostępnych, w domyślnym dystrybucyjnym kernelu - 6, nie 7, NS). Łatwo sprawdzić, które NS zostały dołączone (włączone w procesie kompilacji) - pokażę jak, teraz chcę tylko jeszcze dodać, że po wkompilowaniu danego NS, nie musimy go już w żaden sposób "aktywować", "włączać", czy w jakikolwiek inny sposób "uruchamiać" - każdy wkompilowany namespace jest aktywny od samego początku pracy Linuksa.
Co to konkretnie oznacza? NS są związane z procesami, nie może istnieć NS, w którym nie "działa" (bądź też do którego nie jest przypisany) żaden z procesów. Wyobraź sobie drzewo procesów (podobne do tego, które możesz zobaczyć po wydaniu polecenia "ps axf"), proces z ID = 1 (init/systemd) jest uruchamiany jako pierwszy, i - co w naszym kontekście jest najważniejsze - jest uruchomiany z przypisanym identyfikatorem każdego z wkompilowanych NS. Te identyfikatory możemy sprawdzić na kilka sposobów, zacznijmy od najbardziej "podstawowego" - od sprawdzenia tych informacji w katalogu "/proc". Bez zagłębiania się w szczegóły czym jest katalog "/proc" (informacje można oczywiście znaleźć w Internecie), w tej konkretnej sytuacji ważne jest, że każdy działający w systemie proces posiada podkatalog w katalogu proc i że wśród różnych informacji zawartych w katalogu procesu znajdziemy również podkatalog "ns", który oczywiście zawiera informacje o identyfikatorach NS przypisanych do danego procesu. Czyli katalog "/proc/1/ns" zawiera informacje o identyfikatorach NS, z którymi uruchomiony został proces o PID = 1 (a ponieważ proces z PID = 1 to pierwszy proces uruchomiony w systemie, to NS przypisane do niego możemy traktować jako "domyślne"):
$ sudo ls -l /proc/1/ns total 0 lrwxrwxrwx 1 root root 0 Aug 4 18:18 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 Aug 4 18:18 ipc -> ipc:[4026531839] lrwxrwxrwx 1 root root 0 Aug 4 18:18 mnt -> mnt:[4026531840] lrwxrwxrwx 1 root root 0 Aug 4 18:18 net -> net:[4026531957] lrwxrwxrwx 1 root root 0 Aug 4 18:18 pid -> pid:[4026531836] lrwxrwxrwx 1 root root 0 Aug 4 18:18 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 Aug 4 18:18 uts -> uts:[4026531838]
Każdy proces jest powiązany z dokładnie jednym NS danego typu (powyżej widać, że w tym systemie zostały "wkompilowane" wszystkie 7 NS). Nie ma możliwości, aby dany proces był powiązany z dwoma NS tego samego typu. NS (ich identyfikatory) są dziedziczone po procesie rodzica, chyba że w trakcie uruchamiania jakiegoś procesu jawnie wskażemy, że chcemy, na potrzeby tego konkretnego procesu, stworzyć / wykorzystać nowy (bądź nowe) NS.
To, z pewnością, na przykładach (poniżej) będzie prezentować się dużo prościej.
I jeszcze jedna uwaga: nazwa Namespace odnosi się zarówno do konkretnego NS (konkretnego identyfikatora, których może być niezliczona ilość, bo może być wiele zdefiniowanych NS - wiele identyfikatorów - w ramach każdego z 7 zdefiniowanych typów), jak i do samego "typu" (których aktualnie jest 7) - to nie upraszcza sprawy, trzeba zwracać uwagę na kontekst.
Jak sprawdzić jakie NS mamy do dyspozycji? Oczywiście sprawdzenie katalogu "ns" dla dowolnego procesu jest rozwiązaniem, ale mamy również do dyspozycji dość pomocne polecenie:
$ lsns
Lsns wyświetli listę NS, z którymi pracuje aktualny proces (czyli powłoka, z której wywołujemy polecenie) - a ponieważ, tak jak pisałem, każdy proces ma przypisany identyfikator każdego z dostępnych NS, to lista ta pokaże dokładnie, jakie NS mamy do dyspozycji (czyli maksymalnie 7 różnych NS, gdy wszystkie zostały wkompilowane):
$ lsns NS TYPE NPROCS PID USER COMMAND 4026531835 cgroup 4 421 lukasz /lib/systemd/systemd --user 4026531836 pid 4 421 lukasz /lib/systemd/systemd --user 4026531837 user 4 421 lukasz /lib/systemd/systemd --user 4026531838 uts 4 421 lukasz /lib/systemd/systemd --user 4026531839 ipc 4 421 lukasz /lib/systemd/systemd --user 4026531840 mnt 4 421 lukasz /lib/systemd/systemd --user 4026531957 net 4 421 lukasz /lib/systemd/systemd --user
W kolumnie "COMMAND" widać polecenie, dla którego dany NS został stworzony (czyli po którym, niekoniecznie bezpośrednio, go dziedziczymy). Trzeba zaznaczyć, że mają tutaj znaczenie nasze uprawnienia, np. w powyższym przykładzie uprawnienia użytkownika mają widoczny wpływ na wynik polecenia lsns – proces z pid 4 dziedziczy wszystkie NS po procesie z pid 1 – nasz użytkownik jednak nie ma już takiej informacji. Jeżeli jednak mamy możliwość wykonywania poleceń z uprawnieniami administratora, wtedy możemy to szybko sprawdzić, zacznijmy od sprawdzenia identyfikatora PID procesu powłoki, w której aktualnie pracujemy:
$ echo $$ 1234
W powłokach bash/zsh polecenie "echo $$" zwróci nam właśnie pid procesu "naszej" powłoki. Teraz możemy wykonać polecenie:
$ sudo lsns –p 1234 NS TYPE NPROCS PID USER COMMAND 4026531835 cgroup 233 1 root /sbin/init 4026531836 pid 230 1 root /sbin/init 4026531837 user 233 1 root /sbin/init 4026531838 uts 230 1 root /sbin/init 4026531839 ipc 230 1 root /sbin/init 4026531840 mnt 222 1 root /sbin/init 4026531957 net 229 1 root /sbin/init
Wykorzystaliśmy uprawnienia administratora i otrzymaliśmy dokładniejsze informacje o procesie, po którym dziedziczymy NS.
Samo "sudo lsns" pozwoli nam na wyświetlenie wszystkich aktualnie aktywnych NS, różnica w stosunku do poprzedniego wywołania (bez uprawnień administratora) polega na tym, że lista będzie zawierać również NS zdefiniowane w kontekście innych procesów (o ile takie są), np.:
$ sudo lsns NS TYPE NPROCS PID USER COMMAND 4026531835 cgroup 72 1 root /sbin/init 4026531836 pid 72 1 root /sbin/init 4026531837 user 72 1 root /sbin/init 4026531838 uts 72 1 root /sbin/init 4026531839 ipc 72 1 root /sbin/init 4026531840 mnt 69 1 root /sbin/init 4026531857 mnt 1 13 root kdevtmpfs 4026531957 net 72 1 root /sbin/init 4026532108 mnt 1 203 root /lib/systemd/systemd-udevd 4026532156 mnt 1 317 systemd-timesync /lib/systemd/systemd-timesyncd
Powyżej widzimy, że w systemie Debian GNU/Linux proces kdevtmpfs (pid 13), oraz procesy uruchomione z pid 203 i 317 - definiują swój "własny" NS "mnt" czyli, sprawdzając np. proces 317, widzimy, że proces ten, poza NS "mnt" (zdefiniowanym specjalnie dla niego), wykorzystuje "domyślne" NS (zdefiniowane dla procesu z pid 1):
$ sudo lsns -p 317 NS TYPE NPROCS PID USER COMMAND 4026531835 cgroup 72 1 root /sbin/init 4026531836 pid 72 1 root /sbin/init 4026531837 user 72 1 root /sbin/init 4026531838 uts 72 1 root /sbin/init 4026531839 ipc 72 1 root /sbin/init 4026531957 net 72 1 root /sbin/init 4026532156 mnt 1 317 systemd-timesync /lib/systemd/systemd-timesyncd
No to przyjrzyjmy się każdemu z NS trochę dokładniej :)
Namespace Mount (mnt)
Do krótkiej prezentacji działania tego NS wykorzystam polecenie "chroot" - "chroot", sam w sobie, nie ma nic wspólnego z namespacami (a przynajmniej ja nic o tym nie wiem), ale krótki przykład użycia bez oraz ze wsparciem NS mam nadzieję, dobrze zobrazuje możliwości.
Wykonanie poniższego polecenia sprawi, że zostanie uruchomiony proces powłoki z katalogiem "/katalog" ustawionym jako główny system plików (w kontekście tylko tego "nowego" procesu powłoki):
$ chroot /katalog
Powyższe zadziała, ale aby miało sens (dla naszego przykładu) "/katalog" musi zawierać jakiś podstawowy system plików z poleceniami takimi jak "ps" (i inne), z których będziemy korzystać. Jak przygotować taki system plików w katalogu? W Debianie można skorzystać z debootstrap-a, w innych dystrybucjach musicie sobie jakoś poradzić we własnym zakresie, bądź w ogóle pominąć wykorzystanie "chroot" i po prostu trochę poimprowizować.
Kiedy system plików już mamy przygotowany i wykonaliśmy polecenie chroot, to możemy kontynuować:
(chroot)$ ps ax
Okazuje się, że powyższe nie zadziała, musimy najpierw zamontować "/proc":
(chroot)$ mount -t proc proc /proc
Teraz "ps" już zadziała, jednak gdy zakończymy zabawę z chrootem:
(chroot)$ exit
To okaże się, że musimy jeszcze posprzątać, bo "proc" w "/katalog/proc" ciągle jest zamontowany (można to sprawdzić, wywołując polecenie 'mount' bez żadnych parametrów, lub po prostu sprawdzając zawartość /katalog/proc):
$ sudo umount /katalog/proc
A teraz spróbujmy uprościć sobie życie, wykorzystując NS "mnt":
$ sudo unshare -m chroot /katalog
Unshare to polecenie, które pozwala nam tworzyć nowe NS (będziemy potrzebować uprawnień administratora, stąd użycie "sudo"). Polecenie "unshare", jako argument przyjmuje komendę, którą należy uruchomić w nowo utworzonym(-ych) NS (podobnie jak "chroot" - choć chodzi o inny kontekst). Przypomnę: każdy proces ma przypisany identyfikator każdego ze zdefiniowanych w systemie namespaców - tutaj wszystkie poza NS "mnt" zostaną odziedziczone po procesie rodzica, NS "mnt" zostanie jednak zdefiniowany specjalnie dla tego procesu i aktualnie będzie wykorzystywany tylko przez ten proces.
Po standardowym:
(unshare)$ mount -t proc proc /proc
Możemy sprawdzić kilka rzeczy:
- lsns pokaże nam teraz nowy namespace mnt przypisany do procesu naszej powłoki (można to sprawdzić po PID - "echo $$" wyświetli identyfikator procesu, "pid", powłoki)
- "/proc" z chroot-a nie będzie widoczny w systemie hosta ("mount" czy "ls /katalog/proc" tym razem nie pokażą nam nic - nasz chrootowany "/proc" jest podmontowany w innym namespace i izolowany od "domyślnego" "mnt" NS
- Zakończenie pracy z procesem powłoki w chroot spowoduje usunięcie nowo utworzonego NS (o ile żaden inny proces go nie wykorzystuje) i automatyczne odmontowanie systemu "/proc" zamontowanego "wewnątrz" chroota
Zanim jednak zakończymy eksperymentowanie z chroot i NS (punkt trzeci powyżej) chciałbym przedstawić trzecie, i ostatnie, polecenie pomocne przy pracy z NS - nsenter - to polecenie pozwoli nam "podłączyć" się do dowolnego NS (lub grupy NS). Przypuśćmy, że mamy sytuację jak powyżej, po wykonaniu poleceń:
$ sudo unshare -m chroot /katalog (unshare)$ mount -t proc proc /proc (unshare)$ echo $$ 1234
Teraz, w innym terminalu wykonajmy polecenie:
$ sudo lsns … 4026532373 mnt 1 1234 root /bin/bash -i …
Załóżmy, że chcielibyśmy "podłączyć" do tego NS mnt kolejny proces powłoki - do tego możemy właśnie wykorzystać polecenie nsenter.
$ sudo nsenter –m –t 1234
I w sumie zrobione :) Proces powłoki, który zostanie uruchomiony przez to polecenie, będzie korzystał z tego samego mnt NS co proces 1234 - możemy to sprawdzić, np. przeglądając punkty montowań:
(nsetner)$ mount … proc on /.../katalog/proc type proc (rw,relatime)
W tym konkretnym przypadku (po wykonaniu polecenia "nsenter" jak powyżej) jesteśmy podłączenie do NS, ale nie pracujemy w środowisku chroot – ma to oczywiście swoje konsekwencje – polecam poeksperymentować z powyższymi środowiskami - tym utworzonym po wydaniu polecenia "unshare + chroot" oraz "nsenter"!
Namespace UTS
UTS rozwija się do Unix Timesharing System, a sam NS odpowiada za "izolowanie"... hostname (plus, już ponoć nieistotny, NIS). Warto zauważyć, że zarówno strona podręcznika man, jak i strona w wikipedii poświęcona NS, nie wspominają rozwinięcia nazwy UTS - więc pełna nazwa, ze względu na swoją "małą" dokładność, chyba raczej nie jest używana.
Stwórzmy nowy NS UTS:
$ sudo unshare -u
I dla większej przejrzystości opisu sprawdźmy PID, z którym nasz nowy namespace jest "związany":
(unshare)$ echo $$ 1234
Namespace dziedziczy "hostname" po rodzicu, czyli:
(unshare)$ hostname
powinno dać nam ten sam efekt co w nadrzędnym NS UTS (czyli w naszym systemie).
Teraz zmieńmy hostname:
(unshare)$ hostname foo
Polecenie "hostname" (po zmianie nazwy) w tym konkretnym NS będzie pokazywało już nową nazwę, jeżeli w osobnej konsoli sprawdzimy "hostname" dla naszego systemu (domyślnego NS UTS) to zobaczymy, że nie uległ on zmianie. Zwróćmy jeszcze uwagę na "znak zachęty" dla procesu powłoki z PID = 1234 (o ile widoczny jest w nim "hostname") - tutaj pozostanie on niezmieniony, jednak nie ma to związku z NS. Jeżeli w naszej powłoce z PID = 1234 wystartujemy kolejny proces powłoki:
(unshare)$ bash
To znak zachęty będzie zawierał już "poprawny" hostname dla tego NS, czyli: "foo".
Również, jeżeli wykorzystamy polecenie "nsenter" to podłączenia innej powłoki do tego namespace:
$ sudo nsenter -u -t 1234
to znak zachęty będzie zawierał już zmieniony "hostname" - ot, bash (tutaj używany) nie aktualizuje "hostname" dynamicznie.
Namespace Process ID (pid)
Zarówno ten, jaki i następny NS (User ID) mogą być "zagnieżdżane" i w przypadku tych dwóch NS (PID i User ID) w tle przeprowadzany jest proces mapowania identyfikatora z nadrzędnego NS (rodzica) na identyfikator w NS pochodnym. Zobaczmy prosty przykład:
$ sudo unshare -p -f (unshare)$ echo $$ 1
Dwie rzeczy warte zaznaczenia:
- Zwróć uwagę na dodatkowy parametr "-f" polecenia unshare - jest on stosowany tylko i wyłącznie w połączeniu z parametrem "-p" i jest wymagany dla poprawnego działania procesu w nowo utworzonym namespace (spróbuj go pominąć i zobacz, co będzie się działo przy próbie uruchomienia jakiegokolwiek polecenia w tak utworzonym namespace) - dokładne wytłumaczenie, dlaczego ten parametr jest wymagany, możesz znaleźć np. tutaj
- Polecenie "echo $$" zwraca nam PID procesu powłoki, w którym zostało wykonane - w tym przypadku PID = 1!
Wydaje się, że wszystko działa świetnie, jednak gdy wykonamy polecenie "ps" to okaże się, że widzimy o wiele więcej procesów, niż moglibyśmy się spodziewać! To jednak jest efektem tego, jak działa polecenie "ps" - nie pobiera ono informacji o procesach z aktualnego NS, ale z katalogu "/proc" - stąd, z punktu widzenia "ps", nic się nie zmienia, jeżeli w naszym podrzędnym NS korzystamy z tego samego "/proc" co w NS nadrzędnym (jak w tym przypadku).
Jak to poprawić? Wykorzystajmy dodatkowy namespace "mnt" oraz "przećwiczone" już polecenie "chroot":
$ sudo unshare -p -f -m chroot /katalog (unshare)# mount -t proc proc /proc (unshare)# echo $$ 1 (unshare)# ps ax PID TTY STAT TIME COMMAND 1 ? S 0:00 /bin/bash -i 4 ? R+ 0:00 ps ax
Teraz jest, jak powinno! :)
Spójrzmy jeszcze na przykład kolejnego zagnieżdżenia:
(unshare pid + chroot)# unshare -p -f (1) mesg: ttyname failed: No such file or directory (unshare pid + chroot)# echo $$ (2) 1 (unshare pid + chroot)# ps axf (3) PID TTY STAT TIME COMMAND 1 ? S 0:00 /bin/bash -i 5 ? S 0:00 unshare -p -f 6 ? S 0:00 \_ -bash 11 ? R+ 0:00 \_ ps axf
Czyli w naszym „unsherowanym” i „chrootowanym” środowisku tworzymy kolejne zagnieżdżenie PID NS – pojawia się komunikat o błędzie (1), który możemy zignorować. Nowy proces powłoki, utworzony w nowym PID NS ma PID = 1 (co pokazuje polecenie echo), jednak polecenie "ps" (co już przećwiczyliśmy) identyfikuje ten proces po identyfikatorze nadanym mu w katalogu "/proc" - tutaj jako proces z PID = 6.
I na koniec spróbujmy się "podpiąć" pod PID NS wykreowany pierwszym poleceniem "unshare" - najpierw musimy określić PID procesu powłoki w tym NS:
$ sudo lsns … 4026532455 mnt 4 27902 root unshare -m -f -p chroot FS_AA 4026532456 pid 2 27903 root /bin/bash -i ...
Tak więc interesujący nas PID to 27903 – teraz spróbujmy:
$ sudo nsenter -p -t 27903 (nsenter)# echo $$ 14
Oczywiście, polecenie nsenter uruchamia NOWY proces powłoki, dlatego jego PID nie jest już równy "1", ale wciąż jest to PID, który ma sens tylko w PID NS procesu 27903.
Namespace User ID (user)
Zachęcam tutaj do własnych eksperymentów, działanie tego NS jest podobne do PID NS, mamy tutaj również do czynienia z możliwym zagnieżdżaniem. Docker i Lxc wydają się nie wykorzystywać tego NS (przynajmniej w środowiskach, z którymi ja mam/miałem okazję pracować).
Namespace Interprocess Communication (ipc)
NS pozwalający nam wyizolować systemowe (linuksowe) struktury do komunikacji międzyprocesowej (semafory) - jak do tej pory niestety nie miałem okazji korzystać, więc nie mam żadnego doświadczenia w tym zakresie - pozostawiam temat otwartym, oraz do samodzielnego zgłębiania :)
Namespace Network (net)
Network NS to jakość sama w sobie :) To chyba jedyny NS (no może poza "mnt"), którego samodzielne wykorzystanie ma sens nie tylko "poglądowy", ale również zupełnie praktyczny. Świetnym przykładem tego jest projekt Mininet!
Aby zaprezentować część możliwości, jakie daje nam ten namespace, musimy najpierw zaznajomić się ze sterownikiem veth:
$ ip link add ve0 type veth peer name ve1
Powyższe polecenie pozwoli nam stworzyć dwa nowe interfejsy sieciowe w naszym systemie GNU/Linux - ve0 i ve1. Te interfejsy są od razu połączone bezpośrednio ze sobą, co można sobie wyobrazić jako połączenie kablem sieciowym dwóch kart sieciowych w tej samej maszynie (co z kolei może wyglądać dziwnie, ale w tym przypadku ma / będzie miało sens).
Domyślnie nasze nowe interfejsy nie są skonfigurowane ani włączone, więc aby móc zacząć ich używać należy je podnieść (ja dodatkowo przypiszę adres ip do jednego z nich):
$ ip address add 10.10.10.10/24 dev ve0 $ ip link set ve0 up $ sudo ip l s ve1 up
Teraz można przetestować czy to działa. Sprawdzając aktualną tablicę routingu ("ip r") powinniśmy widzieć już trasę do sieci "10.10.10.0/24", wykonajmy więc w jednej konsoli:
(console 1)$ ping 10.10.10.100
A w drugim terminalu / konsoli:
(console 2)$ tcpdump -i ve1 -n -e arp
Polecenie "ping" będzie zgłaszać błędy, ponieważ adres 10.10.10.100 nie istnieje (nie otrzymamy więc poprawnej odpowiedzi na poziomie protokołu icmp), ale tcpdump (monitorujący ve1) będzie wskazywać na ruch w warstwie 2, a dokładniej na zapytanie ARP wysyłane z ve0: "who has 10.10.10.100?" - połączenie więc działa!
Dlaczego nie przypisać adresu 10.10.10.100 do ve1? Hmmm.... Spróbujcie, jeżeli chcecie :-D
Teraz trick - przenieśmy jeden z interfejsów do oddzielnego NS (na razie egzystują w tym samym). W jednym z terminali wykreujmy nowy net NS za pomocą polecenia unshare, a następnie sprawdźmy "pid" procesu powłoki uruchomionej w tym NS (będzie podstawiony w następnym poleceniu pod ${NETPID})
(console 1)$ unshare -n && echo $$
Możemy jeszcze sprawdzić listę interfejsów w nowo utworzonym namespace ("ip l") - powinien na niej znajdować się tylko interfejs loopback. Teraz "przenieśmy" jeden ze stworzonych wcześniej interfejsów do właśnie utworzonego namespace:
(console 2)$ sudo ip link set ve1 netns ${NETPID}
Polecenie "ip l" w każdym z terminali powinno potwierdzić zmianę - interfejs ve1 "zniknął" z domyślnego namespace (console 2) a pojawił się w nowo utworznym namespace przypisanym do procesu $NETPID (console 1). Przeniesiony interfejs został jednak zresetowany, musimy go więc podnieść ponownie (i, tym razem, przypiszmy mu adres ip):
(console 1)$ ip a a 10.10.10.100/24 dev ve1 (console 1)$ ip l s ve1 up
I końcowy test:
(console 1)$ tcpdump -i ve1 -n -e arp (console 2)$ ping 10.10.10.100
Teraz "ping" powinien działać - jest moc! :)
Namespace Control group (cgroup)
Gdzieś spotkałem się ze stwierdzeniem, że kontenery zbudowane są na dwóch filarach – Namespaces i Control Groups (cgroups) – gdzie NS odpowiadają za to, co proces “widzi” a cgroups za to, co proces może “zrobić”. To pewnie najprawdziwsza prawda, ale brzmi to trochę dziwnie, jeżeli zauważymy, że cgoups to również jeden z NS (ponadto wygląda na to, że jeżeli wykorzystujemy SELinux, to cgroups tracą na znaczeniu – patrz Centos, którego dystrybucyjny kernel w ogóle nie ma wkompliowanego NS cgoups). Jest w tym z pewnością jeszcze wiele do wyjaśnienia / sprawdzenia – ja osobiście (na razie) odpuszczam temat cgroups… może innym razem do tego powrócę.
Docker & Lxc
Tyle o samych namespacach - teraz popatrzmy, jak wygląda to w praktyce na przykładzie Dockera i Lxc. Zacznijmy od Dockera - jeżeli w naszym systemie mamy uruchomiony jakiś dockerowy kontener to sprawdźmy jego PID:
$ docker inspect CONTAINER | grep -i pid
Np.:
$ docker inspect ba26bbd2de76 | grep -i pid "Pid": 4542, "PidMode": "", "PidsLimit": 0,
Następnie możemy sprawdzić identyfikatory NS wykorzystywane przez główny proces działający w kontenerze:
$ lsns -p PID
Tutaj uwaga dla użytkowników systemów MacOS (OS X) czy Windows (czy też po prostu wszystkich innych niż GNU/Linux) - w waszym przypadku Docker będzie działać w maszynie wirtualnej, dlatego też proces wskazany przez polecenie "docker inspect" nie będzie widoczny w waszym lokalnym systemie (systemie hosta) - bo jest uruchomiony w ów maszynie wirtualnej (hyperkit, hyperv bądź VirtualBox). Nawet jeżeli dziełem przypadku w waszym systemie działa proces o danym identyfikatorze PID - to nie jest to proces mający cokolwiek wspólnego z naszym kontenerem. W systemie MacOS, jeżeli korzystamy z hyperkita, który aktualnie jest zalecanym rozwiązaniem, musimy podłączyć się do konsoli tego hypervisora, aby móc kontynuować nasz przykład. Możemy to zrobić za pomocą np. polecenia screen:
$ screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
(ścieżka do pliku/symlinka tty może się różnić w zależności od wersji aplikacji Docker for Mac, więc jeżeli linku "tty" nie ma w powyższej lokalizacji, to należy go poszukać poniżej ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux).
W przypadku VIrtualBox-a myślę, że sprawa jest dużo prostsza i każdy świetnie sobie poradzi :)
Kiedy już pracujemy w konsoli systemu, w którym uruchomiony jest proces naszego kontenera,
i możemy go odnaleźć na liście procesów:
$ ps ax | grep 4542 4542 root 0:00 python2
Wtedy polecenie "lsns -p PID" powinno dać wynik podobny to poniższego:
$ lsns -p 4542 NS TYPE NPROCS PID USER COMMAND 4026531835 cgroup 219 1 root /sbin/init text 4026531837 user 220 1 root /sbin/init text 4026533249 mnt 2 4542 root python2 4026533250 uts 2 4542 root python2 4026533251 ipc 2 4542 root python2 4026533252 pid 2 4542 root python2 4026533254 net 2 4542 root python2
Ciekawą sprawą jest, że Docker nie wykorzystuje NS "cgroup" oraz "user Id" - na powyższym listingu widać, że te NS są "dziedziczone" po procesie init/systemd.
Następnie możemy wykonać nasz główny "trick" - "podłączyć" się do konsoli takiego kontenera za pomocą polecenia nsenter:
$ nsenter -m -u -p -n -i -t 4542 bash
Gdzie 4542 to oczywiście odpowiedni PID wskazany przez polecenie "docker inspect"! Oczywiście powłoka bash musi być dostępna w naszym kontenerze. Rezultat, generalnie, powinien być identyczny z tym jaki otrzymujemy po wykonaniu polecenia:
$ docker exec CONTAINER bash
Prawda, że fajne :)
W przypadku Lxc informacje o danym kontenerze (i PID) możemy uzyskać za pomocą polecenia:
$ sudo lxc-info -n CONTAINER
Po sprawdzeniu NS ("lsns -p PID") okaże się, że LXC wykorzystuje cgroups!
$ sudo lxc-info -n deb01 | grep -i pid PID: 9867 ... $ sudo lsns -p 9867 NS TYPE NPROCS PID USER COMMAND 4026531837 user 79 1 root /sbin/init 4026532169 mnt 9 9867 root /sbin/init 4026532170 uts 9 9867 root /sbin/init 4026532171 ipc 9 9867 root /sbin/init 4026532172 pid 9 9867 root /sbin/init 4026532174 net 9 9867 root /sbin/init 4026532230 cgroup 9 9867 root /sbin/init
Ciekawe jest również to, że proces 9867 (czyli główny proces kontenera LXC) to również "init" - wynika to stąd, że Lxc udostępnia nam tzw. kontenery "systemowe" (zawierające wszystkie procesy standardowo uruchamiane w każdym systemie - stąd Lxc jest dużo bardziej podobne do wirtualizacji). Docker udostępnia natomiast tzw. kontenery "aplikacyjne" - więcej o Dockerze można przeczytać w jedny z poprzednich artykułów).
"Podłączenie" się do kontenera Lxc wygląda identycznie jak w przypadku kontenera Dockerowego (zwróć uwagę na dodatkowy parametr "-C" oznaczający NS cgroups):
$ sudo nsenter -m -u -C -i -p -n -t 9867
To działa i zachęcam do własnych eksperymentów!
Podsumowanie
Pisałem powyżej, że na narzędzia typu Docker czy Lxc można spojrzeć jak na wysokopoziomowe narzędzia do zarządzania NS. Polecenie "nsenter" pozwala nam np. "niskopoziomowo" łączyć się z dowolnym "kontenerem". Oczywiście, jak już również wspominałem, nie zamierzam nikogo namawiać do korzystania z nsenter w codziennej pracy - nie tędy droga! Chodziło mi raczej o ogólny, uproszczony obraz interakcji pomiędzy tymi rozwiązaniami i Linuksem.
Inną rzeczą jest to, że, jak w każdym chyba innym przypadku sięgania do niższych warstw w stosie technologicznym, tak również i tutaj, zyskujemy nowe, większe możliwości! Za pomocą narzędzi takich jak Docker czy Lxc operujemy tylko na zdefiniowanych grupach NS (tworzących "kontenery") - dla narzędzi typu nsenter/unshare pojęcie "kontenera" jest natomiast nieznane - tutaj operujemy tylko i wyłącznie na samych NS. Co to może oznaczać? Wyobraźmy sobie dwa kontenery:
$ docker inspect ba26bbd2de76 | grep -i pid "Pid": 1111, $ docker inspect 425d479f0666| grep -i pid "Pid": 2222,
A teraz wyobraźmy sobie (bądź przetestujmy praktycznie), efekt następujących poleceń:
$ sudo nsenter -n -i -t 1111 (nsenter)$ nsenter -m -u -t 2222 (nsenter)$ echo $$ 3333
Czym jest proces "3333"? Nowym kontenerem, który wykorzystuje NS net i ipc z kontenera 1111 i NS mnt i utc z kontenera 2222 (i "domyślny" NS pid)? Czy w ten sposób zmiksowaliśmy dwa kontenery? Ciekawy efekt, czyż nie? :D
Powyższe jest o tyle interesujące, iż zastanawiam się, czy idea kontenerów (tak jak są one implementowane przez Lxc czy Dockera) w ogóle przetrwa dłuższy okres czasu. Być może już niedługo przestaniemy wykorzystywać abstrakcyjne struktury, jakimi są "kontenery" a zaczniemy pracować bezpośrednio z NS? To pewnie będzie wymagało zerwania (w obrębie tych "postkontenerowych" rozwiązań) z nawiązaniami do wirtualizacji, ale czy utrzymywanie takich "nawiązań" jest nam w ogóle potrzebne? Zdecyduje chyba "świadomość" użytkowników, czy będą oni zainteresowani NS jako zupełnie samodzielnym rozwiązaniem. Pożyjemy - zobaczymy! :)