大規模アプリケーション開発運用をマルチテナント方式のGKEクラスタで実現した話

こんにちは。EC基盤グループの宮口(@smiyaguchi)と池田(@progrhyme)です。

モノタロウではKubernetesのマネージドサービスであるGoogle Kubernetes Engine(以下、GKE)を利用しています。 このKubernetesですがとても便利な反面、管理が大変で開発者がアプリケーションの開発とKubernetesの運用を同時に行うのは負荷が高くなりあまり好ましくありません。 そこでモノタロウでは開発と運用を分離できるように、社内でGKE共通環境と呼んでいるマルチテナント方式のクラスタによるアプリケーションの実行基盤を構築しました。 今回はその紹介をします。

マルチテナント・シングルテナントとは?

本題に入る前にマルチテナントとシングルテナントについて簡単に触れます。 GKEにおけるマルチテナントとはGKEのクラスタの中で論理的に分離したテナントと呼ばれる隔離環境を作成し、同一のクラスタの中で異なるアプリケーションを共存させる方式です。 マルチテナントのメリットはクラスタの管理が楽になることです。アプリケーション毎にクラスタを作成する必要がなくなるので管理するクラスタの対象が減り運用の負荷が下がることが期待できます。

対してGKEにおけるシングルテナントはクラスタを単一のアプリケーションで占有する方法です。クラスタ全体を1つのテナントとみなします。 シングルテナントのメリットはその分かりやすさとコントロールプレーンをアプリケーション毎に分離できるのでセキュリティレベルが上がること等です。ただ、デメリットとしてアプリケーション毎にクラスタが増えることになるので管理の労力はクラスタが増えるほど上がっていきます。

なぜマルチテナントのGKE環境を作ることにしたのか

モノタロウではGKEの利用が進んできており、コンテナを利用したアプリケーションの実行基盤としてGKEが事実上の社内標準になりつつあります。

そのような中、モノタロウの検索基盤を置き換えるというプロジェクトで新たにGKEを利用することになりました。そのプロジェクトでは外部の協力会社から技術支援を受けていて、その中でGKEクラスタの運用方法としてマルチテナントを勧めてもらったのがきっかけです。

今後GCPを利用する案件も増えてくる予定となっていたことと、その際に素早く環境を立ち上げて開発者は開発に専念できるようにしたいという課題もあったため、社内で検討した結果マルチテナントのGKE環境を作ることになりました。

全体概要

今回構築したGKE共通環境の構成は以下のようになっています。

f:id:smiyaguchi:20211203135443p:plain
図. GKE共通環境概要

通常のVPC内ではなく共有VPC内にGKEクラスタを構築しています。 共有VPCとはGCPの各プロジェクト間でVPCを共有可能な仕組みで、これを利用することによりVPCなどのネットワークリソースを一つのプロジェクトで集中管理することができるようになります。 共有VPCが存在するプロジェクトをホストプロジェクト、共有VPCを利用するプロジェクトをサービスプロジェクトと呼び、GKE共通環境はサービスプロジェクトの中に構築しています。

モノタロウはGCPの他にAWS・オンプレミスのデータセンターを利用しており、GKE共通環境の共有VPCはこれらと閉域網接続することで、GKE共通環境からアクセスできるようにもなっています。

また、このGKEクラスタは限定公開クラスタと呼ばれるノードに内部IPのみが付与され外部とは隔離されるクラスタとして構築することで、よりセキュアな環境にしています。

構築にあたり行なった主な作業は以下の通りです。

  • Node、Pod、Serviceに必要なIPアドレスの設計
  • 共有VPC内にGKEクラスタを構築・テスト
  • 外部と通信できるようにIPマスカレードエージェントを設定

ここまでGKE共通環境に関しての概要、全体像を紹介しました。 これからは環境情報であったり、構築にあたって工夫したこと・難しかったことなどをより詳細に書いていきます。

前提・環境情報

前述したようにGKE共通環境はホストプロジェクトの共有VPCを利用する形で、サービスプロジェクトにGKEクラスタを構築しています。 モノタロウではこのホストプロジェクトとゲストプロジェクトの管理をそれぞれ別の部署が担当しており、ホストプロジェクトは共有VPCなどのネットワークリソースを管理することになるので社内のインフラグループが、GKE共通環境として利用するサービスプロジェクトはEC基盤グループが管理しています。 これによりネットワークを集中管理した上でクラスタの管理を分けて責務を分離させ、より効率的な運用が出来る体制を築いています。

このホストプロジェクトですが、実は今回新たに構築した訳でなく既に社内に存在しており、既に利用実績もあったのですぐに利用できるといったメリットもありました。

担当の話が出たので少しモノタロウ内での体制について補足します。 モノタロウでは、世の中の多くのIT企業と同様に、システム開発を行う部署とサーバやネットワークといった社内インフラを管理する部署が分かれています。私たちが所属するEC基盤グループは両者の橋渡し的な立ち位置で、多数のアプリケーション開発者が共通して利用できるツールや仕組みを提供する役割を担っています。 今回GKE共通環境を構築する際は、上述のサーバ・ネットワークを管理する部署とコミュニケーションを取り、サポートもしてもらいながら構築しました。共有VPCを利用する場合は責務が分離出来る一方、コミュニケーションは適切に取っていく必要があります。

GKE共通環境の特徴

前置きが長くなってしまいましたが、ここからはGKE共通環境を構築するにあたり工夫した点を紹介します。

Namespace・ノードプールの分離

まず初めに工夫したことはテナントの分離方法です。

KubernetesのNamespaceだけで分離すると1つのノード内で異なるアプリケーションが起動することが可能になります。この場合、次のようなことが懸念として挙げられます。

  • 複数のNamespaceのアプリケーションが、CPU・メモリなどの共通資源を奪い合う
  • 複数のアプリケーションが動作するのに十分なスペックのノードを用意する必要があり、キャパシティプランニングが難しい。

そこでGKE共通環境ではNamespaceを論理的に分離するのと合わせて、GKEのノードプールを分けることで各Podが起動するノードを物理的に分離出来るようにしました。

新たにGKE共通環境を利用する際はNamespaceとノードプールを新しく作成してテナントを作成し、そのテナントにアプリケーションを配置させることで異なるアプリケーション同士に影響がないようにしています。 また、ノードのマシンタイプはノードプール毎に設定可能なので、アプリケーション毎に異なるマシンタイプを用意するといったことも可能です。 アプリケーションをテナントに配置する際は現状マニフェストにNamespaceとNodeSelectorでノードを指定してもらうことで実現しています。

[利用例]

apiVersion: v1
kind: Pod
metadata:
  namespace: <namespace名>
spec:
  containers:
  - name: myapp
    image: myapp
  nodeSelector:
    cloud.google.com/gke-nodepool: <nodepool名>

RBACによる権限管理

次に工夫したことは権限の管理です。 テナントを作成できるようになりましたが、何も制御しないと利用者全員が全てのテナントのリソースを閲覧・操作出来てしまいます。

それは問題なのでGKE共通環境ではNamespace毎にKubernetesのRBAC(Role Based Access Control)を設定することで利用者が特定のテナントのみ閲覧・操作できるようにしています。 このRBACですが利用者ごとに設定している訳ではなく、Googleグループ単位でRBACを設定して権限を管理しています。こうすることによってGoogleグループ内でアドレスの入れ替えが発生しても変更しなくてすむようにしています。

[利用例]

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: edit
  namespace: <namespace名>
subjects:
  - kind: Group
    name: <Googleグループ>
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

ArgoCDによるデプロイ環境の提供

GKE共通環境ではArgoCDによるデプロイ環境も提供しています。 もちろん利用者によってデプロイ時のルールは様々なので必ずしもArgoCDを利用している訳ではないのですが、簡単にデプロイができるようにデプロイ環境も用意しています。 ArgoCDを設定するにはマニフェストを用意してもらう必要がありますが、それもGKE共通環境の利用ドキュメントに手順を記載し出来る限り導入しやすいようにしています。

Infrastructure as Code

GKE共通環境のインフラはTerraformでコード化し、Kubernetesのマニフェストと一緒に管理しています。 コード化しているので容易に変更を適用できるのですが、変更がある度に手動で適用していては少々手間です。

そこでGKE共通環境のTerraformは社内でTerraform CIと呼んでいるCloud Buildを利用したCI環境によりterraform planとapplyを自動で、KubernetesのマニフェストはArgoCDを利用してデプロイを自動化しています。

Terraform CIの仕組みは単純でリポジトリに変更が加わったタイミングでCloud Buildのトリガーによりブランチを判定し、特定のブランチ(例えばmaster)であった場合はterraform apply、そうでなければterraform planを実行しSlackに通知します。

f:id:smiyaguchi:20211203140043p:plain
図. Terraform CIイメージ

もちろんこのままだとリポジトリに変更があると意図せず本番環境が変更されてしまう恐れがあるので、Bitbucketの設定で意図せず環境に変更が加わらないようにしています。

Terraformのコードとマニフェストを自動で適用にすることで手作業によるミスを防ぐのと、変更が必要な権限をTerrafrom CIとArgoCDに限定することで不必要に権限を与えなくてすむようになりました。

モノタロウの大規模システムならではの課題

これまで工夫したことをご紹介しました。

色々と工夫して構築したGKE共通環境ですが、何事もなく順調に構築が完了した訳ではなくモノタロウという環境だからこそ直面した課題がありました。 次にその課題とどのように解決したかを紹介します。

GKEのアドレス設計

GKEではクラスタの各ノードとKubernetesのリソースであるPod・Serviceが利用するIPアドレスを確保する必要があります。構築当初はGCPで使う予定で確保されていたアドレス帯からGKEのアドレスを利用しようとしていたのですが、設計を進めると全然足りないことが分かりました。 これはモノタロウではECサイトだけでなく倉庫や海外拠点といった利用場所が多くあり、 既にある程度利用されていた為、空きが僅かしかなかったからです。

どうするか社内で検討を進めていた中で外部の協力会社の方に相談したところ、GKEのIPマスカレードを有効にすればPodのIPアドレス範囲が共有VPC内の他のIPアドレス範囲と被っていても問題ないとアドバイスを頂きました。 ただこれには前提があり、まずクラスタがVPCネイティブクラスタであること、あとPodのIPアドレス範囲をアドバタイズしないようにCloud Routerに設定を行う必要があります。

検証を進めた結果、IPマスカレードを有効にすることで必要なIPアドレスを確保出来ることが分かったので、この方針で進めることにしました。

モノタロウのネットワーク制約

GKE共通環境のクラスタはノードに内部IPのみを付与され外部とは隔離された限定公開クラスタという種類のよりセキュアなクラスタとなっています。この限定公開クラスタでは各ノードはPublicなIPをもたずPrivateなIPのみ付与されます。 その為、ノードの中から外部に通信するには別途NATゲートウェイを用意する必要があり、GCPを利用している場合はCloud NATと呼ばれるマネージドのNATサービスを利用することも多いです。

ただ、モノタロウでは開発環境から外部へのアクセスについてACLのような制約を設ける要件があり、Cloud NATではそれを満たすことが出来ませんでした。 そのため、GKE共通環境ではCloud NATは利用せずに、IPマスカレードを有効にした上で社内ネットーワークのプロキシを介することで外部へのアクセスを可能とする構成にしました。

このIPマスカレードですが、各ノードでIPマスカレードエージェント(以下、エージェント)が起動することで実現されます。エージェントはクラスタ作成時に自動でインストールされると思っていたのですが、クラスタの構成によっては手動でインストールする必要があります。 クラスタを作成した当初はPodから外部にアクセスすることが出来ずにエージェントがインストールされていないことに気づかず苦労してしまいました。

最終的にエージェントを手動でインストールして設定することで外部と通信出来るようになりました。

内部HTTP(S) ロードバランサをマルチクラウド間で使うまでの躓き

その他、内部HTTP(S) ロードバランサ(以下、内部LB)を使えるようになるまでも苦労しました。GKEではEnvoyベースの内部LBをGKE Ingressコントローラーを介して構築することができ、事前にロードバランサ専用のサブネットを作成するなどのネットワークの準備を行えばIngressとしてリソースを作成して利用することができます。

GKE共通環境でも公式のドキュメントに沿って内部LBを使おうとしましたが、AWSから内部LBを介してGKE上のアプリケーションに疎通することが出来ませんでした。

前述したように内部LBではロードバランサ専用のサブネットを作成する必要があり、そのサブネットからGKEにアクセス出来る必要があります。その為、Firewallの設定に問題があるか疑ったり、IPマスカレードエージェントの設定を見直したりと色々と調査しましたが原因がつかめませんでした。

最終的に外部の協力会社のサポートを受けて調べた所、開発環境でAWSとGCPの間の通信に利用しているCloud VPNのゲートウェイ・トンネルが通信に失敗していた事が分かりました。これはCloud VPNと内部LBのリソースが異なるリージョンに存在している場合は通信が出来ないという仕様によるものでした。この際はCloud VPNのロケーションを合わせる対応で解決しました。

内部LBの構築も専用のサブネットを用意したりとクラウドサービスの設定を確認することが多いと思います。公式ドキュメントの通りに行なっているのに通信出来ない場合は、視野を広げて中間に位置する通信経路自体を確認してみると、そこに原因があるかもしれません。

まとめ

共有VPC上にマルチテナントのGKE環境を構築した話をしました。 テナントの作り方、RBACによる権限の制御、ArgoCDによるデプロイ環境の提供などセキュリティ面に考慮しつつ利用者が便利に使えるような機能を提供するなど工夫したこと、また構築に当たってはGKEのアドレス設計は難しく、モノタロウのネットワークの環境・制約により苦労しながら作ったことなどを紹介しました。

2021年11月現在、GKE共通環境のクラスタ上で5つほどの異なるアプリケーションが稼働しています。 実際に利用している開発者にアンケートを取った所、開発者が環境を用意しなくて済む点について楽になったという声をもらってます。以下はどのような点が楽になったかの抜粋です。

  • ドキュメントに従って準備するだけで、Argo CDを利用してデプロイできる
  • マルチテナント構成により、他のサービスへ(から)の影響がなく、ノードの管理も不要でいい
  • PodがWorkload Identityで動くのでサービスアカウントキーの管理が不要となり、ArgoCDでデプロイできて手順が簡単

一方で、課題ももちろん存在しています。以下はその具体例です。

  • アドレスが足りなくなった時にどのように拡張するか
    • 現状ノードを数十、Podを5千-6千ほど立てられるがいつまでもつか
  • クラスタのアップグレードの際にどのように運用していくか

これらについて、どのように対応していくかはまだ検討中のところもあります。

また、現状GKE共通環境を作る以前に作成されたGKEクラスタが一定数存在しており、それら既存クラスタへの対応や、セキュリティを強化する余地がまだまだあるので強化して改善して行きたいと思っています。

モノタロウではGCPやGKEを活用した基盤を支えるエンジニアを募集しています。 この記事を読んでモノタロウに興味を持って頂けた方いらっしゃれば是非ご応募お待ちしています。