for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

CI/CD実行時間を50%以上短縮させた話とその1年後の現状

こんにちは。社内向けプロダクト「タレントエージェンシー支援システム(SFA/CRM)」(以下、プロダクト)にてSREをしている表と申します。 入社当初にCI/CDの改善を行い、約50%の時間短縮を実現しました。 CI/CDの改善から約1年が経過したため、実行内容と1年経った現状についてお話ししたいと思います。

前置き

本筋から逸れるため詳細は割愛しますが、本プロダクトのブランチ戦略はGitHub Flowを少し緩めに採用しています。 緩めにというのはmainブランチ、featureブランチに加えて、Stagingブランチの3つのブランチを切る運用のことを指します。 弊社ではStaging環境を複数用意しており、featureブランチからstaging_環境名 のブランチ名を切りpushすることで、CI/CDが起動し指定したStaging環境にデプロイされる仕組みをとっています。

docs.github.com

改善前のCI/CDの状況

プロダクトでは、GitHubActionsを利用しております。 上記のブランチ戦略の下、CI/CDの実行トリガーは以下のように設定しています。

  • StagingブランチをPushした時
  • featureブランチをPushした時
  • mainブランチへMergeした時

StagingブランチをPushした時とmainブランチへMergeした時の実行時間を計測しました。当時の直近20回の実行時間は、以下の通りです。

StagingブランチをPushした時

平均実行時間:32分37秒

改善前_Staging_Push

mainブランチへMergeした時

平均実行時間:48分23秒

改善前_Production_Push

StagingブランチをPushした時とmainブランチへMargeした時のCI/CDの合計実行時間が最低でも1.5〜2時間かかり、一つのPRにCI/CDによる待ち時間が発生していました。

CI/CDの実行時間を短縮するために行ったこと

改善策は、Dockerfile軽量化やマルチステージBuild、キャッシュの利用やテストの分割など複数あると思いますが、以下の改善案に着手しました。

  • ワークフローの実行タイミングの見直し
  • Jobの並列実行

上記2つに着手した理由は、実行時間という観点において一番効果が高いと考えたためです。各Jobの実行に5分ほどかかっており、これらのJobの実行タイミングを整理し並列に実行することで、Jobを直列で実行するよりも待ち時間が緩和されます。 またプロダクトの成長に伴い、コンテナ数の変化や各Job自身の実行時間の増加により、パイプラインの順序や直列での起動が現状に適さなくなり、Jobの実行順序の見直しが必要な箇所や並列実行への置き換え可能な箇所が散見されました。

ワークフローの実行タイミングの見直し

ワークフローの実行タイミングの見直しについては、主にテストの実行タイミングについて検討しました。私が所属するプロダクトでは、StagingブランチをPushした時、featureブランチをPushした時、mainブランチへMergeした時、それぞれのタイミングでコードを担保するためにテストを行っておりました。

(改善前)

改善前_ワークフロー

StagingブランチをPushした時、mainブランチへMergeした時のテストは、Build/Deployのワークフローに組み込まれているため、以下のような順番で実行されておりました。

  1. テスト環境のBuildテスト実施
  2. (テストがNGの場合は、ワークフローが止まる)
  3. (Production/Staging)環境Build
  4. (Production/Staging)環境Deploy

テストを担保した環境をDeployするという観点では問題ない構成でした。しかし、CI/CDの実行時間が約30分ほど発生し、都度待ち時間が発生する影響で、開発効率が低下するという課題がありました。 (上記背景からStagingブランチをPushした時はテストをしないという選択をし、運用しておりました。。。恐ろしい。。。)

実行時間の課題を解消するため、StagingをDeployする時のワークフローからテスト処理を外出しし、Staging環境のBuild/Deployを実行するワークフローとテストのみを実行するワークフローの2つに分割しました。 具体的には、テスト実行用ワークフローの発火条件にfeatureブランチをPushした時とStagingブランチをPushした時を追加しました。

(改善後)

改善後_ワークフロー

Build/Deployとテストを別のワークフローに分けることで、並列実行が可能となったので、環境Deployのワークフロー実行時間が大幅に削減できました。(Jobの分割でも並列実行ができますが、featureブランチをPushした時のテスト処理と共通化を実現したかったため、ワークフローごと分けました)

Jobの並列実行

ワークフロー全体の見直しが完了したので、次はワークフローの中を見ていきたいと思います。 今回はmainブランチへMergeした時のBuild/Deployとテストのワークフローを例に説明します。 検討前のワークフローの中は、以下のようになっておりました。

(改善前)

改善前_Job実行順番

Build&PushのJobとDeployのJobがあり、直列で実行されています。

  • Build&PushのJobでは、下記の処理を実行しています。
    • テスト環境を作成しテストを実行
    • WebイメージのBuildとECRにPush
    • JobイメージのBuildとECRにPush
    • CronイメージのBuildとECRにPush
  • DeployのJobは、下記の処理を実行しています。

テストと各コンテナイメージの作成、Deployについては、依存関係がないにも関わらず、全て直列実行になっているため、非常に時間がかかっておりました。

(改善後)

改善後_ワークフロー

上記の図のようにBuild&Pushの処理を4つのJobに分割し、Deployの処理を3つのJobに分割させ並列で実行させることにしました。

DeployのJobに関しては、前段のJobのいずれかが失敗した状態で、DeployされないようにそれぞれのDeployJobにneedオプションを設定し、テストのJobや各イメージのBuildとPushのJobが完了しないと起動されないように設定しました。

下記needオプションの実装例です。

needオプション

上記により全てのJobが並列で起動し、大幅な時間短縮が可能となりました。

ただ、Jobの並列化を行う際に、検討すべき前提条件と弊害がありますのでご紹介します。

Jobの同時実行数の制限

GitHubのプランにて、Jobの同時実行には制限があります。並列での処理に関しては、他チーム含めてPlanの範囲内で実行できるように調整してください。

https://docs.github.com/ja/actions/learn-github-actions/usage-limits-billing-and-administration

弊社では幸い制限に該当しなかったため、今回は特段対応はしておりません。

同一の処理の記載が増える

GitHubActionsのJob1つにつきRunnerを立ち上げるため、各Job起動時にCheckout等の処理が必要になります。そのため、同一処理が増え冗長になります。

弊社では、下記を参考にして共通化を行いDRYな記載を実現しました。

  • WorkFlowの再利用

docs.github.com

  • 複合アクション(composite)

docs.github.com

改善後のCI/CDの結果

StagingブランチをPushした時

 平均実行時間:17分00秒

改善後_Staging_Push

mainブランチへMergeした時

平均実行時間:22分23秒

改善後_Production_Push

改善の結果、以下の通り最大で約54%の削減を実現できました。

・StagingブランチをPushした時

 32分37秒→17分00秒  約 48%短縮

・mainブランチへMergeした時

 48分23秒→22分23秒  約 54%短縮

1年経過しての現状

パイプライン変更後、チームで見た年間のリリース数は、前年度に比べて10%ほど向上しておりました。全てがパイプライン改善の結果ではありませんが、幾分か影響していると考えております。 また、開発速度も上がりテストケースやイメージサイズもどんどん肥大していく中で、引き続きDockerfileの軽量化やGem、Packageの整理、不要機能の削除など細々とした改善活動を行っております。その影響もあってか、CI/CDパイプラインの実行時間は1年前と比べて増加はしておらず、当時の実行時間を保てております。

現状1年前のような大きな改善は行えておりませんが、フロント分離などの抜本的なアーキテクチャの変更などをチームで議論/検討しております。

現在は同一Deployラインの中に、Vue.jsとRuby on Railsが共存しております。 フロントエンドとバックエンドを分離することで、Deployラインの分割が可能になり、CI/CDの実行時間を大幅に削減できると考えております。(フロントエンド分離実施に向けた論点は、CI/CDではなく副次的な効果にすぎませんが、分離に向けた議論に関しては当記事の内容から外れますので割愛させていただきます)

また今回挙げたCI/CDの構成では、課題点があります。テストと各イメージのBuildとPushを並列で実行しているため、4つのJobの内いずれかが失敗した場合でもECRにPushされてしまうため、下記の事象が起こる可能性があります。

  • テストが成功していないイメージがPushされる
  • 3つのイメージで世代の不整合が起きる

例えば障害が発生した場合を考えます。障害発生前のイメージをDeployする際、障害発覚までの間に4つのJobの内いずれかに失敗したパイプラインがあった場合、1世代戻すだけでは障害前のイメージをDeployできない可能性があります。

現在の運用では、イメージの戻し作業は行っていないので影響はありませんが、今後に向けて対応を検討していきたいと考えております。

まとめ

エンジニアとして、プロダクトとして、ユーザーへの価値提供速度は非常に重要な指標の一つだと考えております。 CI/CDパイプラインは価値提供速度や開発効率に直結するため、非常に重要な改善対象です。 今後も機能開発が進みCI/CDに手を加える必要が出てくると思いますが、継続して改善作業を進めていきます!

あとがき

最後に採用情報です。 当社では、まだまだ採用募集中です。ぜひ一緒に課題解決していきましょう! ご興味ありましたらぜひ一度カジュアルにお話できたらと思います。 採用ページはこちら