Kind를 통한 Docker IN Docker Kubernetes Cluster 환경 구성

Gyullbb·2024년 9월 7일
1

K8S

목록 보기
4/13

Docker In Docker란

컨테이너 내부에 Docker를 사용하는 것을 의미한다.
그 중 Kind는 Node역할을 하는 Container안에서 또 컨테이너를 띄워 클러스터를 구성하는 방식으로, 손쉽게 클러스터를 생성하고 테스트 용도로 활용할 수 있다.

Kind를 통한 multi Control-Plane + Worker Cluster 구성

1.클러스터 생성

Kind 설치 후 아래 yaml을 활용하여 클러스터를 생성한다.

# brew install kind

# cat <<EOT> kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
EOT

# kind create cluster --config kind.yaml

클러스터를 생성하면 Node의 역할을 하는 컨테이너(control-plane, worker)와 LB의 역할을 하는 컨테이너(external-load-balancer)가 생성되고, Node역할의 컨테이너에 Docker와 Kubernetes 리소스가 설치된다.
이 때, MAC에서는 별도의 지정을 하지 않으면 ~/.kube/config 하위에 클러스터의 kubeconfig가 생성되는데, api server 주소 를 확인해보면 localhost:{{external-load-balancer port}}로 지정이 되어 있다.

docker ps로 컨테이너 설정을 확인 해보면, kind-external-load-balancer Pod에서 특정 port로 트래픽이 들어오면 그 트래픽을 Pod내부의 6443포트 즉, kube-apiserver로 전달을 하고 있다.

이 api server 설정을 통해서 kubectl로 생성된 클러스터의 상세 정보를 확인할 수 있다.

# kubectl get nodes                                                                       
NAME                  STATUS   ROLES           AGE     VERSION
kind-control-plane    Ready    control-plane   6h30m   v1.27.3
kind-control-plane2   Ready    control-plane   6h30m   v1.27.3
kind-control-plane3   Ready    control-plane   6h29m   v1.27.3
kind-worker           Ready    <none>          6h29m   v1.27.3

# docker ps                            
CONTAINER ID   IMAGE                                COMMAND                  CREATED       STATUS       PORTS                                  NAMES
92c0d9e82326   kindest/haproxy:v20230606-42a2262b   "haproxy -W -db -f /…"   7 hours ago   Up 7 hours   127.0.0.1:64936->6443/tcp              kind-external-load-balancer
f783aa78721d   kindest/node:v1.27.3                 "/usr/local/bin/entr…"   7 hours ago   Up 7 hours   127.0.0.1:64933->6443/tcp              kind-control-plane2
251c2a851d6b   kindest/node:v1.27.3                 "/usr/local/bin/entr…"   7 hours ago   Up 7 hours   0.0.0.0:31000-31001->31000-31001/tcp   kind-worker
8c134c320691   kindest/node:v1.27.3                 "/usr/local/bin/entr…"   7 hours ago   Up 7 hours   127.0.0.1:64934->6443/tcp              kind-control-plane3
be1adbe4267b   kindest/node:v1.27.3                 "/usr/local/bin/entr…"   7 hours ago   Up 7 hours   127.0.0.1:64935->6443/tcp              kind-control-plane

# cat ~/.kube/config
- cluster:
    certificate-authority-data: LS0tLS1...
    server: https://127.0.0.1:64936
  name: kind-kind
  ...

2. multi Control-Plane 구조 확인

Kind에서 여러개의 Control Plane을 지정하여 클러스터를 생성하면 Node역할을 하는 컨테이너 외에 external-load-balancer라는 이름의 컨테이너가 하나 더 생성된다.

이 컨테이너는 Control Plane 간의 통신을 위한 LB역할을 하는 컨테이너이다.

이 컨테이너에 직접 접근해서 설정을 확인하려 하면 아래와 같은 에러가 뜬다.

# docker exec -it kind-external-load-balancer /bin/sh
OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown

kindest/haproxy 이미지를 확인해보면 ENTRYPOINT설정 외 쉘 명령이나 패키지 설치 기록이 없는 이미지로, 필요한 haproxy 역할 외 다른 기능으로 사용하지 못하도록 하고 있다. (Distroless 이미지 형식과 유사.)

# docker history kindest/haproxy:v20230606-42a2262b --no-trunc
IMAGE                                                                     CREATED         CREATED BY                                                                    SIZE      COMMENT
sha256:816da8fb6d42bb9e25f1b0a21a7d8c81b5334c53b13fcdc61ef6a9961574d702   15 months ago   ENTRYPOINT ["haproxy" "-W" "-db" "-f" "/usr/local/etc/haproxy/haproxy.cfg"]   0B        buildkit.dockerfile.v0
<missing>                                                                 15 months ago   STOPSIGNAL SIGUSR1                                                            0B        buildkit.dockerfile.v0
<missing>                                                                 15 months ago   COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg # buildkit                916B      buildkit.dockerfile.v0
<missing>                                                                 15 months ago   COPY /opt/stage/ / # buildkit                                                 13MB      buildkit.dockerfile.v0
<missing>                                                                 15 months ago   ARG STAGE_DIR=/opt/stage                                                      0B        buildkit.dockerfile.v0
<missing>                                                                 N/A                                                                                           219kB     
<missing>                                                                 N/A                                                                                           346B      
<missing>                                                                 N/A                                                                                           497B      
<missing>                                                                 N/A                                                                                           0B        
<missing>                                                                 N/A                                                                                           64B       
<missing>                                                                 N/A                                                                                           149B      
<missing>                                                                 N/A                                                                                           1.93MB    
<missing>                                                                 N/A                                                                                           29.4kB    
<missing>                                                                 N/A                                                                                           270kB     

각 레이어는 다음과 같은 역할을 한다.

  • ARG STAGE_DIR=/opt/stage : Dockerfile에서 빌드 시 /opt/stage를 사용하여 빌드한다.
  • COPY /opt/stage/ / # buildkit : STAGE_DIR 하위 파일들을 컨테이너 파일시스템의 /로 복사한다.
  • COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg # buildkit : haproxy.cfg 설정을 /usr/local/etc/haproxy 하위로 복사한다.
  • STOPSIGNAL SIGUSR1 : 컨테이너가 종료될 때 기본으로는 SIGTERM을 사용하지만, STOPSIGNAL을 통해 컨테이너 종료 시그널을 변경할 수 있다. HAProxy는 SIGUSR1을 받으면 Graceful Stop을 수행하는 기능을 가지고 있다. 즉, HAProxy가 즉시 종료되지 않고, 요청을 모두 처리한 후 종료될 수 있도록 하는 설정이다.

    HAProxy supports a graceful and a hard stop. The hard stop is simple, when the SIGTERM signal is sent to the haproxy process, it immediately quits and all established connections are closed. The graceful stop is triggered when the SIGUSR1 signal is sent to the haproxy process. It consists in only unbinding from listening ports, but continue to process existing connections until they close. Once the last connection is closed, the process leaves.
    https://cbonte.github.io/haproxy-dconv/2.0/management.html#4

  • ENTRYPOINT ["haproxy" "-W" "-db" "-f" "/usr/local/etc/haproxy/haproxy.cfg"] : 컨테이너가 시작되면 HAPROXY를 복사한 haproxy 설정으로 실행한다.

각 Control-Plane(사실은 Pod)에 들어가서 kubernetes/admin.conf 설정을 확인해본다.

# docker exec -it kind-control-plane /bin/sh
# cat /etc/kubernetes/admin.conf
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS...
    server: https://kind-external-load-balancer:6443
  name: kind

admin.conf 설정에서 api-server 주소로 external-load-balancer의 Pod:6443을 바라보고 있는 것을 확인할 수 있다.
Control Plane(pod)에서 해당 Pod Name의 dns lookup을 확인해본다.

# apt-update && apt install dnsutils
# nslookup kind-external-load-balancer
Server:         192.168.65.254
Address:        192.168.65.254#53

Non-authoritative answer:
Name:   kind-external-load-balancer
Address: 172.18.0.6
Name:   kind-external-load-balancer
Address: fc00:f853:ccd:e793::6

Control Plane(pod)에서 Load Balancer Pod의 DNS를 인식할 수 있는 이유는 Kind 네트워크와 Docker 내장 DNS서버때문이다.
Docker 네트워크 리스트와 Kind 네트워크에 연결된 컨테이너를 확인해본다.

# docker network ls                         
NETWORK ID     NAME      DRIVER    SCOPE
adcd45d03761   bridge    bridge    local
5729ccff2cc6   host      host      local
8f9eb4754dc5   kind      bridge    local
510f916ad4df   none      null      local

# docker network inspect kind
[
    {
        "Name": "kind",
        "Id": "8f9eb4754dc5ec22fb20fc9a39371217916d86246f7cdd66276a595cdd08b2ce",
        "Created": "2024-09-01T11:15:53.672353221Z",
        "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"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "251c2a851d6b977425ce387c088525ae1b42e5d9c4e9c1c92cf98c6df8eb03e6": {
                "Name": "kind-worker",
                "EndpointID": "9f1e99f5b8fac4cd728c9aad7fa231c6e8d95e5edb57c7627782ed3e55c741b8",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": "fc00:f853:ccd:e793::2/64"
            },
            "8c134c320691e72191fa0826a696ff013cfd600c41413e01307ef9cc2c8db35f": {
                "Name": "kind-control-plane3",
                "EndpointID": "64cf903b80680ee12eaf6dc79abcd10b265833fe6e4f0d2e87977457e145810d",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": "fc00:f853:ccd:e793::3/64"
            },
            "92c0d9e82326fad89c789211856ad9dc03da0f9b8f70beca086e291f31e0aac3": {
                "Name": "kind-external-load-balancer",
                "EndpointID": "c28b1bec9d281a9f3011fa2ca0e0ac26c7fc238dcd98c1915a42fe53921924ba",
                "MacAddress": "02:42:ac:12:00:06",
                "IPv4Address": "172.18.0.6/16",
                "IPv6Address": "fc00:f853:ccd:e793::6/64"
            },
            "be1adbe4267bc775bcfa537d26e8cb44ed88fba3794517d67762e40993694dbd": {
                "Name": "kind-control-plane",
                "EndpointID": "3db490ea8a13c39ba0f6f4e6d96607a3a25596e587e67f3a877dc32efdfb17f5",
                "MacAddress": "02:42:ac:12:00:04",
                "IPv4Address": "172.18.0.4/16",
                "IPv6Address": "fc00:f853:ccd:e793::4/64"
            },
            "f783aa78721d2218d28f08ebfe007da951b7e843faca2e453b2c01e7ed45b386": {
                "Name": "kind-control-plane2",
                "EndpointID": "a14df9bcc08714558729021735ade7c22d58723f44842fd652e8f359c077a840",
                "MacAddress": "02:42:ac:12:00:05",
                "IPv4Address": "172.18.0.5/16",
                "IPv6Address": "fc00:f853:ccd:e793::5/64"
            }
        },
        "Options": {
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

Kind 네트워크에 kind-control-plane, kind-control-plane2, kind-control-plane3, kind-worker, kind-external-load-balancer이 연결되어 있고, 각각의 MACAddr, IPv4Addr, IPv6Addr이 명시되어 있다.
Docker의 내장 DNS 서버에 의해 Pod명과 Pod IP를 매핑하여 사용할 수 있다.

cf) host.docker.internal

kind-control-plane에서 /etc/resolv.conf를 확인해보았을 때, nameserver가 192.168.65.254로 잡혀있는 것을 확인할 수 있다. 이는 MAC에서 Docker Desktop을 실행했을 경우, 컨테이너에서 호스트 머신의 IP 주소를 참조할 수 있는 DNS 이름인 host.docker.internal의 IP이다.

# docker exec -it kind-control-plane /bin/sh
# cat /etc/resolv.conf
nameserver 192.168.65.254
options ndots:0

# nslookup host.docker.internal
Server:         192.168.65.254
Address:        192.168.65.254#53

Non-authoritative answer:
Name:   host.docker.internal
Address: 192.168.65.254

컨테이너에서 /etc/resolv.conf를 Docker의 기본 내장 DNS인 127.0.0.11에서 192.168.65.254로 바꾸는 작업은 entrypoint에 명시되어있다.

# docker exec -it kind-control-plane /bin/sh
# cat /usr/local/bin/entrypoint
...
enable_network_magic(){
  # well-known docker embedded DNS is at 127.0.0.11:53
  local docker_embedded_dns_ip='127.0.0.11'

  # first we need to detect an IP to use for reaching the docker host
  local docker_host_ip
  docker_host_ip="$( (head -n1 <(timeout 5 getent ahostsv4 'host.docker.internal') | cut -d' ' -f1) || true)"
  # if the ip doesn't exist or is a loopback address use the default gateway
  if [[ -z "${docker_host_ip}" ]] || [[ $docker_host_ip =~ ^127\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    docker_host_ip=$(ip -4 route show default | cut -d' ' -f3)
  fi

  # patch docker's iptables rules to switch out the DNS IP
  iptables-save \
    | sed \
      # switch docker DNS DNAT rules to our chosen IP \
      -e "s/-d ${docker_embedded_dns_ip}/-d ${docker_host_ip}/g" \
      # we need to also apply these rules to non-local traffic (from pods) \
      -e 's/-A OUTPUT \(.*\) -j DOCKER_OUTPUT/\0\n-A PREROUTING \1 -j DOCKER_OUTPUT/' \
      # switch docker DNS SNAT rules rules to our chosen IP \
      -e "s/--to-source :53/--to-source ${docker_host_ip}:53/g"\
      # nftables incompatibility between 1.8.8 and 1.8.7 omit the --dport flag on DNAT rules \
      # ensure --dport on DNS rules, due to https://github.com/kubernetes-sigs/kind/issues/3054 \
      -e "s/p -j DNAT --to-destination ${docker_embedded_dns_ip}/p --dport 53 -j DNAT --to-destination ${docker_embedded_dns_ip}/g" \
    | iptables-restore

  # now we can ensure that DNS is configured to use our IP
  cp /etc/resolv.conf /etc/resolv.conf.original
  replaced="$(sed -e "s/${docker_embedded_dns_ip}/${docker_host_ip}/g" /etc/resolv.conf.original)"
  if [[ "${KIND_DNS_SEARCH+x}" == "" ]]; then
    # No DNS search set, just pass through as is
    echo "$replaced" >/etc/resolv.conf
  elif [[ -z "$KIND_DNS_SEARCH" ]]; then
    # Empty search - remove all current search clauses
    echo "$replaced" | grep -v "^search" >/etc/resolv.conf
  else
    # Search set - remove all current search clauses, and add the configured search
    {
      echo "search $KIND_DNS_SEARCH";
      echo "$replaced" | grep -v "^search";
    } >/etc/resolv.conf
  fi

코드에는 iptables 규칙을 패치하는 부분이 있는데, iptables를 통해 규칙 반영을 확인할 수 있다.
Docker의 내장 DNS 서버 IP는 127.0.0.11이며, 192.168.65.254로 들어오는 DNS요청을 127.0.0.11의 포트 39871 (TCP) 및 50966 (UDP)으로 DNAT 하고, 127.0.0.11의 포트 39871 및 50966에서 나오는 DNS 응답을 192.168.65.254로 SNAT 한다.

# docker exec -it kind-control-plane /bin/sh

# iptables -t nat -S | grep 192
-A PREROUTING -d 192.168.65.254/32 -j DOCKER_OUTPUT
-A OUTPUT -d 192.168.65.254/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 192.168.65.254/32 -j DOCKER_POSTROUTING
-A DOCKER_OUTPUT -d 192.168.65.254/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:39871
-A DOCKER_OUTPUT -d 192.168.65.254/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:50966
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 39871 -j SNAT --to-source 192.168.65.254:53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 50966 -j SNAT --to-source 192.168.65.254:53

host.docker.internal은 컨테이너 내부에서 Host에 접근할 수 있게 하는 설정으로 Linux가 아닌 Window/MAC 환경에서 Docker Desktop을 띄우는 경우 들어가는 기본 설정이다.

kind-control-plane를 활용해서 컨테이너 내부에서 Host로 직접 통신이 가능한 것을 확인해볼 수 있다.

컨테이너 내부에서는 kube-apiserver의 포트인 6443을 통해 localhost:6443으로 통신이 가능하지만,이 때 컨테이너 내부에서 Host의 portforwarding용 포트인 64936로 직접 접속을 시도하면 당연히 통신이 되지 않는다.

host.docker.internal:64936로 접속을 시도하면 통신이 되는데, 이는 위에 설명한 설정들로 인해 컨테이너 내부에서 Host에 접근할 수 있기 때문이다.

# docker ps | grep kind 
92c0d9e82326   kindest/haproxy:v20230606-42a2262b   "haproxy -W -db -f /…"   10 hours ago   Up 10 hours   127.0.0.1:64936->6443/tcp              kind-external-load-balancer
...

# docker exec -it kind-control-plane curl -k https://localhost:6443/livez ;echo
ok

# docker exec -it kind-control-plane curl -k https://localhost:64936/livez ;echo
curl: (7) Failed to connect to localhost port 64936: Connection refused

# docker exec -it kind-control-plane curl -k https://host.docker.internal:64936/livez ;echo
ok

3. Control-Plane 추가 정보 확인

네트워크 정보 확인

root@kind-control-plane:/# ip -br -c -4 addr
lo               UNKNOWN        127.0.0.1/8 
veth1ad4d02b@if4 UP             10.244.0.1/32 
veth935c07f1@if4 UP             10.244.0.1/32 
veth5c6482c3@if4 UP             10.244.0.1/32 
eth0@if28        UP             172.18.0.4/16 

root@kind-control-plane:/# ip -c route
default via 172.18.0.1 dev eth0 
10.244.0.2 dev veth1ad4d02b scope host 
10.244.0.3 dev veth935c07f1 scope host 
10.244.0.4 dev veth5c6482c3 scope host 
10.244.1.0/24 via 172.18.0.5 dev eth0 
10.244.2.0/24 via 172.18.0.3 dev eth0 
10.244.3.0/24 via 172.18.0.2 dev eth0 
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.4 

프로세스 확인 -> PID 1은 /sbin/init이다.

root@kind-control-plane:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 08:42 ?        00:00:07 /sbin/init=

DinD 컨테이너 확인

root@kind-control-plane:/# crictl version
Version:  0.1.0
RuntimeName:  containerd
RuntimeVersion:  v1.7.1
RuntimeApiVersion:  v1

root@kind-control-plane:/# crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD
3441550cbf2dc       6234a065dec4c       11 hours ago        Running             kube-scheduler            1                   71cbab0fc2502       kube-scheduler-kind-control-plane
1c38faa646c43       aea4f169db16d       11 hours ago        Running             kube-controller-manager   1                   b66a90e7e4e32       kube-controller-manager-kind-control-plane
f2877ad64ab44       97e04611ad434       11 hours ago        Running             coredns                   0                   d9b94fd54fa61       coredns-5d78c9869d-f2k4x
bf6cc889eae28       97e04611ad434       11 hours ago        Running             coredns                   0                   cf2988f63fe8d       coredns-5d78c9869d-bq4s2
1e7cb2240f203       eec7db0a07d0d       11 hours ago        Running             local-path-provisioner    0                   a2afb342ffb08       local-path-provisioner-6bc4bddd6b-2nwbx
7ab97a2daa31f       b18bf71b941ba       11 hours ago        Running             kindnet-cni               0                   d98e993387c86       kindnet-29rgk
dd048a591c6d7       278dd40f83dfb       11 hours ago        Running             kube-proxy                0                   cfb9fd3d19f32       kube-proxy-dqc4t
6824a0ee4ea01       24bc64e911039       11 hours ago        Running             etcd                      0                   658f527c26814       etcd-kind-control-plane
e1820cead3fd8       634c53edb5c14       11 hours ago        Running             kube-apiserver            0                   af772ba71196f       kube-apiserver-kind-control-plane

4. Worker Node 추가 정보 확인

클러스터를 생성할 때 WorkerNode에 hostPort와 containerPort 매핑 정보를 추가하여 생성하였다.

# docker port kind-worker
31000/tcp -> 0.0.0.0:31000
31001/tcp -> 0.0.0.0:31001

nginx Deployment와 Service를 배포하여 포트 매핑을 확인해본다.

cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-websrv
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: deploy-websrv
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: deploy-websrv
spec:
  ports:
    - name: svc-webport
      port: 80
      targetPort: 80
      nodePort: 31001
  selector:
    app: deploy-websrv
  type: NodePort
EOF

# kubectl get deploy,svc,ep deploy-websrv
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-websrv   2/2     2            2           38s

NAME                    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/deploy-websrv   NodePort   10.96.235.169   <none>        80:31001/TCP   38s

NAME                      ENDPOINTS                     AGE
endpoints/deploy-websrv   10.244.3.3:80,10.244.3.4:80   38s

# curl -s localhost:31001 | grep -o "<title>.*</title>"
<title>Welcome to nginx!</title>

0개의 댓글