쿠버네티스에서 우리가 실행하는 애플리케이션은 Pod 단위로 수행되며 Pod는 내부에 하나 이상의 컨테이너를 가지고 있습니다.
이 말은 결국 쿠버네티스에 배포한, 그리고 구성된 시스템은 컨테이너 통신을 기초로 하는 것을 알 수 있습니다.
그렇다면 이 기초적인 컨테이너는 어떻게 네트워크 통신을 수행하는지 도커를 이용하여 컨테이너의 통신을 알아봅시다.
- 이번 주제인 "컨테이너 간 통신"은 최대 세 개의 글로 작성될 예정입니다.
이전 글 목차
https://dev-whoan.xyz/111, 네트워크로 시작하는 쿠버네티스 — 내가 데이터를 보낸다면
사전 준비물
오늘 사전 준비물은 Ubuntu 22.04 시스템 하나만 있으면 됩니다. 컨테이너 네트워크를 시작하기 전 Network Namespace를 이용하여 실제로 어떻게 네트워크를 격리하는지 확인해 볼 예정입니다.
컨테이너와 Network Namespace
컨테이너는 Union Filesystem(overlayfs), Control Group, 그리고 Namespace를 이용하여 가상화된 환경을 제공합니다.
- Union Filesystem을 통해 low layer, upper layer, merged view를 통해 레이어화 된 애플리케이션 실행에 필요한 파일시스템을 제공하고
- Namespace를 통해 애플리케이션의 실행 환경을 분리하며
- Control Group을 통해 애플리케이션 실행에 필요한 하드웨어 자원을 격리, 제한하여 독립된 환경에서 안정적으로 실행될 수 있도록 하는 가상화 기술입니다.
Namespace에는 Mount Namespace, Control Group Namespace, Network Namespac Inter Process Communication Namespace, Process ID Namespace, UTS Namespace, ... 등이 존재합니다.
각각의 이름에서 알 수 있듯이 파일 시스템, 컨트롤 그룹, 네트워크 ... 등을 격리하여 컨테이너를 실행했을 때 Host OS와 격리되어 운영할 수 있도록 합니다. 다만, 컨테이너 가상화는 Virtual Machine과는 다르게 그 특성상 Host OS로부터 완전히 독립적일 수는 없습니다.
우리가 흔히 도커, cri-o 등 컨테이너 런타임 플랫폼을 설치하면 네트워크가 새롭게 할당되는 것을 볼 수 있습니다.
일반적으로 우분투 등 운영체제의 설치를 막 마친 뒤 시스템을 켜면 다음과 같은 네트워크 시스템을 확인할 수 있습니다. (NIC의 이름은 다를 수 있습니다.)
docker@docker:~$ 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: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
valid_lft 1959sec preferred_lft 1959sec
inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 2591937sec preferred_lft 604737sec
inet6 fe80::489e:7eff:fe55:59ee/64 scope link
valid_lft forever preferred_lft forever
docker@docker:~$ ip netns list # 아무것도 출력되지 않음
docker@docker:~$
1번 lo 인터페이스는 loopback 인터페이스입니다. 127.0.0.1을 가리키는 인터페이스로 자기 자신으로의 통신에 사용됩니다.
우리가 애플리케이션을 띄운 뒤 127.0.0.1, 혹은 alias localhost로 요청을 보낼 때 해당 인터페이스가 처리하게 됩니다.
2번 enp0s1 인터페이스는 시스템이 가지고 있는 물리 인터페이스로 (우리는 가상 머신을 이용 중이기 때문에, 이 또한 가상 인터페이스입니다.) 실제 네트워크 연결이 발생하는 인터페이스입니다. 아이피를 할당받아 실질적인 통신을 담당하는 인터페이스입니다.
이 상태에서 nginx를 설치하고 배포하는 작업을 해 보겠습니다.
docker@docker:~$ sudo apt-get update && sudo apt-get install nginx -y
docker@docker:~$ curl localhost
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.
Thank you for using nginx.
Network Namespace 추가하기
Host Namespace에서 nginx의 동작을 확인했으니, 이제 A Namespace를 생성하고 해당 위치로 nginx를 옮겨 확인을 해 봅시다. Ubuntu를 기준으로 ip 패키지를 통해 네임스페이스를 생성할 수 있습니다.
### A라는 이름의 network namespace 추가
docker@docker:~$ sudo ip netns add A
docker@docker:~$ ip netns list
A
A Network namespace에서 실질적인 통신을 담당하는 네트워크 인터페이스를 추가하여야 하는데, 일반적으로 다음의 모드를 이용합니다.
격리 모드:
네트워크 네임스페이스는 아예 독립된 네트워크를 갖습니다.
Host Only 모드:
네트워크 네임스페이스가 Host와만 통신 가능하도록 설정합니다.
Bridged 모드:
Host Network Namespace에 브리지 네트워크를 생성하고, 컨테이너는 브리지 네트워크 대역에서 IP 주소를 할당받아 사용됩니다. 외부 네트워크와 통신은 호스트 네트워크를 경유(NAT 등)하여 통신합니다.
격리 모드
Dummy Type을 활용하여 Network Interface Card 생성하기
Dummy Type의 네트워크 인터페이스는 로컬에서 loopback 인터페이스 처럼 동작하는 인터페이스입니다. 즉 로컬에서의 테스트를 하거나, 격리된 환경에서 동작하는 기능을 제공합니다. 생성은 다음과 같이 수행할 수 있습니다.
docker@docker:~$ sudo ip link add dummy0 type dummy
# dummy0의 network namespace를 A로 설정합니다.
docker@docker:~$ sudo ip link set dummy0 netns A
# A 네임스페이스의 ip 링크, 주소를 확인합니다.
docker@docker:~$ sudo ip netns exec A ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 7e:8e:29:d3:31:94 brd ff:ff:ff:ff:ff:ff
docker@docker:~$ sudo ip netns exec A ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noqueue state DOWN 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
3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 7e:8e:29:d3:31:94 brd ff:ff:ff:ff:ff:ff
# Host Network Namespace에서는 찾을 수 없는 것을 확인할 수 있습니다.
docker@docker:~$ ip link show dummy0
Device "dummy0" does not exist.
위에서 출력해 봤을 때 dummy0은 아직 아이피 주소를 가지고 있지 않기 때문에, 실질적인 통신은 불가능한 상태입니다. 이를 위해 IP를 할당하고, dummy0을 시작해 보겠습니다.
docker@docker:~$ sudo ip netns exec A ip addr add 192.168.222.10/24 dev dummy0
docker@docker:~$ sudo ip netns exec A ip link set dummy0 up
docker@docker:~$ sudo ip netns exec A ip link set lo up
docker@docker:~$ sudo ip netns exec A ip addr show
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
3: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 7e:8e:29:d3:31:94 brd ff:ff:ff:ff:ff:ff
inet 192.168.222.10/24 scope global dummy0
valid_lft forever preferred_lft forever
inet6 fe80::7c8e:29ff:fed3:3194/64 scope link
valid_lft forever preferred_lft forever
docker@docker:~$ sudo apt-get update && sudo apt-get install nginx -y
docker@docker:~$ curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
docker@docker:~$ sudo ip netns exec A curl 192.168.64.3
curl: (7) Couldn't connect to server
Host Network Namespace에서 요청을 보낼 경우 nginx와 통신이 가능하지만, A 네임스페이스에서 요청을 보내면 통신이 불가능한 상태임을 확인할 수 있습니다.
마찬가지로 A Namespace에 nginx를 배포하고 Host Network Namespace에서 시도해 보아도 접근이 불가능한 것을 확인할 수 있습니다.
docker@docker:~$ sudo ip netns exec A nginx
docker@docker:~$ curl 192.168.222.10
curl: (7) Failed to connect to 192.168.222.10 port 80 after 0 ms: Connection refused
Hosted Only 네트워크
보통 Ubuntu에서 Hosted Only 네트워크 네임스페이스의 통신을 위한 경우 veth pair를 이용합니다. veth pair의 경우 설정이 간단하며, Pair (veth0, veth1의 쌍)으로 존재하여 연결하고자 하는 네임스페이스에 각각 할당하여 통신이 가능하게 바로 설정되기 때문입니다.
### veth pair를 생성합니다. 각각의 이름은 veth0, veth1 입니다.
docker@docker:~$ sudo ip link add veth0 type veth peer name veth1
## veth1의 네트워크 네임스페이스를 A로 설정합니다.
docker@docker:~$ sudo ip link set veth1 netns A
## veth0에 ip를 할당합니다.
docker@docker:~$ sudo ip addr add 192.168.202.1/24 dev veth0
docker@docker:~$ sudo ip link set veth0 up
docker@docker:~$ ip addr
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: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 4a:9e:7e:55:59:ee brd ff:ff:ff:ff:ff:ff
inet 192.168.64.3/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
valid_lft 1835sec preferred_lft 1835sec
inet6 fd9e:d858:c06b:b3ed:489e:7eff:fe55:59ee/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 2591955sec preferred_lft 604755sec
inet6 fe80::489e:7eff:fe55:59ee/64 scope link
valid_lft forever preferred_lft forever
7: veth0@if6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
link/ether ea:08:dd:65:53:66 brd ff:ff:ff:ff:ff:ff link-netns A
inet 192.168.202.1/24 scope global veth0
valid_lft forever preferred_lft forever
## veth1에 ip를 할당합니다.
docker@docker:~$ sudo ip netns exec A ip addr add 192.168.202.11/24 dev veth1
docker@docker:~$ sudo ip netns exec A ip link set veth1 up
docker@docker:~$ sudo ip netns exec A ip addr show
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
6: veth1@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether fe:1c:38:ae:01:07 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.202.11/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::fc1c:38ff:feae:107/64 scope link
valid_lft forever preferred_lft forever
## 통신이 원활히 되는 것을 확인할 수 있습니다.
docker@docker:~$ ping 192.168.202.11
PING 192.168.202.11 (192.168.202.11) 56(84) bytes of data.
64 bytes from 192.168.202.11: icmp_seq=1 ttl=64 time=0.056 ms
64 bytes from 192.168.202.11: icmp_seq=2 ttl=64 time=0.191 ms
^C
--- 192.168.202.11 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1013ms
rtt min/avg/max/mdev = 0.056/0.123/0.191/0.067 ms
docker@docker:~$ sudo ip netns exec A ping 192.168.202.1
PING 192.168.202.1 (192.168.202.1) 56(84) bytes of data.
64 bytes from 192.168.202.1: icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from 192.168.202.1: icmp_seq=2 ttl=64 time=0.137 ms
^C
--- 192.168.202.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1006ms
rtt min/avg/max/mdev = 0.027/0.082/0.137/0.055 ms
그렇다면 A 네임스페이스의 veth1에서 인터넷으로 요청을 보낼 수 있을까요? 잠시 멈추시고, 아래의 이유를 생각해 보시길 바랍니다.
### Host Naemspace에서 google로 요청을 보낼 수 있지만
docker@docker:~$ curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
### A Namespace에서 외부로 요청을 보낼 수 없습니다.
docker@docker:~$ sudo ip netns exec A ping -c 4 8.8.8.8
ping: connect: Network is unreachable
### 마찬가지로 google로 요청을 보낼 수 없습니다.
docker@docker:~$ sudo ip netns exec A curl google.com
curl: (6) Could not resolve host: google.com
veth pair는 namespace간 연결을 자동으로 수행해 주기 때문에, 우리는 곧바로 host os에 배포된 nginx에 요청을 보낼 수 있습니다.
### A Namespace -> veth0 OK
docker@docker:~$ sudo ip netns exec A curl 192.168.202.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
### Host OS 테스트
docker@docker:~$ curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
아래의 경우에 대해 이유를 생각해 보시면 좋을 것 같습니다.
docker@docker:~$ sudo ip netns exec A curl 192.168.64.3
curl: (7) Couldn't connect to server
Bridged 모드
Hosted Only 모드가 호스트와 가상 네임스페이스간의 통신만을 허용했다면, Bridged 모드는 Host에 배포된 NIC에 탑승하여 인터넷 통신이 가능하도록 합니다.
이를 위해 마찬가지로 veth pair를 생성하고, Host에 존재하는 네트워크에 탑승하도록 설정하겠습니다.
## Host OS에서 IP Forwarding이 가능하도록 설정합니다.
docker@docker:~$ sudo sysctl -w net.ipv4.ip_forward=1
## Bridge 타입의 인터페이스를 생성하고, 아이피를 할당합니다.
docker@docker:~$ sudo ip link add br0 type bridge
docker@docker:~$ sudo ip addr add 192.168.64.100/24 dev br0
docker@docker:~$ sudo ip link set br0 up
## enp0s1의 주 네트워크 장치는 br0임을 설정합니다.
docker@docker:~$ sudo ip link set enp0s1 master br0
docker@docker:~$ sudo ip link set enp0s1 up
docker@docker:~$ sudo ip link add veth0 type veth peer name veth1
docker@docker:~$ sudo ip link set veth1 netns A
## veth0의 주 네트워크 장치는 br0임을 설정합니다.
## 이를 통해 veth0이 br0을 통해 다른 네트워크 인터페이스와 통신할 수 있습니다.
## 바꿔 말하면, veth0의 쌍인 veth1이 br0을 통해 외부와 통신할 수 있음을 의미합니다.
docker@docker:~$ sudo ip link set veth0 master br0
docker@docker:~$ sudo ip link set veth0 up
## veth1에 아이피를 할당하고, 시작합니다.
docker@docker:~$ sudo ip netns exec A ip addr add 192.168.64.50/24 dev veth1
docker@docker:~$ sudo ip netns exec A ip link set veth1 up
docker@docker:~$ sudo ip netns exec A ip link set lo up
## veth1이 default 라우팅 (0.0.0.0/0)에 대한 next hop으로 192.168.64.100을 지정합니다.
docker@docker:~$ sudo ip netns exec A ip route add default via 192.168.64.100
docker@docker:~$ sudo ip netns exec A ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=56 time=42.4 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=56 time=41.0 ms
...
끝으로 ip link set enp0s1 master br0에 대한 명령어에 대한 이해를 해 보시기 바랍니다.
마치며
오늘의 “컨테이너 통신, Network Namespace” 글은 컨테이너 네트워크를 이해하기 위한 네트워크 격리를 어떻게 수행해야 하는지 기본적인 부분을 다루었습니다. 우리가 Dockerfile을 작성하는 등 network를 정의할 때, 각 모드가 어떻게 동작되고 네트워크를 정의할 경우 다른 컨테이너와의 통신을 어떻게 구성해야 하는지 조금이나마 이해에 도움이 되었으면 합니다.
그렇다면 이것도…
- Hosted Only 모드에서 A Namespace에 존재하는 네트워크는 왜 외부로 요청을 보낼 수 없나요?
- Bridged 모드에서 Host 외부의 시스템과 telent을 이용해 메시지를 주고받으려면 어떻게 해야 하나요?
- Hosted Only와 Bridged 모드는 어떤 때 구분해서 사용해야 할까요?
- 네트워크 격리는 어떤 때 수행해야 할까요?
- 아래로 진행될 수록 코드가 많아졌는데, 설명할 수 있나요?
'DevOps > Kubernetes' 카테고리의 다른 글
네트워크로 시작하는 쿠버네티스 - 내가 데이터를 보낸다면 (0) | 2024.11.14 |
---|---|
쿠버네티스가 쉬워지는 컨테이너 이야기가 어렵다면 - cgroup, cpu 편 (0) | 2024.11.11 |