TL DR;
- Minikube 에서 호스트머신과 클러스터내부 통신시 service/tunnel 명령어 사용
- k8s 노드가 ssh 터널링을 통해 호스트머신과 클러스터 내부를 연결함
- kubectl port-forward의 경우 api-server 및 kubelet이 프록시역할 수행
1. Intro
로컬에서 간단히 Kubernetes 환경을 테스트 할 경우, VM을 생성하여 직접 구성하는 대신 Minikube, Kind 등과 같이 컨테이너 기반의 자동화된 클러스터 구성 도구를 사용하여 테스트 하곤 한다.
해당 도구로 생성된 클러스터의 경우, 클러스터 내부의 동작 (k8s 내 오브젝트 생성 편집 삭제 등)을 확인하고 테스트하기엔 적합하다. 그러나 호스트와 직접 연결된 VM 네트워크 대신 호스트와 격리된 컨테이너 네트워크로 분리되어있어 로컬환경에서 직접 클러스터 내부의 리소스를 호출을 바로 할 수 없는 불편함이 있다
이를 수행하기 위해, Minikube에서는 service/tunnel 명령어를 통해 호스트 머신<>클러스터간 통신을 설정할 수 있고, Kind에서는 LoadBalancer,extraPortMapping 등을 통해 로컬에서 클러스터간 연결을 설정할 수 있다.
대부분의 경우 간단히 명령어를 통해 설정 및 수행되기에, 그 뒷 단에 무엇이 어떻게 작동하는지 인지하지 못하고 있었다. 본 글에서는 Minikube를 기반으로, 명령어 실행시 어떤 절차가 수행되기에 격리된 컨테이너 내부 환경으로 접근이 가능해지는지 확인해 보고자 한다
Accessing apps https://minikube.sigs.k8s.io/docs/handbook/accessing/
환경 구성하기
- OS > Windows (Mac으로 진행하여도 무방)
- Driver > Docker Desktop
- Minikube v1.35.0
- minikube 2 node cluster 생성
minikube start --nodes 2
2. Accessing apps
https://minikube.sigs.k8s.io/docs/handbook/accessing/
Minikube 공식 문서의 Accessing Apps를 참고하면, k8s에서 클러스터 외부노출을 위해서는 NodePort/LoadBalancer 유형의 Service 오브젝트를 사용한다. 이를 확인하면 다음과 같다
- 샘플 앱 배포 및 연결 설정
# 샘플 echo deploy 생성
kubectl create deployment hello-minikube1 --image=kicbase/echo-server:1.0
# deploy에 NodePort 유형의 service 생성
kubectl expose deployment hello-minikube1 --type=NodePort --port=8080 --name np-svc
# deploy에 LoadBalancer 유형의 service 생성
kubectl expose deployment hello-minikube1 --type=LoadBalancer --port=8080 --name lb-svc
# 생성 확인
kubectl get service
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d23h
# lb-svc LoadBalancer 10.101.174.224 <pending> 8080:30910/TCP 3d22h
# np-svc NodePort 10.96.210.158 <none> 8080:30849/TCP 3d22h
생성된 각 Service object에 대해서, 로컬에서 연결을 수행하려면 minikube service <svcName> 또는 minikube tunnel로 요청을 테스트할 수 있다.
# minikube service <svcName>
minikube service lb-svc
# |-----------|--------|-------------|---------------------------|
# | NAMESPACE | NAME | TARGET PORT | URL |
# |-----------|--------|-------------|---------------------------|
# | default | lb-svc | 8080 | http://192.168.49.2:30910 |
# |-----------|--------|-------------|---------------------------|
# 🏃 Starting tunnel for service lb-svc.
# |-----------|--------|-------------|------------------------|
# | NAMESPACE | NAME | TARGET PORT | URL |
# |-----------|--------|-------------|------------------------|
# | default | lb-svc | | http://127.0.0.1:39357 |
# |-----------|--------|-------------|------------------------|
# 🎉 Opening service default/lb-svc in default browser...
# 👉 http://127.0.0.1:39357
# ❗ Because you are using a Docker driver on linux, the terminal needs to be open to run it.
# 요청 보내기
curl http://127.0.0.1:39357
# Request served by hello-minikube1-68d8f56889-xnvjb
#
# HTTP/1.1 GET /
#
# Host: 127.0.0.1:39357
# Accept: */*
# User-Agent: curl/7.68.0
# minikube tunnel
✅ Tunnel successfully started
📌 NOTE: Please do not close this terminal as this process must stay alive for the tunnel to be accessible ...
# lb-service External IP 조회 > External IP가 127.0.0.1로 설정됨
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d23h
lb-svc LoadBalancer 10.101.174.224 127.0.0.1 8080:30910/TCP 3d22h
np-svc NodePort 10.96.210.158 <none> 8080:30849/TCP 3d22h
# 요청 보내기
curl localhost:8080
# Request served by hello-minikube1-68d8f56889-xnvjb
#
# HTTP/1.1 GET /
#
# Host: localhost:8080
# Accept: */*
# User-Agent: curl/7.68.0
위와 같이, minikube service 또는 minikube tunnel 같은 명령어를 사용하면 호스트머신에서 k8s 서비스를 통해 클러스터 내부 pod와 통신이 됨을 확인할 수 있다.
그러면 명령어 실행시 어떤 조작이 가해졌기에 통신이 이루어지게 된걸까? 왜 해당 명령어의 프로세스가 실행될 동안만 작동하게 되는걸까? 문서에서는 이에 대해 자세히 설명해주지 않는다.
3. How does it works?
정답은 ssh 터널링이다. 해당 명령어 사용시, 노드에 대해 ssh 터널링이 수행되어 호스트머신의 요청을 컨테이너노드가 받아 컨테이너 내부의 클러스터로 전달되게 된다.
minikube service
앞서 보았듯이, minikube service <svcName>을 수행하면 http://NodeIP:NodePort에서 http://127.0.0.1:ForwaredPort 총 2번의 portforwarding 이 수행된다. lsof 명령어를 사용하여 해당 포트를 사용하는 프로세스를 식별할 수 있고, 이는 컨테이너 노드에 대해 ssh 터널링을 수행하는 프로세스이다.
# minikube service 실행
minikube service lb-svc
# ❯ |-----------|--------|-------------|---------------------------|
# | NAMESPACE | NAME | TARGET PORT | URL |
# |-----------|--------|-------------|---------------------------|
# | default | lb-svc | 8080 | http://192.168.49.2:30910 |
# |-----------|--------|-------------|---------------------------|
# 🏃 Starting tunnel for service lb-svc.
# |-----------|--------|-------------|------------------------|
# | NAMESPACE | NAME | TARGET PORT | URL |
# |-----------|--------|-------------|------------------------|
# | default | lb-svc | | http://127.0.0.1:44643 |
# |-----------|--------|-------------|------------------------|
# 🎉 Opening service default/lb-svc in default browser...
# 👉 http://127.0.0.1:44643
# ❗ Because you are using a Docker driver on linux, the terminal needs to be open to run it.
# 44643에 해당하는 PID(Process ID)찾기 > PID 14203
lsof -i :44643
# ❯ lsof -i :44643
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# ssh 14203 nasir 4u IPv6 183027 0t0 TCP ip6-localhost:44643 (LISTEN)
#ssh 14203 nasir 5u IPv4 183028 0t0 TCP localhost:44643 (LISTEN)
# PID 14203 명령어 내용 확인
ps aux | grep 14203
# nasir 14203 0.0 0.0 12252 5520 pts/2 SN 20:41 0:00 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 44643:10.101.174.224:8080
# service의 클러스터 내 Internal IP > 10.101.174.224
kgs
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d23h
# lb-svc LoadBalancer 10.101.174.224 <pending> 8080:30910/TCP 3d23h
# np-svc NodePort 10.96.210.158 <none> 8080:30849/TCP 3d23h
minikube service <svcName> 명령어는 결국 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 44643:10.101.174.224:8080 명령어와 동일하며, 해당 명령어를 통해 ssh 터널링이 활성화되어있어야 외부의 요청이 노드를 타고 클러스터 내부로 전달될 수 있다
ssh
- -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes
- ssh 접속 시 known_hosts 같은 보안경고 출력 방지
- -N
- man ssh | grep “-N” > Do not execute a remote command. This is useful for just forwarding ports.
- ssh 접속을 통해 명령어 대신 터널링만 수행하고자 함
- docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa
- 접속 대상 및 방법
- minikube에서 생성한 id_rsa 키를 사용하여, 32772포트를 통해 127.0.0.1 머신의 docker user로 접근
- -L 44643:10.101.174.224:8080
- 호스트머신의 44643 포트를, 클러스터내 서비스의 ClusterIP:TargetPort로 연결
이는 kubernetes 노드로 사용되는 container의 정보에 매핑된 port 정보를 확인하여 재확인할 수 있다.
# minikube 이름의 컨테이너의, 네트워크 설정 중 port 부분만 조회
docker inspect minikube | jq '.[0].NetworkSettings.Ports'
# {
# "22/tcp": [
# {
# "HostIp": "127.0.0.1",
# "HostPort": "32772"
# }
# ],
# "2376/tcp": [
# {
# "HostIp": "127.0.0.1",
# "HostPort": "32771"
# }
# ],
# "32443/tcp": [
# {
# "HostIp": "127.0.0.1",
# "HostPort": "32768"
# }
# ],
# "5000/tcp": [
# {
# "HostIp": "127.0.0.1",
# "HostPort": "32770"
# }
# ],
# "8443/tcp": [
# {
# "HostIp": "127.0.0.1",
# "HostPort": "32769"
# }
# ]
# }
위와같이, minikube 컨테이너의 ssh 포트(22)는 127.0.0.1의 32772에 매핑되어있음을 확인할 수 있다. 그외에 docker daemon 과의 통신을 위한 2376, k8s api-server와의 통신을 위한 8443 포트등의 각각의 로컬포트로 매핑되어있어 호스트머신과 통신할 수 있음을 확인할 수 있다.
따라서 우리가 호스트머신에서 curl 127.0.0.1:44643을 수행한다면 다음의 프로세스를 거친다고 할 수 있다.
- curl localhost:44643 요청 (로컬에서)
- 로컬 포트 44643 > SSH 클라이언트가 미리 설정한 터널로 전달
- SSH 클라이언트는 로컬 44643 요청을 SSH 연결을 통해 minikube 노드(127.0.0.1:32772)로 보냄
- minikube 노드에 SSH 데몬(22/tcp)이 있고, 해당 데몬이 받은 포워딩 요청을 ClusterIP:TargetPort로 전달
- 결과는 역방향으로 다시 돌아와서 curl 응답에 전달
minikube tunnel
minikube service는 개별 서비스 오브젝트에 대해 ssh 터널링을 수행해 요청을 전달한다. 그러면 minikube tunnel 은 어떻게 작동할까?
무언가 다르지 않을까 기대했지만 tunnel의 경우도 명령어를 실행하면, LoadBalancer type의 k8s svc에 대해 ssh 터널링을 통해 트래픽을 전달한다.
# tunnel 실행 (별도 터미널)
minikube tunnel
# 실행 이후 8080포트를 사용하는 PID 조회 > PID 31417의 ssh command
lsof -i :8080
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# ssh 31417 nasir 4u IPv6 342300 0t0 TCP ip6-localhost:http-alt (LISTEN)
# ssh 31417 nasir 5u IPv4 342301 0t0 TCP localhost:http-alt (LISTEN)
# PID 31417 process 정보 조회
ps aux | grep 31417
# nasir 31417 0.0 0.0 12252 5468 pts/2 SN 21:27 0:00 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 8080:10.101.174.224:8080
# LoadBalancer 유형의, 다른 포트를 사용하는 svc 추가 생성
kubectl expose deployment hello-minikube1 --type=LoadBalancer --port=8081 --name lb-svc-8081
kubectl expose deployment hello-minikube1 --type=LoadBalancer --port=8082 --name lb-svc-8082
# ssh 터널링 추가생성 조회 > 각 svc에 대한 ssh 터널 자동 추가
ps aux | grep 808
# nasir 31417 0.0 0.0 12252 5468 pts/2 SN 21:27 0:00 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 8080:10.101.174.224:8080
# nasir 36011 0.0 0.0 12252 5636 pts/2 SN 21:41 0:00 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 8081:10.96.241.125:8081
# nasir 36098 0.0 0.0 12252 5472 pts/2 SN 21:41 0:00 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -N docker@127.0.0.1 -p 32772 -i /home/nasir/.minikube/machines/minikube/id_rsa -L 8082:10.111.17.75:8082
minikube service와 마찬가지로, minikube tunnel 또한 동일한 ssh 터널링을 통해 구현된다.
다만 차이점은 기존 EXTERNAL-IP가 <pending>상태에서 로컬호스트의 IP인 127.0.0.1로 변경되는것인데 이것은 명령어 실행시 동적으로 변경되는게 아니라.. 명령어 실행시 호스트의 정보를 긁어서 kube-apiserver로 던져서 변경(patch)하는듯 하다. 따라서 명령어를 종료하여도 다시 pending으로 돌아오진 않고, 실행시마다 긁어서 던지는듯함
- tunnel.go
- https://github.com/kubernetes/minikube/blob/master/cmd/minikube/cmd/tunnel.go#L117
- tunnel-manager.go func StartTunnel
- https://github.com/kubernetes/minikube/blob/master/pkg/minikube/tunnel/tunnel_manager.go#L61-L67
- func newTunnel
- https://github.com/kubernetes/minikube/blob/master/pkg/minikube/tunnel/tunnel.go#L47-L83
- cluster_inspector.go func getStateAndRoute()
- https://github.com/kubernetes/minikube/blob/master/pkg/minikube/tunnel/cluster_inspector.go#L61C1-L80
4. kubectl port-forward
minikube에서 제공하는 명령어세트인 minikube service/tunnel외에, 클러스터 내부로 통신하기 위해서 kubectl 명령어세트에 포함된 port-forward를 사용할 수도 있다.
# 로컬 18080 포트를 k8s내 서비스의 targetPort(8080)으로 포트포워딩 (터미널 유지 필요)
kubectl port-forward service/lb-svc 18080:8080
# Forwarding from 127.0.0.1:18080 -> 8080
# Forwarding from [::1]:18080 -> 8080
# 요청 확인
curl localhost:18080
# HTTP/1.1 GET /
#
# Host: 127.0.0.1:18080
# Accept: */*
# User-Agent: curl/7.68.0
minikube의 명령어세트와 유사하게, 해당 포트를 점유하는 프로세스를 찾으면.. 실행중인 kubectl 프로세스만 조회되며 그 디테일한 동작은 호스트 머신에서 확인할 수 없다.
# PID 18080에 해당하는 PID 찾기
lsof -i :18080
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# kubectl 39407 nasir 8u IPv4 393376 0t0 TCP localhost:18080 (LISTEN)
# kubectl 39407 nasir 9u IPv6 393377 0t0 TCP ip6-localhost:18080 (LISTEN)
# 해당 프로세스 및 command 조회
ps -ef | grep 39407
nasir 39407 16707 0 21:51 pts/5 00:00:00 /snap/kubectl/3546/kubectl port-forward service/lb-svc 18080:8080
이는 minikube 명령어세트는 호스트에서 조회가능한 컨테이너에 대해 ssh 터널링을 기반으로 작동하지만, kubectl port-forward는 호스트에서 던진 kubectl 명령어에 대해, kube-apiserver가 받아 local node의 kubelet이 portforwardwebsocket을 생성하여 통신을 핸들링하기 때문이다.
구체적인 작동을 확인하려면 컨테이너 노드 내부로 들어가 api-server와 kubelet의 구성 및 로그(수준변경해서) 확인이 필요하다. 이는 별개의 프로세스이므로.. 다음에 기회가되면 설명하는것으로 넘기겠다
5. Outro
단순히 minikube service / tunnel 명령어를 사용해 연결테스트를 수행해 왔었다. 그 뒷단의 실질적인 동작은 노드에 대한 ssh 터널링을 통해 수행된다는점을 확인하였다.
ssh 터널링은 기존에 퍼블릭망과 단절된 private subnet에 구성된 RDS에 대해, 퍼블릭망에 bastion host를 띄우고 이를 통해 외부에서 VPC 내부의 DB클러스터에 사용하곤 한다고 듣기만 했다. 실질적인 사용사례 하나를 확인해서 좋았고 역시 k8s는 단순 기술이라기보다는 리눅스/네트워크/보안등 여러가지를 알아야 제대로 쓸수있는 종합예술이구나 싶기도 하고
minikube cluster 생성시 –nodes 2 옵션을 주었던것은 k8s svc의 spec.externalTrafficPolicy 값을 local로 두었을때 어떻게 동작하는지 보려고 하였으나.. 생각했던대로 동작되고 특이사항이 없어서 본문에는 포함하지 않았다. svc 생성시 externalTrafficPolicy의 기본값은 cluster 모드이다. 지금 예시처럼 실제 echo-server pod는 worker node인 minikube-m02에 생성되나, svc의 ssh 터널링은 controlplane인 minikube 노드에 설정되므로 서로다른 노드에 각 오브젝트가 생성된다. 이때 cluster 모드면 어떤 노드로 요청이와도 대상 pod가 존재하는 노드로 요청이 전달되어 수행되나 local 모드면 minikube 노드에 생성된 pod만 응답할 수 있고 minikube-m02 노드의 pod는 응답을 못해야하는데.. 실제로 그랬고 minikube 적인 동작이 아닌 k8s적인 동작이라 의미가 없어서 그냥 생략하였음
그래서 어떤명령어를 주로 쓰느냐? 하면 sudo minikube tunnel > /dev/null 2>&1 &
로 그냥 터널링을 백그라운드에 밀어놓고 종종 쓰곤 했다. 그러다가 MacOS와 docker network와 바로 연결해주는 Docker Mac Net Connect (https://github.com/chipmk/docker-mac-net-connect) 같은 프로젝트를 발견하고 구성하여 그냥 바로 붙여서 사용하고 있다