사용한 코드입니다.
https://github.com/atgane/docker-k8s-network-tutorial/tree/main/blog5
이번에는 클러스터 내부에서 pod가 어떻게 통신하는지 확인하겠습니다.
flask이미지를 만들어서 로컬 이미지 저장소에 배포하고 k8s에 pod를 만들고 배포해 보겠습니다. 배포 후 다른 pod로 요청을 보내면 도달하는지 확인해 봅시다. 로컬 이미지 저장소를 이용한 kind k8s 클러스터 배포방법은 https://atgane.tistory.com/182 이 포스팅에서 다루고 있습니다.
시작하기 전에 create-kind-cluster.sh을 작업 디렉토리에 복사해서 실행시켜 주겠습니다.
./create-kind-cluster.sh
실습 환경은 다음과 같습니다(리눅스에 docker, kind가 설치된 환경이면 상관없지만 wsl은 다를 수 있습니다).
- virtualbox 7.0.6
- ubuntu 22.04.2 LTS
- docker
- kind 0.17.0
순서는 다음과 같습니다.
1. flask Dockerfile만들기
2. myflask 이미지 k8s 클러스터로 배포하기
3. node와 pod
4. pod의 컨테이너간 포트 충돌
1. flask Dockerfile만들기
하나의 파드에 두 개의 flask 컨테이너를 띄워보겠습니다. 굳이 두 개를 띄우는 이유는 포트를 같게 했을 때 제대로 띄워지는지를 보기 위함입니다. 그래서 작업 디렉토리를 다음처럼 구성하였습니다.
create-kind-cluster.sh은 이전 포스팅 https://atgane.tistory.com/182 에 사용했던 파일을 그대로 이용했습니다.
tree
# .
# ├── create-kind-cluster.sh
# ├── deploy-docker-image.sh
# ├── deployment.yaml
# └── dockerfile
# ├── myflask1
# │ ├── app.py
# │ └── Dockerfile
# └── myflask2
# ├── app.py
# └── Dockerfile
먼저 myflask1/app.py입니다. flask로 8000번 포트로 호스팅 합니다.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "hello myflask1 1.0"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
다음은 myflask2/app.py입니다. flask로 8001번 포트로 호스팅 합니다.
# myflask2/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "hello myflask2 1.0"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8001)
myflask1/Dockerfile과 myflask2/Dockerfile은 같게 설정했습니다.
# Dockerfile은 myflask1, 2 모두 동일
FROM python:3.9-slim
COPY . /app
RUN pip3 install flask
WORKDIR /app
CMD ["python3", "app.py"]
쉘 스크립트로 docker 이미지를 빌드하고 로컬 레지스트리에 넣어주기 위해 deploy-docker-image.sh을 다음처럼 작성해 주었습니다.
# deploy-docker-image.sh
docker build -t 127.0.0.1:5001/myflask1:1.0 dockerfile/myflask1
docker build -t 127.0.0.1:5001/myflask2:1.0 dockerfile/myflask2
docker push 127.0.0.1:5001/myflask1:1.0
docker push 127.0.0.1:5001/myflask2:1.0
이제 작업 위치에서 해당 쉘을 실행시켜 봅시다.
./deploy-docker-image.sh
다음 명령어로 로컬 레지스트리에 저장된 이미지를 확인할 수 있습니다. nginx는 없어도 됩니다!
curl localhost:5001/v2/_catalog
# {"repositories":["myflask1","myflask2"]}
2. myflask 이미지 k8s 클러스터로 배포하기
빌드한 2개의 이미지를 이번에는 pod가 아닌 deployment로 묶어서 배포해 보겠습니다. deployment로 pod를 2개의 replica로 배포하는 파일을 다음과 같이 작성했습니다.
deployment.yaml파일입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myflask-deployment
labels:
app: myflask
spec:
replicas: 2
selector:
matchLabels:
app: myflask
template:
metadata:
labels:
app: myflask
spec:
containers:
- name: myflask1
image: localhost:5001/myflask1:1.0
ports:
- containerPort: 8000
- name: myflask2
image: localhost:5001/myflask2:1.0
ports:
- containerPort: 8001
replicas를 2개로, containers는 myflask1, myflask2를 각각 8000. 8001 포트로 배포해 보겠습니다.
kubectl apply -f deployment.yaml
k9s를 이용한다면 다음처럼 pod가 제대로 떠있음을 확인할 수 있습니다.
한편 k9s를 이용하지 않는 경우 kubectl을 이용하여 확인할 수 있습니다.
kubectl get all
# NAME READY STATUS RESTARTS AGE
# pod/mygflask-deployment-5df795f9f6-6pjm5 2/2 Running 0 12m
# pod/mygflask-deployment-5df795f9f6-b6tg6 2/2 Running 0 12m
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 20m
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/mygflask-deployment 2/2 2 2 12m
#
# NAME DESIRED CURRENT READY AGE
# replicaset.apps/mygflask-deployment-5df795f9f6 2 2 2 12m
pod의 세부정보를 보려면 describe 명령어를 이용하여 확인합니다. pod의 IP를 확인하기 위해 아래의 명령어를 입력해 보겠습니다. pod이름은 kubectl get all로 확인한 pod의 name입니다.
kubectl describe pod mygflask-deployment-5df795f9f6-6pjm5
# Name: mygflask-deployment-5df795f9f6-6pjm5
# Namespace: default
# Priority: 0
# Service Account: default
# Node: kind-worker/172.18.0.3
# Start Time: Sun, 05 Mar 2023 20:47:21 +0900
# Labels: app=myflask
# pod-template-hash=5df795f9f6
# Annotations: <none>
# Status: Running
# IP: 10.244.1.2
# IPs:
# IP: 10.244.1.2
# Controlled By: ReplicaSet/mygflask-deployment-5df795f9f6
# ~
kubectl describe pod mygflask-deployment-5df795f9f6-b6tg6
# Name: mygflask-deployment-5df795f9f6-b6tg6
# Namespace: default
# Priority: 0
# Service Account: default
# Node: kind-worker2/172.18.0.2
# Start Time: Sun, 05 Mar 2023 20:47:21 +0900
# Labels: app=myflask
# pod-template-hash=5df795f9f6
# Annotations: <none>
# Status: Running
# IP: 10.244.2.2
# IPs:
# IP: 10.244.2.2
# Controlled By: ReplicaSet/mygflask-deployment-5df795f9f6
# ~
분명 docker에서 network를 확인할 때 컨테이너마다 IP가 부여되었습니다. 그런데 pod는 여러 컨테이너가 포함되어 있는데 pod자체에 IP가 고정되어 있습니다. 또한 Node라고 되어있는 정보도 보이는데, 이 Node는 해당 pod가 띄워져 있는 머신을 의미합니다. 옆에 나온 IP주소는 당연히 Node의 IP주소를 의미합니다.
만약 host머신이라면, 이 상태에서 pod의 IP인 10.244.1.2:8000에 요청을 보내도 어떤 응답을 받을 수 없습니다.
curl 10.244.1.2
# curl: (28) Failed to connect to 10.244.1.2 port 80 after 129818 ms: Connection timed out
host 머신에서 pod의 IP와 연결되기 위한 어떠한 정의도 없기 때문입니다. 따라서 host머신에서는 pod가 제대로 동작하는지 확인할 수 없습니다. 따라서 클러스터 내부의 노드로 접속해 보겠습니다.
여기서 제대로 flask 서버가 동작하는지 확인하기 위해 worker node로 접속해 보겠습니다. kind는 노드를 도커 컨테이너로 배포하기 때문에 docker ps -a 명령으로 노드를 확인해 볼 수 있습니다.
docker ps -a
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 29d5d09497db kindest/node:v1.25.3 "/usr/local/bin/entr…" 23 minutes ago Up 23 minutes kind-worker
# 3dd6c5506cab kindest/node:v1.25.3 "/usr/local/bin/entr…" 23 minutes ago Up 23 minutes kind-worker2
# 7d182158d2ca kindest/node:v1.25.3 "/usr/local/bin/entr…" 23 minutes ago Up 23 minutes 127.0.0.1:35799->6443/tcp kind-control-plane
# 40272643544e registry:2 "/entrypoint.sh /etc…" 7 hours ago Up 3 hours 127.0.0.1:5001->5000/tcp kind-registry
docker exec -it kind-worker /bin/bash
kind-worker에 접속한 상태에서 pod에 요청을 보내봅니다.
# kind-worker 내부입니다
curl 10.244.1.2:8000
# hello myflask1 1.0
curl 10.244.1.2:8001
# hello myflask2 1.0
curl 10.244.2.2:8000
# hello myflask1 1.0
curl 10.244.2.2:8001
# hello myflask2 1.0
3. node와 pod
k8s의 여러 개념 중에 node와 pod가 있습니다. node는 컨테이너가 올라가기 위한 가상 머신이든, 물리 머신이든 컴퓨터 자체를 의미합니다.
pod는 여러 컨테이너가 묶여서 배포되는 k8s의 가장 작은 단위를 의미합니다.
kind는 docker를 이용하여 worker node를 도커 컨테이너를 이용하여 가상으로 만들어줍니다. 실습한 환경에서는 worker node를 2대를 띄워서 myflask1과 myflask2의 이미지로부터 배포되는 pod를 2대를 생성하여 배포했습니다.
pod의 상세정보를 보면, pod는 분명 다른 node에 띄워져 있습니다. 그렇지만, 임의의 node에 접속해서 컨테이너의 주소로 요청을 보냈을 때 제대로 요청이 도달함을 확인할 수 있었습니다. 어떻게 이게 가능했을까요?
지금까지 kind cluster를 생성했을 때의 네트워크를 그려보면 다음처럼 그릴 수 있습니다.
이 상태에서 myflask1과 2를 각각의 node에 pod로 띄운다면 아래와 같이 표현할 수 있습니다.
먼저 1번의 연결을 확인해 보겠습니다. 1번은 이전 포스팅에서 docker network와 라우트 테이블로 확인할 수 있었습니다. 호스트 머신에서 확인해 보겠습니다.
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 9b690d040473 bridge bridge local
# 5a2e39f8645a host host local
# 38ab5c69257d kind bridge local
# c0c50e70f0f8 none null local
docker network inspect kind
# [
# {
# "Name": "kind",
# "Id": "38ab5c69257d91c3c8bc96b71cc04716b1c6800cfa73adba9f4027210848b5e7",
# "Created": "2023-03-01T23:50:47.046527387+09:00",
# "Scope": "local",
# "Driver": "bridge",
# "EnableIPv6": true,
# "IPAM": {
# "Driver": "default",
# "Options": {},
# "Config": [
# {
# "Subnet": "172.18.0.0/16",
# "Gateway": "172.18.0.1"
# },
# {
# "Subnet": "fc00:f853:ccd:e793::/64",
# "Gateway": "fc00:f853:ccd:e793::1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {
# "29d5d09497db7c7c8ce120c4082e97da38da0b580b5ec4f139c504107a7536ce": {
# "Name": "kind-worker",
# "EndpointID": "8b92e8695c908fa56d701eb3180737ae3991941d056cd7512739ec952979d295",
# "MacAddress": "02:42:ac:12:00:03",
# "IPv4Address": "172.18.0.3/16",
# "IPv6Address": "fc00:f853:ccd:e793::3/64"
# },
# "3dd6c5506cab23adc68d6aefa60e4d63dc3add89e0f54ed7c6363dc7902a34b9": {
# "Name": "kind-worker2",
# "EndpointID": "52dad7fa70c4fe91d12038ed975910f1d12423d762c5541945cc1b31129c0823",
# "MacAddress": "02:42:ac:12:00:02",
# "IPv4Address": "172.18.0.2/16",
# "IPv6Address": "fc00:f853:ccd:e793::2/64"
# },
# "40272643544eac5ddeed33893d8c1c2701be207ec8cbc8907212c149f9759b29": {
# "Name": "kind-registry",
# "EndpointID": "c0aa340320c20100004a65983af29870849b402f9bea6737df52763b75b10ee6",
# "MacAddress": "02:42:ac:12:00:05",
# "IPv4Address": "172.18.0.5/16",
# "IPv6Address": "fc00:f853:ccd:e793::5/64"
# },
# "7d182158d2cab969ca4bd684f2bcf025a7c893f45eec1b731e86588adbbf7b9e": {
# "Name": "kind-control-plane",
# "EndpointID": "7409a463dcc1792cc41df5919b65c2a5be71df4166111f3dfeabfa9ae27d2d66",
# "MacAddress": "02:42:ac:12:00:04",
# "IPv4Address": "172.18.0.4/16",
# "IPv6Address": "fc00:f853:ccd:e793::4/64"
# }
# },
# "Options": {
# "com.docker.network.bridge.enable_ip_masquerade": "true",
# "com.docker.network.driver.mtu": "1500"
# },
# "Labels": {}
# }
# ]
또 라우트 테이블과 브리지 네트워크는 다음과 같습니다.
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
# 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 br-38ab5c69257d
# 192.168.200.0 0.0.0.0 255.255.255.0 U 101 0 0 enp0s8
brctl show
# bridge name bridge id STP enabled interfaces
# br-38ab5c69257d 8000.02426e23f32c no veth0c5f545
# vetha35c1e7
# vethe6d03da
# vethfc9f2dc
# docker0 8000.0242b7985d16 no veth499144f
kind의 클러스터 대역은 172.18을 사용하므로 br-38~~의 네트워크 인터페이스로 전달됩니다. 이 인터페이스는 가상 네트워크 인터페이스인 컨테이너의 네트워크 인터페이스에 연결되어 통신이 가능함을 확인할 수 있습니다.
이제는 node와 pod내부로 들어가서 동작을 살펴보겠습니다. 먼저 docker exec -it kind-worker /bin/bash명령어로 kind-worker 내부에서 route 테이블을 확인해 보겠습니다. 결과로 나온 값을 확인하면 10.244 대역의 IP의 경우 각각의 k8s node로 전달함을 확인할 수 있습니다. 유일하게 다른 것은 10.244.1.2로 그림의 2번의 자신 위에서 동작하는 pod의 대역은 vethfa~~~ 인 인터페이스에 전달하는 것을 확인할 수 있습니다. 다른 pod는 eth0 인터페이스를 타고 밖으로 트래픽이 전달될 것입니다.
docker exec -it kind-worker /bin/bash
# root@kind-worker:/# route
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# default 172.18.0.1 0.0.0.0 UG 0 0 0 eth0
# 10.244.0.0 kind-control-pl 255.255.255.0 UG 0 0 0 eth0
# 10.244.1.2 0.0.0.0 255.255.255.255 UH 0 0 0 vethfa9f1726
# 10.244.2.0 kind-worker2.ki 255.255.255.0 UG 0 0 0 eth0
# 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
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: vethfa9f1726@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# link/ether 8e:e0:70:36:f1:c5 brd ff:ff:ff:ff:ff:ff link-netns cni-76341e7a-b79c-ed5a-898c-e7e8dd5d4e08
# inet 10.244.1.1/32 scope global vethfa9f1726
# valid_lft forever preferred_lft forever
# 26: eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
# valid_lft forever preferred_lft forever
# inet6 fc00:f853:ccd:e793::3/64 scope global nodad
# valid_lft forever preferred_lft forever
# inet6 fe80::42:acff:fe12:3/64 scope link
# valid_lft forever preferred_lft forever
그러면 node에서 임의의 pod에 띄워져 있는 컨테이너에 트래픽을 전달했을 때 응답을 받는 것은 당연해 보입니다. 마찬가지로 pod 내부의 컨테이너에서도 가능할 것 같습니다. 한번 kind-worker위에 올라가 있는 myflask1 컨테이너에서도 다른 컨테이너 주소로 트래픽을 보내봅시다. 먼저 다음 명령어로 접속할 수 있습니다.
kubectl exec -it mygflask-deployment-5df795f9f6-6pjm5 -c myflask1 -- /bin/bash
내부에 들어가서 다음 명령어를 입력해 주겠습니다.
# 컨테이너 내부입니다
apt update -y
apt install net-tools
apt install bridge-utils
apt install -y iproute2
apt install -y iptables
apt install -y curl
route
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# default 10.244.1.1 0.0.0.0 UG 0 0 0 eth0
# 10.244.1.0 10.244.1.1 255.255.255.0 UG 0 0 0 eth0
# 10.244.1.1 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
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: eth0@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# link/ether f6:2d:a6:17:47:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0
# inet 10.244.1.2/24 brd 10.244.1.255 scope global eth0
# valid_lft forever preferred_lft forever
# inet6 fe80::f42d:a6ff:fe17:4777/64 scope link
# valid_lft forever preferred_lft forever
10.244.1의 IP 대역은 게이트웨이, kind-worker에서 확인할 수 있는 가상 네트워크 인터페이스인 veth로 전달됨을 확인할 수 있습니다. 그래서 10.244.2.2의 주소를 입력해도 10.244.1.1의 네트워크 인터페이스에 전달되기 때문에 curl이 동작하지 않을까요? 확인해 보겠습니다.
curl 10.244.1.2:8000
# hello myflask1 1.0
curl 10.244.1.2:8001
# hello myflask2 1.0
curl 10.244.2.2:8000
# hello myflask1 1.0
curl 10.244.2.2:8001
# hello myflask2 1.0
이를 그림으로 그려보면 다음과 같습니다.
4. pod의 컨테이너 간 포트 충돌
이제 pod내부에서 container가 동일한 포트를 사용할 수 있는지 확인해 보겠습니다. myflask2의 app.py를 다음과 같이 포트를 8000으로 수정해 줍니다.
# myflask2/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "hello myflask2 1.0"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
그리고 다시 docker 이미지를 빌드하고 배포해 봅시다.
kind delete cluster
./create-kind-cluster.sh
./deploy-docker-image.sh
kubectl apply -f deployment.yaml
kubectl get pod
# NAME READY STATUS RESTARTS AGE
# myflask-deployment-5df795f9f6-7h8f4 1/2 CrashLoopBackOff 1 (10s ago) 31s
# myflask-deployment-5df795f9f6-jsnpv 1/2 CrashLoopBackOff 1 (10s ago) 31s
사용할 수 없는 이유는 pod의 컨테이너에 직접 접속하면 확인할 수 있습니다. ip a 명령어로 네트워크 인터페이스를 확인해 보면 네트 eth0가 서로 같음을 확인할 수 있습니다. MAC주소와 IP주소가 모두 같습니다. 즉, 동일한 네트워크 인터페이스 위에 올라가 있는 것입니다.
docker 컨테이너는 서로 다른 인터페이스를 사용했는데 왜 pod의 컨테이너는 같은 인터페이스를 사용할까요? 이 이유는 pod가 생성될 때 제일 먼저 생성되는 컨테이너로 pause라는 컨테이너가 뜨고 pod의 인프라를 담당해 주기 때문입니다. 이 컨테이너는 pod 내부의 컨테이너에게 network namespace을 공유합니다. 그렇기에 pod 내부의 컨테이너는 같은 네트워크 인터페이스를 사용하여 통신하게 됩니다. 심지어 컨테이너 내부에서 localhost로 다른 컨테이너에 접근할 수 있습니다.
실습 환경 제거
kind delete cluster
docker rm -f kind-registry
지금까지 확인해 봤을 때 얻을 수 있는 결론은 다음과 같습니다.
- pod와 node에 k8s가 알아서 다른 pod에 접근할 수 있는 주소를 세팅해 준다.
- pod 내부의 컨테이너는 자원을 공유할 수 있도록 설계되어 있다.
- pod는 이렇게 공유되는 자원이 있는 단위로 묶인다.
- 한 pod내에 동일 포트를 사용할 수 없는데 pause 컨테이너가 인터페이스를 공유해 주기 때문이다.
궁금한 점
- 어떻게 k8s의 내부에서 알아서 IP와 인터페이스를 세팅해 주는가?
- pause 컨테이너는 어떻게 인터페이스를 공유해 주는가? -> 이것도 ns의 느낌이...
- 그러면 pod가 제거되고 다시 생성되면 IP가 달라질 수 있는데? -> service
참고자료
- https://coffeewhale.com/k8s/network/2019/04/19/k8s-network-01/
'개발 > docker, k8s, CNCF' 카테고리의 다른 글
golang, openTelemetry, gRPC, zipkin hands-on (33) | 2023.10.12 |
---|---|
docker, k8s 네트워크 뜯기(6) - docker network none 상태에서 외부랑 통신해보자 (0) | 2023.03.09 |
docker, k8s 네트워크 뜯기(4) - local registry로 kind k8s cluster에 배포하기 (0) | 2023.03.05 |
docker, k8s 네트워크 뜯기(3) - docker 컨테이너는 어떻게 서로 통신할까? (0) | 2023.03.05 |
docker, k8s 네트워크 뜯기(2) - docker, kind, helm, k9s 설치 (0) | 2023.03.05 |
댓글