Blog

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ı:

pcap

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.