StatefulSet과 멀티 클러스터 통신
Linkerd의 멀티 클러스터 확장은 클러스터 간에 서비스 정보를 "미러링"하는 방식으로 동작해요. 대상 클러스터에서 내보낸(exported) 서비스는 clusterIP 복제본으로 미러링돼요. 기본적으로 내보낸 모든 서비스는 clusterIP로 미러링돼요. StatefulSet처럼 headless 서비스가 필요한 워크로드를 실행할 때, Linkerd의 멀티 클러스터 확장은 headless 서비스에 대한 지원을 구성해 서비스 유형을 보존할 수 있어요. 내보낸 headless 서비스는 소스 클러스터에서 headless로 미러링되어, DNS 레코드 생성과 개별 파드 주소 지정 같은 기능이 보존돼요.
이 가이드에서는 headless 서비스 지원과 함께 Linkerd 및 멀티 클러스터 확장을 설치하고 구성하는 방법을 단계별로 알아보고, StatefulSet을 대상 클러스터에 배포하는 예를 보여줄게요. 배포 후에는 소스 클러스터의 클라이언트에서 대상 클러스터의 StatefulSet에 있는 임의의 파드와 통신하는 방법도 살펴볼 거예요. headless 서비스에 대한 멀티 클러스터 지원이 어떻게 동작하는지 더 자세히 보려면 multi-cluster communication 문서를 확인하세요.
본문
사전 요구 사항
- 두 개의 Kubernetes 클러스터. 각각 east와 west라고 부를 건데, east가 "소스" 클러스터이고 west가 "대상" 클러스터예요. 이들은 어떤 클라우드나 로컬 환경에도 있을 수 있으며, 이 가이드는 두 로컬 클러스터를 구성하기 위해 k3d를 사용할 거예요.
- Linkerd 설치용 인증서를 생성할 smallstep/CLI.
- 최신 linkerd 릴리스(2.18 이상).
클러스터 생성과 설치를 돕기 위해 데모 저장소가 제공돼요. 가이드 전반에서 저장소의 스크립트를 사용할 거지만, 저장소를 복제하거나 스크립트를 사용하지 않고도 따라 할 수 있어요.
headless 지원을 포함한 Linkerd 멀티 클러스터 설치
데모를 시작하고 모든 것을 실제로 보기 위해, east 클러스터의 파드가 west 클러스터의 임의의 파드와 통신하려고 하는 멀티 클러스터 시나리오를 살펴볼 거예요.
첫 번째 단계는 로컬 머신에 데모 저장소를 복제하는 것이에요.
# clone example repository
$ git clone [email protected] [/cdn-cgi/l/email-protection]:linkerd/l2d-k3d-statefulset.git
$ cd l2d-k3d-statefulset
두 번째 단계는 east와 west라는 두 개의 k3d 클러스터를 만드는 것이에요. 여기서 east 클러스터는 소스이고 west 클러스터는 대상이에요. 클러스터를 만들 때는 공유 trust root가 필요해요. 다행히 방금 복제한 저장소에는 모든 것을 크게 간소화해주는 스크립트 몇 개가 포함되어 있어요.
# create k3d clusters
$ ./create.sh
# list the clusters
$ k3d cluster list
NAME SERVERS AGENTS LOADBALANCER
east 1/1 0/0 true
west 1/1 0/0 true
클러스터가 생성되면 Linkerd와 멀티 클러스터 확장을 설치할 거예요. 마지막으로 둘 다 설치되면 두 클러스터를 연결해 서비스가 미러링될 수 있게 해야 해요. 이전과 마찬가지로 이 단계들은 제공된 스크립트를 통해 자동화되어 있어요. 컨트롤러와 링크가 두 클러스터 모두에 어떻게 생성되는지 스크립트를 살펴보세요.
# Install Linkerd and multicluster, output to check should be a success
$ ./install.sh
# Next, link the two clusters together
$ ./link.sh
좋아요! 오류 없이 여기까지 왔다면 좋은 신호예요. 다음 장에서는 서비스를 배포하고 통신이 어떻게 동작하는지 살펴볼 거예요.
파드 간 통신: east에서 west로
설치 단계를 마쳤으니 이제 파드 간 통신에 집중할 수 있어요. 먼저 파드와 서비스를 배포할 거예요:
- east와 west의 기본 네임스페이스를 메시할 거예요.
- west에는 자체 headless 서비스인 nginx-svc를 가진 nginx StatefulSet을 배포할 거예요.
- east에서는 스크립트가 curl 파드를 배포해 nginx 서비스를 curl할 거예요.
# deploy services and mesh namespaces
$ ./deploy.sh
# verify both clusters
#
# verify east
$ kubectl --context=k3d-east get pods
NAME READY STATUS RESTARTS AGE
curl-56dc7d945d-96r6p 2/2 Running 0 7s
# verify west has headless service
$ kubectl --context=k3d-west get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 443/TCP 10m
nginx-svc ClusterIP None 80/TCP 8s
# verify west has statefulset
#
# this may take a while to come up
$ kubectl --context=k3d-west get pods
NAME READY STATUS RESTARTS AGE
nginx-set-0 2/2 Running 0 53s
nginx-set-1 2/2 Running 0 43s
nginx-set-2 2/2 Running 0 36s
더 나아가기 전에 nginx-svc의 endpoints 객체를 살펴보도록 해요:
$ kubectl --context=k3d-west get endpoints nginx-svc -o yaml
...
subsets:
- addresses:
- hostname: nginx-set-0
ip: 10.42.0.31
nodeName: k3d-west-server-0
targetRef:
kind: Pod
name: nginx-set-0
namespace: default
resourceVersion: "114743"
uid: 7049f1c1-55dc-4b7b-a598-27003409d274
- hostname: nginx-set-1
ip: 10.42.0.32
nodeName: k3d-west-server-0
targetRef:
kind: Pod
name: nginx-set-1
namespace: default
resourceVersion: "114775"
uid: 60df15fd-9db0-4830-9c8f-e682f3000800
- hostname: nginx-set-2
ip: 10.42.0.33
nodeName: k3d-west-server-0
targetRef:
kind: Pod
name: nginx-set-2
namespace: default
resourceVersion: "114808"
uid: 3873bc34-26c4-454d-bd3d-7c783de16304
endpoints 객체를 보면 서비스에 세 개의 엔드포인트가 있고, 각 엔드포인트는 StatefulSet 파드에 해당하는 hostname의 주소(또는 IP)를 가진다는 걸 알 수 있어요. 이 엔드포인트 중 하나에 직접 curl하면 응답을 받을 수 있을 거예요. curl 파드를 west 클러스터에 적용해 이를 테스트할 수 있어요:
$ kubectl --context=k3d-west apply -f east/curl.yml
$ kubectl --context=k3d-west get pods
NAME READY STATUS RESTARTS AGE
nginx-set-0 2/2 Running 0 5m8s
nginx-set-1 2/2 Running 0 4m58s
nginx-set-2 2/2 Running 0 4m51s
curl-56dc7d945d-s4n8j 0/2 PodInitializing 0 4s
$ kubectl --context=k3d-west exec -it curl-56dc7d945d-s4n8j -c curl -- sh
/$ # prompt for curl pod
이제 이 인스턴스 중 하나를 curl하면 응답을 받을 거예요.
# exec'd on the pod
/ $ curl nginx-set-0.nginx-svc.default.svc.west.cluster.local
"
Welcome to nginx!
# Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to
nginx.org [http://nginx.org/].
Commercial support is available at
nginx.com [http://nginx.com/].
*Thank you for using nginx.*
"
이제 이번에는 east 클러스터에서 동일한 작업을 해볼게요. 먼저 서비스를 내보낼 거예요.
$ kubectl --context=k3d-west label service nginx-svc mirror.linkerd.io/exported="true"
service/nginx-svc labeled
$ kubectl --context=k3d-east get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 443/TCP 20h
nginx-svc-west ClusterIP None 80/TCP 29s
nginx-set-0-west ClusterIP 10.43.179.60 80/TCP 29s
nginx-set-1-west ClusterIP 10.43.218.18 80/TCP 29s
nginx-set-2-west ClusterIP 10.43.245.244 80/TCP 29s
endpoints 객체를 보면 이상한 점을 발견할 거예요. nginx-svc-west의 엔드포인트는 동일한 hostname을 가지지만, 각 hostname은 위에서 보는 서비스 중 하나를 가리켜요:
$ kubectl --context=k3d-east get endpoints nginx-svc-k3d-west -o yaml
subsets:
- addresses:
- hostname: nginx-set-0
ip: 10.43.179.60
- hostname: nginx-set-1
ip: 10.43.218.18
- hostname: nginx-set-2
ip: 10.43.245.244
이것이 튜토리얼 시작 부분에서 설명한 내용이에요. 대상 클러스터(west)의 각 파드는 clusterIP 서비스로 미러링돼요. 이것이 왜 중요한지는 곧 알게 될 거예요.
$ kubectl --context=k3d-east get pods
NAME READY STATUS RESTARTS AGE
curl-56dc7d945d-96r6p 2/2 Running 0 23m
# exec and curl
$ kubectl --context=k3d-east exec curl-56dc7d945d-96r6p -it -c curl -- sh
# we want to curl the same hostname we see in the endpoints object above.
# however, the service and cluster domain will now be different, since we
# are in a different cluster.
#
/ $ curl nginx-set-0.nginx-svc-k3d-west.default.svc.east.cluster.local
Welcome to nginx!
# Welcome to nginx!
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to
href="http://nginx.org/">nginx.org.
Commercial support is available at
href="http://nginx.com/">nginx.com.
*Thank you for using nginx.*
보시다시피 동일한 응답을 받았어요! 그런데 nginx는 다른 클러스터에 있는데요. 그럼 뒤에서 무슨 일이 벌어졌던 걸까요?
- headless 서비스를 미러링할 때 각 파드에 대해 clusterIP 서비스를 만들었어요. 서비스는 DNS 레코드를 만들기 때문에, 대상의 hostname으로 각 엔드포인트 이름을 짓는 것이 이 파드 FQDN(nginx-set-0.(...).cluster.local)을 만들어냈어요.
- Curl은 파드 DNS 이름을 IP 주소로 해석했어요. 우리 경우 이 IP는 10.43.179.60이에요.
- 요청이 전송되면 linkerd2-proxy가 이를 가로채요. IP 주소를 보고 우리의 clusterIP 서비스와 연관시켜요. 이 서비스 자체는 게이트웨이를 가리키므로, 프록시는 요청을 대상 클러스터 게이트웨이로 포워딩해요. 이것이 일반적인 멀티 클러스터 시나리오예요.
- 대상 클러스터의 게이트웨이는 요청을 보고 원래 대상 주소를 조회해요. 우리 경우 "엔드포인트 미러"이므로, 같은 클러스터 안의 nginx-set-0.nginx-svc로 가야 한다는 것을 알아요.
- 요청은 게이트웨이에 의해 파드로 다시 포워딩되고, 응답이 돌아와요.
그리고 끝이에요! 이제 클러스터 간에 파드로 요청을 보낼 수 있어요. 3개의 StatefulSet 파드 중 아무 것이나 조회해도 동일한 결과가 나올 거예요.
참고
headless 서비스를 headless로 미러링하려면, 서비스의 엔드포인트에 명명된 주소가 하나 이상 있어야 해요(예: IP에 대한 hostname). 그렇지 않으면 미러링할 엔드포인트가 없으므로 서비스가 clusterIP로 미러링돼요. headless 서비스는 정상적인 조건에서 포트를 노출하지 않고 생성될 수도 있지만, 멀티 클러스터 service-mirror는 이를 지원하지 않아요. 포트가 없으면 Kubernetes 검증을 통과하는 서비스를 만들 수 없기 때문이에요.
정리
정리를 위해 k3d CLI를 사용해 두 클러스터를 완전히 제거할 수 있어요:
$ k3d cluster delete east
cluster east deleted
$ k3d cluster delete west
cluster west deleted