はじめまして、技術本部 サービス開発部の上島です。
私はSansanのプロダクトの裏側でデータ統合の役割を担う「名寄せシステム」の開発に携わっています。
今回は、その名寄せシステムのEC2で稼働しているRailsアプリのデプロイの仕組みを、CodePipeline+CodeDeploy+CodeBuildを使った仕組みに移行して、デプロイ環境を改善した取り組みについて、紹介したいと思います。
名寄せシステムとは
関連のある名刺データを統合することで、顧客情報を一元管理できるようにしたり、世の中の様々なビジネスデータを収集し、名刺に紐づけることで、名刺の価値を高めるシステムです。
Ruby(Rails)で開発しており、AWSの利用サービスは EC2、RDS(Aurora MySQL)、S3、ElastiCache(Redis)などがあり、一般的な構成のアプリケーションです。
独自のデプロイシステムが抱えていた課題
名寄せシステムでは、独自に開発したデプロイシステム(Capistrano + Consul + Stretcherを使ったPull型・シンボリックリンク切り替え方式)を使って、デプロイサーバにログインしてコマンドを実行しながら、EC2インスタンスに対してデプロイを行っていました。
しかし、以下のような課題が顕在化するようになり、運用負荷が上がっている状況でした。

これらの課題を解決するために、独自のデプロイシステムから、マネージドなAWSサービスであり、自由なデプロイ構成が可能である CodePipeline+CodeDeploy+CodeBuildを組み合わせたデプロイシステムに置き換えることを決めました。
また、ここでコンテナ化もしないの?と思われた方がいるかも知れません。今日では、コンテナ(ECSやEKS等)を使ったインフラ構成を採用することも多くなり、弊社でも名刺データ化システムでコンテナ化がされている事例*1はあります。
しかし、今回のケースではデプロイシステムに対しての課題が集中しており、まずは独自のデプロイシステムから脱却して変更が容易なデプロイシステムにしたいこと、工数的な問題、今後コンテナ化するにしてもマネージドなデプロイサービスに開発チームメンバーが慣れることを優先し、今回はコンテナ化せず、既存の資産であるEC2のインフラ構成を活かしながら、デプロイシステムのみを置き換える判断をしました。
CodePipeline+CodeDeploy+CodeBuildを使ったデプロイシステム
最終的にでき上がったのが以下です。

デプロイ手順のほとんどを自動化し、AWSのコンソールを数クリックすればデプロイできるようになりました。
そして、実際に変更を反映する前に、手動承認アクションを設けて、反映タイミングをコントロールできるようにしています。
なお、CodePipelineのSourceの取得は、ソースコード管理でGitHubを利用しているのでGitHubのインテグレーションを利用して取得したかったのですが、CodePipelineは開始の際に引数を取ることができないため、デプロイ対象のブランチ名が固定になってしまう制約があります。
なので、任意のブランチ名を指定できるようにするため、CodePipelineの前段にブランチ名の環境変数を引数に取るCodePipeline開始用のCodeBuildを用意して、そこでソースコードの取得&S3にアップロード&デプロイ用CodePipelineを開始するようにし、CodePipeline側でS3上のアップロードされたソースコードを参照することで、ブランチ名で差し替え可能な仕組みにしています。
CodeBuildとRun Commandを使ったコマンド実行
アプリケーションのビルド(bundle install・アセットプリコンパイルなど)やDBマイグレーションは、CodeBuild内ではなく、既存の特定のEC2インスタンス内で実行する必要があったため、CodeBuildからAWS System ManagerのRun Commandという遠隔コマンド実行サービスを呼び出して処理しています。Run CommnadはSSHを使わず安全かつ簡単に、同時に複数のインスタンスに対してコマンド実行できる仕組みなので、とても便利ですね。
CodeDeployでゼロダウンタイムデプロイ
CodeDeployでEC2インスタンスに対して選べるデプロイ方式には、既存のインスタンスに対してデプロイするインプレース方式とBlueGreen方式の2種類がありますが、今回はデプロイ対象に既存のインスタンスを含むのでインプレース方式を採用しました。
その上で、瞬時に新しいバージョンのアプリケーションに切り替えがしたかったので、All-at-once方式という一度に全部のインスタンスに対して変更を加えるオプションを選択しました。
そして、CodeDeployではデプロイフェーズに関連したライフサイクルイベント*2があり、イベントにフックして任意のスクリプトを呼ぶことができるので、スクリプト内で新旧のアプリケーションパスをシンボリックリンクで切り替える実装をすることで、ゼロダウンタイムでアプリケーションをデプロイすることができました。
また、Web用インスタンスとBatch用インスタンスでは、デプロイの中で行うことが異なるため(例えばWeb用インスタンスはWebサーバの再起動が必要など)、デプロイグループと呼ばれるデプロイ対象のインスタンスセットで、WebとBatchのグループに分けるようにしており、それぞれでappspec.ymlというデプロイの定義書を持つ構成になっています。
苦労したところ
CodeDeployの成功判定
CodeDeployによるデプロイの成功判定の設定は、全インスタンスで成功しているかという設定はなく、デプロイ中に正常なホストの数が維持できているかという基準のため、デプロイ設定によって異なりますが、一つのインスタンス以上や半分のインスタンス以上が成功であれば、成功とみなすようになっています。
そのため、全インスタンスでデプロイが成功しているかを見るために、別途CodeBuildでデプロイ詳細結果取得APIを使用して、実際にデプロイした台数とデプロイすべきEC2インスタンスの台数が一致するか等のチェックする実装をしています。また、別のデプロイグループに分けた場合も横断してチェックするには作り込みが必要でした。
改善の結果
新しいデプロイシステムにリプレイスしてから2ヶ月ほど経ちましたが、以下の結果となりました。
- ほとんどの作業が自動化されて、リリースの負荷が軽減した(開発チームメンバーからの声)
- 細かくプロダクトの改善がリリースされている (本番デプロイ回数 2倍増)
- 気軽にステージング環境でデプロイして検証することが増えた(ステージングデプロイ回数 3倍増)
- トラブルもなく安定的にデプロイできている
最後に
今回はEC2で運用しているシステムで、デプロイ環境を改善する取り組みを紹介しました。
コンテナが普及してきている時代ですが、EC2でシステムを稼働するケースはまだまだあると思います。既存の資産を活かしながらも、デプロイ環境を改善できるということを、一つのケースとしてご参考になれば幸いです。