ZOZOMATのマルチテナントEKSクラスタへの移行

OGP

はじめに

こんにちは。計測プラットフォーム開発本部SREブロックの西郷です。普段はZOZOSUITやZOZOMAT、ZOZOGLASSなどの計測技術に関わるシステムの開発、運用に携わっています。先日私達のチームでは、シングルクラスタ・マルチテナントを前提として構築したEKSクラスタにZOZOMATシステムを移行しました。本記事では移行ステップや作業時に工夫した点について紹介したいと思います。

目次

移行の概要とそのアプローチ

マルチテナントのEKSクラスタに移行した理由は、EKSクラスタが多く存在することで管理コストの高さが目立ってきたためです。

というのも、EKS導入当初はまだ手探りな部分が多かったため、各サービスごとにEKSクラスタを作成していました。しかしながら当初の想定以上にサービスが増えてしまったため、今回単一のクラスタで複数のサービスを運用するマルチテナントのEKSクラスタへ移行するに至りました。

この辺については先だって公開したこちらの記事(1)で詳しく書かれているので、是非併せてご覧ください。

まずは今回の前提と要件を整理します。

前提

  • AWSリソースはCloudFormation(以降CFnと記載します)で管理している

要件

  • 無停止で切り替えること
  • ロールバックが容易であること
  • コンピューティングタイプをEC2からFargateに変える

コンピューティングタイプを変える点について少し補足すると、マルチテナント化に伴うマシンリソースの競合および権限の分離や、運用負荷の軽減を目的としたEC2からの脱却が背景にあります。

移行方針

以上を踏まえ、EKSクラスタ間の切り替えについてはマルチテナントのEKSクラスタにZOZOMATリソースを追加した上で、Route 53に設定済みのALBの値を変更しました。図で示すと以下のようになります。

また、ZOZOMATで利用しているAWSリソースについても一部を除いて新しいCFnスタックに新規作成し、管理するCFnスタックを切り替えることにしました。理由は日々の運用作業と干渉せず移行作業をしやすかったことや、リソースの命名則を変更したかったためです。各AWSリソースのスタック間での移行方針はリソースごとに異なるため、わかりやすくまとめると以下のようになります。

AWSサービス 移行方針 特記事項
EKSクラスタ 既存のマルチテナントのEKSクラスタを利用 -
FargateProfile
IAMRole等
新規作成 -
CodePipeline
CodeDeploy
CodeBuild
新規作成 -
ECR 新規作成 作業時点でCFnのResource Importが未対応だったため。1世代分のイメージのみ移行
Redis 新規作成 -
DynamoDB Resource Import -
RDS Snapshotを元に新規作成&データ同期 -
S3 新規作成&データ同期 -

そもそものZOZOMATの構成については以前の記事(2)に詳しく書かれているので、興味がある方は是非そちらをご覧ください。

まとめると、今回の移行のポイントは以下のようになります。

  • ZOZOMATが利用するEKSクラスタをマルチテナントのEKSクラスタに切り替える
  • 付随して、ZOZOMATが利用するAWSリソースも新しいCFnスタック管理に切り替える

各移行ステップとその詳細

今回の移行作業は大きく4段階に分けて行いました。

STEP1:移行先CFnスタックへのAWSリソース作成、インポート

STEP2:移行先へのデータマイグレーション

STEP3:移行先のEKSクラスタにkubernetesリソースを追加

STEP4:EKSクラスタの切り替え

ここから先は各ステップについて、工夫した点や注意点を交えて説明いたします。

STEP1:移行先CFnスタックへのAWSリソース作成、インポート

まずは前述のとおり、新しいCFnスタックに必要なAWSリソースを作成していきます。

ECRは、この作業を行なった2021年5月時点ではCFnのResource Importに未対応でした。そのため、既存のECRを一度削除して再作成し、あらかじめ取得しておいた最新1世代分のイメージをpushすることにしました。現在はResource Importに対応している(3)ようです。

DynamoDBは命名則の修正が必要なく、Resource Importも対応していたため、既存のテーブルをそのまま新しいスタックにインポートしました。

移行の流れをわかりやすくするため、現時点の状態を図で示します。

STEP2:移行先へのデータマイグレーション

このステップでデータのマイグレーションが発生するデータソースはRDSとS3です。まずはZOZOMAT環境に存在するデータソースとその中に含まれるデータについて簡単に説明します。

  • DynamoDB
    • 計測データ
      • IDやメタデータを含むsession
      • 足のサイズや3Dデータを含むscan(左、右)
  • RDS
    • 計測データ
      • DynamoDBの計測データ(session、左右のscan)を統合し、1レコードとして管理
  • S3
    • 計測データ
      • RDSと同じ形式のデータをJSON形式で保存
    • ログデータ
      • ALBやCloudFront等のログ
      • ネイティブアプリのデバッグログ

計測データは1次データソースとしてDynamoDBに投入された後、以下のようにDynamoDBStreamと後続のLambdaを通してRDSとS3にも保存されます。

この構成について、RDSにデータを保存している理由を説明します。ZOZOMATでは1つのsessionに対して左右の足それぞれの計測値と3Dデータを持つため、計測結果を参照する際にはこれらのデータの結合が必要になります。DynamoDBには結合するためのAPIがないため、アプリケーションで結合処理を行うとsessionで1回、scanで左足、右足の2回、合計で3回クエリの発行が必要になります。ZOZOMATではこれらのデータを1つのレコードに結合した状態でRDSに保存しているのですが、これによってアプリケーションからのクエリ発行は1回で済み、通信コストを抑えることができます。

こういった背景もありZOZOMATではCQRSを採用し、更新系(Command)をDynamoDBに、参照系(Query)をRDSに分離しています。より詳しくはこちらの記事(4)を参照ください。

S3についてはデータ連携の観点からです。分析やサイズ推奨のモジュール開発に役立てるため、計測結果のデータを関連チームに連携する必要がありました。そのため、S3にJSON形式で保存しています。

さて、STEP1で説明した通りDynamoDBはCFnのResource Importによって既存のテーブルをそのまま利用するため、データのマイグレーションは発生しませんでした。一方、RDSとS3は新規作成するため、過去のデータはもちろんのこと、継続的に同期し続ける必要がありました。ここからはどのようにデータ同期を実現したのかについて説明していきます。

S3

S3に保管されているデータはオブジェクトの最終更新日時を保持したまま同期したかったこと、工数を割かずに継続的なデータ同期を実現する仕組みが必要だったこと、リアルタイムでなくとも同期されていればよかったことから、S3のレプリケーション機能を利用しました。これはS3間でオブジェクトを自動で非同期的にコピーしてくれる仕組みです。

最終更新日時を保持したかった背景としては、普段の監視業務やユーザからの問い合わせ対応等において、それらの情報が重要だったことが挙げられます。s3 syncs3 cp コマンド等では最終更新日時が変わってしまいますが、レプリケーション機能であればオブジェクトのメタデータを保持したまま同期が可能です。

さて、デフォルトではレプリケーション対象のオブジェクトはレプリケーション有効化後にputされたオブジェクトのみです。しかしながら、移行完了後は移行前に利用していたS3を削除する予定だったため、今回は有効化前にputされたオブジェクトの同期も必要でした。解決策としてはサポートケースを起票することで既存オブジェクトのレプリケーション有効化が可能だったため、そちらで対応しました。申請時に必要な情報は下記AWSのドキュメントを御覧ください。

既存のオブジェクトのレプリケーション

注意点

レプリケーションを利用する際、特に注意が必要だと感じた点は次のとおりです。

  • 既存オブジェクトのレプリケーション有効化の際、サポートケース起票から機能の有効化が完了するまでに3週間程かかる
    • 利用者は有効化されるタイミングを指定できない
    • 今回はスケジュールに猶予があったため問題にはならなかったが、考慮した上でのスケジュール設定が必要
  • オブジェクトのバージョンIDを指定せず削除した際、レプリケーション設定が最新バージョンでない場合は削除マーカーをレプリケートする
    • 最新バージョンの場合、削除マーカーはレプリケートされない
    • オブジェクトバージョンを指定して削除した場合は削除マーカーをレプリケートしない
  • 既存オブジェクトのレプリケーションルールはCFnでは設定できない

それ以外にもレプリケーション機能の利用についてはいくつか制約があるため、利用の際にはAWSのドキュメント(5)を確認することをおすすめします。

RDS

次にRDSのデータの同期についてです。こちらはRDSをSnapshotから復元し、その後DynamoDBStreamに既存と同じLambdaをもう1つ紐付けることでデータ同期を実現しました。

ただし、Snapshot作成から新しいRDSがRunningになるまでの間、新たに作成されたデータをどのように同期するか、という点は一工夫必要でした。というのもDynamoDBStreamへ紐づけているLambdaの開始位置がLATESTになっており、最新のレコードから読み込む設定になっていたためです。

これについてはDynamoDBStreamへ紐づけているLambdaの開始位置をLATESTからTRIM_HORIZONに変更することで解消しました。図にすると以下のとおりです。

TRIM_HORIZONの場合は、ストリームに保存されている24時間以内のレコードを古いものから順に読みとる挙動となります。全ての項目を処理し追えるまで実行され、その後は新しいレコード分に対して処理が実行されます。この時すでにRDSに含まれるデータも処理対象になる可能性はあるのですが、Lambdaで行っている処理は冪等なため問題ないと判断しました。

CFnテンプレート上は以下のように指定します。

  LambdaEventSourceMappingDynamoDBStreamSessions:
    Type: 'AWS::Lambda::EventSourceMapping'
    Properties:
      EventSourceArn: !GetAtt DynamoDBTableSessions.StreamArn
      FunctionName: !GetAtt LambdaFunctionDynamoDBStreamSessions.Arn
      StartingPosition: 'TRIM_HORIZON' #ここをLATESTから変更

注意点

  • DynamoDBStreamレコードが保持されるのは24時間のため、DynamoDBStreamにLambdaを紐付けるまでの時間がそれ以上になる場合は適さない
  • DynamoDBStreamに3つ以上のLambdaを紐付けるとリクエスト失敗の可能性が高くなる
    • この場合はファンアウトパターンが推奨されている

ファンアウトパターンについてはAWSのブログ(6)で詳しく説明されているので、興味のある方は是非ご覧ください。

ここまででAWSリソースの対応は完了です。

STEP3:移行先のクラスタにkubernetesリソースを追加

まずはkubernetesのマニフェストファイルに対して、今回の移行に際して必要な以下の修正を加えていきます。これらの作業の背景や詳細はこちら(1)に詳しく書かれているので、本記事では割愛させていただきます。

  • ZOZOMAT専用のEKSクラスタ独自で管理していたmetrics-serverやexternal-dns等の廃止
  • ZOZOMAT用ネームスペースの作成及び指定
  • Fargate化に伴うIRSA対応

上記の修正をし、マルチテナントのEKSクラスタにkubernetesリソースをデプロイするのですが、ingressのspec.rules.host部分が指定されているとexternal-dnsによってRoute53のAレコードの値が上書きされてしまいます。そのため、以下のようにコメントアウトした状態でデプロイすることにより回避しました。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: mat-api-ingress
# --------- omit
spec:
  rules:
  - #host: mat-api.zozo.com
    http:
      paths:
      - path: /healthz
        backend:
          serviceName: api-server-service
          servicePort: 8000
# --------- omit

この対応を含め、本ステップによりzozomatネームスペースにFargateで稼働するpod群が作成されます。

STEP4:EKSクラスタの切り替え

当初はingressのkubernetesマニフェストでhost指定を有効化するだけの作業を想定していたのですが、最終的に行なった手順は以下になります。

  1. external-dnsのdomain-filterをマルチドメインに変更する
  2. 既存EKSクラスタのexternal-dnsのpolicyをcreate-onlyに変更する
  3. ingressのspec.rules.hostのコメントアウトを解除し、有効にする

以降は各作業が必要になった背景を踏まえながら、詳しく見ていきたいと思います。

external-dnsのdomain-filterをマルチドメインに変更する

ZOZOGLASSとZOZOMATは利用するHosted Zoneが異なります。しかしながらマルチテナントのEKSクラスタに存在するexternal-dnsのdomain-filterは、ZOZOGLASSが利用するHosted Zoneのみ指定していました。ZOZOMATが利用するHosted Zoneは指定されていないため、そのままだとingressのspec.rules.hostを有効にしてもAレコードに設定されているALBの値は変わりません。そのため、予めdomain-filterを複数指定することで解消しました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
spec:
  template:
    spec:
      containers:
      - name: external-dns
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=glass-domain.zozo.com
        - --domain-filter=mat-domain.zozo.com # ここを追加
        - --provider=aws
        - --policy=upsert-only
        - --aws-zone-type=public
        - --registry=txt
        - --txt-owner-id=my-hostedzone-identifier

既存EKSクラスタのexternal-dnsのpolicyをcreate-onlyに変更する

既存のEKSクラスタとマルチテナントのEKSクラスタの両方に、それぞれexternal-dnsが存在します。external-dnsのpolicyにはsync、upsert-only、create-onlyがあり、それぞれ変更を検知すると次のように動作します。

  • sync
    • レコードの作成、変更、削除全てを行う
  • upsert-only
    • レコードの作成、変更のみ行う
  • create-only
    • レコードの作成のみ行う

私達の環境では誤ってレコードを削除してしまう事態を防ぐため、両external-dnsのpolicyをupsert-onlyにしていました。そのため、片方でingressのspec.rules.hostを有効化しRoute53のレコードの値を上書きすると、もう片方が変更を検知し値を更に上書きする事態が発生してしまいます。

これについては既存のEKSクラスタのexternal-dnsで、予めpolicyをcreate-onlyに変更することで解消しました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
spec:
  template:
    spec:
      containers:
      - name: external-dns
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=glass-domain.zozo.com
        - --domain-filter=mat-domain.zozo.com
        - --provider=aws
        - --policy=create-only #ここを変更
        - --aws-zone-type=public
        - --registry=txt
        - --txt-owner-id=my-hostedzone-identifier

ingressのhost指定を有効にし、 Route53レコードのALBの値を変更する

最後にingressのkubernetesマニフェストでspec.rules.hostのコメントアウトを解除し、デプロイします。これによってRoute53のAレコードの値が移行元のALBのDNS名から移行先のALBのDNS名に切り替わりました。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: mat-api-ingress
# --------- omit
spec:
  rules:
  - host: mat-domain.zozo.com #コメントアウトを外す
    http:
      paths:
      - path: /healthz
        backend:
          serviceName: api-server-service
          servicePort: 8000
# --------- omit

以上の作業を踏まえ、ZOZOMATが利用するEKSクラスタをマルチテナントのEKSクラスタに切り替えることができました。

振り返り

今回のような稼働中サービスのシステム移行は個人的に初めてだったので、当初は完遂出来るか不安な部分もありました。特にデータ同期というとスクリプトを書いて定期実行するイメージでしたが、S3レプリケーションやDynamoDBStreamといったAWSの仕組みや、それらを利用したZOZOMATのデータ投入の仕組みをフル活用して大きなトラブルなく終えることができたのは良い経験になりました。また、事あるごとに躓いていましたが、リーダーとのオフィスアワーやチームメンバーとのペアプロ等、周りの力を借りやすかったチーム環境も非常に有り難かったです。

終わりに

計測プラットフォーム開発本部では今後もZOZOSUIT 2等の新しいサービスのローンチを予定しています。更にスピード感を持った開発が求められますが、このような課題に対して楽しんで取り組み、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。

hrmos.co

また、カジュアル面談も随時実施中ですのでお気軽にご応募ください。

hrmos.co

参照記事

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

2: ZOZOMATにおけるEKSやgRPCを用いたシステム構成と課題解決

3: AWS::ECR::Repository support for importing into existing stack

4: ZOZOSUITからZOZOMATへ - CQRSによる解決アプローチ

5: レプリケーションの要件

6: Amazon DynamoDB ストリームを使用して、順序付けされたデータをアプリケーション間でレプリケーションする方法

カテゴリー