soobook
KUBERNETES

Open Cluster Management

여러 Kubernetes 클러스터를 hub 하나에서 관리하는 pull 기반 멀티클러스터 컨트롤 플레인

Open Cluster Management(OCM)은 여러 Kubernetes 클러스터를 hub 하나에서 관리하는 도구다. 핵심은 hub가 각 클러스터로 들어가지 않고, 각 클러스터에 심어 둔 에이전트가 hub로 나와 일을 받아 가는 pull 모델이다.

Intro

클러스터가 하나일 때 관리는 단순하다. kubeconfig 하나를 들고 kubectl을 치면 된다. 그런데 클러스터가 여럿이 되면, 그것도 서로 다른 클라우드에, 서로 다른 리전에 흩어져 있으면 이야기가 달라진다.

이때 흔히 떠올리는 그림은 “여러 클러스터에 같은 매니페스트를 배포하는” GitOps다.

하지만 멀티클러스터에서 진짜 어려운 부분은 배포가 아니라 도달이다.

중앙의 control plane이 각 워크로드 클러스터의 API server를 상대로 리소스를 만들고(CRUD), 상태를 지켜보고(WATCH), reconcile loop를 중앙에서 돌리고 싶다.

노드를 cordon하거나 drain하고, 원격 클러스터의 Pod 로그를 읽고, 문제가 생긴 워크로드에 exec로 들어가 보고 싶다.

문제는 워크로드 클러스터가 대개 outbound-only라는 데 있다.

보안 정책과 클라우드 네트워크 구성상, 워크로드 클러스터는 밖으로 나가는 연결은 열려 있어도 밖에서 들어오는 연결은 받지 않는다.

그런데 방금 나열한 요구는 전부 hub가 spoke로 들어가는, spoke 입장에서는 inbound 트래픽이다. 도달 자체가 막히는 것이다.

OCM은 이 도달 문제를 정면으로 푸는 도구다. 그래서 OCM을 “멀티클러스터 배포 도구”로 이해하면 초점이 어긋난다. 출발점은 배포가 아니라 reachability다.

The Reachability Problem

outbound-only 클러스터에 hub가 닿는 방법을 하나씩 따져 보면 OCM이 왜 필요한지가 드러난다.

각 클러스터의 API server를 공인 IP로 노출하는 방법은 보안과 정책상 대개 선택지가 못 된다. 클러스터마다 VPN을 놓는 방법은 클러스터가 늘수록 관리 부담이 빠르게 커진다. 남는 현실적인 길은 spoke가 밖으로 여는 연결을 되돌려 쓰는 reverse tunnel이다.

reverse tunnel 자체는 잘 알려진 도구가 여럿 있다.

FRP나 Chisel 같은 범용 터널은 빠르게 구축된다. 다만 클러스터가 수십 개로 늘면 포트와 토큰을 클러스터마다 관리하는 일에 손이 많이 간다.

Kubernetes 안에서 이 문제의 사실상 표준은 Konnectivity(apiserver-network-proxy)다. spoke가 hub로 여는 gRPC 터널 위로 API 트래픽을 되돌려 주고, 인증을 Kubernetes의 체계 안에서 처리한다.

그런데 Konnectivity는 터널만 준다. 어떤 클러스터가 등록돼 있는지, 누가 어떤 권한으로 무엇을 미는지, 밀어 넣은 워크로드의 상태를 어떻게 회수하는지는 전부 그 위에 직접 만들어야 한다.

클러스터가 계속 늘어나는 환경에서 이걸 매번 손으로 조립하는 건 또 다른 부담이다.

Hub and Spoke

OCM의 구조는 이름 그대로 hub와 spoke로 나뉜다.

  • hub cluster는 관리 주체다. 어떤 클러스터가 등록됐는지, 각 클러스터에 무엇을 돌려야 하는지를 리소스로 들고 있다.
  • managed cluster(흔히 spoke)는 실제 워크로드가 도는 곳이다. 각 spoke에는 klusterlet이라는 에이전트가 뜬다.

klusterlet은 다시 두 역할로 나뉜다. 하나는 등록을 담당하는 registration-agent, 다른 하나는 일을 받아 실행하는 work-agent다. 둘 다 spoke에서 hub 방향으로만 연결을 연다.

hub cluster가 위에, 두 managed cluster(spoke)가 아래에 있고, 각 spoke의 klusterlet이 outbound-only 경계를 넘어 hub로 dial out하는 pull 연결만 존재한다

방향이 핵심이다. 커넥션은 언제나 spoke에서 hub로만 열린다. hub는 spoke로 먼저 연결하지 않는다. 그래서 spoke가 NAT 뒤에 있든, 어느 클라우드에 있든, outbound만 열려 있으면 등록될 수 있다. 한 번 등록되면 hub는 그 클러스터를 안다.

Registration

등록은 한쪽이 다른 쪽을 일방적으로 끌어들이는 방식이 아니라, 양쪽이 각자 동의하는 double opt-in handshake로 성립한다.

  1. spoke가 수명이 짧은 bootstrap kubeconfig로 hub에 접속해, 자기 private key로 만든 CSR(Certificate Signing Request)을 올리며 등록을 요청한다.
  2. hub 쪽이 이 요청을 승인한다. 수동으로 승인할 수도 있고, 정책으로 auto-approval을 걸 수도 있다.
  3. 승인되면 klusterlet은 자기 전용 client 인증서를 발급받아 이후 통신에 쓴다. 처음 쓴 bootstrap kubeconfig는 버려진다.

등록 결과는 hub에 ManagedCluster 리소스로 남는다.

apiVersion: cluster.open-cluster-management.io/v1
kind: ManagedCluster
metadata:
  name: cluster-a
spec:
  hubAcceptsClient: true

hubAcceptsClient가 hub 쪽 동의고, spoke가 올린 CSR이 spoke 쪽 동의다. 어느 쪽도 상대를 몰래 끌어들이지 못한다.

이 handshake를 hub 쪽에서 처리하는 컴포넌트는 registration controller다. registration operator가 hub에 설치하는 것으로, spoke의 registration-agent와 짝을 이뤄 등록 수명주기를 관리하고, 등록 뒤에는 서로 heartbeat를 확인한다.

동작 방식은 Kubernetes 컨트롤러의 일반적인 패턴 그대로다. registration controller가 hub의 API server를 informer로 watch하다가, spoke가 올린 CSR과 ManagedCluster가 생기면 그 변경을 통지받아 reconcile한다.

승인은 사실 두 권한으로 갈려 있다. CSR을 approve하는 권한과 ManagedCluster를 accept하는 권한이다. 뒤쪽 accept는 webhook이 검증하고, 둘 다 쥔 쪽은 hub admin뿐이라 spoke가 자기 등록을 스스로 통과시키지는 못한다.

한 가지 더, approve와 sign은 주체가 다르다. registration controller가 CSR을 approve해도, 실제 서명은 Kubernetes의 CSR signing controller가 cluster CA로 한다. 이 둘이 갈라져 있다는 사실이 뒤 Tradeoffs에서 다시 중요해진다.

여기서 눈여겨볼 건 이 handshake의 인증 방식이 하나가 아니라는 점이다. OCM은 registration driver를 여러 개 둔다. 기본은 CSR로, spoke가 올린 CSR을 hub의 Kubernetes CA가 서명한다. 이 외에 hub의 gRPC bootstrap 서버로 등록하는 gRPC driver, hub가 관리형 클러스터일 때 쓰는 방식 등이 있다.

driver가 여러 개인 건 우연이 아니다. 관리형 control plane은 외부 클라이언트용 인증서를 자기 CA로 서명해 주지 않는 경우가 있다. 그러면 CSR 기반 등록이 hub 단계에서 막힌다. 이 지점은 뒤의 Tradeoffs에서 다시 돌아온다.

ManifestWork

등록이 끝나면 일을 보낼 통로가 생긴다. 그 단위가 ManifestWork다.

hub의 해당 클러스터 namespace에 ManifestWork를 하나 두면, 그 안에 담긴 Kubernetes 리소스들을 spoke의 work-agent가 pull해서 apply한다. 예를 들어 KubeVirt VM 하나를 spoke에 띄우려면, VM 정의를 ManifestWork로 감싸 hub에 두기만 하면 된다.

apiVersion: work.open-cluster-management.io/v1
kind: ManifestWork
metadata:
  name: vm-workload
  namespace: cluster-a          # 대상 클러스터의 hub 측 namespace
spec:
  workload:
    manifests:
      - apiVersion: kubevirt.io/v1
        kind: VirtualMachine
        metadata:
          name: demo-vm
        spec:
          # ... VM 정의
  manifestConfigs:
    - resourceIdentifier:
        group: kubevirt.io
        resource: virtualmachines
        namespace: default
        name: demo-vm
      feedbackRules:
        - type: WellKnownStatus

두 가지가 중요하다.

첫째는 status feedback이다. work-agent는 apply한 리소스의 상태를 hub로 되돌려 준다. 다만 리소스 전체를 동기화하면 부하가 크므로, feedbackRules로 유의미한 필드만 골라 회수한다. 그 리소스가 Ready인지 같은 스칼라 값만 올라오고, 값이 바뀌지 않으면 트래픽도 없다.

둘째는 소유권과 garbage collection이다. ManifestWork에서 리소스를 빼면 spoke에서도 그 리소스가 삭제된다. hub가 desired state의 소유권을 갖는 것이다. 끝난 Job이나 Pod은 TTL로 자동 정리해 hub의 상태가 무한정 쌓이는 걸 막는다.

hub의 ManifestWork에 담긴 desired state를 spoke의 work-agent가 pull해서 apply하고, 리소스의 scalar status만 feedback으로 hub의 ManifestWork status에 되돌아온다

이 구조 덕분에, 어느 클라우드의 어떤 클러스터든 워크로드를 던지는 방식이 똑같아진다. 벤더마다 다른 배포 파이프라인을 만들 필요가 없다. 새 클러스터는 등록만 하면 같은 ManifestWork를 받을 수 있다.

Placement

ManifestWork가 “무엇을 어디에 두느냐”라면, Placement는 그 “어디”를 고르는 문제를 따로 떼어낸다. 어느 클러스터에 일을 돌릴지 정하는 스케줄과, 실제로 일을 미는 execution을 분리하는 것이다.

ManagedClusterSet으로 클러스터를 그룹으로 묶고, Placement가 label이나 claim 조건으로 그 안에서 대상을 고른다. 고른 결과는 PlacementDecision의 status에 clusterName 목록으로 남는다.

Placement 자체는 사람이 선언적으로 apply하는 오브젝트다. 어떤 조건으로 고를지를 적어 두면, 실제 선택 계산과 PlacementDecision 갱신은 hub의 placement controller가 클러스터가 붙고 빠질 때마다 이어서 한다.

apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
  name: apps-in-region
  namespace: default
spec:
  clusterSets:
    - production
  predicates:
    - requiredClusterSelector:
        labelSelector:
          matchLabels:
            region: ap-northeast

Placement, PlacementDecision, ManifestWork가 모두 hub cluster 안에 있고, hub의 placement controller가 ManagedClusterSet을 predicate로 걸러 PlacementDecision에 cluster-a, cluster-b를 남긴다. glue가 클러스터마다 ManifestWork를 만들면 선택된 spoke의 work-agent가 그것을 pull한다. cluster-c는 선택되지 않아 아무것도 받지 않는다

여기서 Placement는 결정만 한다. clusterName 목록을 PlacementDecision에 적어둘 뿐, 스스로 ManifestWork를 만들거나 무언가를 배포하지는 않는다. 그 목록을 읽어 클러스터마다 실제 배포로 옮기는 glue가 따로 있어야 한다.

glue는 대개 두 갈래다. 직접 만든 컨트롤러가 PlacementDecision을 읽어 클러스터마다 ManifestWork를 만들거나, 이미 Argo CD를 쓰고 있다면 ApplicationSet이 clusterDecisionResource generator로 같은 PlacementDecision을 읽어 클러스터마다 Application을 fan-out한다. Argo CD는 OCM의 일부가 아니라 이 목록을 소비하는 한 방법일 뿐이다.

어느 쪽이든 Placement 조건에 맞는 클러스터가 동적으로 바뀌면 배포 대상도 자동으로 따라 바뀐다. “특정 리전의 클러스터 전부에 이걸 깔아라”를 라벨 조건 하나로 표현하고, 클러스터가 새로 붙거나 빠지면 손대지 않아도 반영되는 식이다.

물론 Placement 없이도 OCM은 얼마든지 쓸 수 있다. 대상 클러스터를 직접 정한다면 그 클러스터 namespace에 ManifestWork를 바로 두면 된다. Placement는 “어디에”를 라벨로 자동화하고 싶을 때 얹는 선택적 계층이다.

Add-ons and cluster-proxy

지금까지가 등록과 워크로드 전달이라면, 그 위에 기능을 얹는 확장 지점이 add-on이다. hub 전역 설정인 ClusterManagementAddOn과 클러스터별 인스턴스인 ManagedClusterAddOn(둘 다 addon.open-cluster-management.io/v1alpha1)의 짝으로 관리된다.

가장 대표적인 add-on이 cluster-proxy다. 앞서 Reachability에서 이야기한 Konnectivity 터널을, 등록 체계와 함께 add-on으로 포장한 것이다. cluster-proxy를 깔면 hub에서 spoke의 API server로 향하는 요청이 spoke가 열어 둔 outbound 터널을 타고 되돌아간다. 그 결과 kubectl exec, kubectl logs, describe 같은 명령이 outbound-only 클러스터에서도 동작한다. 중앙에서 원격 클러스터의 Pod에 셸로 들어갈 수 있다는 것 하나만으로도 이 add-on을 까는 이유가 된다.

hub의 kubectl exec 요청이 cluster-proxy를 지나, spoke가 outbound로 열어 둔 터널을 타고 proxy-agent와 kube-apiserver를 거쳐 spoke의 Pod까지 되돌아 들어간다

이 밖에 hub가 spoke 안에서 쓸 ServiceAccount와 토큰을 관리해 주는 managed-serviceaccount 같은 add-on이 있다.

OCM은 앞 절에서 “터널만 준다”던 Konnectivity를 등록과 인증, RBAC, 수명주기와 묶어 프레임워크로 제공한다. Reachability 문제로 시작한 이야기가 여기서 매듭지어진다.

When to Use OCM

OCM이 값을 하는 상황은 대략 이렇다.

  • 여러 클러스터가 서로 다른 클라우드나 네트워크에 흩어져 있고, 상당수가 NAT나 outbound-only 뒤에 있다.
  • 중앙 control plane이 그 클러스터들 안으로 도달해 리소스를 만들고 상태를 지켜봐야 한다.
  • 벤더나 클러스터가 계속 늘어나고, 새 클러스터를 등록만으로 흡수하고 싶다.
  • 단일 federated apiserver로 통합하기보다 각 클러스터의 자율성을 유지하고 싶다.

반대로 이럴 땐 오버킬이다.

  • 클러스터가 한둘이고 전부 inbound 도달이 된다.
  • 하는 일이 매니페스트 배포뿐이고 Argo CD로 이미 충분하다.
  • 규모가 작은데 OCM 위에 다시 자체 operator 계층까지 얹으려 한다.

OCM은 “여러 클러스터에 도달해야 한다”는 문제를 풀려고 만든 도구다. 그 문제가 없다면 굳이 쓸 일도 없다.

References