はじめに
STORES 予約でSREを担当している矢作です。
STORES 予約では今年(2022年)の9月にアプリケーションを構成する大多数のサーバーをEC2からECSへと移行することでコンテナ化(以降ECS化)しました。
今回はコンテナ化をすることで解決を目指した課題についてや、コンテナ環境でどういったアーキテクチャを構築し、その後移行していったのかについてお話ししていきます。
コンテナ化以前の構成
コンテナ化する前のSTORES 予約のざっくりとしたインフラ構成は以下のようになっています。
一台のEC2インスタンスの中に、Nginx(緑のアイコン)、Next.js(黒のアイコン)を利用して作られたコードベース、Railsを利用して作られたコードベースの3者が同梱されています。
Nginxがリクエストを受け取り、パスベースでNext.jsかRailsのプロセスへとインスタンス内でリバースプロキシしているといった具合です。
課題感
この構成の課題として以下のようなものを抱えていました。
サーバーの増強時にインスタンスを1からセットアップする必要があり、増強までのリードタイムが長い
- 最新のSource AMIを指定してpacker buildを行い、ゴールデンイメージを作成する
- 作成したゴールデンイメージからEC2インスタンスを起動する
- 起動したEC2インスタンス内にNext.jsとRailsのコードベースを配布し、プロセスを立ち上げる
- プロセスが立ち上がったEC2インスタンスをターゲットグループへ追加する
以上の作業を行うことでやっとインスタンスを増やすことができます。
スムーズに行えると大体1時間ほどで増強を行うことができます。 アクセスのスパイクを観測してからでは致命的に遅いです。
Railsの部分だけ増強したくても、Next.js、Nginxのセットアップも必要
増強のリードタイムの遅さにも繋がってくるのですが、実際にアクセスがスパイクした際に最初にボトルネックになってくるのが、Railsのプロセスというパターンがほとんどで、Railsのプロセス数を増やしたいが為にインスタンスの増強を行っているのに、Next.jsのためにNode.jsをセットアップする作業等が行われている間は大人しく待つ必要があります。
まとめると、スケーラビリティが低い といったことに集約できるかなと思います。
細かい部分を上げると、セキュリティパッチがあたっていない古いSource AMIからビルドされたインスタンスが生き残り続けてしまいがちだったり、システムの利用度に合わせて運用するインスタンス数も増えていったため、そういったセキュリティパッチを充てていくための運用コストも嵩んできていて、コンテナ化を機に作業コストを圧縮したい といった実情もあります。
目指す構成
ECS化する際に、以下のような構成を目指すことにしました。
今まで一つのEC2インスタンス内に同梱されていた、Nginx, Rails, Next.jsをそれぞれ別のECSサービスとして分離
各ECSサービスはそれぞれ独立してスケーリング、Blue/Greenデプロイを行えるようにするために、サービス間にALBを配置しています。
切り替え手順
切り替え作業はこのような流れで行いました。
EC2とECS両環境に対してデプロイ処理が行われるようにする
本番環境でデプロイ処理が安定して動作することを担保するために、ECS環境へトラフィックを流し始める前(1~2週間ほど前)から並行でデプロイ処理が行われるようにしました。
言葉だけではイメージが伝わりにくいと思うので、実際にデプロイ時に利用しているGithub Actionsのスクリプトをかなり簡素化したものを貼り付けておきます。
複数のECSサービスを運用する都合上、ecs-deploy
のステップでさらにmatrixビルドして同時に複数のECSサービスへとデプロイしています。
Capistrano側(EC2へのデプロイ)は単純に特定のタグが付与されたインスタンス全てにコードベースを配布する作りをアプリケーションの内部で定義しているため、デプロイスクリプト上は単純に見えています。
name: Rails Deploy on: workflow_dispatch: inputs: branch: description: 'branch name' required: false default: 'master' environment: description: '環境名' required: true default: 'sandbox' env: TAG: ${{ github.sha }} jobs: # EC2へのデプロイ cap-deploy: name: Deploy Rails by Capistrano runs-on: ubuntu-latest steps: # 中略(諸々のセットアップ処理) - name: deploy run: bundle exec cap ${{ github.event.inputs.environment }} deploy BRANCH=${{ github.event.inputs.branch }} # ここからECSへのデプロイ処理を記述 build-and-push-image: name: Rails Build and Push Image runs-on: ubuntu-latest timeout-minutes: 20 permissions: id-token: write contents: read steps: - uses: actions/checkout@v3 # imageをビルドし、タグにコミットハッシュを付与してECRへpush ecs-deploy: name: Rails Deploy to ECS needs: build-and-push-image strategy: fail-fast: false matrix: target-service: ["main", "ex001", "ex002"] runs-on: ubuntu-latest steps: # 中略 - name: deploy run: ecspresso deploy --config ${CONFIG_FILE_PATH}/${ENV}/config-${{ matrix.target-service }}.yaml
EC2のみで運用していた頃はcap-deploy
のみが定義されていましたが、その下にecs-deploy
といったjobを追加することで同時にデプロイ処理を走らせるようにしました。
完全にECS化した暁には、cap-deploy
のjobがなくなり、ecs-deploy
のみが残る想定です。
ECS環境へのデプロイはecspressoといったOSSのツールを利用して行なっています。 github.com
徐々にECS環境にリクエストを流す
ALBのリスナールールの加重ルーティング機能を利用し、徐々にECS側へ流すトラフィックの割合を上げていきました。
ECS環境特有かつ、すぐ顕在化してくるタイプの不具合に気づけるように、このタイミングのみ普段の機能開発のリリースを止めてもらって作業を行いました。
全てのリクエストをECS環境へ流す
この時点ではまだEC2側の環境は削除せず、またリリース時のデプロイ処理も並行で走らせたままにしておいて、約1週間程、いつでもEC2環境へ切り戻せるように並行稼働しました。
EC2環境を削除する
1週間程の並行稼働の後、ECS環境での運用が安定してきたと確信を持てたのでEC2側の環境を削除
まとめ
ECS環境側でリクエストを受け付け出す前に、デプロイの処理そのものに対する不安感が無くせたり、何かあったらすぐにEC2環境側へ戻せる状態で移行作業を行なったため、移行作業自体はとても安定した精神状態で行うことができました。
並行運用期間中、何度かEC2側に切り戻して確認することもあったのですが、すぐに戻して確認が行えたことで、これはECS環境起因のエラーではない(ソースコード側のバグ) といった確信を持てたため、必要以上に慌てることもありませんでした。
また何かあったらいつでもEC2環境側へ戻せる状態を最後まで維持し続けたことで、普段の機能開発のリリースサイクルを止めてもらう時間を数時間程度に留めることが出来ました。
普段の機能開発のリリースの様子は以下の記事にまとまっています。
product.st.inc
今回は、STORES 予約のインフラ環境の大部分がコンテナ化されましたよ といったことの報告とそこに至るまでの過程についてをお話ししましたが、一番気になるのはその後、本当に期待していた効果が得られたのか や、トラブルなくスムーズに移行が行えたのか、ECS環境側のデプロイ戦略どうなっているの だと思います。 そちらについては、後編(または中編)で公開出来たらなと思っております。
次の記事公開まで待ちきれないよ って方は是非カジュアル面談等で話を聞きに来てください! お待ちしております!