본문 바로가기
개발/docker, k8s, CNCF

docker, k8s 네트워크 뜯기(3) - docker 컨테이너는 어떻게 서로 통신할까?

by 앗가 2023. 3. 5.
728x90
반응형

전에 ubuntu 22.04 환경에서 docker를 설치하였습니다. 이번에는 docker nginx 이미지를 만들고 서로 어떻게 통신하는지 눈으로 확인해 보도록 하겠습니다. 

 

실험 환경은 다음과 같습니다(리눅스에 도커가 설치된 환경이면 상관없지만 wsl은 다를 수 있습니다).

- virtualbox 7.0.6

- ubuntu 22.04.2 LTS

- docker

 

순서는 다음과 같습니다. 

1. nginx 이미지 테스트

2. 컨테이너안에서 다른 컨테이너로 요청하기

3. 호스트에서 확인하기


1. nginx 이미지 테스트

이제 네트워크를 본격적으로 뜯어보기 전에 테스트로 nginx 컨테이너를 만들고 네트워크 툴을 설치한 다음 다시 이미지로 만들어보겠습니다. 가장 먼저 nginx 컨테이너를 띄워봅시다. 

docker run -itd -p 80:80 --name mynginx1 mynginx:1.0

curl localhost:80
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# <style>
# html { color-scheme: light dark; }
# body { width: 35em; margin: 0 auto;
# font-family: Tahoma, Verdana, Arial, sans-serif; }
# </style>
# </head>
# <body>
# <h1>Welcome to nginx!</h1>
# ~~~

80번 포트를 컨테이너 내부의 80번 포트로 매핑하여 요청이 잘 전달되었음을 확인할 수 있습니다. 이제 컨테이너 내부로 들어가보겠습니다. 

docker exec -it mynginx1 /bin/bash

이제 내부에 들어가서 명령어를 입력하겠습니다. 패키지를 업데이트하고 네트워크를 분석하기 좋은 도구를 설치하겠습니다. 

# mynginx 컨테이너 내부입니다. 

apt update -y   
apt install net-tools
apt install bridge-utils
apt install -y iproute2
apt install -y iptables

# 각종 명령어 동작을 확인해봅시다. 
# ifconfig, ip l, brctl show, iptables, route등의 같은 명령어를 확인해볼 수 있습니다. 

exit

이제 컨테이너를 이미지로 만들겠습니다.

 docker commit mynginx1 mynginx:1.0

이제 해당 이미지를 이용하여 네트워크를 분석해봅시다. 진행하기 위해 mynginx2 컨테이너를 하나 더 만들어주겠습니다. 

docker run -itd -p 81:80 --name mynginx2 mynginx:1.0

curl localhost:81
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# <style>
# html { color-scheme: light dark; }
# body { width: 35em; margin: 0 auto;
# font-family: Tahoma, Verdana, Arial, sans-serif; }
# </style>
# </head>
# <body>
# <h1>Welcome to nginx!</h1>
# ~~~

2. 컨테이너안에서 다른 컨테이너로 요청하기

이제 컨테이너 안에 접속해서 다른 컨테이너로 요청을 보내보겠습니다. 그런데 요청을 보낼 주소를 알아야 합니다. 터미널 창을 2개를 열고 왼쪽에는 mynginx1, mynginx2를 열고 IP주소를 확인해 보겠습니다.

왼쪽은 mynginx1, mynginx2에 IP를 조회하면 mynginx1 컨테이너는 172.17.0.2, mynginx2 172.17.0.3으로 나오는 것을 확인할 수 있습니다. 

mynginx1에서 curl로 mynginx2 컨테이너의 주소로 요청을 보내면 응답이 오고, mynginx2에서 curl로 mynginx1 컨테이너의 주소로 요청을 보내면 응답이 오는 것을 확인할 수 있습니다.

 

지금까지 라우트 테이블을 조회해 보면 mynginx1, mynginx2에서 172.17.0.1의 주소로 eth0라는 네트워크 인터페이스를 이용하여 트래픽이 전달됩니다. 아마도 172.17.0.1은 호스트 머신에서 확인할 수 있을 것 같습니다. 

 

네트워크 인터페이스도 컨테이너 내부에서 확인해 보겠습니다.


3. 호스트에서 확인하기

이번엔 컨테이너 내부가 아니라 호스트에서 확인해 보겠습니다. 먼저 도커 네트워크를 보겠습니다. 

docker network ls
# NETWORK ID     NAME      DRIVER    SCOPE
# 20a3680b844f   bridge    bridge    local
# 5a2e39f8645a   host      host      local
# 38ab5c69257d   kind      bridge    local
# c0c50e70f0f8   none      null      local

기본적으로 네트워크 연결을 설정을 하지 않았기 때문에 bridge 네트워크에 연결되므로 bridge 네트워크를 확인해 봅시다. 게이트웨이는 172.17.0.1/16이고 여기에 연결된 컨테이너는 mynginx1, mynginx2이고 각각의 IP주소를 호스트에서도 확인할 수 있습니다. 

docker network inspect bridge
# [
#     {
#         "Name": "bridge",
#         "Id": "20a3680b844faf2cb2846a3918015bf48c10e3ab6942e6105c2d4aecd34b8065",
#         "Created": "2023-03-04T19:48:43.353896412+09:00",
#         "Scope": "local",
#         "Driver": "bridge",
#         "EnableIPv6": false,
#         "IPAM": {
#             "Driver": "default",
#             "Options": null,
#             "Config": [
#                 {
#                     "Subnet": "172.17.0.0/16",
#                     "Gateway": "172.17.0.1"
#                 }
#             ]
#         },
#         "Internal": false,
#         "Attachable": false,
#         "Ingress": false,
#         "ConfigFrom": {
#             "Network": ""
#         },
#         "ConfigOnly": false,
#         "Containers": {
#             "3e0f44eb2b19d41a88a5f567227fdbbc9e164fc81851eec3a7e541f71ac93e61": {
#                 "Name": "mynginx2",
#                 "EndpointID": "9ac148cd89628d446d31fa3f0b9bf1431b6d0ccc6f9a4bbe6218a193412d32d9",
#                 "MacAddress": "02:42:ac:11:00:03",
#                 "IPv4Address": "172.17.0.3/16",
#                 "IPv6Address": ""
#             },
#             "e351dc6df8a68ecd90e5a817d4ae97fedee53666b8fa77f811b96707d314d041": {
#                 "Name": "mynginx1",
#                 "EndpointID": "bc6953c1ad9d2e6aaf21c34ab008946d0291df4ac4fa076ee12a87efcb8a2698",
#                 "MacAddress": "02:42:ac:11:00:02",
#                 "IPv4Address": "172.17.0.2/16",
#                 "IPv6Address": ""
#             }
#         },
#         "Options": {
#             "com.docker.network.bridge.default_bridge": "true",
#             "com.docker.network.bridge.enable_icc": "true",
#             "com.docker.network.bridge.enable_ip_masquerade": "true",
#             "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
#             "com.docker.network.bridge.name": "docker0",
#             "com.docker.network.driver.mtu": "1500"
#         },
#         "Labels": {}
#     }
# ]

 

이제 호스트 머신에서 네트워크 인터페이스를 조회해 보겠습니다.

ip a라는 명령어를 이용하여 네트워크 인터페이스를 조회할 수 있는데, enp0s3, enp0s8과 같은 기본적으로 가상머신에서 사용하는 네트워크 인터페이스와 lo와 같은 loopback에 해당하는 인터페이스도 확인할 수 있습니다. 

 

또한 여기서 docker0라는 네트워크 인터페이스를 확인할 수 있고, 이에 해당하는 IP가 172.17.0.1임을 확인할 수 있습니다. 아까 라우트 테이블로 확인한 주소가 docker0임을 알 수 있습니다. 

ip a
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
#     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
#     inet 127.0.0.1/8 scope host lo
#        valid_lft forever preferred_lft forever
#     inet6 ::1/128 scope host 
#        valid_lft forever preferred_lft forever
# 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
#     link/ether 08:00:27:f4:0a:39 brd ff:ff:ff:ff:ff:ff
#     inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s3
#        valid_lft 83477sec preferred_lft 83477sec
#     inet6 fe80::5813:186b:eb4c:f323/64 scope link noprefixroute 
#        valid_lft forever preferred_lft forever
# 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
#     link/ether 08:00:27:83:e2:78 brd ff:ff:ff:ff:ff:ff
#     inet 192.168.200.102/24 brd 192.168.200.255 scope global noprefixroute enp0s8
#        valid_lft forever preferred_lft forever
#     inet6 fe80::2759:81b3:e3c5:419c/64 scope link noprefixroute 
#        valid_lft forever preferred_lft forever
# 4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
#     link/ether 02:42:4a:16:f3:f7 brd ff:ff:ff:ff:ff:ff
#     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
#        valid_lft forever preferred_lft forever
#     inet6 fe80::42:4aff:fe16:f3f7/64 scope link 
#        valid_lft forever preferred_lft forever
# ~~~

한편 브리지 네트워크도 확인해보겠습니다. brctl show로 브릿지 네트워크를 조회하면 docker0라는 브리지에 인터페이스가 연결되어 있음을 확인할 수 있습니다.

brctl show 
# bridge name     bridge id               STP enabled     interfaces
# br-38ab5c69257d         8000.024208d01418       no
# docker0         8000.02424a16f3f7       no              veth2ca8f04
#                                                         veth8dc6de0

브리지는 L2 스위치와 같은 역할을 하는데, MAC주소로 패킷을 전달하는 역할을 합니다. 이로부터 docker0라는 가상의 스위치는 연결된 인터페이스에 패킷을 전달하는 역할을 함을 알 수 있습니다. 

 

호스트의 라우트 테이블은 다음과 같습니다. 

route
# Kernel IP routing table
# Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
# default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
# default         192.168.200.1   0.0.0.0         UG    20101  0        0 enp0s8
# 10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
# link-local      0.0.0.0         255.255.0.0     U     1000   0        0 enp0s8
# 172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 docker0
# 192.168.200.0   0.0.0.0         255.255.255.0   U     101    0        0 enp0s8

 

여기서 지금까지 확인한 내용을 정리해 보면,

만약 mynginx1에서 mynginx2로 요청을 보내는 경우의 트래픽 흐름이 다음과 같다고 예상해 볼 수 있습니다. curl 172.17.0.3을 요청했을 때 먼저 라우트 테이블을 확인하여 eth0 네트워크 인터페이스를 통과하여 브리지인 docker0로부터 mynginx2의 컨테이너 주소로 도착하고 nginx 프로세스로 도달합니다. 이를 그려보면 다음과 같음을 확인할 수 있습니다. 


실습 환경 제거

docker rm -f mynginx1
docker rm -f mynginx2
docker rmi mynginx:1.0

 

지금까지 확인해 봤을 때 얻을 수 있는 결론은 다음과 같습니다. 

- 컨테이너를 띄우면 가상 네트워크 인터페이스가 생성되고 네트워크 설정을 하지 않으면 기본으로 브리지 네트워크에 연결된다. 

- 브리지 네트워크는 docker0이고, IP주소를 갖고 외부와 통신한다. 

- 이 docker0를 이용하여 다른 컨테이너와 통신할 수 있다. 

- 라우트 테이블이 알아서 명시된다. 

- docker0에 먼저 IP가 부여되어 있고 컨테이너가 IP를 나중에 부여받는다. 

 

그리고 다음과 같은 생각을 해봤습니다. 

- 브리지는 L2 수준에서 스위칭을 할거같은데 왜 IP주소가 부여돼야 할까? 그리고 왜 라우트 테이블에 명시되어야 할까 -> public 인터넷에 도달할 수 있기 위해? 그러면 network속성을 none으로 주면 외부와도 연결되지 않을까 합니다. apt update같은 명령어도 사용하지 못할 거 같습니다. 

- 여러 컨테이너가 하나의 네트워크 인터페이스를 이용한다면? -> 실제로 k8s의 pod내부 컨테이너는 이를 이용합니다. 

- 이런 네트워크 기술은 어떻게 구현되어 있을까? -> 네트워크 네임스페이스 격리 기술

-docker0 브리지를 master로 간주하는 네트워크 인터페이스가 있는데 이게 아무래도 container의 가상 네트워크 인터페이스일 거 같아요

 

참조자료

- https://coffeewhale.com/k8s/network/2019/04/19/k8s-network-01/

728x90

댓글