Kubernetesスペシャリストが注目する関連ツール探求 20

「Capsule」でKubernetesのマルチテナント環境を構築する

第20回の今回は、Kubernetesクラスターでマルチテナント環境を実現する「Capsule」について解説します。

小林 舜

4月8日 6:30

はじめに

3-shakeの小林(@moz_sec_)です。第20回目の今回は、Kubernetesクラスターでマルチテナント環境を実現する「Capsule」について解説します。
KubernetesではNamespaceによってリソースを分離できますが、チームや組織単位で複数のNamespaceを管理する機能は標準では提供されていません。そこで、Capsuleはシングルクラスターのまま組織単位での管理を可能にするための仕組みを提供します。

【引用】https://github.com/projectcapsule/capsule/blob/main/assets/logo/capsule.png

Capsuleの概要

Kubernetesでは、Namespaceによりクラスターを論理的に分割することができます。しかし、Namespaceはフラットな構造であり、テナントという概念は存在しません。同じ組織に属する複数のNamespaceをまとめて管理したり、共通のポリシーを適用したりする機能は標準では提供されておらず、シングルクラスターでマルチテナント構成を実現しようとすると運用が複雑になりがちです。これを避けるために、マルチクラスター構成を採用するケースもありますが、この場合クラスター数の増加による運用コストが問題になります。
Capsuleは、この問題をテナントという抽象化レイヤーを導入することで解決します。複数のNamespaceを1つのテナントとしてまとめ、テナント単位での権限管理・ポリシー適用・リソース制御を可能にします。
Capsuleではユーザーを3つの役割に分けます。

  • クラスター管理者
    テナントの作成や全体ポリシーの定義を行う
  • テナントオーナー
    限定された管理権限を持ち、自身のテナント配下のNamespaceを管理する
  • テナントユーザー
    テナント内で特定の操作のみを行う

クラスター管理者からテナントオーナーへ権限を委譲することで、組織のガバナンスを保ちながら各チームが自律的にクラスターを運用できるようになります。

Capsuleのインストール

Capsuleは、Helmでインストールできます。

$ helm repo add projectcapsule https://projectcapsule.github.io/charts
$ helm install capsule projectcapsule/capsule -n capsule-system --create-namespace

インストールが完了すると、capsule-controller-managerという Pod が起動します。このコントローラーがCapsuleのカスタムリソースを監視し、Namespaceへの反映やポリシー適用を行います。

シナリオ

本記事では、1台のPC上で、それぞれの役割の kubeconfig に切り替えながらCapsuleの使い方を紹介します。
想定しているシナリオは以下の通りです。

  • tenant1 / tenant2 の2テナント
  • 各テナントにdev(開発用)とprod(本番用)のNamespace

Capsule の3つのロールと本記事で使用するユーザーの関係は次の通りです。

  • クラスター管理者(cluster-admin)
    • 本記事の読者
  • テナントオーナー
    • tenant1-manager
    • tenant2-manager
  • テナントユーザー
    • tenant1-developer
    • tenant2-developer
    • sre

manager はテナントオーナーであり、自身のテナントに属する Namespace に対して管理操作を行うことができます。ただし、他のテナントやクラスタ全体にはアクセスできません。
developer はアプリケーション開発を担当するテナントユーザーであり、開発用のdev Namespace のみ編集できます。本番用のprod Namespace にはアクセスできません。
sre は複数テナントを横断して状態を確認するテナントユーザーです。障害調査や監視は可能ですが、リソースの変更や削除はできません。
本記事の読者はクラスター管理者として、Capsule のインストール、テナントの作成やリソース管理、権限管理を行います。

テナントNamespaceユーザー権限
tenant1tenant1-dev
tenant1-prod
tenant1-managerテナントのNamespaceのみ管理
tenant1-devtenant1-developertenant1-devのみ変更可能
tenant2tenant2-dev
tenant2-prod
tenant2-managerテナントのNamespaceのみ管理
tenant2-devtenant2-developertenant2-devのみ変更可能
tenant1
tenant2
tenant1-dev
tenant1-prod
tenant2-dev
tenant2-prod
sre全てのNamespaceを閲覧のみ可能
 all読者全ての権限

それぞれのユーザーの認証情報(kubeconfig)を作成します。ユーザー作成用のスクリプトであるcreate-user.shを使用します。

$ ./create-user.sh tenant1-manager tenant1
$ ./create-user.sh tenant2-manager tenant2
$ ./create-user.sh tenant1-developer tenant1
$ ./create-user.sh tenant2-developer tenant2
$ ./create-user.sh sre all

テナント管理

Capsuleでは、複数のNamespaceを「テナント」という論理単位にまとめ、RBACや各種ポリシーを一括適用することができます。テナントの定義にはTenant カスタムリソースを使用します。

Tenant

テナントオーナーには、テナント配下の各 Namespace に対して、admin ClusterRole を参照する RoleBinding が自動的に作成されます。ただし、これはテナント配下の Namespace に限定されるため、テナント外の Namespace は操作できないようになっています。
テナントユーザーに対して、テナント配下のすべての Namespace に共通の RoleBinding を自動生成することも可能です。spec.additionalRoleBindingsで定義した ClusterRole は、テナント配下の各 Namespace に RoleBinding として自動的に作成されます。
本記事の例では、テナントオーナーとしてtenant1-managerを登録し、sreにはテナント配下の Namespace を横断して参照できるようview ClusterRoleを付与しています。

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: tenant1
spec:
  owners: # テナントオーナー
    - name: tenant1-manager
      kind: User
  additionalRoleBindings: # テナント配下のNamespaceに自動で生成
    - clusterRoleName: view
      subjects:
        - apiGroup: rbac.authorization.k8s.io
          kind: User
          name: sre

同様に、tenant2のテナントも作成します。

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: tenant2
spec:
  owners:
    - name: tenant2-manager
      kind: User
  additionalRoleBindings:
    - clusterRoleName: view
      subjects:
        - apiGroup: rbac.authorization.k8s.io
          kind: User
          name: sre

テナントを確認すると、Namespace数が0であることが分かります。

$ kubectl get tenants
NAME     STATE    NAMESPACE COUNT   READY   STATUS       AGE
tenant1  Active   0                 True    reconciled   79s
tenant2  Active   0                 True    reconciled   3s

tenant1-managerの認証情報を使って、tenant1-dev Namespaceを作成します。ここでは省略しますが、tenant1-prod Namespace も作成しておきましょう。

$ kubectl --kubeconfig tenant1-manager-tenant1.kubeconfig create namespace tenant1-dev
namespace/dev created

また、同様にtenant2-managerの認証情報を使ってtenant2-devとtenant2-prod Namespaceも作成しておいてください。
Namespaceを作成すると、tenant1-managerとsreに割り当てられるRoleBindingが自動生成されます。

$ kubectl get rolebindings -n tenant1-dev
NAME                                          ROLE                                    AGE
capsule-tenant1-0-admin                       ClusterRole/admin                       16s
capsule-tenant1-1-capsule-namespace-deleter   ClusterRole/capsule-namespace-deleter   16s
capsule-tenant1-2-view                        ClusterRole/view                        16s

tenant1-developerにはtenant1-dev Namespaceのedit ClusterRole だけ付与したいため、手動でRoleBindingを作成します。spec.additionalRoleBindingsに定義すると全NamespaceにRoleBindingが作成されるため、ここでは手動でRoleBindingを作成しています。

$ kubectl -n tenant1-dev create rolebinding tenant1-developer-edit \
  --clusterrole=edit \
  --user=tenant1-developer

次に、tenant1-developerの認証情報を使ってPodを作成します。tenant1-dev Namespaceでの編集や閲覧は可能ですが、kube-system/tenant2-dev Namespaceでは権限エラーが出力されます。

# tenant1のdeveloper権限に切り替え
$ export KUBECONFIG=tenant1-developer-tenant1.kubeconfig

# tenant1-dev NamespaceにPodを作成
$ kubectl -n tenant1-dev run nginx1 --image=docker.io/nginx
pod/nginx1 created

# tenant1配下のNamespaceにあるPodは見れる
$ kubectl -n tenant1-dev get pods
NAME    READY   STATUS    RESTARTS   AGE
nginx1   1/1     Running   0          18s

# kube-system NamespaceのPodは見れない
$ kubectl -n kube-system get pods
Error from server (Forbidden): pods is forbidden: User "tenant1-developer" cannot list resource "pods" in API group "" in the namespace "kube-system"

# tenant2-dev NamespaceのPodは見れない
$ kubectl -n tenant2-dev get pods
Error from server (Forbidden): pods is forbidden: User "tenant1-developer" cannot list resource "pods" in API group "" in the namespace "tenant2-dev"

カスタムリソースの使用

テナントオーナーには、テナント配下の各Namespaceにadmin ClusterRole を参照するRoleBindingが自動生成されます。ただし、CRD(CustomResourceDefinition)は cluster-scoped なリソースであり、作成にはcluster-admin ClusterRole(または同等の権限)が必要です。
そのため、クラスター管理者があらかじめ CRD をインストールし、カスタムリソースを操作するための ClusterRole を定義します。そして、その ClusterRole を RoleBinding で参照することで、テナント内に限定してカスタムリソースを操作できるようになります。
例えば、クラスター管理者があらかじめ Argo CD をデプロイしている場合、次のような ClusterRole を定義します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: argocd-provisioner
rules:
- apiGroups: ["argoproj.io"]
  resources: ["applications", "appprojects"]
  verbs: ["create", "get", "list", "watch"]

このClusterRoleをspec.additionalRoleBindingsに指定することで、Capsule がテナント配下の各 Namespace に RoleBinding を自動生成し、テナントオーナーやテナントユーザーが Argo CD を操作できるようになります。

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: tenant1
spec:
  owners:
  - name: tenant1-manager
    kind: User
  additionalRoleBindings:
  - clusterRoleName: 'argocd-provisioner'
    subjects:
    - apiGroup: rbac.authorization.k8s.io
      kind: User
      name: tenant1-manager

テナントの制限

テナントオーナーはテナント配下の Namespace に任意のラベルやアノテーションを追加できます。しかし、NetworkPolicy などで「特定のラベルを持つ Namespace からの通信のみ許可する」といった設計にしている場合、テナント側が自由にそのラベルを付与できてしまうと、意図しない通信が許可される可能性があります。
そのため、クラスター管理者は Tenant 作成時に以下のような制約をかけることができます。

  • 特定のラベルやアノテーションの設定を禁止
  • Namespace 作成時に追加メタデータを強制付与
  • 作成可能な Namespace 数を制限

付与するメタデータには固定値だけでなく、{{tenant.name}}{{namespace}}といったプレースホルダーも使用できます。
以下はその例です。

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: tenant1
spec:
  owners:
    - name: tenant1-manager
      kind: User
  namespaceOptions:
    quota: 3              # Namespaceの数を制限
    forbiddenAnnotations: # 特定のアノテーションを禁止
      denied:
        - foo.acme.net
    forbiddenLabels:
      deniedRegex: .*.acme.net # 正規表現で記述可能
    additionalMetadataList: # 追加のメタデータを自動付与
      - annotations:
          templated-annotation: "{{ tenant.name }}" # プレースホルダーの使用
        labels:
          projectcapsule.dev/backup: "true" # 任意の値
          templated-label: "{{ namespace }}" # プレースホルダーの使用

テナントに対して制限を与えることで、Namespace が増えすぎることを防ぎつつ、NetworkPolicy などを前提とした安全なテナント間分離を実現できます。

リソース管理

クラスター管理者はテナントごとに利用できるリソース量を制限できます。これにより、一部のテナントがリソースを使い切ってしまうことを防ぎます。
Capsuleでは、次の2種類のカスタムリソースを使ってリソース管理を行います。

  1. ResourcePool
    クラスター管理者がテナントごとの最大リソース枠を定義
  2. ResourcePoolClaim
    テナント側が実際に使用するリソース量をResourcePoolに対して要求

ResourcePool と ResourcePoolClaim をもとに対象となる Namespace にKubernetes 標準の ResourceQuota を自動生成し、使用可能なリソース量を制御します。

ResourcePool 

クラスター管理者がテナントごとに利用できる最大リソースを定義します。
Capsuleは、このResourcePoolを参照して、指定したテナント配下のNamespaceにResourceQuotaを自動的に作成します。

apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePool
metadata:
  name: tenant1
spec:
  quota:
    hard: # 要求可能な総リソース量
      limits.cpu: "2"
      limits.memory: 2Gi
      requests.cpu: "2"
      requests.memory: 2Gi
  selectors:
  - matchLabels: # テナントの指定
      capsule.clastix.io/tenant: tenant1

ResourcePoolを作成すると、tenant1-dev/tenant1-prod Namespaceに空のResourceQuotaが自動生成されます。

$ kubectl get resourcequotas -A
NAMESPACE      NAME                  REQUEST    LIMIT   AGE
tenant1-dev    capsule-pool-tenant1                     39s
tenant1-prod   capsule-pool-tenant1                     39s

この時点では、まだ具体的な制限値は反映されていません。

ResourcePoolClaim

テナントオーナーは、Namespace 単位で実際に使用するリソースを要求します。
本記事では動作の理解を優先し、ResourcePoolClaim はクラスター管理者が作成します。実運用では、ResourcePoolClaim の作成をテナントオーナーに委譲することも可能ですが、その場合は RBACを付与する必要があります。

apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePoolClaim
metadata:
  name: limit-cpu
  namespace: tenant1-dev
spec:
  pool: "tenant1" # ResourcePoolの指定
  claim:
    limits.cpu: "2"

ResourcePoolClaimを作成すると、そのNamespaceのResourceQuotaに値が反映され、tenant1-devで使用できるCPUが 2 core に制限されます。

$ kubectl get resourcequotas -n tenant1-dev
NAME                  REQUEST    LIMIT             AGE
capsule-pool-tenant1             limits.cpu: 0/2   4m25s

このようにResourcePoolがテナント全体の最大枠を定義し、ResourcePoolClaim によってNamespaceごとの具体的な制限が有効になります。
ResourcePoolClaimの合計値は、ResourcePoolで定義された上限を超えることはできません。もし合計が上限を超える ResourcePoolClaim を作成した場合はリソースを確保できず、PoolExhausted になります。例えば、すでに tenant1-dev Namespace で limits.cpu=2 を要求しており、ResourcePool の上限が 2 core の場合、tenant1-prod でさらに 2 core を要求するとPoolExhausted になります。

$ kubectl get resourcepoolclaims.capsule.clastix.io -A
NAMESPACE    NAME      POOL    STATUS REASON          AGE
tenant1-dev  limit-cpu tenant1 Bound  Succeeded       8m56s
tenant1-prod limit-cpu tenant1 Bound  PoolExhausted   6m16s

また、ResourcePool の selectors に一致しない Namespace からの ResourcePoolClaim も拒否されます。

レプリケーション

NamespaceスコープのKubernetesリソース(例: ConfigMap、Secret、Deployment、Custom Resource)を任意のNamespaceに配布することができます。
レプリケーションには、2種類のカスタムリソースを使用します。

  1. GlobalTenantResource
    クラスター管理者がテナントに対してリソースを配布
  2. TenantResource
    テナントオーナーがテナント配下のNamespaceにリソースを配布

どちらのカスタムリソースも以下の機能を共通で備えています。

  • 既存リソースをコピーして配布
  • 新規リソースを生成して配布
  • {{tenant.name}}{{namespace}}といったプレースホルダー
  • 定期的に自動同期

GlobalTenantResource

クラスター管理者がテナントにリソースを配布するための仕組みであり、テナントオーナーやテナントユーザーは作成できません。
ここでは、プライベートレジストリにアクセスするためのimagePullSecretを用意し、tenant1テナント配下のNamespaceにのみ配布する例を紹介します。
まず配布元となる private Namespace を作成し、そこに次のようなSecretを作成しておきます。

apiVersion: v1
kind: Secret
metadata:
  name: private-registry-pull-secret
  namespace: private
  labels:
    app: private-registry
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: ewogICJhdXRocyI6IHsKICAgICJyZWdpc3RyeS5leGFtcGxlLmNvbSI6IHsKICAgICAgInVzZXJuYW1lIjogImR1bW15LXVzZXIiLAogICAgICAicGFzc3dvcmQiOiAiZHVtbXktcGFzc3dvcmQiLAogICAgICAiYXV0aCI6ICJaSFZ0YlhrdGRYTmxjanBrZFcxdGVTMXdZWE56ZDI5eVpBPT0iCiAgICB9CiAgfQp9

次に、配布対象となるtenant1テナントにラベルを付与します。GlobalTenantResource はこのラベルを条件にテナントを選択します。

$ kubectl label tenant tenant1 company.com/private-registry=enabled

続いて、tenantSelctorで配布対象のテナントを絞り込み、配布するリソースを指定してGlobalTenantResource を作成します。

apiVersion: capsule.clastix.io/v1beta2
kind: GlobalTenantResource
metadata:
  name: private-registry-pull-secret
spec:
  tenantSelector: # テナント指定
    matchLabels:
      company.com/private-registry: enabled
  resyncPeriod: 60s
  resources:
    - namespacedItems: # 既存リソースをコピーして配布
        - apiVersion: v1
          kind: Secret
          namespace: private
          selector:
            matchLabels:
              app: private-registry

しばらくすると、tenant1-dev NamespaceにSecretが自動的に作成されます。

$ kubectl get secrets -n tenant1-dev
NAME                          TYPE                            DATA   AGE
private-registry-pull-secret  kubernetes.io/dockerconfigjson  1      11s

TenantResource

テナントオーナーがテナント配下のNamespaceに共通リソースを配布するために使用します。
ただし、テナントオーナーに付与される admin ClusterRole にはTenantResource を操作する権限がないため、あらかじめ ClusterRole を作成してテナントオーナーに付与する必要があります。ここでは、集約ClusterRole で admin ClusterRole にTenantResourceの操作権限を集約しています。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: tenant-resources
  labels:
    rbac.authorization.k8s.io/aggregate-to-admin: "true" # adminに集約
rules:
- apiGroups: ["capsule.clastix.io"]
  resources: ["tenantresources"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

次に、テナント配下のすべての Namespace に共通の ConfigMap を配布する例を示します。ここでは rawItems を使用し、tenant1-manager の権限で TenantResource を作成します。
TenantResource はテナントに属する Namespace からのみ作成できるため、事前に配布元となる tenant1-system Namespace を作成し、tenant1 テナントに属させておく必要があります。

# tenant1のmanager権限に切り替え
$ export KUBECONFIG=tenant1-manager-tenant1.kubeconfig

$ kubectl create ns tenant1-system

$ kubectl apply -f - <<EOF
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: common-app-config
  namespace: tenant1-system
spec:
  resyncPeriod: 60s
  resources:
    - rawItems: # 新規リソースを生成して配布
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: "{{tenant.name}}-common-app-config"
          data:
            LOG_LEVEL: "info"
            TENANT: "{{tenant.name}}"
            NAMESPACE: "{{namespace}}"
EOF

プレースホルダーは、TenantResourceによりリソースが各Namespaceに作成される際に動的に展開されます。
しばらくすると、tenant1-dev Namespace にConfigMapが自動で作成されます。同様に tenant1-prod Namespaceにも同じ ConfigMap が作成されます。

$ kubectl get configmaps -n tenant1-dev tenant1-common-app-config -o jsonpath="{.data}"
{"LOG_LEVEL":"info","NAMESPACE":"tenant1-dev","TENANT":"tenant1"}

おわりに

今回は、Capsuleを使って、シングルクラスター上でマルチテナント環境を構築する方法を解説しました。
Kubernetes は本来フラットな Namespace 構造を持ちますが、組織がスケールするにつれて、「チーム単位で責任境界を明確に分離しつつ、必要なリソースを安全に共有・管理できる仕組み」が求められます。近年では Platform Engineering という考え方の広がりとともに、インフラを集約しながらも権限やリソースの境界を明確に設計することが重要になっています。
シングルクラスターでマルチテナントを実現するか、マルチクラスターで分離するかは、組織規模やセキュリティ要件によって異なります。本記事が、その設計判断の一助となれば幸いです。
同じくシングルクラスター上でマルチテナントを実現するツールとして 「vCluster」 があります。vCluster については第14回で解説しているため、あわせて参照してみてください。
 

【参考】

この記事のキーワード

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored