Containers met elkaar verbinden: User Defined Networks
Containers elkaar laten vinden: User Defined Networks
Als je je webserver en database elk in een eigen Docker parkeert, moet je webserver wel weten hoe hij de database kan benaderen. Deze containers werden voorheen aan elkaar geknoopt, maar die manier van werken is verouderd en komt te vervallen. De nieuwe manier van werken: User Defined Networks met een embedded DNS-server voor je containers, inclusief name-resolving.
Voor elke service een eigen container, dat is de kern van het werken met Docker. Als je dit zeer letterlijk neemt, is het bye-bye LAMP-server met Linux, Apache, MySQL en PHP en stap je over op minimaal een webserver- plus databasecontainer. Soms wordt PHP ook nog in een eigen container geparkeerd. Maar het beoogde doel, de services los van elkaar draaien, heeft ook zijn nadelen: er moeten communicatiekanalen worden ingesteld.
Bij de traditionele starre LAMP-server is de configuratie niet zo van belang. De webserver en MySQL-database draaien op dezelfde host en wisselen lokaal gegevens uit via de loopbackinterface of een socket. Als ze in Docker-containers zijn gezet gaat het om van elkaar gescheiden processen met een eigen 'werkruimte' en ip-adres die wel met elkaar in contact moeten blijven. Als voorbeeld hebben we een webserver met PHPMYAdmin als database-frontend en een losse MySQL-database. Ze zijn allebei als klant-en-klare container-image te vinden via Docker Hub.
Start de webserver-container met het commando
docker run --name webserver \ -p 80:80 -d phpmyadmin/phpmyadmin
Nu wordt eerst eenmalig de PHPMyAdminimage gedownload van Docker Hub. We geven niet aan in welk netwerk de container moet komen, dus Docker hangt hem aan de default bridge met als netwerkrange 172.17.0.0/24. Daardoor krijgt de MySQLdatabase een ip-adres uit het subnet van de standard bridge bridge. Welke dat is, merk je pas na het opstarten via docker network inspect bridge, bijvoorbeeld 172.17.0.2.
Oude link
Je hebt in de standaard bridge geen invloed op het uitgeven van ip-adressen uit de netwerkrange. Je kunt niet eens handmatig een ip-adres aan een container toewijzen. Dat wordt een probleem als je de database-container opstart:
docker run --name dbserver \ -e MYSQL_ROOT_PASSWORD=123456 \ -d mysql:8
Ook deze container belandt zonder afwijkende netwerkconfiguratie in de standard bridge en krijgt een ip-adres uit de range 172.170.0.0/24. Ook al blijven de ip-adressen langere tijd hetzelfde (alleen een herstart van de hosts kan adressen husselen), het door Docker aangewezen ip-adres van de database in PHPMyAdmin op de webserver zetten gaat niet werken. Tot nu toe kon je als oplossing de optie --link gebruiken:
docker run --name webserver \ --link dbserver:db -p 80:80 \ -d phpmyadmin/phpmyadmin
Op deze manier maakt Docker in de webservercontainer een verwijzing voor de hostnaam db naar het ip-adres van de Docker-container met de naam dsbser-
ver, zodat PHPMyAdmin via de hostnaam db contact kan leggen met de MySQLdatabase. Maar daarvoor moeten wel alle (PHP-)toepassingen van de webserver de name-resolving gebruiken en niet ergens een ingeklopt ip-adres nodig hebben. Het vastleggen van db als (interne) hostnaam voor de databasecontainer is te danken aan het PHPMyAdmin-project. Dit soort informatie staat meestal vermeld in de losse Docker-images op Docker Hub.
Maar zoals we al aangaven, is de linkoptie al een tijdje deprecated, oftewel verouderd. Het zou bij een toekomstige release zomaar weggehaald kunnen worden. De User Defined Networks zijn de vervanger.
Op maat gemaakt
Het grootste voordeel van User Defined Networks zit hem in het ontkoppelen, geheel volgens de Docker-mentaliteit. De verschillende netwerken zijn van elkaar gescheiden. De containers die met het bewuste netwerk in verbinding staan zijn de enige die verbinding hebben. Dat mogen ook meerdere netwerken zijn, bijvoorbeeld bij een container die databaseback-ups van containers van verschillende klanten verzamelt. De container heeft dan meerdere virtuele netwerkapparaten.
Het opvallendste verschil is dat je voordat je een container start, eenmalig het netwerk moet aanmaken:
docker network create webnet
Als je net als in dit voorbeeld alleen de naam van een nieuw netwerk aangeeft, maakt Docker automatisch een nieuw klasse B netwerksubnet als bridge aan, die je net als de standaard bridge (bridge) kunt gebruiken. Containers in dit netwerk hebben internettoegang en zijn zelfs van buitenaf te benaderen, voor zover portforwards staan ingesteld. En dit User Defined Network webnet heeft ook een zelflerende DNS-server voor het resolven van de namen van de containers. Om te zorgen dat de databaseserver in het webnet wordt gestart, hoef je bij het aanroepen van docker run alleen maar de parameter --network webnet toe te voegen:
docker run --network webnet \ -e MYSQL_ROOT_PASSWORD=123456 \ --name dbserver -d mysql:8 PHPMyAdmin in de webservercontainer configureer je via de omgevingsvariabele PMA_HOST. Door die te declareren met -e PMA_HOST= gevolgd door de naam van de databasecontainer, weet hij waar de database te vinden is:
docker run --network webnet --name \ webserver -e PMA_HOST=dbserver \ -p 80:80 -d phpmyadmin/phpmyadmin
Als je de aliasfunctie van de embedded DNS-server gebruikt, kun je de extra omgevingsvariabele en de extra parameter bij het opstarten van de webserver weglaten. Daarmee benut je dat de PHPMyAdmin-container zo ingesteld staat dat de database standaard via de hostnaam db wordt aangeroepen. Daardoor hoef je bij het linken via --link geen extra parameter voor de hostnaam voor de database in te voeren. Als je bij de databasecontainer een netwerkalias db instelt, let er dan op dat PHPMyAdmin de database dankzij de standaard configuratie ook op eigen houtje kan vinden in het User Defined Network:
docker run --network webnet \ -e MYSQL_ROOT_PASSWORD=123456 \ --name dbserver --network-alias db \ -d mysql:8 Dit soort standaardwaarden vind je ook bij veel andere Docker-images terug. Aliassen maken het mogelijk om containers ongewijzigd direct vanuit Docker Hub te gebruiken, zonder alles een makkelijk herkenbare naam te hoeven geven. De naam van de databasecontainer, die bij alle Dockercommando's een rol speelt, kun je zonder problemen de naam van een klant of service geven. Dankzij de alias is hij altijd als db vanuit de webserver te bereiken.
Via alias naar template
Een ander vaak onderschat voordeel is dat meerdere containers in hetzelfde netwerk dezelfde alias mogen krijgen. Zodra er twee of meer containers met dezelfde alias draaien, kiest de embedded DNSserver van het User Defined Network bij query's een random container en geeft diens adres terug. Zo is de belasting net als bij een Round-Robin DNS-server over meerdere containers te verdelen.
Het heeft echter weinig zin om de belasting te verdelen over meerdere identieke containers op dezelfde host. Een verzameling containers op meerdere nodes is daar beter voor geschikt.
Wel wordt het interessant zodra er een upgrade van de databasecontainer gepland staat. Als je de bijgewerkte container een nieuwe naam maar dezelfde alias
toewijst, kan die worden gestart en schakel je daarna meteen de oude uit. Query's worden dan automatisch door de nieuwe container beantwoord. Mocht die niet zo lekker draaien, dan slinger je de oude weer aan die dan naadloos weer verdergaat. Op die manier voorkom je downtime.
Beter nog: als je je eigen webserverimage vanaf het begin zo instelt dat hij steeds via de alias verbinding legt met de database, heb je een soort van templateimage die je zonder iets te hoeven wijzigen voor meerdere klanten of verschillende websites kunt gebruiken. Je moet alleen nog voor elke klant of elke website via docker network create en docker run een nieuw User Defined Network en nieuwe webserver- en databasecontainer met duidelijke naam aanmaken. De containers wisselen zonder problemen data uit dankzij de alias. Dit maakt het beheer makkelijker. Het maakt namelijk niet uit bij welke klantcontainer je kijkt, de database is altijd onder db te vinden.
Veel namen
De User Defined Networks kunnen ook overweg met een situatie waarbij het omgekeerde geldt: een container heeft meerdere aliassen en ze verschillen van netwerk tot netwerk. Dat is handig als je een webserver met CMS en PHPMyAdmin voor databasebeheer aanbiedt en drie containers gebruikt: een webserver met CMS, een met PHPMyAdmin en de MySQL-databasecontainer.
Verwacht het CMS de databaseserver via de naam sqlserver te kunnen benaderen, terwijl PHPMyAdmin db gebruikt, voeg je de tweede alias simpelweg toe bij het starten van de databasecontainer:
docker run --network webnet \ -e MYSQL_ROOT_PASSWORD=123456 \ --name dbserver --network-alias db \ --network-alias sqlserver -d mysql:8
Op deze manier is de databasecontainer via de namen dbserver, db en sqlserver te bereiken.
Het wordt lastiger, als een databasecontainer de gegevens van meerdere andere containers uit verschillende User Defined Networks moet verwerken, bijvoorbeeld omdat de website van je sportclub een database gebruikt en je voor je eigen persoonlijke website geen tweede databasecontainer wilt starten. In dat geval start je eerst de databasecontainer met alleen een verbinding met het netwerk van je sportclub:
docker run --network webnet \ -e MYSQL_ROOT_PASSWORD=123456 \ --name dbserver \ --network-alias sqlserver \ -d mysql:8
Om te zorgen dat je eigen webserver, die bijvoorbeeld in het netwerk privatenet draait, de databasecontainer kan benaderen via de naam db, moet je het volgende commando ingeven na het starten van de databasecontainer:
docker network connect --alias db \ privatenet dbserver
Dit heeft ook tot gevolg dat de databasecontainer van de webserver van je sportclub niet via db maar alleen via sqlserver is te bereiken. Zo kan het niet per abuis tot een botsing tussen aliassen komen omdat er twee verschillende diensten in verschillende netwerken hetzelfde alias gebruiken.
Containers zijn niet alleen door verschillende User Defined Networks van elkaar te scheiden. Standaard staat de zogenaamde Inter Container Connectivity binnen alle netwerken ingeschakeld. Dit is via het aanmaken van een nieuw netwerk makkelijk uit te schakelen:
docker network create -o \ 'com.docker.network.bridge.enable_icc—
=false' isolated Alle containers die in het netwerk isolated zitten, hebben een werkende internetverbinding en krijgen vanuit Docker ip-adressen uit hetzelfde subnet, maar onderling communiceren kunnen ze niet.
De internetverbinding wordt geregeld door de netwerkdriver bridge. Deze wordt gebruikt bij het aanmaken van een nieuw User Defined Network, tenzij je iets anders aangeeft. De extra parameter --internal beperkt de toegang van alle containers tot alleen het lokale netwerk. Ze hebben zo dus geen internetverbinding meer. De driver null verbiedt alle netwerktoegang. Deze hoef je niet aan te maken, deze bestaat al standaard onder de naam none.
Het none-netwerk is vooral handig voor containers die alleen lokaal werk verrichten en voor toepassingen die om veiligheidsredenen geen netwerktoegang mogen krijgen. Een voorbeeld is PHP bij een Nginx-webserver. PHP wordt via een Unix-socket aan Nginx geknoopt.
Voor het geval je de socket op de host aanmaakt en als volume zowel de Nginxcontainer als de PHP-container benut, kunnen de webserver en PHP zonder netwerktoegang communiceren. Mocht een aanvaller een veiligheidslek exploiten en zo de controle krijgen over PHP of de totale container, dan is er weinig aan de hand. Hij kan niet bij andere systemen en de container is ook niet te misbruiken voor DDoS-aanvallen. Het houdt ook in dat je als gebruiker een aantal functies mist, bijvoorbeeld toegang tot een databaseserver voor zover deze ook niet via een socket of het bestandssysteem wordt geregeld. (avs)