들어가며
안녕하세요. 오픈소스컨설팅 Linux 엔지니어 조철진 입니다.
알마 전, 지인과 쿠버네티스 클러스터 관리에 대한 이야기를 나누던 중에, 분명 쿠버네티스 서버 위에 방화벽이 동작하는 데도 불구하고 Rancher는 클러스터를 등록해서 잘 쓰고 있다는 이야기를 듣게 되었습니다.
생각해보면 Rancher는 자체 엔진인 RKE로 배포할 수 있을 뿐더러 EKS, GKE 또는 직접 설치한 쿠버네티스 등 다양한 플랫폼에 올라가 있는 쿠버네티스 클러스터를 지원하는데, 이게 가능한 원리가 문득 궁금해졌습니다.
오늘은 Rancher가 어떻게 쿠버네티스 클러스터와 통신하는지 조사해본 내용을 공유해보는 시간을 갖도록 하겠습니다!
SSH 리버스 터널링
SSH 리버스 터널링은 통신하는 방법이 Rancher가 쿠버네티스에 연결되는 방식과 매우 유사하고 리버스 터널링의 대표적인 케이스로 이를 먼저 소개하면 이해에 도움이 될 것 같아 먼저 소개 드립니다.
먼저 일반적인 상황을 가정해 보겠습니다.
- 클라이언트 Host A는 서버 Host B에 인바운드 방화벽 정책에 의하여 SSH 접속이 불가능한 상황입니다.
- 일반적으로 방화벽은 아웃바운드 정책을 허용합니다.
따라서, Host B 서버에서 Host A로 접근이 가능한 상황입니다.
이 같은 상황에서 SSH 리버스 터널링을 수행하여 Host B에 접속해 보겠습니다.
- Host A의 SSH 서버가 구동 중이라고 가정합니다.
- Host B에서 Host A로 SSH 연결하고 Host B의 22번으로 연결되는 새 포트 12345를 Listen 합니다.
이 과정이 바로 리버스 터널링 입니다.
[root@hostb ~]# ssh -R 12345:localhost:22 root@hosta root@hosta's password: [root@hosta ~]# netstat -tnlp | grep 12345 tcp 0 0 127.0.0.1:12345 0.0.0.0:* LISTEN 1850/sshd: root@pts
해당 상태에서 Host A의 소켓 상황을 보면 12345 포트가 Listen 되고 있음을 확인 가능합니다
- Host A에서 새로 Listen한 12345 포트로 SSH를 연결하면 Host B로 접근이 가능해집니다.
[root@hosta ~]# ssh root@localhost -p 12345 root@localhost's password: [root@hostb ~]#
지금까지의 과정을 웹소켓으로 구현한 것이 rancher가 사용하는 reverse tunneling dialer 입니다.
Communicating with Downstream User Clusters
Rancher 공식 문서에서 설명하는 쿠버네티스 클러스터와의 통신 아키텍쳐에 대한 내용입니다. [1]
이 중 클러스터와 통신하는 Cluster Controllers and Cluster Agents의 설명을 보며 구조를 확인 해보겠습니다.
Cluster Controllers and Cluster Agents
Rancher에 여러 대의 하위 쿠버네티스 클러스터가 등록되어 있다고 가정합니다. 각 쿠버네티스 클러스터에는 Rancher의 Cluster Controller에 대한 터널을 열어주는 클러스터 Cluster Agents를 보유합니다.
Rancher는 하나의 하위 클러스터 마다 하나의 Cluster Controller를 가지며, 쿠버네티스 클러스터는 하나의 Cluster Agent를 가집니다.
Cluster Controller는 다음과 같은 역할을 수행합니다:
- 하위 클러스터의 리소스 변화를 감시합니다.
- 하위 클러스터의 현재 상태를 원하는 상태로 가져옵니다.
- 클러스터 및 프로젝트에 대한 접근 제어 정책을 구성합니다.
- 필요한 Kubernetes 엔진(RKE, EKS, GKE 등)를 호출하여 클러스터를 프로비저닝합니다.
Cluster Agent는 cattle-cluster-agent 라고 부르며 하위 클러스터에서 실행되는 구성 요소입니다.
클러스터 에이전트는 다음 작업을 수행합니다.
- 쿠버네티스 API 서버에 연결합니다.
- 각 클러스터 내에서 pod, deployment 생성과 같은 워크로드를 관리합니다.
- 각 클러스터의 정책에서 정의된 Role 및 Binding을 적용합니다.
- 이벤트, 통계, 노드 정보 및 상태에 대해 클러스터와 Rancher 서버 간의 터널을 통해 통신합니다.
Reverse Tunneling Dialer
마지막으로 Rancher가 사용하는 Reverse Tunneling Dialer에 대해 알아보며 어떻게 동작하는지 알아보겠습니다. 이 프로그램은 Rancher의 공식 GitHub 리포지토리에서 제공됩니다. [2]
remotedialer는 서버와 클라이언트 간에 양방향 연결을 생성하여 서버에서 net.Dial을 수행하고 localhost 또는 NAT 또는 방화벽 뒤에서 실행 중인 클라이언트 서비스에 연결할 수 있도록 합니다.
Data flow
remotedialer의 데이터 흐름을 나타낸 그림입니다.
여기서 Server는 “Rancher”, 그리고 Client는 “쿠버네티스”라고 보시면 되겠습니다.
- 클라이언트는 서버의 URL /connect을 호출하여 서버에 연결을 요청합니다.
- 서버는 이를 웹소켓 conn(ws) 연결로 업그레이드해 클라이언트에서 접속할 수 있는 세션을 생성합니다.
- 클라이언트는 웹소켓 연결을 통하여 서버로 세션을 생성해 양방향으로 데이터 교환이 가능해집니다.
- 이제 서버는 생성된 웹소켓 연결을 통해 클라이언트의 특정 서비스(포트)로 요청을 할 수 있습니다.
클라이언트는 일반적으로 쿠버네티스 API 서버와 같은 HTTP 서버 앞에서 작동하며, 해당 HTTP 리소스에 대한 리버스 프록시 역할을 수행합니다. 서버에서 사용자가 어떠한 리소스를 요청하면, 요청을 먼저 remotedialer로 전송하고 이를 올바른 클라이언트로 라우팅 하는 역할을 합니다.
Rancher 환경의 remotedialer
remotedialer는 Rancher가 관리하는 하위 클러스터에 연결하여 Cluster Agent가 Rancher 서버를 통해 클러스터에 액세스할 수 있도록 하는 데 사용됩니다. remotedialer는 주로 다음 세 가지 방법으로 사용됩니다
- Agent config and tunnel server
cattle-cluster-agent가 시작하면 초기에 Rancher 서버의 /v3/connect/register
를 호출해 연결을 설정하고, 노드에 대한 초기 데이터를 설정하는 권한 부여 프로세스를 실행합니다. 그 후, 에이전트는 주기적으로 /v3/connect
엔드포인트에 연결을 유지합니다. 이후 각 연결에서 /v3/connect/config
를 호출하여 노드 구성 정보가 담긴 데이터를 가져옵니다.
다음은 실제 쿠버네티스 cattle-cluster-agent 파드에서 register 및 권한 부여가 진행되는 로그입니다.
time="2023-10-31T00:30:44Z" level=info msg="Listening on /tmp/log.sock" time="2023-10-31T00:30:44Z" level=info msg="Rancher agent version eed6333e7 is starting" time="2023-10-31T00:30:44Z" level=info msg="Connecting to wss://10.0.0.2/v3/connect/register with token starting with 4lsp6zhmxwxsf5v2xpzkhf4njt9" time="2023-10-31T00:30:44Z" level=info msg="Connecting to proxy" url="wss://10.0.0.2/v3/connect/register" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=User controller" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=Token controller" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=UserAttribute controller" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=GroupMember controller" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=Cluster controller" time="2023-10-31T00:30:45Z" level=info msg="Starting management.cattle.io/v3, Kind=Group controller" time="2023-10-31T00:30:46Z" level=info msg="Starting apiextensions.k8s.io/v1, Kind=CustomResourceDefinition controller" time="2023-10-31T00:30:46Z" level=info msg="Starting management.cattle.io/v3, Kind=APIService controller" time="2023-10-31T00:30:46Z" level=info msg="Starting apiregistration.k8s.io/v1, Kind=APIService controller" time="2023-10-31T00:30:46Z" level=info msg="Starting rbac.authorization.k8s.io/v1, Kind=ClusterRoleBinding controller"
- Steve Aggregation
Agent에서 동작하는 steve aggregation 서버는 Rancher와 remotedialer 세션을 설정하여 클러스터의 steve API를 Rancher에서 액세스 가능하게 하여 리소스 감시를 용이하게 합니다.
time="2023-10-30T08:53:07Z" level=info msg="Starting steve aggregation client"
time="2023-10-30T08:53:11Z" level=info msg="Steve auth startup complete"
- Health Check
클러스터와 연결된 Rancher의 controller는 설정된 터널을 사용하여 클러스터가 살아있는지 확인하고, 그렇지 않은 경우 클러스터 개체에 경고를 설정합니다.
time="2023-10-31T04:38:14Z" level=debug msg="Wrote ping"
time="2023-10-31T04:38:16Z" level=debug msg="Wrote ping"
time="2023-10-31T04:38:19Z" level=debug msg="Wrote ping"
time="2023-10-31T04:38:21Z" level=trace msg="NodeController calling handler podresource w1-k8s"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
time="2023-10-31T04:38:21Z" level=trace msg="NodeController calling handler podresource m-k8s"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
time="2023-10-31T04:38:21Z" level=trace msg="NodeController calling handler podresource w2-k8s"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
time="2023-10-31T04:38:21Z" level=trace msg="DeploymentController calling handler workloadServiceGenerationController cattle-system/cattle-cluster-agent"
직접 검증해보기
앞서 정리된 내용을 토대로 정말로 그렇게 동작하는지 제 나름의 방식대로 검증해보았습니다.
- cattle-cluster-agent는 쿠버네티스 API 서버의 리버스 프록시 역할을 한다.
검증을 위해 마스터노드의 에이전트 pod 에서 확인해 보았습니다.
[root@m-k8s ~]# kubectl get pods -o wide -n cattle-system NAME READY STATUS RESTARTS AGE IP NODE cattle-cluster-agent-6f5fcb5646-sdw4g 1/1 Running 0 5h45m 192.168.221.152 w1-k8s cattle-cluster-agent-6f5fcb5646-t849g 1/1 Running 0 5h45m 192.168.171.80 m-k8s rancher-webhook-68d64cd897-vbj5g 1/1 Running 0 5h45m 192.168.103.185 w2-k8s [root@m-k8s ~]# kubectl exec -it cattle-cluster-agent-6f5fcb5646-t849g -n cattle-system -- bash cattle-cluster-agent-6f5fcb5646-t849g:/var/lib/rancher # ss -n Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port tcp ESTAB 0 0 192.168.171.80:56142 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56160 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:50990 10.0.0.2:443 tcp ESTAB 0 0 192.168.171.80:56150 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:51012 10.0.0.2:443 tcp ESTAB 0 0 192.168.171.80:50988 10.0.0.2:443 tcp ESTAB 0 0 192.168.171.80:56162 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56138 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:55846 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56158 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56140 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56146 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56156 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56164 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56154 10.96.0.1:443 tcp ESTAB 0 0 192.168.171.80:56200 10.96.0.1:443
무엇 때문인지 로컬에서 10.96.0.1 서버로 많은 요청을 연결 중이었습니다.
여기서 10.96.0.1 서버는 무엇일까요? 바로 쿠버네티스 API 서버의 ClusterIP 서비스 IP 입니다.
[root@m-k8s ~]# kubectl describe svc kubernetes
Name: kubernetes
Namespace: default
Labels: component=apiserver
provider=kubernetes
Annotations: <none>
Selector: <none>
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.0.1
IPs: 10.96.0.1
Port: https 443/TCP
TargetPort: 6443/TCP
Endpoints: 192.168.1.10:6443
Session Affinity: None
Events: <none>
[root@m-k8s ~]# ss -nlpt | grep 6443
LISTEN 0 128 [::]:6443 [::]:* users:(("kube-apiserver",pid=1396,fd=7)))
default 네임스페이스의 kubernetes 서비스를 확인해보면 kube-apiserver의 포트로 연결하고 있음을 확인 가능합니다. 따라서, cattle-cluster-agent가 리버스 프록시 역할을 하여 API 서버에 연결된다는 것이 증명되었습니다.
- cattle-cluster-agent가 Rancher로 접속하여 세션을 생성하고 웹소켓을 통해 클러스터를 관리한다.
앞서 검증 시 kube-apiserver가 아닌 다른 IP로의 연결이 일부 존재했습니다. 10.0.0.2는 Rancher의 IP 입니다.
cattle-cluster-agent-6f5fcb5646-t849g:/var/lib/rancher # ss -n | grep 10.0.0.2 Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port tcp ESTAB 0 0 192.168.171.80:50990 10.0.0.2:443 tcp ESTAB 0 0 192.168.171.80:51012 10.0.0.2:443 tcp ESTAB 0 0 192.168.171.80:50988 10.0.0.2:443
Rancher에서 확인해보면 동일한 포트로 agent가 Peer Address로 Rancher에 연결해 있다는 것이 확인됩니다.
참고로, 제 Rancher는 Docker 환경으로 172.17.0.2 IP가 출력 되었고, 마스터 노드의 IP는 10.0.0.10 입니다.
d8441fe8bfe2:/var/lib/rancher # ss -n | grep 10.0.0.10 Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port tcp ESTAB 0 0 [::ffff:172.17.0.2]:443 [::ffff:10.0.0.10]:50990 tcp ESTAB 0 0 [::ffff:172.17.0.2]:443 [::ffff:10.0.0.10]:51012 tcp ESTAB 0 0 [::ffff:172.17.0.2]:443 [::ffff:10.0.0.10]:50988
이번에는 마스터 노드(10.0.0.10)에서 Rancher(10.0.0.2) 사이의 패킷을 캡쳐를 해보았습니다.
[root@m-k8s ~]# tcpdump -nn host 10.0.0.2
17:23:59.074785 IP 10.0.0.2.443 > 10.0.0.10.50988: Flags [P.], seq 2503:2527, ack 4632, win 2319, options [nop,nop,TS val 702384156 ecr 702302256], length 24
17:23:59.114006 IP 10.0.0.10.50988 > 10.0.0.2.443: Flags [.], ack 2527, win 1387, options [nop,nop,TS val 702302297 ecr 702384156], length 0
17:24:01.370530 IP 10.0.0.10.51012 > 10.0.0.2.443: Flags [P.], seq 616:644, ack 325, win 1393, options [nop,nop,TS val 702304553 ecr 702381453], length 28
17:24:01.371527 IP 10.0.0.2.443 > 10.0.0.10.51012: Flags [P.], seq 325:349, ack 644, win 10476, options [nop,nop,TS val 702386452 ecr 702304553], length 24
17:24:01.371678 IP 10.0.0.10.51012 > 10.0.0.2.443: Flags [.], ack 349, win 1393, options [nop,nop,TS val 702304554 ecr 702386452], length 0
17:24:01.713059 IP 10.0.0.10.50990 > 10.0.0.2.443: Flags [.], ack 3405089948, win 283, options [nop,nop,TS val 845997056 ecr 846048874], length 0
17:24:01.713577 IP 10.0.0.2.443 > 10.0.0.10.50990: Flags [.], ack 1, win 290, options [nop,nop,TS val 846078954 ecr 845843478], length 0
위의 포트들을 사용하여 https(443) 포트를 사용하는 웹소켓으로 양방향 통신이 이루어짐을 확인하였습니다.
검증을 해본 결과, 이렇게 터널을 통해 맺어진 세션을 통해서 Rancher → 쿠버네티스 서버 → agent 파드 → 쿠버네티스 API 서버 순으로 요청이나 응답이 이루어지는 것을 알 수 있었습니다!
마무리
지금까지 Rancher가 쿠버네티스 클러스터와 통신하는 과정에 대하여 알아보았습니다.
요약을 해보자면, Rancher는 단순하게 Rancher → 쿠버네티스로 통신하는 것이 아니라 쿠버네티스에 Rancher와 통신을 위한 에이전트 pod가 생성되고 에이전트가 역으로 Rancher와 터널링 후 통신을 하게 되어 격리된 방화벽이나 NAT 환경의 클러스터도 등록해 운영이 가능했던 것입니다.
저의 단순한 호기심으로 인해 조사를 해보게 되었지만, 이를 통해서 Rancher가 지향하는 멀티 클러스터 관리를 구현하기 위해 그들은 어떠한 고민을 하였는지 알 수 있게 되었던 경험인 것 같습니다!