中身が謎な闇と化したEC2に光を当ててECSに移した話

エオルゼアの情勢が落ち着いたので次は王になろうと狭間の地へ向かい、大ルーンを集めていたら流れでラニ様にお仕えすることになり星の世紀を開いたAzoopのソフトウェアエンジニアの武田です。ELDEN RINGはトロコンしました。DLCが楽しみです。

 

以前、会社のnoteに書いたトラッカーズマネージャーのECS on Fargateへの移行に続く第2弾として中古トラックのオンライン売買プラットフォームのトラッカーズもほぼ同じ構成で移行したのでこれまた同じように語りたいと思います。

サービスの命に関わる「闇」

下の図は移行前のトラッカーズの構成です。

(移行後の構成については先述のnoteに書いたトラッカーズマネージャーとほぼ同じ構成になっているので省略します。)

トラッカーズのECS移行前

移行前

冗長化のため複数台で構成されたアプリサーバとデプロイに用いるJenkinsサーバがありそれぞれEC2に構築されています。

デプロイはJenkinsによって行われており、デプロイしたい人がデプロイ用ブランチに差分をマージしPipelineの実行ボタンを押すことで本番に反映されます。ここまではどこかしらで似たような構成を見たり聞いたことがあるような気がする構成です。

そしてこの構成に多くの人が開発と運用に関わり、時には履歴が追跡できない方法で変更されるなど歴史が積み重なった結果巨大な「闇」と化しました。

EC2ホストのディレクトリをマウントしたDockerコンテナ

アプリサーバが起動しているEC2にはDockerがインストールされており、Rubyやアプリケーションが利用するパッケージ類はコンテナ化されていました。ところがアプリケーション本体はイメージに含まれておらずホストのディレクトリがマウントされていました。設定はリポジトリ内のdocker-compose.ymlの環境変数、クレデンシャルはデプロイ時にJenkinsから配布されるenvファイル内に書かれていましたが長年の運用によりホストに直接設置されたファイルがいくつか存在している無法地帯と化していました。

また、デプロイはJenkinsから各サーバへSSHしコマンドを発行しておりソースコードGithubからクローンしコンテナイメージのビルド、アセットパイプラインの実行まですべて本番環境上で実行されていました。このため時々リソース特にメモリ不足によりデプロイに失敗することがありました。そのためアプリケーションの動作に必要なリソース量の1.5倍~2倍のスペックを用いておりコストが増える要因になっていました。

アップデートも復元もできないJenkins

デプロイに使っているJenkinsは自分が入社した頃から使われておりおそらくサービスの開発を始めた頃からあるものです。社内のどこにもこのJenkinsを構築した資料やツールが見当たらないことから手動で構築されたものと思われます。Jenkins Pipelineをはじめとする設定類はバージョン管理されておらずバックアップはEC2のスナップショットのみ、さらに最後のバックアップは1年以上前と障害等で再構築が必要になっても救出できないかもしれない状態でした。*1

また構築されてから移行までアップデートを実施しておらず最新版とはかなり乖離があったため素直にアップデートができなくなっていました。

闇に光を当てる

トラッカーズマネージャーの移行作業で得たノウハウをもとにトラッカーズの移行を始めることが決まりました。(調査に結構な時間がかかることからまずはトラッカーズマネージャーの移行をしてノウハウを得て実施する、と判断していました)

いよいよ闇と化したEC2に光を当てるときが来たのです。

本番環境にしかない必要なファイルを救出し「正しいフロー」に乗せる

まずは「プロダクトのリポジトリには存在しないがEC2上にはあるファイルやディレクトリ」の調査から取り掛かりました。これはEC2上にgit cloneされ、デプロイ時にgit pullしていたためgit statusを実行しバージョン管理外になっているファイルを1つずつ確認していく作業を繰り返します。
*2

一通り洗い出しが終わったらオープンにして大丈夫なファイルはプロダクトのリポジトリへ、クレデンシャルはJenkinsから配布するように設定します。

他にもデプロイでしか使わないスクリプトが見つかったためひとまずGithubに専用のリポジトリを作って避難させました。*3

Dockerイメージを作る

これまでホストのディレクトリをマウントすることを前提にしていたDockerイメージをアプリケーションに必要なコンポーネントをまとめたものに作り直します。
トラッカーズ本体はごくごく普通のRailsアプリなので難しいことはなくマルチステージビルドを使って最終的に出来上がるイメージには動作に必要なもののみ入ってる状態にしています。

また、移行完了まで並行運用期間が存在するため既存のDockerfileとは独立して作成しています。

脱・Jenkins

デプロイに使っていたJenkinsは前述の通りとても古いためこのまま使い続けるわけにはいかず、またJenkinsをアップデートしようにも再構築に近い作業になりそうでした。また既存のインフラとの並行運用期間が存在する以上そちらに支障をきたすような事象は避けなければなりません。このため新たにCodePipelineでデプロイフローを構築しました。

本番環境の切り替え

ここまでで一通り準備ができたのでいよいよ本番を切り替えます。(Cronはトラマネと同様ECS Scheduled task + ecscheduleで管理するようにしました)

アプリサーバの切り替え

トラッカーズはお客様はもちろん社内でも出品の代行など業務での使用者が多いプロダクトです。そのためテストに協力をしてもらいやすかったためHTTPヘッダーに特定のキーと値をセットした状態でアクセスするとECS側に接続されるようにALBのリスナールールを設定しました。この状態で協力してもらったスタッフに1週間5営業日程度使用してもらいログやコンテナの監視を入れつつ最後の調整を行いました。

完全切り替え時はALBのリスナールールを修正するだけなのでスムーズに移行することができました。

サイトマップの配信

トラッカーズではSEO対策としてサイトマップを配信しています。EC2で動作していたときにはcronスクリプトでXMLをDockerコンテナにマウントされたホストのディレクトリに生成していました。

ところがFargate on ECSを使うとデプロイ時にコンテナが破棄されるため別途永続ストレージを用意しなければ生成したサイトマップが消失してしまいます。

EFSをコンテナにマウントする方法もありますが、今回はシンプルにS3にサイトマップを生成しそこから配信することにしました。
ただし配信するドメインには注意が必要で、基本的には同一ドメインから配信する必要があるようです。

CloudFrontから配信することも考えましたがアプリと同じドメインから配信する必要がある制約から「できないことはないがちょっとしんどい」*4ということがわかりs3からサイトマップを取得するLambdaを作成しそれをALBにぶら下げて配信に使用することにしました。このLambda関数の中身はaws-sdkを使って書いた単純なnode.jsのハンドラになっています。これをGithub Actionsを使ってDockerイメージにビルドしLambdaにデプロイしています。

まとめ

多くの人が開発と運用に関わり、長い時が経過した結果構成の意図が行方不明になってしまい「闇」となったインフラに光を当てた話でした。

当時はこれが最速で立ち上げることができる手段として選択されなんやかんやで表に出てしまうような問題がなく運用できていたのは先人たちのおかげなので感謝を捧げつつ必要な改善が必要なタイミングで実行できる環境に作り変えました。

そして「できたから安心」としていると何ヶ月何年経過したときに将来のメンバーにまた同じような負債を押し付けてしまうことになるのでこれからも引き続き改善に取り組んでいく所存であります。

*1:救出できたところで使っていたものとも乖離しているため使い物にならないでしょう

*2:一時ファイルやcrontabに設定されているスクリプトによって生成されるファイルがそこそこの数あったため骨が折れる作業でした……

*3:最終的に不要になったので削除しました

*4:同一ドメインで配信するためにCFの前にnginxを建てるなど