こんにちは。技術部技術基盤グループのshibatchです。プラットフォームエンジニアとして、主にSUZURIやminne、カラーミーといった複数プロダクトをより良くするおしごとをしています。
ちょうど1年前、私は なぜSUZURIはHerokuから「EKS」へ移設する決定をしたのか という記事を執筆しました。これはHerokuコンテナを移設するにあたり、Amazon EKSにするのかAmazon ECSにするのかを悩みに悩んで判断した事例が、どなたかのお役に立てればと考えてのことでした。
実はこの記事が出た直後くらいには移設は完了しています。遅れに遅れたのですが、さすがにそろそろ…と思い移設の詳細についてご紹介します。
移設の手法
EKSといいますか、AWSのVPCへ移設することにしたのは、以前の記事で紹介した通り、明確に移設のリスクが低い手法を採れるからです。
実はHerokuはAWSのVPCの上で構成されており、Enterprise版だと専用環境(Private Space)を構築できます。まさにSUZURIはそのような使い方をしているのですが、この場合はVPC Peeringを通じて自身で構築したVPCと疎通することができるのです。
つまり、実質AWSの別のネットワークからペパボ(SUZURI)のネットワークに動かすイメージで移設を行えることを意味しています。
これを順に説明していきますね。
移設前の構成
まずはじめの状態です。Herokuは以下で構成していました:
- Dynoと呼ばれるコンテナ
- Redis(アドオン)
- PostgreSQLデータベース(アドオン)
- Schedulerと呼ばれるジョブ(アドオン)
先ほど言った通り、これらは運用する上ではほぼ意識はしませんが AWSのVPC、東京リージョンで使っていました。
移設の8つのステップ
1. ネットワーク設計
まずは移設先のVPCを新しく作成しました。
VPCは/16 で作成し、サブネットは以下の通りに分けました。Kubernetes専用ではなく、将来にわたって使っていくことを見越したVPCになります:
- Global IPをつけることができるPublic Subnet (/24) を各リージョン1つずつ、計3つ分
- RedisやDatabaseを移設する予定のPrivate Subnet (/22)を各リージョン1つずつ、計3つ分
- Kubernetes(Amazon EKS)専用のKubernetes Subnet (/20)を各リージョン1つずつ、計3つ分
このSubnetを本番用と検証用の2セットを、同じVPC内に作っています。初めはLoadBalancer専用のPublic subnetも作ったほうが良いかと考えましたが、そんなに多く作っても運用が大変になるので最小限の個数にしました。
VPCを作成すると、VPC PeeringをHeroku側VPC、新しく作ったVPC両方に作成しました。これでどちらのVPCからも疎通が通る状態にしました。
2. 検証環境で動作させてみる
VPCを作った後、新VPCの検証環境を使って一連の移設の流れをリハーサルしました。
このとき気をつけたのは検証用であっても面倒臭がらずVPCのSubnetやElastiCacheやAuroraはちゃんとTerraformでコードにしておくことです。
今回の移設がほぼ問題なく達成できた大きな要素がこの動作検証にあります。最初に検証し、本番環境はただterraform のworkspaceを切り替えてapplyさえすれば同じものができるようにしたことで、ほぼ失敗することなく完遂できたと考えています。
動作検証するにあたり、EKSで動かすための周辺コンポーネントも理解できてきました:
- external secret
- aws loadbalancer controller
といったコンポーネントもこの時点で必要である旨が認識でき、コード化できたので本番環境のクラスタ構築では楽できました。
逆にいうと、この検証環境で動作させる部分がいちばん忙しかったと言っても過言ではないです。えてして上流で先に苦労するミッションのほうが成功するものです。今回の場合は先に動作検証することで後の変更が楽になったので今後も意識したいところです。
3. Redisの設計、移設
ネットワーク設計とPoCができたら、次にVPC上にElastiCache for Redisを構築し、移設していきます。
Heroku Redisはクラスタ構成ではなく、Active/Standby構成でした。移設の機会にクラスタ構成に変えることも検討はできますが、今回移設の確度を高めるため、極力構成変更はしない方針とし、構成はそのままとしました。
また、インスタンスタイプもHerokuで使っている容量に近しいものにすることで、移設以外でのトラブルは極力排除する方針としました。
移設方式
素朴にメンテナンス時間を入れて移行する方式としました:
- メンテナンス中にHeroku Redisでバックアップを取得
- S3バケットにアップロード
- ElastiCacheは初回起動時にバックアップファイルをインポート
- Dynoに設定しているRedisのエンドポイントを切り替え
移設後はDynoコンテナからキャッシュを参照する際はVPC Peeringをまたがって通信するようになりました。
4. データベースの設計、移設
次はデータベースの設計と移設です。データベースはPostgreSQLを使っています。Redisの移設と同じく、極力構成変更をしない方針としました。
AWSでRDBMSなら RDS もしくは RDS Auroraが手堅い選択肢となるでしょう。
この2つを比較したとき、レプリケーション遅延が構造上発生せず、また障害復旧する手段が多いAuroraのほうが運用コストが少なくなるだろうという予測からAuroraを採用しました。
Auroraは運用コストを抑えられるメリットはありますが、標準仕様ではI/O課金が発生します。移設後のリクエスト数をシビアに算出するより、まずはI/O Optimizedの方式にしてI/Oでのコスト上昇リスクを解消した状況とし、移設後に再度料金プランを見直してみることにしました。
また、移設前後では大きな変更点として以下がありました:
- Heroku PostgreSQL:グローバルなIPアドレスでアクセス
- RDS Aurora:InternalなIPアドレスしか持たず、VPCからのみアクセス
アプリケーションからのアクセスがほとんどなのでほぼ困らなかったのですが、唯一、グローバルからのアクセスがGoogle Cloud BigQueryからあったため、他チーム(データ基盤チーム)の方に踏み台でEC2インスタンスを作成することでアクセス経路を作っていただきました(ありがとうございました🙏)
そのほか細かい点では、Heroku PostgreSQLではpgbouncerを利用して接続プーリングを構成していましたが、Aurora接続ではRDS Proxyで代替しました。
移設方式
Redisと同じくメンテナンス時間を入れてスナップショットを取得し、pg_restoreコマンドでスナップショットの内容をAuroraに流し込むという、素朴な方法です。
無難な方法ですが、データベースはそれなりにサイズが大きいので、ローカルのPCにダウンロードしてリストアを実施するのは、あまり現実的ではありませんでした。
ではどのようにしてデータをコピーしたかというと、Heroku PostgreSQLはS3バケットにスナップショットを取得して署名付きURLを発行してくれるので、Auroraと同じサブネットにEC2インスタンスを立てて、そこから署名付きURLでスナップショットを取得することで解決しました。
具体的な手順
まず、Heroku PostgreSQLでは以下のコマンドでスナップショットを取得します:
heroku pg:backups:capture
<snip>
Backing up BRONZE to b1553... done
するとバックアップ番号が発行されます。ここでは b1553とします。
次にこのバックアップ番号で署名付きURLを生成します。便利ですね!
% heroku pg:backups:url b1553
https://example-bucket.s3.amazonaws.com/example-backup-id/2024-03-05T03%3A54%3A46Z/example-file-id?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=EXAMPLE%2F20240305%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240305T045305Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=example-signature
最後に、Auroraと同じサブネットに作業用にEC2インスタンスを構築し、curlで取得します:
[ec2-user@ip-10-20-129-186 ~]$ time curl -o production-data.dump 'https://example-bucket.s3.amazonaws.com/example-backup-id/2024-03-05T03%3A54%3A46Z/example-file-id?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=EXAMPLE%2F20240305%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240305T045305Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=example-signature'
あらかじめAuroraにはデータベースとroleを作成しておいた上で、pg_restoreコマンドでリストアします:
[ec2-user@ip-10-20-129-186 ~]$ pg_restore --verbose --clean --no-acl --no-owner -h example-cluster.cluster-example.ap-northeast-1.rds.amazonaws.com -U applicaton_user -d <db名> /home/ec2-user/production-data.dump > restore.log 2>&1;date
リストア自体は1時間弱で終了しました。
最後に、アプリケーションの環境変数をRDS Auroraのエンドポイント(正確にはRDS Proxyのエンドポイント)に設定して完了です。
この変更によりDynoはVPCをまたがってデータベースへアクセスする方式となりました。
移設後の影響
移設後はレイテンシが気になっていましたが、正直に明かすと数msどうしても悪化してしまいました。ただSLOは満たしている状態なので移設完了まで許容することとしました。
5. Kubernetesクラスタ設計と構築
EKSクラスタは、設計当初はFargateを想定していました。Fargateはノード管理の責務がAWS側になり、メンテナンスコストを抑えられることに魅力を感じていたためです。
ただ、Fargateは:
- Imageキャッシュが使えないためdeployに時間がかかる
- Spotインスタンスを使うことができない
といったデメリットが大きく、今回導入するサービスへの要求としてはあまりマッチしなかったため、EC2を用いたKubernetesクラスタとしました。
さて話は前後しますが、移設の設計中に Cloud Native Days Tokyo 2023 というカンファレンスへ行ったとき、Karpenterというプロダクトを知りました。
Karpenterは:
- EC2を用いるがノードのスケールが既存の仕組みより早い
- SPOTインスタンスを使う設定が充実
- インスタンスタイプを柔軟に、自動で選択することでコスト最適化が運用の手間をかけずにできる
という点に魅力を感じ、導入してみました。
結果これは良い選択で、SPOTインスタンスを中心に使いながら必要に応じてon-demandインスタンスを使える柔軟性とコストパフォーマンス、安定している上にノード管理に手間がかからない運用の容易さで非常に満足のいく結果をもたらしました。
このあたりの導入の顛末や苦労話はCloud Native Days Summer プレイベントに登壇し公開しました。こちらにまとめましたので興味のある方は参照いただけると幸いです😉
6. SUZURIアプリケーションのリリースフローの構成変更
HerokuからEKSに移設する上ではSUZURIアプリケーションのリリースフローを変更する必要があります。ここはビルドパイプラインを更新してECRにプッシュするところと、ECRプッシュ後のデプロイするところを改修・作成する必要がありました。
- 前者:アプリケーションエンジニアのshimojuさんに担当いただき(ECR プッシュまではshimojuさんのブログを参照ください)
- 後者:私はECR プッシュされたImageを社内基盤にあるArgoCDを用いてデプロイする箇所を作りました
7. ジョブの移設
Herokuには Schedulerという、毎時や毎週タスクを実行する機能をもったアドオンがあります。これをすべてKubernetes Cronjobに切り替えました。
数が多いこと以外は特に問題はなく、幸い依存関係にあるようなジョブはなかったため、問題なく完了できました。Heroku SchedulerとKubernetes Cronjobはそれぞれメリットとデメリットがあるのですが、今までできていなかったコード管理ができるようになったのでトータルではプラスです。
メリット:
- Heroku Scheduler はUTCでの時刻指定でしたが、CronjobではJSTが使えるためすべてJSTの時刻で設定し、メンテナンス性を高めました
- Schedulerではできなかったコード管理ができるようになりました
デメリット:
- GUIでの運用からCLIになったためジョブ全体をWebコンソールから見渡すような視認性はなくなりました
- Kubernetes Dashboardを導入すると改善しますが、リリース優先で導入はしなかったので、今後の残課題となっています
8. コンテナの切り替え
ここまでできて、ついにDNSでsuzuri.jp のAレコードを切り替えることで移設は完了です。
すでにデータベースとRedisキャッシュは移設されているので、リスク低減された状態での切り替えができました。結果、サービス停止することなく切り替えは問題なく完了しました🎉
切り替え後に対応したこと
運用を始めてこんにちまで、大きな不具合なく動きつづけています。
1. Auroraは I/O Optimized での運用を継続中
データベース移設の際、AuroraでI/O課金がどの程度になるのか読みきれなかったのでI/O Optimized の設定にしたことを記載しました。
こちらは移設後にリクエスト数からコスト試算し、I/O Optimizedのほうがコスト有利な状況が継続しているため、こんにちまでI/O Optimizedで運用しています。
2. ArgoCDがECR へのImage プッシュで反応しないことがあった
運用していったところ目立った不具合としてはPull Requestをrevertした際に ECRにImageをプッシュしても、ArgoCD (ArgoCD Image Updater)が検知できないことがあったことです。
こちらの詳細は別記事にまとめていただいているのでご参考ください:
Revert した変更が Argo CD Image Updater に検知されない問題の解決方法
まとめ
いかがだったでしょうか?この記事ではひとつの移設プロジェクトを最初から最後まで、一気通貫で紹介する、ということを試みてみました。
実際この移設は私を主体として少人数で18ヶ月くらいかけたプロジェクトになりますが、アプリケーションエンジニア(特にshimojuさん)と協力してスムーズに進み、最終的に大きな不具合なく切り替えられました。
移設成功のポイント
- 段階的な移設:VPC Peeringを活用したリスクの最小化
- 徹底した検証:PoC段階でのコード化と本番環境への完全コピー
- 構成変更の最小化:移設の確度を高めるための保守的なアプローチ
- 適切な技術選択:Karpenterによるコスト最適化と運用効率化
昨今生成AIが隆盛で、この移設自体は現在ほど生成AI活用が盛んではない状況であったため、今やるなら設計がより補助してもらえるとかはありそうですが、手順自体はほぼ変わらずに行われるのではないでしょうか。
(プラットフォーム移設自体が生成AI前提の世の中ではどうなっていくのかの議論はありますが、それは本題からずれるのでここでは置いておきましょう)
この記事がどなたかの参考になるなら幸いです。また、このような移設の作業に興味がある方はぜひJoin US(。•̀ᴗ-)✧ お待ちしております。