🚢

Ubieのマルチクラスタ移行とプラットフォームエンジニアリング

2025/01/15に公開

始めに

Ubieでプラットフォームエンジニア兼SREをしているonoteruです。

本記事では、2023年に公開した「Ubieにおけるプラットフォームエンジニアリングの取り組み2023」の続編として、2024年に実行したインフラのマイグレーションについて紹介します。以前のブログでは、新しいUbieプラットフォームの構想と進行中のプロジェクトについて共有しました。そして2024年、その構想のもとに以下3つのマイグレーションを実施しました。

  • 単一のGKEクラスタからマルチクラスタ構成への移行
  • Argo Workflowからフルマネージドなワークフローへの移行
  • CloudSQLからスケーラブルで管理性の高いDB(AlloyDB)への移行

これらの移行の狙いは、Ubieのサービスの信頼性、スケーラビリティ、そして開発効率を向上させることです。

本記事では、これらの移行の詳細、直面した課題、そして得られた成果について深く掘り下げていきます。特に、Ubieプラットフォームの実装と、それを支えるPlatform Engineeringの実践に焦点を当てて説明します。

Ubieプラットフォームへの招待

背景と課題

さて本題に入る前に、2023年の記事を振り返ってみましょう。この記事では、Ubieプラットフォームが直面していた課題と、プラットフォームエンジニアリングのアプローチによる解決策の構想を紹介しました。

まず前提条件として、Ubieでは日本と米国に合わせて70以上の独立したマイクロサービスを運用しており、全てGoogle Cloud上で動作しています。それらマイクロサービスのインフラ設定は、プラットフォームエンジニア側で用意したクラスタやTerraformモジュールを使い、原則としてアプリケーション開発者が作成、管理する開発体制をとっていました。

このような環境下で私たちは複数の課題に直面していました。例えば以下のようなものが挙げられます。

  • サービスの立ち上げや変更に関する認知負荷の増大
  • マニフェスト作成におけるベストプラクティスの適用の難しさ
  • マニフェストの一貫性の欠如

こういった課題感のある中で、マルチクラスタへの移行プロジェクトが立ち上がります。このプロジェクトは、もともと単一のGCPプロジェクト、単一のGKEクラスタ上で運用されていた複数のサービスを、役割ごとに異なるクラスタに再配置しなおすといったものです。移行計画の主な狙いは次のようなものがありました。

  • ヘルスケア領域における法令やガイドラインへの準拠コストの低減
  • セキュリティ・プライバシーリスクの低減
  • 可用性の向上、インシデント時の影響範囲の縮小

シングルクラスタからマルチクラスタへ

しかし、ただでさえ複雑なインフラがマルチプロジェクト・マルチクラスタ構成になった日には、認知負荷が大きく上がってしまうことが明らかでした。

例えば、アプリケーションエンジニアは次のような問題に直面することになるでしょう。

  • 開発したサービスをどのクラスタにデプロイすべきか分からない
  • 稼働中のサービスがどのプロジェクト、クラスタで動いているのか分からないのでトラブルシューティングができない
  • クラスタ間で通信が発生する場合の設定方法が分からない

これでは、開発生産性に悪影響を及ぼしてしまいます。そのため、マルチクラスタ移行の際に何らかの手を打つ必要がありました。

解決策: ubieformによる自動生成

これらの課題に対処するため、私たちはサービステンプレーティングツール「ubieform」を開発しました。ubieformは、最小限のパラメータから一貫性のある設定ファイルを生成することができます。ubieformはCUE言語を使用してスキーマ定義とバリデーションを行い、サービスのコアとなる複数のKubernetesマニフェスト、適切なGKEクラスタへデプロイするためのCI/CD設定、サービスカタログ設定など、様々な設定を生成する機能を持っています。

これらの機能は、開発者の認知負荷の軽減、サービス立ち上げと変更の迅速化、インフラ設定の一貫性と品質の向上を狙ったものです。それにより、マルチクラスタ構成への移行後も開発者は複雑なインフラに疲弊せず、高い生産性を維持しながら自チームのサービス運用を継続できることが期待されました。

具体的に見ていきましょう。以下に示すのは、ubieformの最もシンプルな設定ファイルです。

name: "sample-service"
env: ["qa", "stg", "prd"]
segment: "sample-segment"
service_config: {
  manifest: {
    app: {
      main_container: {
        image_path: "asia-northeast1-docker.pkg.dev/example/{{.Env}}/sample-service"
        ports: [
          {
            name: "http"
            container_port: 8080
          },
        ]
      }
    }
  }
  cloud_deploy: {
    pipelines: [
      {
        content: {
          envs: ["qa", "stg", "prd"]
        }
      }
    ]
  }
}

この設定は、別のファイルにあるスキーマ定義を元にバリデーションされ、問題なければ以下のようなファイル構造が生成されます。

sample-service/
├── backstage/
│   ├── qa.yaml
│   ├── stg.yaml
│   └── prd.yaml
├── clouddeploy/
│   └── qa-stg-prd.yaml
├── skaffold.yaml
├── app/
│   ├── qa/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── virtualservice.yaml
│   ├── stg/
│   │   └── [omitted..]
│   └── prd/
│       └── [omitted..]
└── .github/
    └── workflows/
        └── sample-service_qa-stg-prd.yaml

KubernetesのDeploymentやService、IstioのVirtual Service、CI/CDの設定ファイルが生成されているのが分かります。ここで生成される設定ファイルはubieform内部で管理されるテンプレートから生成されているため、書き手の習熟度によってその品質が左右されにくくなります。

また、ここには開発者ポータルフレームワークであるBackStageの設定ファイルも含まれています。Ubieでは「UbieHub」という名前でBackStageを社内に提供しており、ubieformでサービスを生成すると、Cloud LoggingやGrafanaダッシュボードへのリンクを含んだページが自動でUbieHub上のサービスカタログに追加されます。

ubieformのスキーマは以下のようになっており、Ubieのポリシーや既存の設定に合わせて制約条件やデフォルト値が設定されているのがわかります。
開発者が書いたubieform configは、このスキーマに適合するかCIで検証され、複数の設定ファイルが生成されます。

  name: =~ "^[a-z][a-z0-9-]{1,22}[a-z0-9]$"
  env: [#Env, ...#Env]
  segment: #Segment
  service_config: close({
    remote_repository: close({
      org: *"ubie-inc" | string
      name: *"releases" | string
    })
    manifest: close({
      app?: #App
      ...
    })
    ...
  })

  #Segment: "shared" | "cpp" | "mis" | "phr" // ...
  ...

ubieformに関するさらなる詳細はここでは割愛します。気になる方は、Platform Engineering Kaigi 2024の登壇資料(マルチクラスタの認知負荷に立ち向かう! Ubieのプラットフォームエンジニアリング)をご覧ください。

以上が以前の記事の要点になります。この時点では、ubieformはベータ版として限定的に使用されていました。2024年はこの取り組みを本格的に展開し、実際のインフラマイグレーションを実行する年となりました。

マルチクラスタ構成への移行

移行後のアーキテクチャ

全てのクラスタはそれぞれ専用のcomputeプロジェクトに所属し、それらが同じShared VPCをネットワークとして使う構成になりました。また、運用負荷を最小限に抑えるためにGoogle Cloudのマネージドサービスを積極的に活用しました。全てのクラスタをGKE Autopilotで構築し、それらをFleetで一元管理する構成を採用しました。

Fleetは複数のGKEクラスタをまとめて管理するためのGoogle Cloudの機能であり、Anthos Service Mesh(ASM)を利用するために必要になります。

ASMはマネージドIstioです。managed control planeモードを使うと、IstioのコントロールプレーンをGoogle Cloud側に管理してもらうことができ、バージョンアップもGKEと同様のChannelを通して自動的に管理されるため、アップデートに伴う作業も最小限で済みます。

なお記事執筆時点で、ASMは後継となるCloud Service Mesh(CSM)へと移行されることが発表されています。

https://cloud.google.com/kubernetes-engine/fleet-management/docs

https://cloud.google.com/service-mesh/docs/overview

新クラスタへのサービス乗せ替え

マルチクラスタ構成に移行するだけなら以下のようなプロセスを踏むだけで完了します。

  1. 新たに複数のクラスタを異なるGCPプロジェクトに作成する
  2. 既存のサービスのマニフェストを、適切な新クラスタに対してapplyする
  3. トラフィックの転送先を新クラスタへ向ける

しかし実際は、以下のようなプロセスで移行しました。

  1. 新たに複数のクラスタを異なるGCPプロジェクトに作成する
  2. 既存のサービス設定を全てubieformのconfigで書き直す
  3. ubieformを使ってサービス設定を生成し直す
  4. 新たに生成したサービスのマニフェストを、適切な新クラスタに対してapplyする
  5. トラフィックの転送先を新クラスタへ向ける

つまりubieformをマルチクラスタ移行に利用しました。これには様々な理由があります。

まず既存のマニフェストをそのまま移行に使い、新規のサービスだけubieformを使う方法を考えてみましょう。この方法を取ると、「あるサービスは手で直接書かれている一方で、別のサービスはubieformで管理されている」状態になってしまいます。この状態は認知負荷や、サービス設定の均質性という観点において望ましくありません。これを避けるには、移行のタイミングで全てのサービス設定をubieformで再生成する必要がありました。さらに、移行作業を行うプラットフォームチーム内でubieformを使い倒す、いわゆるドッグフーディングを行うことで、バグや改善点をチーム内で洗い出せるというメリットもありました。

70以上ある全ての既存のサービス設定をubieform configに書き直すという作業はかなり大変でしたが、以下の方法で乗り切りました。

  • 移行ツールを作る
  • チームメンバーの力を借りる

1つ目に関しては、既存のKubernetesマニフェストからConfigMapやSecretに対応するubieform configを生成するツールを作成しました。このツールはYAMLを読み取ってCUEのスニペットを出力するので、挙動としてはubieformと全く逆方向の処理になります。

2つ目に関してはチームメンバーに対してサービスごとに書き直し作業を割り振りました。これにより作業負荷を分散できただけではなく、チームメンバーにubieform configを実際に書いてもらうことで、動作原理や設定方法に対する理解を深めてもらうことができました。プラットフォームチームは、今後アプリケーションエンジニアが記述するubieform configをレビューする立場にあるため、それに向けた良いトレーニングになりました。

トラフィックの切り替え

ubieformを使って全てのサービスを新クラスタ上に構築した後は、実際のトラフィックを新クラスタへ切り替える必要があります。

トラフィックの切り替え方法としてまず思いつくのは、サービスを1つずつ段階的に切り替えていく方法です。この方法だと各切り替えの影響範囲が限定的であり、問題が発生した際の原因特定が容易というメリットがあります。しかしUbieの場合、70以上のマイクロサービスが存在し、それぞれを個別に切り替えて検証を行うのは時間とリソースの観点から現実的ではありませんでした。

そこで私たちは、Fastlyレイヤでの一括切り替えを選択しました。Ubieでは全てのトラフィックがFastlyを経由する構成となっており、医療機関向けサービスやtoC向けサービスなど、用途に応じて複数のドメインを使い分けています。この構成を活かし、Fastlyで公開している各ドメインを単位として切り替えを行うことにしました。

具体的には、あるドメインに対するトラフィックが通過する全てのマイクロサービスを新クラスタ上に構築し、その集合を一つの単位として切り替えました。この方法だと、複数個のマイクロサービスを一気に新クラスタ上のものに切り替えられます。さらに、特殊なHTTP Headerを持つリクエストのみ新クラスタに向けるといった分岐をFastlyで設定することで、切り替え前の社内テストが容易にできます。

切り替えのタイミングでは、失敗のリスクを最小限に抑えるため、Fastlyの重み付けロードバランシング機能を利用したカナリアリリースを実施しました。これにより、段階的にトラフィックを新クラスタへ移行し、問題が発生した場合でも即座に元の状態に戻せる安全性を確保しました。

移行に際して挙がった問題・課題

この移行には2つの側面があります。

  • シングルクラスタをマルチクラスタに分割する、クラウドエンジニアリング的な側面
  • テンプレーティングツールを使ったサービス運用フローに乗せ替える、プラットフォームエンジニアリング的な側面

通常これらの移行は別々に実行することが多いかと思いますが、我々はリソースや時間的な制約から同じタイミングで行うことにしました。インフラ構成や運用フローを大きく変えることになるので、それに伴う様々な問題や課題が生じました。

ここではそれらの中からクラウドエンジニアリング的な問題と、プラットフォームエンジニアリング的な課題をそれぞれ紹介します。

ClusterIPがGKEクラスタ間で重複する

マルチクラスタ移行中に発見した興味深い問題の一つが、GKEクラスタ間でのClusterIPの重複でした。

GKEでは、ServiceのIPアドレス範囲を明示的に指定しない場合、34.118.224.0/20の範囲から自動的にIPアドレスが割り振られます。私たちは当初、IPアドレス範囲の管理にかかる手間を省きたかったため、この自動割り振り機能を利用して複数のクラスタを作成しました。しかし、後になってクラスタ間で重複するClusterIPアドレスが割り振られていることが判明しました。

さらに興味深いことに、この問題は即座には判明せず、その時点でクラスタ間のサービスは問題なく通信ができていました。これは私たちがASMを使用してクラスタ間にIstioサービスメッシュを構築していたことに起因します。

Istioの公式ドキュメントには次のような記述があります。

Requests are routed based on the port and Host header, rather than port and IP. This means the destination IP address is effectively ignored. For example, curl 8.8.8.8 -H "Host: productpage.default.svc.cluster.local", would be routed to the productpage Service.

つまり、Istioは宛先のポートとHost Headerを使用してトラフィックの転送をするため、実際のIPアドレスは事実上無視されます。そのため、ClusterIPが重複しているにもかかわらず、サービス間通信が成功していたのです。

この問題は、たまたまService一覧を確認している際に発見されました。GCPのサポートに問い合わせたところ、これは仕様とのことでした。つまり、同じShared VPCを使用するFleet内のGKE同士では、自動割り当てのClusterIPが重複しうる、ということになります。これは全てのクラスタが34.118.224.0/20をService IP範囲として割り当てられてしまったというのが原因のはずなので、単一クラスタ内で割り振られるClusterIPは重複しません。

Istioを使用している限り、この重複は実用上の問題を引き起こしませんでした。しかし、この時点では本番のトラフィックを新クラスタに対して流していなかった点と、将来的に予期せぬ不具合が発生する可能性がある点を考慮し、各GKEクラスタに重複しないService IPの範囲を明示的に割り当て、クラスタを再作成することにしました。

なかなかの驚きポイントでしたが、本記事執筆時点ではこの挙動は修正されている可能性もあります。新規にGKEクラスタを構築する際は、サポートに問い合わせることをお勧めします。

ubieform開発の難しさ

前述のClusterIP重複のような技術的な課題がある一方で、プラットフォームエンジニアリングの観点からは、ubieformの開発における設計上の難しさがありました。

最も大きな課題は、ubieformにどの程度まで生成の自由度を持たせるかという点です。ubieformのようなテンプレーティングツールは、本質的にインフラを抽象化するものであり、抽象化の過程では細部が削ぎ落とされていきます。私たちは、Ubieのインフラを運用する際の制約を明確に定めることで、開発者が入力すべきパラメータを最小限に抑えることを目指しました。

例えば、以下のようなものです。

  • Podのメインコンテナの名前は必ずappで生成される
  • PodDisruptionBudget(PDB)は必ず生成され、デフォルトのminAvailableは30%
  • Cloud Deployは ConfigやSecret -> Migration -> メインApp の順番でパイプラインが組まれる

このような制約により、開発者は迷うことなく設定を行えるようになりました。しかしその一方で、複雑なデプロイパイプラインの設定や細かなチューニングといったことはできなくなります。

開発者から「この設定もできるようにしてほしい」といった要望を受けることもあります。アプリケーション開発者がプラットフォームのユーザーである以上、こうした声に耳を傾けることは非常に重要です。しかし、全ての要望を受け入れることは必ずしも正しい選択ではありません。というのも、私たちはプラットフォーム全体にとって最適となる手段をとる必要があるからです。それができるのは、プラットフォームの全体像を把握している私たちのようなチームのエンジニアだけです。

今回のようなツールの例で言うと、自由度を高めすぎてしまうと、マニフェストを直接書いているのと変わらない開発体験になってしまうかもしれません。

私たちは、自由度を適切に制限することで、ツール利用のハードルを下げることを重視しました。アプリケーション開発者はインフラのエキスパートとは限らないため、開発者から要望があった際は「ツールの拡張」という手段にとらわれず、本当に実現したい事を共に議論することが重要だと考えています。

移行後

これまで説明してきた移行プロジェクトを経て、Ubieのインフラ環境は大きく進化しました。全てのサービス設定がubieformによって管理され、UbieHub上でカタログ化されるようになりました。この変更により、マルチクラスタ化による恩恵を受けながらも、サービス構築時の負担を大幅に軽減することができました。実際、開発者へのヒアリングでは「以前と比較してサービス構築の大変さは半減した」という声が多く聞かれました。

特に成功したと感じている取り組みは、移行と同時にUbieHubを立ち上げたことです。UbieHubは今のところサービスカタログ兼、Cloud LoggingやGrafanaダッシュボードへのリンク集として機能しています。これにより、開発者がログやメトリクスの場所を探したり、フィルタリングしたりする手間なく、即座に見たい情報にジャンプできるようになりました。その結果として、「自身のサービスに対してより強くオーナーシップを持てるようになった」という嬉しい声も聞かれました。

各開発チームが自律的にサービスを運用することは、DevOpsを根付かせるためにとても重要なことです。そのような環境が整ったことで、より自立分散的な運用体制が取れるようになったと感じています。

まとめと次回について

本記事では、2024年に実施したUbieのインフラマイグレーションプロジェクトについて紹介しました。マルチクラスタ構成への移行という技術的なチャレンジと、ubieformというプラットフォームツールの導入を同時に進めるというハードな取り組みでしたが、結果として開発者の生産性向上とインフラの信頼性向上の両立を実現することができました。

特に、以下の点において大きな成果が得られたと考えています

  • マルチクラスタ化によるセキュリティとスケーラビリティの向上
  • ubieformによるサービス構築の標準化と効率化
  • UbieHubを通じた開発者の自律性とオーナーシップの向上

次回は、この記事では詳しく触れられなかった「フルマネージドなワークフローへの移行」と「AlloyDBへの移行」について解説する予定です。特に、Cloud WorkflowsやAlloyDBといったマネージドサービスの採用によって、どのようにプラットフォームの運用負荷を軽減できたのかについて詳しく説明したいと思います。

Ubie テックブログ

Discussion