社内勉強会 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 つのジョブで構成されています。
- ライブラリのインストール
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 からコンパイルを行なっていた
- コンパイル後のファイルをアップロードする S3 バケットに大量のファイルが存在する
毎回 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 の活用と改善の取り組みについて紹介させていただきました。 今回紹介させていただいた以外にも様々な用途があり、今後もさらにうまく活用していきたいと思っています。
ご覧いただきありがとうございました。