Networking in Docker
August 27, 2019
Konteynerizasyon, Sanallaştırma vb. alanların en önemli konusu olduğunu düşündüğüm Network konusunun Docker’da nasıl bir işleyişe sahip olduğundan bahsedeceğim.
Bu yazıda yer yer uygulamalı anlatımlar vardır. Bunları geçebilirsiniz de ancak geçmemenizi tavsiye ederim. Ayrıca bu yazı İngilizce ve Türkçe karıştırılarak yazılmıştır. Türkçe olmasının sebebi Türkçe kaynağı artırmaktır. Bu yazıdakilerden çok daha fazlasını zaten İngilizce olarak bulabilirsiniz.
Docker, kendi Container Networking standardı olan CNM’i (Container Network Model) kullanır.
Kubernetes, Mesos ve Cloud Foundry gibi platformlar CNCF tarafından kabul edilen ve endüstri standardı olarak görülen CNI‘i kullanır. CNI, CoreOS tarafından rkt ile beraber yapılmıştır. Makalenin konusu Docker olduğundan, bu ikisinin farkını ve neden birden fazla Container Networking standardı olduğunu başka bir zaman açıklayacağım.
Kısa kısa bazı temel kavramlardan bahsedelim. Bu kavramların fazla derinine inmemize bu yazı için gerek yok ama şu kitabı tavsiye ederim.
Understanding Linux Network Internals
Linux Namespaces
Linux çekirdeğindeki Namespace özelliği sayesinde pek çok şey mümkün. Namespace özelliği bize soyutlama yeteneği kazandırıyor. Namespace türleri vardır.
- Mount
- Process ID
- Network
gibi ve dahası da vardır. Bunlar bize oldukça kontrollü bir soyutlama yeteneği sağlar. Konteyner teknolojisinin temelidir.
Network Namespaces
İzole edilmiş bir network yığınıdır. Bir network namespace’in kendine ait arayüzleri ve firewall kuralları vardır. Network namespace ayrımı sayesinde iki konteyner aynı makinede olsa bile birbirleriyle iletişim kuramayabilirler. Yine de konteynerler bir network namespace alanını ortak kullanabilirler.
Linux Bridge
Fiziksel switch cihazının sanal halidir. Layer 2’de çalışır dolayısıyla MAC adresine göre yönlendirme yapar. Docker’daki bridge sürücüsü bunun daha yüksek seviyeli bir uygulamasıdır. Farklı bir Network Namespace oluşturulur ve isteğe bağlı olarak port-mapping yapılabilir.
Virtual Ethernet Devices
İki network namespace arasında bir kablo görevi gören bir networking interface olmasına karşın veth olarak anılırlar.
Bir konteyner Docker ağına bağlanıldığında, bu bağlantının bir ucu konteynerde ethX olarak bulunurken diğer ucu Docker ağında vethX olarak bulunur.
Uygulama 1.0
Deneyerek görelim. Bir konteyner başlatalım ve kabuğa bağlandığımızda ifconfig
komutunu çalıştıralım. Çıktınız şuna benzer olmalı.
$ docker run --rm -ti alpine:latest sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:4 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:364 (364.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
Konteyneri durdurmayın yani kabuktan exit komutu göndermeyin ve kendi makinenizde ifconfig
komutunu çalıştırın. Çıktınızda bir veth göreceksiniz. Örneğin bendeki veth şöyle.
veth51efbec: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::3408:5ff:fe47:69aa prefixlen 64 scopeid 0x20<link>
ether 36:08:05:47:69:aa txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 23 bytes 3422 (3.4 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Docker’da Networking mimarisi, CNM olarak anılan bir dizi arayüzler üzerine kuruludur. CNM’in felsefesi, farklı altyapılar arasında uygulama taşınabilirliği sağlamaktır.
IPTables
Linux çekirdeğinin varsayılan paket filtreleme sistemidir ve güvenlik duvarıdır. Docker network sürücüleri trafiği yönetmek(Load Balancing) ve port mapping işlemleri için iptables kullanır.
Docker Native Network Drivers
Bu sürücüler, konteynerlerin kullanabilmesini sağlayabileceğimiz sürücülerdir. Bu sürücüleri kullanarak sayısız network yaratabilir ve bunları konteynerlerin ayrı ayrı veya birlikte kullanmasını sağlayabiliriz.
Bridge
Bridge sürücüsü, host makinede Docker tarafından yönetilen bir Linux Bridge yaratır. Varsayılan olarak konteynerler dışarıyla ve birbirleriyle bridge ile haberleşir. En çok kullanılan sürücü olduğunu söylemek yanlış olmaz.
Ön yüklü gelen bir Bridge ağı var ve bunun adının bridge olmasının yanı sıra network arayüzü adı ise docker0 olarak bilinir. docker network ls
komutuyla bridge isminde ve ifconfig
komutuyla ise docker0 ismiyle görünür. brctl show
komutuyla ise yine docker0 olarak görünür. Bu arayüz ön tanımlı gelir. Silemezsiniz.
docker run ...
komutuna herhangi bir network belirtmezseniz varsayılan olarak bridge isimli ağı kullanacaktır.
Konteynerlere bir network atamazsanız veya bridge isimli ağı atarsanız konteynerler docker0 Bridge ağı üzerinden dış dünya ile ve diğer konteynerler ile iletişime geçecektir.
Uygulama 2.0
Bir Bridge yaratalım ve onu farklı yollar ile görüntüleyelim. Buradan sonrası, bir sonraki başlığa kadar kodlar ve örneklerle geçecektir.
$ docker network create --driver bridge my-bridge
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
1f5fb1658386 bridge bridge local
5ff3f729ae95 host host local
d0687d9e5468 my-bridge bridge local
4b47c4a16574 none null local
$ docker network inspect my-bridge
[
{
"Name": "my-bridge",
"Id": "d0687d9e54684af40aa5f3cebe41f0795f46235e03cd7ea8c3e4a9be9cab7fbe",
"Created": "2019-08-20T16:19:22.101443569+03:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.22.0.0/16",
"Gateway": "172.22.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
$ ifconfig
br-d0687d9e5468: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.22.0.1 netmask 255.255.0.0 broadcast 172.22.255.255
ether 02:42:be:5e:b2:dc txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
$ brctl showstp br-d0687d9e5468
br-d0687d9e5468
bridge id 8000.0242be5eb2dc
designated root 8000.0242be5eb2dc
root port 0 path cost 0
max age 20.00 bridge max age 20.00
hello time 2.00 bridge hello time 2.00
forward delay 15.00 bridge forward delay 15.00
ageing time 300.00
hello timer 0.00 tcn timer 0.00
topology change timer 0.00 gc timer 135.24
flags
Olabildiğince detaya indik. Ama bu bize neyi gösterdi.
Bizim için bir Bridge Network yaratıldı. Henüz hiçbir konteyner bu ağı kullanmıyor bu yüzden boş. 172.22.0.1 adresinde bir Bridge aygıtı çalışıyor.
Bu Bridge ağına bir konteyner atayalım ve takipte kalalım.
$ docker run --network=my-bridge --rm --network-alias=nginx --name=nginx nginx
Ardından başka bir konteyneri daha bu Bridge ağına atayalım ve ilk yarattığımıza konteyner olan nginx konteynerine nginx
host adı üzerinden ulaşmaya çalışalım.
$ docker run --rm -ti --network=my-bridge alpine sh
/ # apk add curl
...
OK: 7 MiB in 18 packages
/ # curl nginx
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</body>
</html>
/ #
Bir de nginx çıktılarına göz atalım
172.22.0.3 - - [20/Aug/2019:13:36:13 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.65.1" "-"
Buradan çıkarılan sonuç Alpine konteynerinin IP adresinin 172.22.0.3 olduğudur.
Görüldüğü üzere iki farklı konteyneri bir ağa(kendi oluşturduğumuz) yerleştirdik ve bunlara hostname atayarak birbirleriyle birbirlerinin IP adreslerini bilmeksizin iletişim kurmasını sağladık.
Tam bu noktada bir şey göstermek istiyorum.
$ docker network inspect my-bridge
"Containers": {
"08cdb789073bf6127dc41704a0bf8047c86a29bcc92cd0c6f0e38fc3cb830718": {
"Name": "relaxed_gauss",
"EndpointID": "d0f941c4f888459f342e015439d557c1929b4fbb1b9456882fe7740b3ec9db9d",
"MacAddress": "02:42:ac:16:00:03",
"IPv4Address": "172.22.0.3/16",
"IPv6Address": ""
},
"b0054d2ca90ece33627d27012af431d96292875b8651baa4f874608fa774b3af": {
"Name": "nginx",
"EndpointID": "69be7b2974b73cafb4fd80fe504806ed6cba601a2446cd0c9f3fd86cf9bb286e",
"MacAddress": "02:42:ac:16:00:02",
"IPv4Address": "172.22.0.2/16",
"IPv6Address": ""
}
},
Buradan anlaşılan şey ise Alpine konteynerinden(relaxed_gauss), nginx
hostname yerine IP adresi kullanarak da erişebilirdik. curl 172.22.0.2
.
Ancak bunu öncesinden bilmek ve buna güvenerek iş yapmak pek doğru olmayabilir.
Artık her makinenin IP adresini bildiğimize göre nginx konteynerine kendi makinenizden de erişebilirsiniz.
$ curl 172.22.0.2
Peki ya bu sefer nginx konteyneri nasıl bir remote_ip adresi yakaladı?
172.22.0.1 - - [20/Aug/2019:13:42:14 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.58.0" "-"
Bu adres tanıdık geldi mi? Bu adres oluşturduğumuz Bridge ağının adresiydi.
Ulaşmaya çalıştığımız adres 172.22.xxx.xxx
ve bizim makinemizde 172.22.0.1
IP adresine sahip bir Bridge olduğundan makinemiz bu arayüzü kullandı. Dolayısıyla bu adres üzerinden haberleştik. Buradaki Bridge ise bizim paketimizin Destination etiketine bakarak bir forward işlemi yaptı ve paketimiz 172.22.0.2 adresindeki nginx uygulamasına ulaştı.
Wireshark çıktısı:
Host
Bu sürücü sayesinde, konteyner host makinenin network arayüzlerini direkt olarak kullanabilir ve konteyner üzerinde herhangi bir namespace ayırması yapılmaz.
Uygulama 3.0
Deneyerek görelim.
docker network ls
komutunun çıktısında bir de host ağı göreceksiniz. Bu Docker tarafından öntanımlı gelir ve bunu silemezsiniz. Bunu kullanarak bir nginx konteyneri başlatalım.
docker run --rm --network host nginx
komutuyla başlatalım ve çıktıları gözlemlemek adına konteyneri durdurmayalım.
Nginx konteynerini başlatmadan önce 80 portunu kontrol etmenizde fayda var. netstat -plnt | grep 80
Ardından bir Alpine konteyneri başlatıp içine curl kuralım ve localhost adresine gidelim.
$ docker run --rm -ti --network host alpine sh
/ # apk add curl
...
OK: 7 MiB in 18 packages
/ # curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</body>
</html>
Her konteynere bir network namespace verildiğini biliyorduk. Peki ya bu nasıl oldu?
Alpine konteynerinin network arayüzlerine bakalım.
/ # ifconfig
docker0 Link encap:Ethernet HWaddr 02:42:B5:F6:8B:8E
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
inet6 addr: fe80::42:b5ff:fef6:8b8e/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:567 errors:0 dropped:0 overruns:0 frame:0
TX packets:898 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:40434 (39.4 KiB) TX bytes:1591926 (1.5 MiB)
enp4s0 Link encap:Ethernet HWaddr B0:25:AA:2E:FD:08
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:174277 errors:0 dropped:0 overruns:0 frame:0
TX packets:174277 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:20270347 (19.3 MiB) TX bytes:20270347 (19.3 MiB)
wlo1 Link encap:Ethernet HWaddr 04:EA:56:DE:E5:EF
inet addr:172.16.2.139 Bcast:172.16.2.255 Mask:255.255.255.0
inet6 addr: fe80::eb48:f271:7ed8:9fd9/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1007740 errors:0 dropped:0 overruns:0 frame:0
TX packets:484613 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1253773884 (1.1 GiB) TX bytes:70453077 (67.1 MiB)
Kendi bilgisayarımızın network arayüzlerine bakalım.
$ ifconfig
docker0 Link encap:Ethernet HWaddr 02:42:B5:F6:8B:8E
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
inet6 addr: fe80::42:b5ff:fef6:8b8e/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:567 errors:0 dropped:0 overruns:0 frame:0
TX packets:898 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:40434 (39.4 KiB) TX bytes:1591926 (1.5 MiB)
enp4s0 Link encap:Ethernet HWaddr B0:25:AA:2E:FD:08
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:174277 errors:0 dropped:0 overruns:0 frame:0
TX packets:174277 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:20270347 (19.3 MiB) TX bytes:20270347 (19.3 MiB)
wlo1 Link encap:Ethernet HWaddr 04:EA:56:DE:E5:EF
inet addr:172.16.2.139 Bcast:172.16.2.255 Mask:255.255.255.0
inet6 addr: fe80::eb48:f271:7ed8:9fd9/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1007740 errors:0 dropped:0 overruns:0 frame:0
TX packets:484613 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1253773884 (1.1 GiB) TX bytes:70453077 (67.1 MiB)
Görüldüğü üzere bir konteynere host network verilirse host makinedeki bütün arayüzleri kullanabilir olur. Bu demek oluyor ki port-mapping yapmaksızın(—publish 80:80) bir konteyneri kullanabiliriz.
Peki ya nginx çıktıları?
127.0.0.1 - - [21/Aug/2019:08:09:15 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.65.1" "-"
Production ortamı için düşünüldüğünde, konteynerlere host network vermek pek tercih edilen bir yöntem değildir. Genellikle host ağını LoadBalancer veya ReverseProxy olarak kullanılan konteynerlere kullandırırız.
Overlay
SDN (Software Defined Network) türlerinden biridir. Var olan donanımdaki Network altyapısını kullanarak, birden fazla makine veya uç nokta(endpoint) arasında sanal bir bağlantı kurar. Dolayısıyla birden fazla makineye veya uç noktaya tek network üzerinden erişebilirsiniz. Yanlızca Swarm modunda kullanılabilir.
Bu sürücü Swarm için olduğundan, Swarm yazımda bahsedeceğim.
None
Bridge modundaki konteynere verilen ethX arayüzünün de verilmediğini yani konteynerde sadece loopback arayüzünün olduğunu düşünün. None bir sürücü değildir ancak bilinmesi gereken bir opsiyondur.
Pratik
Bu kısım tamamen uygulamaya yöneliktir.
Ön yüklü gelen ağları görmek için
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
1f5fb1658386 bridge bridge local
5ff3f729ae95 host host local
4b47c4a16574 none null local
Konteynerlere ister başlatırken ister çalışma zamanında oldukça kolay bir şekilde network atayabilirsiniz.
Denemek için bir Nginx başlatalım.
docker run --rm --name nginx nginx:alpine
Herhangi bir network parametresi belirtmedim ancak yine de ön yüklü gelen bridge ağını kullanacaktır. Kontrol edelim
$ docker container inspect nginx | jq '.[0].NetworkSettings.Networks | keys'
[
"bridge"
]
Sonrasında bir network yaratalım
docker network create my-network
Sürücü parametresi vermediğimiz için bridge sürücüsünü kullanan bir network yaratacaktır.
docker network connect my-network nginx --alias=nginx
Bu komut çalışan bir konteyner içindir. Bu komut ile kendi makinenizden görebileceğiniz bir veth oluşturulur konteyneriniz ve belirlediğiniz ağa bağlanır. Böylece konteyneriniz aynı anda 2 ağdan erişilebilir olur.
Alias parametresini, my-network ağındaki konteynerlerin bizim nginx konteynerimizi nginx host adından bulabilmesi için verdim.
Konteynerin bulunduğu ağları listeleyelim.
$ docker container inspect nginx | jq '.[0].NetworkSettings.Networks | keys'
[
"bridge",
"my-network"
]
Görüldüğü üzere iki ağa sahip bir konteyner. Peki ya bu konteynerin network arayüzleri ne durumda?
$ docker exec -ti nginx sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:55 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:7858 (7.6 KiB) TX bytes:0 (0.0 B)
eth1 Link encap:Ethernet HWaddr 02:42:AC:16:00:02
inet addr:172.22.0.2 Bcast:172.22.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:33 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4681 (4.5 KiB) TX bytes:0 (0.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
eth0 arayüzü, docker run
komutuyla konteynerimize atanan varsayılan sürücüdür ve bridge bu arayüzün network adıdır. Parametre vererek bunu değiştirebilir veya hiçbir şey olmamasını sağlayabilirdik.
eth1 arayüzü ise sonradan bu konteynere verdiğimiz ağdır(my-network). Çeşitli şekillerle bu arayüzleri kullanıp test edebilirsiniz.
Şuan nginx konteyneri my-bridge isimli ağ üzerinden nginx olarak biliniyor ancak bridge isimli ağ üzerinden herhangi bir isme sahip değil. Bunu değiştirmek için ya konteyneri başlatırken hostname parametresini kullanmalısınız ya da konteyneri ağdan koparıp tekrar bağlarken alias parametresini kullanmalısınız.
Konteyneri bir ağdan koparmak için
$ docker network disconnect my-network nginx
Şuan bulunduğumuz tarih itibariyle yazının sonu burası. Bu demek oluyor ki güncelleme gelebilir. Her türlü geri dönüşe açığım ve bunlara açım.