컨테이너 내부에 Docker를 사용하는 것을 의미한다.
그 중 Kind는 Node역할을 하는 Container안에서 또 컨테이너를 띄워 클러스터를 구성하는 방식으로, 손쉽게 클러스터를 생성하고 테스트 용도로 활용할 수 있다.
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
...
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
각 레이어는 다음과 같은 역할을 한다.
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
각 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를 매핑하여 사용할 수 있다.
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
네트워크 정보 확인
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
클러스터를 생성할 때 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>