はじめに
3-shakeの小林(@moz_sec_)です。第20回目の今回は、Kubernetesクラスターでマルチテナント環境を実現する「Capsule」について解説します。
KubernetesではNamespaceによってリソースを分離できますが、チームや組織単位で複数のNamespaceを管理する機能は標準では提供されていません。そこで、Capsuleはシングルクラスターのまま組織単位での管理を可能にするための仕組みを提供します。
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 | ユーザー | 権限 |
| tenant1 | tenant1-dev tenant1-prod | tenant1-manager | テナントのNamespaceのみ管理 |
| tenant1-dev | tenant1-developer | tenant1-devのみ変更可能 | |
| tenant2 | tenant2-dev tenant2-prod | tenant2-manager | テナントのNamespaceのみ管理 |
| tenant2-dev | tenant2-developer | tenant2-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 3stenant1-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 16stenant1-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種類のカスタムリソースを使ってリソース管理を行います。
- ResourcePool
クラスター管理者がテナントごとの最大リソース枠を定義 - 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: tenant1ResourcePoolを作成すると、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種類のカスタムリソースを使用します。
- GlobalTenantResource
クラスター管理者がテナントに対してリソースを配布 - 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 11sTenantResource
テナントオーナーがテナント配下の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回で解説しているため、あわせて参照してみてください。
【参考】
