🌊

Karmada による k8s マルチクラスタ管理を試す

2024/06/26に公開

karmada とは

karmada は k8s マルチクラスタ管理を実現するための OSS プロジェクトです。Github star は現時点で 4 k 程で CNCF の Incubating project となっています。

https://karmada.io/

元はスマホ等で有名なファーウェイのクラウド企業 HUAWEI Cloud で開発されていましたが、2021 年 9 月に CNCF に寄贈されました。2023/12 には CNCF の blog にも記事が投稿されています。

https://www.cncf.io/blog/2023/12/12/karmada-brings-kubernetes-multi-cloud-capabilities-to-cncf-incubator/

karmada では複数クラスタを管理するための karmada control-plane を構成し、管理下のクラスタに対してリソースをデプロイしたり pod スケジュールを行うことでマルチクラスタ管理を実現するアーキテクチャとなっています。


karmada のアーキテクチャ。https://karmada.io/docs/core-concepts/architecture より引用

karmada の主な特徴は Key Features にまとまっています。

  • マルチクラウド・マルチクラスタ管理
    • karmada control-plane から管理下のクラスタへの通信は kube-apiserver もしくは karmada agent を利用するため、特定のクラウドプロパイダーに制限されない。よって AWS や Azure, オンプレミスの k8s クラスタをまとめて管理することが可能。
  • マルチクラスタへのデプロイ
    • 管理下のクラスタに同時にリソースをデプロイでき、override Policy と呼ばれる CRD によって展開先クラスタに応じて一部のプロパティを動的に変更するといったことが可能。
  • クラスタ間の高度なスケジュール、Failover
    • クラスタ間で Affinity, Splitting/Rebalancing などを適用し、通常の k8s クラスタにおける pod 配置戦略やノード不具合時の Failover をクラスタレベルで実現できる。
  • クラスタ間サービスディスカバリー・AutoScaling
    • submariner などのクラスタ間ネットワークを実現する外部機能を利用することで、クラスタ間のサービスディカバリーや Auto Scaling などを実現できる。

なお、k8s ではマルチクラスタ管理として Kubernetes Federation V2 (kubefed) というものがありましたが、karmada は これらの後継としての開発 が進められているようです。

Notice: this project is developed in continuation of Kubernetes Federation v1 and v2. Some basic concepts are inherited from these two versions.

Karmada の構築

実際に karmada control-plane と管理対象のクラスタを用意してどのようなマルチクラスタ管理ができるのか体験してみます。

作業環境

今回は karmada によるマルチクラスタ管理の動作を確認するために 3 つの k8s クラスタを用意し、1 つは karmada control-plane の構築用、残りは karmada で管理するクラスタとして使用します。各クラスタは control-plane 用のノード 1 台、worker ノード 1 台のシンプルな構成にします。


検証用クラスタの構成

karmada インストール

karmada は ドキュメントのインストール手順 にあるように、構築用スクリプト、helm、operator など様々な方法でインストールできるようになっています。ここでは karmada operator をインストールした後、CRD で karmada instance を作成することで構築します。

Operator のインストール

Operator のインストール手順
operator は helm でインストールできるようになっていますが、chart repository には登録されていないので chart を含む git repository を clone してローカルからインストールする手順となっています。まずは clone を実行。

git clone https://github.com/karmada-io/karmada.git
cd karmada

あとは helm install でインストールできますが、現時点の最新バージョンで進めていると途中でエラーにより正常にインストールが完了しなかったため、今回は v1.10.0 のバージョンを使用します。operator のバージョンを変更するには operator 用の chart value 内のイメージタグを以下のように書き換えます。

charts/karmada-operator/values.yaml
operator:
  image:
    registry: docker.io
      repository: karmada/karmada-operator
-      tag: latest
+      tag: v1.10.0

helm install を実行

helm install karmada-operator -n karmada-system  --create-namespace --dependency-update ./charts/karmada-operator --debug

これにより karmada-system namespace に operator pod が作成されます。インストール時に使用するため kubeconfig の内容を secret として作成。

kubectl create secret generic my-kubeconfig --from-file=$HOME/.kube/config -n karmada-system

operator 起動後は Karmada Instance と呼ばれる CRD を作成することで karmada control-plane の構築に必要なコンポーネントが作成されます。

kubectl create namespace test
kubectl apply -f - <<EOF
apiVersion: operator.karmada.io/v1alpha1
kind: Karmada
metadata:
  name: karmada-demo
  namespace: test
EOF

ただ、ここでは karmada の各コンポーネントも v1.10.0 に合わせるため、Karmada Instance マニフェストのサンプル https://github.com/karmada-io/karmada/blob/master/operator/config/samples/karmada.yaml を元に各コンポーネントのイメージタグを v1.10.0 に書き換えます。

karmada.yml
apiVersion: operator.karmada.io/v1alpha1
kind: Karmada
metadata:
  name: karmada
  namespace: karmada
spec:
  components:
    etcd:
      local:
        imageRepository: registry.k8s.io/etcd
        imageTag: 3.5.13-0
        replicas: 1
        volumeData:
          # hostPath:
          #   type: DirectoryOrCreate
          #   path: /var/lib/karmada/etcd/karmada-demo
          volumeClaim:
            metadata:
              name: etcd-data
            spec:
              accessModes:
                - ReadWriteOnce
              resources:
                requests:
                  storage: 3Gi
    karmadaAPIServer:
      imageRepository: registry.k8s.io/kube-apiserver
      imageTag: v1.28.9
      replicas: 1
      serviceType: NodePort
      serviceSubnet: 10.96.0.0/12
    karmadaAggregatedAPIServer:
      imageRepository: docker.io/karmada/karmada-aggregated-apiserver
      imageTag: v1.10.0
      replicas: 1
    karmadaControllerManager:
      imageRepository: docker.io/karmada/karmada-controller-manager
      imageTag: v1.10.0
      replicas: 1
    karmadaScheduler:
      imageRepository: docker.io/karmada/karmada-scheduler
      imageTag: v1.10.0
      replicas: 1
    karmadaWebhook:
      imageRepository: docker.io/karmada/karmada-webhook
      imageTag: v1.10.0
      replicas: 1
    kubeControllerManager:
      imageRepository: registry.k8s.io/kube-controller-manager
      imageTag: v1.28.9
      replicas: 1
    karmadaMetricsAdapter:
      imageRepository: docker.io/karmada/karmada-metrics-adapter
      imageTag: v1.10.0
      replicas: 2
    # karmadaSearch: # the component `Karmadasearch` is not installed by default, if you need to install it, uncomment it and note the formatting
      # imageRepository: docker.io/karmada/karmada-search
      # imageTag: v1.10.0
      # replicas: 1
    # karmadaDescheduler: # the component `KarmadaDescheduler` is not installed by default, if you need to install it, uncomment it and note the formatting
      # imageRepository: docker.io/karmada/karmada-descheduler
      # imageTag: v1.10.0
      # replicas: 1
  hostCluster:
    networking:
      dnsDomain: cluster.local

これをデプロイ。

kubectl create ns karmada
kubectl apply -f karmada.yml

デプロイが正常に完了すると karmada control-plane を構成する pod が起動します。

$ kubectl get pod
NAME                                               READY   STATUS    RESTARTS   AGE
karmada-aggregated-apiserver-58b4558c96-ld8dq      1/1     Running   0          2m54s
karmada-apiserver-86648f5476-g6kn9                 1/1     Running   0          3m5s
karmada-controller-manager-754496f5bf-xnr68        1/1     Running   0          2m50s
karmada-etcd-0                                     1/1     Running   0          3m6s
karmada-kube-controller-manager-74f76db4c8-9fdg5   1/1     Running   0          2m50s
karmada-metrics-adapter-69cc7c74f5-mpml9           1/1     Running   0          2m50s
karmada-metrics-adapter-69cc7c74f5-zt7st           1/1     Running   0          2m50s
karmada-scheduler-76bb67cb4d-ptvk7                 1/1     Running   0          2m50s
karmada-webhook-7f78445f89-s42l6                   1/1     Running   0          2m50s

また、karmada に関する操作は基本的に karmada 専用の CLI (karmadactl) を使って行うためこちらもインストールします。インストール方法 はいくつかありますが、krew を使っている場合は以下でインストールするのが楽です。

kubectl krew install karmada

karmada-apiserver について

karmada control-plane の構築が完了すると指定した namespace に karmada-admin-config という secret が作成されます。中身は karmada-apiserver と通信するための設定ファイルとなっており、基本的に kubeconfig と同じですが server が karmada-apiserver 宛になっていたり(以下の例では karmada-apiserver の nodePort)、certificate 関連が karmada 構築時の認証情報になっているなどの差分があります。

$ k get secrets karmada-admin-config -o yaml | yq -r ".data.kubeconfig"  | base64 -d
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data:
    ...
    server: https://10.0.0.40:30238
  name: karmada-apiserver
contexts:
- context:
    cluster: karmada-apiserver
    user: karmada-admin
  name: karmada-admin@karmada-apiserver
current-context: karmada-admin@karmada-apiserver
kind: Config
preferences: {}
users:
- name: karmada-admin
  user:
    client-certificate-data:
    ...
    client-key-data:
    ...

ドキュメントにおいて --kubeconfig=<karmada kubeconfig> と表記されている箇所は基本的にこの karmada-apiserver 向けの config のことを指しています。今後の作業ではデフォルトの kubeconfig と karmada の kubeconfig を切り替える場面が多いので、アクセスを容易にするために secret から内容をコピーしてホスト側のファイルに保存しておくと便利です。

ここで karmada-apiserver について簡単に触れておきます。karmada control-plane では 通常の k8s クラスタにおける kube-apiserver のように karmada-apiserver が karmada 関連の API を処理する構成になっています。また、以下の通り通常の kubernetes API との互換性も持ち合わせています。

Karmada API server directly uses the implementation of kube-apiserver from Kubernetes, which is the reason why Karmada is naturally compatible with Kubernetes API. That makes integration with the Kubernetes ecosystem very simple for Karmada, such as allowing users to use kubectl to operate Karmada, integrating with ArgoCD, integrating with Flux and so on.


karmada-apiserver が通常の k8s クラスタにおける kube-apiserver の役割を果たしている。https://karmada.io/docs/core-concepts/components#karmada-apiserver より引用

karmada API を使って karmada で管理する対象のクラスタにデプロイ等の操作を行う際は karmada-apiserver と通信を行う必要があるため、上記の通り karmada-apiserver 向けの kubeconfig が必要となっています。
また、アーキテクチャ図では etcd にデータを保存するようになっていますが、こちらは karmada control-plane に作成される pod に保存されるようになっており、k8s クラスタにデフォルトで存在する etcd とは別の保存先になっています。そのため、kubeconfig と karmada kubeconfig で context を切り替えた際に表示される pod が異なる点に注意が必要です。

クラスタの登録

karmada control-plane の構築が完了したので、次に管理対象のクラスタを登録します。クラスタの登録・管理方法は Cluster Mode として Push 型、Pull 型の 2 種類が用意されています。https://karmada.io/docs/userguide/clustermanager/cluster-registration

  • Push 型
    • karmada control-plane が管理対象の k8s クラスタ上の kube-apiserver と直接通信を行う。
  • Pull 型
    • 管理対象の k8s クラスタ上に karmada-agent をインストールし、karmada-agent から karmada control-plane に通信することで登録・管理を行う。

クラスタ登録はどちらの方法でも可能ですが、Pull 型では karmada control-plane とクラスタ外部の通信が可能となるよう追加の設定が必要になります。手順が増えてやや面倒なのでここでは Push 型でクラスタを登録します。
クラスタの登録はドキュメントの通り karmada join コマンドで実行できますが、<member1 kubeconfig> として対象クラスタの kube-apiserver にアクセスするための kubeconfig が必要となるので作業環境に内容をコピーしておきます。

今回は作業環境を karmada クラスタの control-plane node とし、karmada kubeconfig と対象クラスタの kubeconfig をノード上の以下のパスに保存します。

  • karmada kubeconfig: ~/.kube/karmada.config
  • 対象クラスタの kubeconfig: ~/.kube/sub1.config

クラスタを管理対象に参加させる際のコマンドの書式は以下。

kubectl karmada join <cluster_name> --kubeconfig=<karmada kubeconfig> --cluster-kubeconfig=<member1 kubeconfig>

今回は対象クラスタを member1 という名前で登録します。コマンド実行後に joined successfully が表示されれば登録に成功。

$ kubectl karmada join member1 --kubeconfig=/home/ubuntu/.kube/karmada.config --cluster-kubeconfig=/home/ubuntu/.kube/sub1.config  -v 5
I0615 06:13:22.627225  312437 join.go:158] Joining cluster. cluster name: member1
I0615 06:13:22.627268  312437 join.go:159] Joining cluster. cluster namespace: karmada-cluster
I0615 06:13:22.628820  312437 join.go:183] Joining cluster config. endpoint: https://10.0.0.30:6443
cluster(member1) is joined successfully

karmada API 関連の操作を実行する場合は基本的に --kubeconfig で karmada config を指定する必要があります。いちいち指定するのは面倒なので使用する際は export KUBECONFIG に設定すると楽です(context に追加して kubectx で切り替えてもいいですが)。

export KUBECONFIG=/home/ubuntu/.kube/karmada.config

同様の方法でもう一つのクラスタの kubeconfig を ~/.kube/sub2.config としてノード上にコピーし、 member2 として登録します。

$ kubectl karmada join member2 --kubeconfig=/home/ubuntu/.kube/karmada.config --cluster-kubeconfig=/home/ubuntu/.kube/sub2.config  -v 5

これにより 2 つのクラスタを管理対象に追加できました。クラスタの登録状況は kubectl get clusters で確認できます。

$ k get clusters
NAME      VERSION   MODE   READY   AGE
member1   v1.29.6   Push   True    2d6h
member2   v1.29.6   Push   True    2d6h

複数クラスタにリソースをデプロイする

karmada でマルチクラスタにリソースをデプロイする際は PropagationPolicy と呼ばれる CRD を使って実現します。手順を簡単にまとめると以下のようなシンプルな構成になっています。

  1. デプロイしたいリソースを定義。作成する。
  2. PropagationPolicy で上記のリソースを指定し、対象クラスタにデプロイする。

Resource Propagating に簡単なチュートリアルがあるのでこちらを試してみます。

まずはデプロイ対象リソースである nginx deployment のマニフェストを作成します。これに関しては karmada 独自の記述はなく通常の k8s マニフェストとして作成できます。

nginx.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

これを karmada control-plane 側にデプロイします。

$ export KUBECONFIG=/home/ubuntu/.kube/karmada.config
$ k apply -f nginx.yml

この時点では karmada クラスタにもメンバークラスタにも pod は作成されません。karmada 側では deployment リソース自体は作成されていますが、pod 数は 0 に設定されています。

$ k get deployments.apps
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   0/2     0            0           95s

次に PropagationPolicy CRD を作成して上記の deployment をメンバークラスタにデプロイします。

  • デプロイするリソースは resourceSelectors で指定。ここではデプロイ対象リソースの kind や name を指定する。
  • namespace はデプロイ先クラスタでリソースを作成する namespace を指定する。
  • デプロイ対象のクラスタは placement で指定。クラスタ名を直接指定したり、pod 配置のように label selector などを指定できる。
propagate.yml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: nginx # The default namespace is `default`.
  namespace: default
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: nginx # If no namespace is specified, the namespace is inherited from the parent object scope.
      namespace: default
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2

デプロイ。

$ k apply -f propagate.yml
propagationpolicy.policy.karmada.io/nginx created

管理下のクラスタにデプロイされたリソースは通常の kubectl と同様に kubectl karmada get [resource] で取得できます。本来はこれで取得できるのですが、試したところエラーが発生しました。

$ k karmada get pod
cluster(member2) is inaccessible, please check authorization or network
cluster(member1) is inaccessible, please check authorization or network

karmada コマンドでは kubectl と同様に -v で詳細レベルを設定可能です。エラーが発生する場合は -v の指定がないと情報が少なすぎるので -v 5 以上を指定することを推奨。
-v 10 にして実行した結果が以下。

I0621 08:02:46.549252 2522618 round_trippers.go:553] GET https://10.0.0.40:30238/apis/cluster.karmada.io/v1alpha1/clusters/member1/proxy/api 403 Forbidden in 7 milliseconds
I0621 08:02:46.549351 2522618 round_trippers.go:570] HTTP Statistics: GetConnection 0 ms ServerProcessing 7 ms Duration 7 ms
I0621 08:02:46.549363 2522618 round_trippers.go:577] Response Headers:
I0621 08:02:46.549370 2522618 round_trippers.go:580]     Cache-Control: no-cache, private
I0621 08:02:46.549375 2522618 round_trippers.go:580]     Cache-Control: no-cache, private
I0621 08:02:46.549380 2522618 round_trippers.go:580]     Cache-Control: no-cache, private
I0621 08:02:46.549385 2522618 round_trippers.go:580]     Content-Type: application/json
I0621 08:02:46.549390 2522618 round_trippers.go:580]     Date: Fri, 21 Jun 2024 08:02:46 GMT
I0621 08:02:46.549448 2522618 round_trippers.go:580]     Content-Length: 344
I0621 08:02:46.549513 2522618 round_trippers.go:580]     Audit-Id: a2ef0b6e-420d-44dd-8d2b-63d349e3922d
I0621 08:02:46.549574 2522618 round_trippers.go:580]     Audit-Id: a2ef0b6e-420d-44dd-8d2b-63d349e3922d
I0621 08:02:46.549633 2522618 round_trippers.go:580]     Audit-Id: a2ef0b6e-420d-44dd-8d2b-63d349e3922d
I0621 08:02:46.549692 2522618 round_trippers.go:580]     X-Content-Type-Options: nosniff
I0621 08:02:46.549769 2522618 request.go:1212] Response Body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"users \"system:admin\" is forbidden: User \"system:serviceaccount:karmada-cluster:karmada-impersonator\" cannot impersonate resource \"users\" in API group \"\" at the cluster scope","reason":"Forbidden","details":{"name":"system:admin","kind":"users"},"code":403}

どうやらメンバークラスタの情報を取得する際に 403 となっており、Response Body にあるとおり system:serviceaccount:karmada-cluster:karmada-impersonator の SA に system:admin の許可が不足しているようです。
実際に member1 クラスタ側で SA と clusterRole を確認すると確かに権限が不足しています。

$ k get clusterrole karmada-impersonator -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    resourcetemplate.karmada.io/managed-annotations: resourcetemplate.karmada.io/managed-annotations,resourcetemplate.karmada.io/managed-labels,resourcetemplate.karmada.io/uid,work.karmada.io/name,work.karmada.io/namespace
    resourcetemplate.karmada.io/managed-labels: karmada.io/managed,work.karmada.io/permanent-id
    resourcetemplate.karmada.io/uid: ""
    work.karmada.io/name: karmada-impersonator-7cbb6bd5c9
    work.karmada.io/namespace: karmada-es-member1
  creationTimestamp: "2024-06-21T08:00:21Z"
  labels:
    karmada.io/managed: "true"
    work.karmada.io/permanent-id: ae904d66-7c0f-4347-ba4d-a65c26e25688
  name: karmada-impersonator
  resourceVersion: "2073"
  uid: d44d0d8e-a765-49b7-b7a9-b18032939361
rules:
- apiGroups:
  - ""
  resourceNames:
  - system:kube-controller-manager
  resources:
  - users
  verbs:
  - impersonate
- apiGroups:
  - ""
  resourceNames:
  - generic-garbage-collector
  - namespace-controller
  - resourcequota-controller
  resources:
  - serviceaccounts
  verbs:
  - impersonate
- apiGroups:
  - ""
  resourceNames:
  - system:masters
  resources:
  - groups
  verbs:
  - impersonate

なぜ不足しているかは不明ですが、今回はあくまで検証なので直接 clusterRole を編集して権限を追加します。ただ内部的にリソースを karmada で管理しているらしくそのまま edit を実行しても内容が変化しないため、label の karmada.io/managed: "true" を削除する必要があります。

$ k edit clusterrole karmada-impersonator

metadata:
  labels:
-    karmada.io/managed: "true"
    work.karmada.io/permanent-id: ae904d66-7c0f-4347-ba4d-a65c26e25688
  name: karmada-impersonator
  resourceVersion: "3496"
  uid: d44d0d8e-a765-49b7-b7a9-b18032939361
rules:
  - apiGroups:
    - ""
    resourceNames:
    - system:kube-controller-manager
+   - system:admin
    resources:
    - users
    verbs:
    - impersonate

memebr1, memebr2 クラスタでそれぞれ上記の対応を実行することで通信できるようになり、各クラスタにデプロイされた pod が確認できるようになります。マニフェストのレプリカ数で指定したとおり、各メンバークラスタ member1, member2 の worker node に pod が 2 つずつデプロイされていることが確認できます。

$ k karmada get pod -o wide
NAME                     CLUSTER   READY   STATUS    RESTARTS   AGE    IP             NODE      NOMINATED NODE   READINESS GATES   ADOPTION
nginx-77d8468669-2d4tr   member1   1/1     Running   0          4d3h   10.244.1.122   k8s-w1    <none>           <none>            N
nginx-77d8468669-c2v8v   member1   1/1     Running   0          4d3h   10.244.1.121   k8s-w1    <none>           <none>            N
nginx-86dcfdf4c6-kzrd5   member2   1/1     Running   0          4d3h   10.244.1.8     sub1-w1   <none>           <none>            N
nginx-86dcfdf4c6-pdxb9   member2   1/1     Running   0          4d3h   10.244.1.7     sub1-w1   <none>           <none>            N

このように PropagationPolicy を使うことで管理対象のクラスタにまとめてリソースをデプロイできます。

マルチクラスタ Failover を試す

karmada における Failover は、管理対象クラスタのいずれかがダウンした際に生き残っているクラスタに pod などのワークロードを移行する機能となっています。通常の k8s クラスタでは worker node がダウンした際に残りの node で pod が起動する動作となっていますが、それをクラスタレベルに拡張したものをイメージすれば分かりやすいと思います。


Failover の概要図。Cluster member1 がダウンすると PropagationPolicy の設定に基づいて member2 か member3 上で新しい Pod が起動する。Failover Overview より引用。

こちらの機能についても動作を確認してみます。
まず nginx pod を 10 個起動するマニフェストを作成し、これをメンバークラスタにデプロイする PropagationPolicy を作成します。

failover.yml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: nginx # The default namespace is `default`.
  namespace: default
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: nginx # If no namespace is specified, the namespace is inherited from the parent object scope.
      namespace: default
  placement:
    replicaScheduling:
      replicaDivisionPreference: Weighted
      replicaSchedulingType: Divided
      weightPreference:
        staticWeightList:
          - targetCluster:
              clusterNames:
                - member1
            weight: 1
          - targetCluster:
              clusterNames:
                - member2
            weight: 1

先程の PropagationPolicy との違いは、各クラスタに pod を均等に配置するように placement.replicaScheduling を設定している点となっています(詳細はMultiple strategies of replica Scheduling を参照)。上記のように replicaScheduling を設定すると、targetCluster に指定したクラスタの weight に基づいて pod が分割して起動します。今回の場合 2 クラスタの weight をいずれも 1 に設定しているため、10 pod のうち 1 クラスタに 5 pod ずつ起動するようになります。
実際に PropagationPolicy をデプロイすると member1, member2 のクラスタに 5 pod ずつ 起動することが確認できます。

$ k karmada get pod -o wide
NAME                     CLUSTER   READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES   ADOPTION
nginx-76749dc54c-846qb   member1   1/1     Running   0          35s   10.244.1.98    sub1-w1   <none>           <none>            N
nginx-76749dc54c-b4p9k   member1   1/1     Running   0          35s   10.244.1.99    sub1-w1   <none>           <none>            N
nginx-76749dc54c-hm9lj   member1   1/1     Running   0          35s   10.244.1.100   sub1-w1   <none>           <none>            N
nginx-76749dc54c-mmlpq   member1   1/1     Running   0          35s   10.244.1.97    sub1-w1   <none>           <none>            N
nginx-76749dc54c-tnj4k   member1   1/1     Running   0          35s   10.244.1.101   sub1-w1   <none>           <none>            N
nginx-76749dc54c-76n6w   member2   1/1     Running   0          35s   10.244.1.89    sub2-w1   <none>           <none>            N
nginx-76749dc54c-9d54j   member2   1/1     Running   0          35s   10.244.1.90    sub2-w1   <none>           <none>            N
nginx-76749dc54c-hm96h   member2   1/1     Running   0          35s   10.244.1.92    sub2-w1   <none>           <none>            N
nginx-76749dc54c-jl25p   member2   1/1     Running   0          35s   10.244.1.93    sub2-w1   <none>           <none>            N
nginx-76749dc54c-rqm2n   member2   1/1     Running   0          35s   10.244.1.91    sub2-w1   <none>           <none>            N

内部的には weight に基づいて replica を分割した deployment が各クラスタに作成されているようです。

$ k get deployments.apps
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   10/10   10           10          63s

$ k karmada get deployments.apps
NAME    CLUSTER   READY   UP-TO-DATE   AVAILABLE   AGE   ADOPTION
nginx   member1   5/5     5            5           71s   Y
nginx   member2   5/5     5            5           71s   Y

次に、クラスタ障害により member2 クラスタに接続できなくなるような状況を模擬するため、member2 クラスタの control-plane node ノード上で sudo shutdown now を実行してシャットダウンします。
少し待つと karmada cluster から member2 クラスタへ通信できなくなるため、member2 クラスタのステータスは ready: false になります。

$ k get cluster
NAME      VERSION   MODE   READY   AGE
member1   v1.29.6   Push   True    3d5h
member2   v1.29.6   Push   False   3d5h

member2 クラスタとの通信ができなくなると、deployment のレプリカ数 10 を満たすために member2 クラスタ上で稼働していた pod が Failover により member1 クラスタで起動されます。同時に新規に pod を配置しないよう member2 クラスタに taint が設定されます。これは k describe cluster member2 の Event で確認できます。

$ k describe cluster member2

Events:
  Type    Reason               Age                 From                Message
  ----    ------               ----                ----                -------
  Normal  TaintClusterSucceed  46s (x3 over 3d5h)  cluster-controller  Taint cluster succeed: cluster now has taints([{Key:cluster.karmada.io/not-ready,Effect:NoSchedule}]).
  Normal  TaintClusterSucceed  15s (x2 over 3d4h)  cluster-controller  Taint cluster succeed: cluster now has taints([{Key:cluster.karmada.io/not-ready,Effect:NoSchedule},{Key:cluster.karmada.io/not-ready,Effect:NoExecute}]).

member1 クラスタの pod を確認すると pod 数が 10 になっており、AGE 3m29s となっているものが新規に起動した pod となっています。

$ k karmada get pod -o wide
NAME                     CLUSTER   READY   STATUS    RESTARTS   AGE     IP             NODE      NOMINATED NODE   READINESS GATES   ADOPTION
nginx-76749dc54c-27fxb   member1   1/1     Running   0          3m29s   10.244.1.104   sub1-w1   <none>           <none>            N
nginx-76749dc54c-846qb   member1   1/1     Running   0          12m     10.244.1.98    sub1-w1   <none>           <none>            N
nginx-76749dc54c-8zt7h   member1   1/1     Running   0          3m29s   10.244.1.102   sub1-w1   <none>           <none>            N
nginx-76749dc54c-b4p9k   member1   1/1     Running   0          12m     10.244.1.99    sub1-w1   <none>           <none>            N
nginx-76749dc54c-hm9lj   member1   1/1     Running   0          12m     10.244.1.100   sub1-w1   <none>           <none>            N
nginx-76749dc54c-kj5hs   member1   1/1     Running   0          3m29s   10.244.1.106   sub1-w1   <none>           <none>            N
nginx-76749dc54c-lp9nn   member1   1/1     Running   0          3m29s   10.244.1.105   sub1-w1   <none>           <none>            N
nginx-76749dc54c-mmlpq   member1   1/1     Running   0          12m     10.244.1.97    sub1-w1   <none>           <none>            N
nginx-76749dc54c-tnj4k   member1   1/1     Running   0          12m     10.244.1.101   sub1-w1   <none>           <none>            N
nginx-76749dc54c-zb98z   member1   1/1     Running   0          3m29s   10.244.1.103   sub1-w1   <none>           <none>            N
error: cluster(member2) is inaccessible, please check authorization or network

なお、member2 クラスタでは kube-apiserver を含む control-plane node はダウンした状態となっていますが、worker node で起動している pod (コンテナ) は起動したままの状態となっています。これはノード上のコンテナを調べることで確認できます。

member2 クラスタ worker node 上
$ sudo nerdctl -n k8s.io ps | grep nginx:latest
19b82ddf0372    docker.io/library/nginx:latest                                                                                      "/docker-entrypoint.…"    18 minutes ago    Up                 k8s://default/nginx-76749dc54c-rqm2n/nginx
614267ce0264    docker.io/library/nginx:latest                                                                                      "/docker-entrypoint.…"    18 minutes ago    Up                 k8s://default/nginx-76749dc54c-hm96h/nginx
8d7684a71103    docker.io/library/nginx:latest                                                                                      "/docker-entrypoint.…"    18 minutes ago    Up                 k8s://default/nginx-76749dc54c-76n6w/nginx
9620b39596e5    docker.io/library/nginx:latest                                                                                      "/docker-entrypoint.…"    18 minutes ago    Up                 k8s://default/nginx-76749dc54c-9d54j/nginx
b7d10132fedc    docker.io/library/nginx:latest                                                                                      "/docker-entrypoint.…"    18 minutes ago    Up                 k8s://default/nginx-76749dc54c-jl25p/nginx

そのため deployment のステータスとしての pod 数は (もともとあった 10 pod) + (Failover によって新規に起動した 5 pod) = 15 pod となり、一時的にマニフェストの 10 pod よりも多くなっていることが確認できます。

$ k get deployments.apps nginx
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   15/10   15           15          14m

$ k karmada get deployments.apps
NAME    CLUSTER   READY   UP-TO-DATE   AVAILABLE   AGE   ADOPTION
nginx   member1   10/10   10           10          14m   Y
error: cluster(member2) is inaccessible, please check authorization or network

このように稼働中のクラスタが障害によりダウンすると Failover により他のクラスタで pod が起動するようになっています。

さて、先程シャットダウンした member2 クラスタのノードを再起動してみます。karmada control-plane と対象クラスタの kube-apiserver 間の通信が通るようになるとクラスタステータスは再び READY: True に戻ります。

$ k get cluster
NAME      VERSION   MODE   READY   AGE
member1   v1.29.6   Push   True    3d5h
member2   v1.29.6   Push   True    3d5h

ダウンしていたクラスタが使えるようになっても pod の再配置等は実行されないため、member1 クラスタ上の pod 数は 10 のままであり、member2 クラスタでもダウン前に起動していた 5 pod はそのままとなっています。

NAME                     CLUSTER   READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES   ADOPTION
nginx-76749dc54c-27fxb   member1   1/1     Running   0          16m   10.244.1.104   sub1-w1   <none>           <none>            N
nginx-76749dc54c-846qb   member1   1/1     Running   0          25m   10.244.1.98    sub1-w1   <none>           <none>            N
nginx-76749dc54c-8zt7h   member1   1/1     Running   0          16m   10.244.1.102   sub1-w1   <none>           <none>            N
nginx-76749dc54c-b4p9k   member1   1/1     Running   0          25m   10.244.1.99    sub1-w1   <none>           <none>            N
nginx-76749dc54c-hm9lj   member1   1/1     Running   0          25m   10.244.1.100   sub1-w1   <none>           <none>            N
nginx-76749dc54c-kj5hs   member1   1/1     Running   0          16m   10.244.1.106   sub1-w1   <none>           <none>            N
nginx-76749dc54c-lp9nn   member1   1/1     Running   0          16m   10.244.1.105   sub1-w1   <none>           <none>            N
nginx-76749dc54c-mmlpq   member1   1/1     Running   0          25m   10.244.1.97    sub1-w1   <none>           <none>            N
nginx-76749dc54c-tnj4k   member1   1/1     Running   0          25m   10.244.1.101   sub1-w1   <none>           <none>            N
nginx-76749dc54c-zb98z   member1   1/1     Running   0          16m   10.244.1.103   sub1-w1   <none>           <none>            N
nginx-76749dc54c-76n6w   member2   1/1     Running   0          25m   10.244.1.89    sub2-w1   <none>           <none>            N
nginx-76749dc54c-9d54j   member2   1/1     Running   0          25m   10.244.1.90    sub2-w1   <none>           <none>            N
nginx-76749dc54c-hm96h   member2   1/1     Running   0          25m   10.244.1.92    sub2-w1   <none>           <none>            N
nginx-76749dc54c-jl25p   member2   1/1     Running   0          25m   10.244.1.93    sub2-w1   <none>           <none>            N
nginx-76749dc54c-rqm2n   member2   1/1     Running   0          25m   10.244.1.91    sub2-w1   <none>           <none>            N

ある程度時間が立つと member2 クラスタ上の pod が終了され、マニフェスト上の定義である 10 pod に戻ります。

NAME                     CLUSTER   READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES   ADOPTION
nginx-76749dc54c-27fxb   member1   1/1     Running   0          23m   10.244.1.104   sub1-w1   <none>           <none>            N
nginx-76749dc54c-846qb   member1   1/1     Running   0          32m   10.244.1.98    sub1-w1   <none>           <none>            N
nginx-76749dc54c-8zt7h   member1   1/1     Running   0          23m   10.244.1.102   sub1-w1   <none>           <none>            N
nginx-76749dc54c-b4p9k   member1   1/1     Running   0          32m   10.244.1.99    sub1-w1   <none>           <none>            N
nginx-76749dc54c-hm9lj   member1   1/1     Running   0          32m   10.244.1.100   sub1-w1   <none>           <none>            N
nginx-76749dc54c-kj5hs   member1   1/1     Running   0          23m   10.244.1.106   sub1-w1   <none>           <none>            N
nginx-76749dc54c-lp9nn   member1   1/1     Running   0          23m   10.244.1.105   sub1-w1   <none>           <none>            N
nginx-76749dc54c-mmlpq   member1   1/1     Running   0          32m   10.244.1.97    sub1-w1   <none>           <none>            N
nginx-76749dc54c-tnj4k   member1   1/1     Running   0          32m   10.244.1.101   sub1-w1   <none>           <none>            N
nginx-76749dc54c-zb98z   member1   1/1     Running   0          23m   10.244.1.103   sub1-w1   <none>           <none>            N

member2 クラスタが復旧するとダウン時に設定されていた taint は自動で削除されるため、復旧後に新規に作成した pod は member2 にも配置されるようになります。

Events:
  Type    Reason               Age                 From                Message
  ----    ------               ----                ----                -------
  Normal  TaintClusterSucceed  24m (x3 over 3d5h)  cluster-controller  Taint cluster succeed: cluster now has taints([{Key:cluster.karmada.io/not-ready,Effect:NoSchedule}]).
  Normal  TaintClusterSucceed  23m (x2 over 3d5h)  cluster-controller  Taint cluster succeed: cluster now has taints([{Key:cluster.karmada.io/not-ready,Effect:NoSchedule},{Key:cluster.karmada.io/not-ready,Effect:NoExecute}]).
  Normal  TaintClusterSucceed  11m (x2 over 3d4h)  cluster-controller  Taint cluster succeed: cluster now has taints([{Key:cluster.karmada.io/not-ready,Effect:NoExecute}]).
  Normal  TaintClusterSucceed  10m (x3 over 3d5h)  cluster-controller  Taint cluster succeed: cluster now does not have taints.

このようにクラスタ復旧後の pod 数は karmada 側で自動で調整してくれます。ただ上記で見たように調整後に pod が起動しているクラスタが偏っている場合の再配置等は実行されません。上記の状況では member2 クラスタ上の nginx deployment 自体が削除されているので pod 削除や kubectl rollout restart コマンドでは member2 クラスタに配置されず、いったん deployment の削除 → 再作成を行う必要があります。

$ k karmada get deployments.apps  -C member1
NAME    CLUSTER   READY   UP-TO-DATE   AVAILABLE   AGE   ADOPTION
nginx   member1   10/10   10           10          45m   Y

$ k karmada get deployments.apps  -C member2
No resources found in  namespace.

その他の機能

karmada では他にも様々な機能がありますが数が多いので、ここでは個人的に面白そうな機能について簡単に紹介します。

Override Policy

Overrides Policy ではリソースを複数のクラスタにデプロイする際、対象クラスタの条件に基づいて一部のプロパティを動的に追加、削除、置換する機能となっています。基本的に同じ deployment をデプロイしたいけど label や annotation をクラスタ毎に変更・追加したいといった用途に使えます。ドキュメントでは以下の override が記載されています。

override の種類 説明
ImageOverrider コンテナイメージを変更する
CommandOverrider コンテナの command を変更する
ArgsOverrider コンテナの args を変更する
LabelsOverrider リソースの label を変更する
AnnotationsOverrider リソースの annotation を変更する
PlaintextOverrider kubectl path に相当し、リソースの任意のプロパティを変更する

Argocd によるデプロイ

前の検証ではデプロイ対象のリソースと PropagationPolicy のマニフェストを手動でデプロイしましたが、本格的な運用では Argocd などのデプロイツールを利用して自動でデプロイを実行するケースも多いかと思います。ドキュメントでは Argocd と Flux を用いた karmada のデプロイ方法について記載されています。

といっても検証で見たように基本的に PropagationPolicy と対象リソースのマニフェストをそれぞれデプロイすればいいだけなので、Argocd を使う場合でも Github repo にこれらのマニフェストを保存し、Argocd でデプロイを実行するという流れになります。

クラスタ間ネットワーク

karmada で管理するメンバークラスタはあくまでそれぞれ個別のクラスタであるため、クラスタ間の通信は CNI 等を使ったクラスタ内のフラットなネットワークではなく通常のネットワークでの通信到達性を考える必要があります。しかしクラスタ間ネットワークを実現するような OSS 等を利用することにより、クラスタ内でのフラットネットワークをクラスタレベルに拡張し、クラスタ間での Service Discovery や AutoScaling を実現できるようになっています。
ただし karmada 自体にこの機能が組み込まれているわけではなく外部のコンポーネントを利用する必要があるようです。ドキュメント ではクラスタ間のフラットなネットワーク通信を提供する OSS submariner を利用する方法について記載されています。

https://submariner.io/

submariner を利用したクラスタ間 Service Discovery についても検証していましたが、あるクラスタから別のクラスタのサービスに通信する部分がうまく行かず断念しました(機会があればまた試すかも)。
途中までの検証は検証メモの Submariner によるクラスタ間ネットワーク に記載しています。

検証メモ

検証中のメモ

Submariner によるクラスタ間ネットワーク

karmada のクラスタ間ネットワークで記載されているように submariner を使うとクラスタ間で pod や svc の通信が可能となります。submariner では Broker と呼ばれるコンポーネントがクラスタ間ネットワークのメタデータを管理し、各クラスタは Gateway Engine を通じてクラスタ間通信を実現します。

submariner のアーキテクチャ。https://submariner.io/getting-started/architecture/globalnet/ より引用

また、デフォルトの構築方法では各クラスタの ServiceCIDR が重複してはいけないという前提条件があり、重複している場合は Globalnet と呼ばれる仕組みを使う必要があります。

However, by default, a limitation of Submariner is that it doesn’t handle overlapping CIDRs (ServiceCIDR and ClusterCIDR) across clusters. Each cluster must use distinct CIDRs that don’t conflict or overlap with any other cluster that is going to be part of the ClusterSet.

今回の検証に使用した 3 つのクラスタではいずれも POD CIDR と ServiceCIDR が重複しているので globalnet を使用して構築します。


ここでは submariner ドキュメントの EXTERNAL NETWORK (EXPERIMENTAL) に沿ってクラスタ間ネットワークを構築します。ドキュメントでは 2 つのクラスタノードと 1 つの 非クラスタ VM を準備していますが、VM は今回使用しないので不要です。

submariner では subctl CLI で各種操作を行うのでインストール。

curl -Ls https://get.submariner.io | bash
export PATH=$PATH:~/.local/bin
echo export PATH=\$PATH:~/.local/bin >> ~/.profile

はじめに Broker の設定を行います。submariner では Broker がクラスタ間の通信を交換するためのコンポーネントとなっており、クラスタ間ネットワークに含まれるすべてのクラスタがアクセス可能な api server 上に構築する必要があります。今回の検証では karmada cluster が各メンバークラスタと通信可能になっているため、karmada クラスタの通常のクラスタ側 (karmada control-plane ではない) で以下のコマンドを実行して broker を構築します。

export KUBECONFIG=/home/ubuntu/.kube/config
subctl deploy-broker  --globalnet

次に他のクラスタを Broker に参加させますが、その前に今回使用しているノード一覧について書き出しておきます。

ノード名 cluster ノードの IP address POD CIDR role
main-m1 karmada 10.0.0.40 10.244.0.0/16 control-plane
main-w1 karmada 10.0.0.41 - worker
sub1-m1 member1 10.0.0.50 10.244.0.0/16 control-plane
sub1-w1 member1 10.0.0.51 - worker
sub2-m1 member2 10.0.0.30 10.244.0.0/16 control-plane
sub2-w1 member2 10.0.0.31 - worker
  • CLUSTER_CIDR はドキュメントの通り POD CIDR の 10.244.0.0/16 に設定。
  • EXTERNAL_CIDR は全クラスタのノード IP アドレスの範囲とかぶらないように設定する。ここでは 10.0.0.96/27

上記の CIDR を元に karmada クラスタを broker に参加。

CLUSTER_CIDR=10.244.0.0/16
EXTERNAL_CIDR=10.0.0.96/27
subctl join broker-info.subm --clusterid karmada --natt=false --clustercidr=${CLUSTER_CIDR},${EXTERNAL_CIDR}

次に member1, 2 クラスタを broker に参加させます。

subctl join --kubeconfig /home/ubuntu/.kube/sub1.config  broker-info.subm --clusterid member1 --natt=false
subctl join --kubeconfig /home/ubuntu/.kube/sub2.config  broker-info.subm --clusterid member2 --natt=false

現在のクラスタの接続状況については subctl show all で確認できます。connection や gateway が表示され、エラー等が表示されなければ ok。

$ subctl show all

Cluster "main_k8s"
 ✓ Detecting broker(s)
NAMESPACE               NAME                COMPONENTS                        GLOBALNET   GLOBALNET CIDR   DEFAULT GLOBALNET SIZE   DEFAULT DOMAINS
submariner-k8s-broker   submariner-broker   service-discovery, connectivity   yes         242.0.0.0/8      65536

 ✓ Showing Connections
GATEWAY   CLUSTER   REMOTE IP   NAT   CABLE DRIVER   SUBNETS        STATUS      RTT avg.
sub1-w1   member1   10.0.0.51   no    libreswan      242.1.0.0/16   connected   852.419µs
sub2-w1   member2   10.0.0.31   no    libreswan      242.2.0.0/16   connected   835.722µs

 ✓ Showing Endpoints
CLUSTER   ENDPOINT IP   PUBLIC IP        CABLE DRIVER   TYPE
karmada   10.0.0.41     118.158.73.193   libreswan      local
member1   10.0.0.51     118.158.73.193   libreswan      remote
member2   10.0.0.31     118.158.73.193   libreswan      remote

 ✓ Showing Gateways
NODE      HA STATUS   SUMMARY
main-w1   active      All connections (2) are established

 ✓ Showing Network details
    Discovered network details via Submariner:
        Network plugin:  generic
        Service CIDRs:   [10.96.0.0/12]
        Cluster CIDRs:   [10.244.0.0/16,10.0.0.96/27]
        Global CIDR:     242.0.0.0/16

 ✓ Showing versions
COMPONENT                       REPOSITORY           CONFIGURED   RUNNING                     ARCH
submariner-gateway              quay.io/submariner   0.17.1       release-0.17-fdfcc8797bc2   amd64
submariner-routeagent           quay.io/submariner   0.17.1       release-0.17-fdfcc8797bc2   amd64
submariner-globalnet            quay.io/submariner   0.17.1       release-0.17-fdfcc8797bc2   amd64
submariner-metrics-proxy        quay.io/submariner   0.17.1       release-0.17-1e49b8404147   amd64
submariner-operator             quay.io/submariner   0.17.1       release-0.17-0133a99b08a6   amd64
submariner-lighthouse-agent     quay.io/submariner   0.17.1       release-0.17-b9c6b562f556   amd64
submariner-lighthouse-coredns   quay.io/submariner   0.17.1       release-0.17-b9c6b562f556   amd64

なお、構築が完了すると submariner-operator namespace に以下の pod が作成されます。

$ k get pod -n submariner-operator
NAME                                             READY   STATUS    RESTARTS   AGE
submariner-gateway-s9hcc                         1/1     Running   0          3d3h
submariner-globalnet-gklkd                       1/1     Running   0          3d3h
submariner-lighthouse-agent-754bfcf58c-klf5q     1/1     Running   0          3d3h
submariner-lighthouse-coredns-7fb549f74d-jq2zx   1/1     Running   0          3d3h
submariner-lighthouse-coredns-7fb549f74d-vrgf5   1/1     Running   0          3d3h
submariner-metrics-proxy-dmg4w                   2/2     Running   0          3d3h
submariner-operator-ffc859cfd-w9sfw              1/1     Running   0          3d3h
submariner-routeagent-7jlwn                      1/1     Running   0          3d3h
submariner-routeagent-h8dlk                      1/1     Running   0          3d3h

これでクラスタ間ネットワークか構成されたので、karmada の Multi-cluster Service Discovery に沿って動作確認します。

まず serviceexports, serviceimport CRD をメンバークラスタ上に作成します。以下のような ClusterPropagationPolicy マニフェストを作成。

svc-crd.yml
# propagate ServiceExport CRD
apiVersion: policy.karmada.io/v1alpha1
kind: ClusterPropagationPolicy
metadata:
  name: serviceexport-policy
spec:
  resourceSelectors:
    - apiVersion: apiextensions.k8s.io/v1
      kind: CustomResourceDefinition
      name: serviceexports.multicluster.x-k8s.io
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2
---
# propagate ServiceImport CRD
apiVersion: policy.karmada.io/v1alpha1
kind: ClusterPropagationPolicy
metadata:
  name: serviceimport-policy
spec:
  resourceSelectors:
    - apiVersion: apiextensions.k8s.io/v1
      kind: CustomResourceDefinition
      name: serviceimports.multicluster.x-k8s.io
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2

karmada 上でデプロイすることで 2 つのメンバークラスタにまとめて CRD を作成します。

export KUBECONFIG=/home/ubuntu/.kube/karmada.config
k apply -f svc-crd.yml

次にドキュメントの通り以下のマニフェストを作ってデプロイします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: serve
spec:
  replicas: 1
  selector:
    matchLabels:
      app: serve
  template:
    metadata:
      labels:
        app: serve
    spec:
      containers:
      - name: serve
        image: jeremyot/serve:0a40de8
        args:
        - "--message='hello from cluster member1 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'"
        env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
---
apiVersion: v1
kind: Service
metadata:
  name: serve
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: serve
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: mcs-workload
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: serve
    - apiVersion: v1
      kind: Service
      name: serve
  placement:
    clusterAffinity:
      clusterNames:
        - member1
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceExport
metadata:
  name: serve
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: serve-export-policy
spec:
  resourceSelectors:
    - apiVersion: multicluster.x-k8s.io/v1alpha1
      kind: ServiceExport
      name: serve
  placement:
    clusterAffinity:
      clusterNames:
        - member1
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceImport
metadata:
  name: serve
spec:
  type: ClusterSetIP
  ports:
  - port: 80
    protocol: TCP
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: serve-import-policy
spec:
  resourceSelectors:
    - apiVersion: multicluster.x-k8s.io/v1alpha1
      kind: ServiceImport
      name: serve
  placement:
    clusterAffinity:
      clusterNames:
        - member2

上記をデプロイすると以下のような動作になります。

  1. serve pod, svc が member1 クラスタ 上に作成される。
  2. ServiceExport CRD によりクラスタ間でアクセス可能な serve svc が作成される。
  3. ServiceImport CRD により serve にアクセスするための svc が member2 クラスタ上に作成される。

submariner の性質により subctl export コマンドで特定のサービスを公開するか ServiceExport CRD を作成すると、クラスタ間でグローバルにアクセス可能なサービスが作成されます。これは globalingressips リソースとして確認できます。

$ k karmada get globalingressips.submariner.io -A
NAMESPACE   NAME            CLUSTER   IP              ADOPTION
default     serve           member1   242.1.255.253   N
default     derived-serve   member2   242.2.255.253   N

上記の serve が member1 クラスタ上に作成した svc のグローバルエンドポイントに対応し、IP がエンドポイントに接続するための IP アドレスになっています。
また、member1 クラスタの svc を確認すると submariner-.. の svc が作成され、上記で確認した IP アドレスが EXTERNAL-IP に設定されていることが確認できます。

$ k get svc
NAME                                          TYPE        CLUSTER-IP       EXTERNAL-IP     PORT(S)   AGE
kubernetes                                    ClusterIP   10.96.0.1        <none>          443/TCP   26h
serve                                         ClusterIP   10.102.199.123   <none>          80/TCP    98s
submariner-etcfrt5unwnek3jrjr4joya6kfly4q6b   ClusterIP   10.99.110.190    242.1.255.253   80/TCP    98s

上記の IP アドレスにアクセスすることで member1 クラスタのノードだけでなく karmada クラスタ、memebr2 クラスタのノードからでも memebr1 クラスタ上の serve pod に到達できます。

$ curl 242.1.255.253
'hello from cluster member1 (Node: sub1-w1 Pod: serve-8765f9f98-7n4zm Address: 10.244.1.91)'

また、作成されたグローバルエンドポイントは pod 内ネットワークで [service_name].[namespace].svc.clusterset.local で名前解決できます。member2 クラスタ内に適当に pod を立てて、pod 内から nslookup 等を実行することで確認できます。

member クラスタ2 の POD 内で実行
root@nginx-76749dc54c-czzsk:/# nslookup  serve.default.svc.clusterset.local
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   serve.default.svc.clusterset.local
Address: 242.1.255.253

# dig
root@nginx-76749dc54c-czzsk:/# dig  serve.default.svc.clusterset.local

; <<>> DiG 9.18.24-1-Debian <<>> serve.default.svc.clusterset.local
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27186
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: f738fcff83c7111c (echoed)
;; QUESTION SECTION:
;serve.default.svc.clusterset.local. IN A

;; ANSWER SECTION:
serve.default.svc.clusterset.local. 5 IN A      242.1.255.253

;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10) (UDP)
;; WHEN: Sat Jun 22 10:10:13 UTC 2024
;; MSG SIZE  rcvd: 125

このように submariner では k8s service にクラスタ間で有効なグローバルな IP アドレスを割り当てる動作となっています。

上記で作成されたサービスの関係を図示すると以下のようになります。svc を service export で公開するとそれに対応した submariner-... svc が作成され、EXTERNAL-IP (globalingressips) が付加されます。これを通じて submariner に属するクラスタ間アクセスを実現できます。


イメージ図

Step 3: Access the service from member2 cluster では karmada の Service Discovery 機能として、本来は derived-serve にアクセスすることで member1 クラスタの pod にルーティングされるように設定できるはずですが、手元で試しても svc と pod が結びつかずうまくいきかせんでした。設定が間違っているのかもしれません。

おわりに

k8s マルチクラスタ管理の OSS karmada を使用してマルチクラスタへのリソースデプロイや Failover の動作を確認しました。karmada 自体の独自概念やコンポーネントが多く学習コストがやや高いことや、そもそもマルチクラスタを本格的に管理しようとすると管理コストや運用方式等を考える必要があるのでなかなか使いどころが難しいという事情はありますが、もし OSS でマルチクラスタ管理を実現したいのであれば選択肢の 1 つには入ってくるといった立ち位置かなと感じました。
とはいえ CNCF の incubator プロジェクトとなっているので、近年のマルチクラウドやマルチクラスタ管理の需要に応じて今後の発展や機能拡張が期待されます。

Discussion