EKSのマルチテナント化を踏まえたZOZOGLASSのシステム設計

こんにちは。計測プラットフォーム本部バックエンド部SREチームの市橋です。

私たちのチームではZOZOSUIT、ZOZOMAT、ZOZOGLASSといった計測技術に関わるシステムの開発、運用を担当しています。現在のZOZOMATとZOZOGLASSは、どちらも独立したEKSクラスタ上で動いていますが、ZOZOGLASSの環境を構築する際に将来のマルチテナント化を踏まえ大きく設計を見直しました。今回は、この設計見直し時に考慮した点を紹介します。

ZOZOGLASSとは

ZOZOGLASSは顔の情報を計測し、イエローベースとブルーベースの2タイプ、及び春夏秋冬の4タイプの組み合わせからなるパーソナルカラーを診断するサービスです。計測した顔の情報から肌の色に近いファンデーションを推薦します。2021年7月時点で、ZOZOGLASSが推薦するコスメアイテムはファンデーションのみですが、今後はファンデーション以外のコスメアイテムも追加される予定です。ZOZOGLASSは無料で予約可能ですので、ご興味のある方はこちらからご予約ください。ちなみに私のパーソナルカラー診断の結果はブルーベースの冬でした。

EKSマルチテナントの構想

マルチテナント化を検討した背景

ZOZOGLASSでマルチテナント化を検討した背景について触れる前に、マルチテナントについて整理します。マルチテナント(シングルクラスタ)とは単一のEKSクラスタの上で複数のサービスを運用することを指します。比較対象となる構成としてマルチクラスタ・シングルテナントがあり、これらを図示すると以下のようになります。

両者を比較するとそれぞれ次のような特徴があると考えています。

構成 マルチクラスタ・シングルテナント シングルクラスタ・マルチテナント 説明
費用 マルチクラスタ・シングルテナントの場合、管理しているクラスタ数分の課金、及びクラスタ管理用リソースのためのマシンリソース分の料金が追加で必要になる。
運用コスト × マルチクラスタ・シングルテナントの場合、管理しているクラスタ数分のアップデート作業が必要になる。
また、クラスタ設定の構成管理とその自動化(IaC、CI/CDなど)、監視・観測設定もクラスタ分必要になる。
クラスタ障害時の影響範囲 シングルクラスタ・マルチテナントでクラスタ障害が発生した場合、クラスタ内で運用しているサービス全てに影響が及ぶ。
権限管理の容易性 シングルクラスタ・マルチテナントの場合、テナントごとに権限を分離するための考慮や設定が必要になる。

両者の選択については様々な記事が公開されており、それによると組織構造やプロダクトの成熟度も考慮すべき事項だと感じました。弊チームは複数のサービスを1つの開発チームと1つのクラスタ管理チームで運用する体制になっています。このような体制であるにも関わらず、下図のようにサービス毎にEKSクラスタが存在していました。

この場合、運用する上で冗長な部分が多くありました。具体例を1つ挙げると、EKSクラスタのアップデート対応です。ZOZOMATの時点で、非本番環境を含めると多数のクラスタが存在しており、アップデート対応に苦しめられました。ZOZOGLASSを開発するにあたり、ZOZOMATの設計を踏襲すると、負荷が更に増えてしまいます。

以上のことから、クラスタの管理コスト削減を目指し、ZOZOGLASSでは将来のマルチテナント化を前提とした設計を進めることにしました。マルチテナント化後の体制は下図のイメージになります。

また、今後も新規サービスが計画されており、その開発時にもクラスタの作成やそれに付随する諸作業を省略できると考えました。

マルチテナント化は次のように進めます。

  1. マルチテナント化前提にZOZOGLASSを設計し、共通基盤としてのEKSクラスタmeasurement-platformで運用(本記事の主題)
  2. ZOZOMATをmeasurement-platformへ移行(2021年7月時点で実施中)

移行のイメージは以下の図の通りです。

ZOZOMATのクラスタ移行については、現在実施中ということもあり深く触れません。本記事ではEKSでマルチテナントを実現するために、考慮した事について紹介します。

設計時の考慮事項

マルチテナント化を進めるにあたっては、AWS公式ブログの記事を参考にして考慮事項を1つずつ確認しました。この記事ではテナントを以下の3つの観点から分離することが重要であると述べています。

  • コンピューティングの分離
  • ネットワークの分離
  • ストレージの分離

私たちの環境ではKubernetesのストレージ機構を利用していないため、コンピューティング、ネットワークの観点を中心に確認しました。その他、Kubernetesマニフェストをどのリポジトリで管理するかについても重要なポイントだと感じたため、少しアレンジして以下の観点で確認しました。

  • コンピューティングの分離
  • ネットワークの分離
  • Kubernetesマニフェストの管理リポジトリの分離

コンピューティングの分離

マルチテナント化に向けた1つ目の観点として、コンピューティングの分離を考えます。先のブログ記事では次のことを担保すべきとしています。

  • 権限が分離できること
  • マシンリソースの競合を回避できること

私たちはコンピューティングタイプとしてFargateを採用しています。Fargateを利用することでこの2つの要件を比較的容易に解決できました。

権限分離の観点

KubernetesはRBACと呼ばれるロールベースのアクセス制御の機構を持ち、これを利用して誰が何を実行できるかを決定します。これにはクラスタ全体で共通利用できるClusterRoleと、単一のnamespaceでのみ有効なRoleがあります。テナントごとに利用する権限を設定する場合にはRoleを利用するため、namespaceをテナントごとに作成しておいた方が権限を分離しやすくなります。そのため、今回はテナントごとに1つのnamespaceを作成しました。この後にも度々namespaceが登場しますが、分離度を高める上で重要な概念のため、どのような単位でnamespaceを分けるか予めよく考える必要があります。

podがAWSサービスにアクセスするにはRBACとAWSのIAMを統合して利用します。この際、権限の分離度を高めるためにはIAM roles for service accounts(以下、IRSA)を利用することが望ましいです。これはpodに付与されているServiceAccountsにIAMRoleを紐づけるもので、これによりpod内のコンテナからAWSリソースの操作が可能になります。

コンピューティングタイプがEC2の場合は、podがEC2インスタンスに付与されているIAMロールを利用できます。そのため、1インスタンスで動作する全podのAWSサービスへのアクセス権限をひとまとめにして、1つのIAMロールにポリシーを詰め込んで使うこともできます。シングルテナントの場合はこれで問題ないケースもありますが、マルチテナントの場合は他のテナントで使っているAWSリソースの操作も許可されてしまうため、権限を制限しておくことで安全性が増します。なお、Fargateの場合はインスタンスの権限を利用できないため、IRSAの利用が必須となります。そのため、必然的にpodごとに権限の分離度を高める機会を得られることになります。IRSAの設定は以下の流れで行います。

STEP1 OIDCプロバイダーの作成

まず、podからIAMRoleを利用する際に必要となるOIDCプロバイダーを作成します。私たちが構築した当時はCloudFormationが対応しておらず、eksctlも機能不足1により利用できなかったため、Webコンソールから作成しました。

現在は両方対応しており、手元で試せてはいませんがCloudFormationはAWS::IAM::OIDCProviderリソースを使って、eksctlであれば以下のコマンドで作成できるはずです。

$ eksctl utils associate-iam-oidc-provider --cluster <cluster_name> --approve
STEP2 IRSA用のIAMRoleの作成

次にIAMRoleを作成します。以下のYAMLは、IRSA用IAMロールを作成するCloudFormationテンプレートのサンプルです。IRSAを利用するために、AssumeRolePolicyDocumentへSTEP1で作成したOIDCプロバイダと信頼関係を結ぶ設定を記述します。

  IAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'irsa-for-api'
      AssumeRolePolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Federated": "arn:aws:iam::${AWS::AccountId}:oidc-provider/${EKSOIDCProvider}"
              },
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Condition": {
                "StringEquals": {
                  "${EKSOIDCProvider}:sub": "system:serviceaccount:${Namespace}:api"
                }
              }
            }
          ]
        }
      Path: '/'
      Policies:
        - PolicyDocument:
            Statement:
              - Effect: 'Allow'
                Action: 's3:PutObject'
                Resource:
                  - !Sub '${S3Bucket.Arn}/*'
          PolicyName: 'irsa-for-api-s3'

${EKSOIDCProvider}には以下のコマンドで取得できるURLのうち、idの部分を指定します。

$ aws eks describe-cluster --name <cluster_name> --query "cluster.identity.oidc.issuer" --output text
STEP3 ServiceAccountの作成

最後に、podに割り当てるServiceAccountを作成します。annotationsにSTEP2で作成したIAMRoleのArnを指定します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: serviceaccount-api
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::000000000000:role/irsa-for-api

ここで生成されたServiceAccountをpodに付与することで、IAMRoleのポリシーに含まれるAWSリソースへのアクセス権限を得られます。

マシンリソースの競合の回避

「コンピューティングの分離」の最後はマシンリソースの競合について考えます。これを回避する方法にはpodの割当リソースに制限をかけるか、配置するノードを分けるか、2つの選択肢があります。

前者はリソースクォータを利用することで実現可能です。リソースクォータはnamespaceごとに定義できるため、これを利用することでテナント間のマシンリソースの奪い合いを制限できます。

後者はコンピューティングタイプをEC2、Fargateのどちらで運用しているかで対応方法が変わります。EC2の場合、テナントごとにノードグループを作成してnodeSelectorsやnodeAffinityを使って、どのノードグループにpodを配置するかを制御することでリソースの競合を回避できます。Fargateの場合はそもそもVM環境が分離された状態でpodが実行され、podの要求リソースに応じたマシンリソースを持ったノードが自動で割り当てられます。そのため、特に意識することなくマシンリソースの競合を回避できます。細かい設定をせずにマシンリソースを分離できる点はFargateを利用するメリットの1つかと思います。

ネットワークの分離

マルチテナント化に向けた2つ目の観点として、ネットワークの分離を考えます。デフォルトではクラスタ内であればnamespace越しの通信が許可されています。もし、これを許容できない要件があれば、NetworkPolicyを利用することで制限できます。ただし、NetworkPolicyが利用できるのはコンピューティングタイプがEC2の場合のみで、Fargateを利用する場合はこれを利用できません。代替案としては、AppMeshの機能で通信を制御する方法が推奨されています。詳しく調べられていないのですが、podの全通信にEnvoyが介在するような形になり、Egressのフィルタリングでnamespace越しの通信を遮断するような形になると考えています。それぞれ図示すると以下のようになります。

最終的にAppMeshの導入については、導入工数とシステムへの影響を鑑みて充分にPoCを行った上で導入可否を決定した方が良いと考え、ZOZOGLASSのリリース時点では導入を見送りました。リリース時点ではシングルテナントの構成であり、ZOZOMATアプリケーションを集約してマルチテナント構成になるまでに対策を講じるという判断です。その後、どうすべきかあれこれ考えているうちに、Fargate podにセキュリティグループを設定可能になるリリースが2021年6月に発表されました。これによりFargateでも上図のNetworkPolicyのような制御が可能になると考えています。容易に導入できるメリットから、これを最有力候補として検討しています。ちょうどいいタイミングでリリースされて助かりました。

Kubernetesマニフェストの管理リポジトリの分離

マルチテナント化に向けた3つ目、最後の観点として、Kubernetesマニフェストの管理リポジトリの分離を考えます。ここでの内容はマルチクラスタ・シングルテナントを前提としていたZOZOMATを、マルチテナント型に作り替えることを考えた時に感じたやりにくさを踏まえたものになります。そのため、まずはZOZOMATでのマニフェスト管理を説明します。

ZOZOMATでは全てのマニフェストファイルをアプリケーション用のリポジトリで管理していました。これにはaws-authやmetrics-server、ClusterRoleのようなクラスタ内に1つあればよいリソースも含みます。シングルテナントであればアプリケーションとクラスタの設定が集約されるため、シンプルな管理が可能です。

一方、マルチテナントで運用する場合、この設計は設定変更の足かせとなります。別サービスの事情でクラスタ管理用のリソースへ変更を加える際、ZOZOMATのアプリケーションリポジトリにも手を加える必要があります。以上のことから、クラスタ管理用リソースとサービス固有のリソースで、リポジトリを分けることにしました。ingressとaws-loadbalancer-controllerを例に説明します。

aws-loadbalancer-controllerは、ingressリソースが作成されたことを検知してELBを作成するリソースです。aws-load-balancer-controllerはクラスタ内に1つあればよく、ingressはELBが必要なテナントごとに作成します。下図のイメージになります。

この場合、aws-loadbalancer-controllerはクラスタ管理用リポジトリ、ingressはサービス固有のアプリケーションリポジトリでマニフェストファイルを管理するようにしました。こうすることで、同じような構成でテナントを増やす際、既存のマニフェストファイルをテンプレートとして再利用しやすくなりました。

まとめ

以上の考慮事項を踏まえてマルチテナントを前提としたEKSクラスタを構築し、無事にZOZOGLASSの計測システムをリリースできました。マルチテナント化のネクストアクションは、ZOZOMATリソースを共通基盤へ移行することで、目下この作業に取り組んでいます。

今後リリースを予定しているZOZOSUIT 2ZOZOMAT for Handsはパートナー企業様と協業で進めるサービスとなっています。これらのサービスは信頼性やアクセス管理の面において、より厳格な要件を求められることが予想されます。今回得られた知見を生かして開発に取り組み、要件を満たした上でいち早くサービス提供することに尽力していきます。

私たちはこのような課題に対して楽しんで取り組み、一緒にサービスを作り上げてくれる方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。

tech.zozo.com


  1. 当時は、AWS SSOから生成される認証情報を使ってeksctlを実行できませんでした。こちらのissueで報告されており、 2021年5月ごろ対応が完了したようです。エラーが出た場合はeksctlのバージョンを上げることで回避できる可能性があります。

カテゴリー