Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

社内勉強会TechLunchでジョブメドレーでのCircleCIの活用と改善について発表しました

こんにちは、メドレープロダクト開発室 エンジニアの岸田です。

先日、社内勉強会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つのジョブで構成されています。

  1. ライブラリのインストール
    • bundle installyarn installなどを行い、ユニットテスト・構文チェックの実行に必要な依存ライブラリをインストールし、CircleCIのキャッシュ機能を用いてキャッシュを行なっています
  2. RSpecの実行
    • 2コンテナを利用しての並列実行を行なっています
  3. 構文チェックとJestの実行
    • RSpecと比較してJestでのテストコードはボリュームが少なく・実行時間が短いため、構文チェックと一緒に1つのジョブにしています

f:id:medley_inc:20200430190805p:plain

このWorkflowはどのブランチでもGitHubへPushが行われるたびに実行されるようにしています。

EC2へのデプロイ

このWorkflowではデプロイを行うための準備と、文字通りEC2へのデプロイを行なっています。 デプロイにはRuby gemのCapistranoを利用しています。

下記のような2つのジョブで構成しています。

  1. ライブラリのインストール
  2. ビルドとデプロイ
    • Railsのassets:precompileやyarn buildなどの処理と、Capistranoを用いたデプロイを行なっています

f:id:medley_inc:20200430190826p:plain

このWorkflowは特定のブランチでしか実行されないようになっています。

ECSへのデプロイ

このWorkflowではECSへデプロイするためのDocker image作成とそのためのビルドなどを行なっています。

ジョブは下記のように4つで構成されています。

  1. npm packageのインストール
  2. フロントエンドのビルド
  3. GemのインストールとRailsのassets:precompile
  4. Docker imageの作成とPush

f:id:medley_inc:20200430190429p:plain

こちらのWorkflowも特定のブランチでしか実行されないようになっています。

抱えていた課題とassets:precompile

ジョブメドレーの開発ではCircleCIの各ジョブが全て正常に完了することをPRをマージする条件の1つにしています。 しかし各Workflow・ジョブの実行時間が長く、ジョブの実行待ちをしなければいけないという状況がよく起こってしまっていました。

特に時間がかかっていたのが、下記の3つでした。

  1. 「EC2へのデプロイ」Workflowの「デプロイ」ジョブ
  2. 「ECSへのデプロイ」Workflowの「GemのインストールとRailsのasset_precompile」ジョブ
  3. RSpecの実行」ジョブ
    • RSpecの書き方などを改善することで短縮できるため、割愛させていただきます

これらについて調査したところ、1・2は、assets:precompileが主に時間を使っていることが分かりました。 この点について原因と行なった改善を説明をさせていただこうと思います。

assets:precompileに時間がかかる

ジョブメドレーではRailsのアセットパイプラインを利用して、アセットファイルのコンパイル・最小化などを行なっています。 これを実行する際に assets:precompile コマンドを利用しています。 また、同コマンド実行時にコンパイルしたファイルをAWS S3バケットにアップロードするために asset_sync gemを利用しています。

このコマンドの実行には9分から10分ほどの時間がかかっており、下記の2つの原因で遅くなっていました。

  1. 毎回1からコンパイルを行なっていた
  2. コンパイル後のファイルをアップロードするS3バケットに大量のファイルが存在する

毎回1からコンパイルを行なっていた

こちらについては読んで字のごとくですが、デプロイWorkflowが実行されるたびにアセットファイルの変更有無に関わらず、毎回3分ほどを費やして全てのアセットファイルをコンパイル・最小化していました。 こちらの解決策としては、CircleCIの公式ドキュメントでも例が載せられていますが、 assets:precompile コマンドで生成されるファイルが置かれるディレクトリ(public/assets)をCircleCIでキャッシュさせることで対応しました。 キャッシュさせることで、毎回3分ほどかかっていた処理を1分ほどに短縮することができました。

コンパイル後のファイルをアップロードするS3バケットに大量のファイルが存在する

こちらついてはもう少し背景が複雑かつ、まだ解決まではできていません。

まず asset_sync を利用したファイルのアップロード処理ですが、毎回6分以上の時間がかかっていました。 CircleCIのログをよくよく見てみるとアップロード自体に時間がかかっているのではなく、「どのファイルをアップロードすべきか」を判断する処理に多くの時間を費やしていることが分かりました。

f:id:medley_inc:20200430190719p:plain

(1ファイルもアップロードしていないが6分20秒かかっている)

そこで、asset_sync gemのソースコードを読んでみると、「アップロード先のS3バケットにあるファイルを全て取得し、 assets:precompile コマンドが生成したファイルのファイル名と比較する」という処理がありました。 この処理が怪しいのではないかと思い、S3バケットを確認してみたところ数十万件以上のファイルが存在することが分かりました。

数十万件以上のファイルの情報を取得していることを考えると6分以上時間がかかるのも納得です。

この問題の解決策は下記の2つが考え得ると考えました。

  1. 不要なファイルをS3バケットから削除する
  2. 前回のデプロイとの比較をして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:precompileRSpecの実行でしたが、細かな点としては他にも小さい改善をしたことがあります。

コードのチェックアウト

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の活用と改善の取り組みについて紹介させていただきました。 今回紹介させていただいた以外にも様々な用途があり、今後もさらにうまく活用していきたいと思っています。

ご覧いただきありがとうございました。