こんにちは、メドレープロダクト開発室 エンジニアの岸田です。
先日、社内勉強会TechLunchにて、弊社の提供する医療介護分野の人材プラットフォーム「ジョブメドレー」の開発で利用しているCircleCIでのCI/CDについての取り組みを発表しましたので、紹介させていただきたいと思います。
ジョブメドレーの開発でCircleCIをどのように利用しているか
ジョブメドレーの開発では、主に次の2つをCircleCIを用いて行なっています。
- ユニットテスト・構文チェック
- デプロイ
デプロイに関しては、ECS環境とEC2環境への2通りのデプロイをCircleCIを利用して行なっています。
そのためCircleCIのWorkflowは「ユニットテスト・構文チェック」「EC2へのデプロイ」「ECSへのデプロイ」の3つに分かれています。
3つのWorkflowを大まかに説明させていただきます。
ユニットテスト・構文チェック
ジョブメドレーは主にサーバサイドをRuby on Rails、フロントエンドをReactを使って開発をしています。 そのためユニットテストにはRSpec・Jestを、構文チェックにはRubocopとESLint利用しています。
このWorkflowは3つのジョブで構成されています。
- ライブラリのインストール
bundle install
・yarn install
などを行い、ユニットテスト・構文チェックの実行に必要な依存ライブラリをインストールし、CircleCIのキャッシュ機能を用いてキャッシュを行なっています
- RSpecの実行
- 2コンテナを利用しての並列実行を行なっています
- 構文チェックとJestの実行
- RSpecと比較してJestでのテストコードはボリュームが少なく・実行時間が短いため、構文チェックと一緒に1つのジョブにしています
このWorkflowはどのブランチでもGitHubへPushが行われるたびに実行されるようにしています。
EC2へのデプロイ
このWorkflowではデプロイを行うための準備と、文字通りEC2へのデプロイを行なっています。 デプロイにはRuby gemのCapistranoを利用しています。
下記のような2つのジョブで構成しています。
- ライブラリのインストール
- 前述のユニットテスト・構文チェックのWorkflowの1つ目と同じ役割です
- ビルドとデプロイ
- Railsのassets:precompileや
yarn build
などの処理と、Capistranoを用いたデプロイを行なっています
- Railsのassets:precompileや
このWorkflowは特定のブランチでしか実行されないようになっています。
ECSへのデプロイ
このWorkflowではECSへデプロイするためのDocker image作成とそのためのビルドなどを行なっています。
ジョブは下記のように4つで構成されています。
- npm packageのインストール
- フロントエンドのビルド
- GemのインストールとRailsのassets:precompile
- Docker imageの作成とPush
こちらのWorkflowも特定のブランチでしか実行されないようになっています。
抱えていた課題とassets:precompile
ジョブメドレーの開発ではCircleCIの各ジョブが全て正常に完了することをPRをマージする条件の1つにしています。 しかし各Workflow・ジョブの実行時間が長く、ジョブの実行待ちをしなければいけないという状況がよく起こってしまっていました。
特に時間がかかっていたのが、下記の3つでした。
- 「EC2へのデプロイ」Workflowの「デプロイ」ジョブ
- 「ECSへのデプロイ」Workflowの「GemのインストールとRailsのasset_precompile」ジョブ
- 「RSpecの実行」ジョブ
- RSpecの書き方などを改善することで短縮できるため、割愛させていただきます
これらについて調査したところ、1・2は、assets:precompileが主に時間を使っていることが分かりました。 この点について原因と行なった改善を説明をさせていただこうと思います。
assets:precompileに時間がかかる
ジョブメドレーではRailsのアセットパイプラインを利用して、アセットファイルのコンパイル・最小化などを行なっています。
これを実行する際に assets:precompile
コマンドを利用しています。
また、同コマンド実行時にコンパイルしたファイルをAWS S3バケットにアップロードするために asset_sync
gemを利用しています。
このコマンドの実行には9分から10分ほどの時間がかかっており、下記の2つの原因で遅くなっていました。
毎回1からコンパイルを行なっていた
こちらについては読んで字のごとくですが、デプロイWorkflowが実行されるたびにアセットファイルの変更有無に関わらず、毎回3分ほどを費やして全てのアセットファイルをコンパイル・最小化していました。
こちらの解決策としては、CircleCIの公式ドキュメントでも例が載せられていますが、 assets:precompile
コマンドで生成されるファイルが置かれるディレクトリ(public/assets
)をCircleCIでキャッシュさせることで対応しました。
キャッシュさせることで、毎回3分ほどかかっていた処理を1分ほどに短縮することができました。
コンパイル後のファイルをアップロードするS3バケットに大量のファイルが存在する
こちらついてはもう少し背景が複雑かつ、まだ解決まではできていません。
まず asset_sync
を利用したファイルのアップロード処理ですが、毎回6分以上の時間がかかっていました。
CircleCIのログをよくよく見てみるとアップロード自体に時間がかかっているのではなく、「どのファイルをアップロードすべきか」を判断する処理に多くの時間を費やしていることが分かりました。
(1ファイルもアップロードしていないが6分20秒かかっている)
そこで、asset_sync
gemのソースコードを読んでみると、「アップロード先のS3バケットにあるファイルを全て取得し、 assets:precompile
コマンドが生成したファイルのファイル名と比較する」という処理がありました。
この処理が怪しいのではないかと思い、S3バケットを確認してみたところ数十万件以上のファイルが存在することが分かりました。
数十万件以上のファイルの情報を取得していることを考えると6分以上時間がかかるのも納得です。
この問題の解決策は下記の2つが考え得ると考えました。
- 不要なファイルをS3バケットから削除する
- 前回のデプロイとの比較をしてS3にアクセスせずにアップロードすべきファイルかどうかを判断するようにする
1つ目の方法は正攻法で、数十万ファイルを全て使っているわけではないため利用されていないファイルを削除してしまう方法です。
asset_syncも早くなり、S3の利用料も少なくなるためこの方法を取れるのであれば、この方法で解決するのが良いように思います。 ジョブメドレーでもこの方法を取れないかと検討しましたが、ジョブメドレーから配信しているHTMLメールなどでも利用しているファイルがあるため一概にアクセスされていないからといって削除することができず、この方法での解決は一旦断念しました。
2つ目の方法はassets:precompile
コマンドが生成するmanifestファイル(生成したファイルのリストなどが記述されている)とCircleCIのキャッシュ機能を使って短縮する方法です。
manifestファイルは、コンパイル後のアセットファイルが出力されるディレクトリ(public/assets/
)に同じように出力され、また、上記でpublic/assets
をCircleCIのキャッシュ機能でキャッシュするようにしたため、manifestファイルも一緒にキャッシュされるようになっています。
assets:precompile
の実行により今回作成されたmanifestファイルと、キャッシュされていたmanifestファイルを比較して差分が出たファイルだけをS3にアップロードするようにしようという試みです。
この処理はasset_sync
gemではできそうになかったので、シェルスクリプトとRakeタスクを作成して実行してみたところ、「アップロードすべきファイルかどうか」を判断するための時間がほぼなくなり、6分以上の短縮をすることができました。
ただし検証が不十分なため、実運用に乗せることはまだできていません。
CircleCIのジョブ実行時間を短縮する小さな改善事項
上述の通り、各Workflowで大きく時間を費やしているのはassets:precompile
とRSpecの実行でしたが、細かな点としては他にも小さい改善をしたことがあります。
コードのチェックアウト
CircleCIでは組み込みのコマンドとして checkout
コマンドがあります。
これは対象のリポジトリのブランチをクローンしてくれるコマンドですが、ジョブメドレーはモノレポに近い構成になっており、コードベースのサイズや履歴が大きいためクローンするだけである程度の時間がかかってしまっています。
そこでまずは、.git
ディレクトリをCircleCIのキャッシュ機能を利用してキャッシュするようにしてみました。
すると、checkout
コマンド自体は大きく短縮しましたが、キャッシュのsave/restoreに短縮した以上の時間がかかるようになってしまいました。
別の方法としてcheckout
コマンドを利用せずに、GitのShallow cloneやSparse cloneを駆使して必要なファイルや履歴だけをクローンするということができます。
現在はSparse cloneを一部導入してみており、Shallow cloneも導入したいと考えています。
Docker imageの作成
ジョブメドレーではECSへのデプロイの際にCircleCI上でDocker imageをビルドしているため、docker build
コマンドの実行時間も可能な限り抑えたいと考えています。
短縮する方法は様々あるかと思いますが、CircleCI上でもDokcer Buildkitを利用することがきますので、それを利用してビルドすることで簡単に短縮することができます。
詳しくはDockerのガイドに記載の通りではありますが、DOCKER_BUILDKIT=1を指定してdocker build
を実行するだけで利用することができ、ジョブメドレーでは2分ほどかかっていたビルドを40秒ほど短縮することができました。
まとめ
今回は、TechLunchで発表したジョブメドレーにおけるCircleCIの活用と改善の取り組みについて紹介させていただきました。 今回紹介させていただいた以外にも様々な用途があり、今後もさらにうまく活用していきたいと思っています。
ご覧いただきありがとうございました。