Cloud Build上でNext.jsのコンテナイメージのビルド速度を改善した話

はじめに

SREチームの磯村です。 去年入社してからフロントエンドエンジニアとして働いていましたが今年6月からはSREチームに転属しました。 SRE見習いとして奮闘中です。

コミューンはアプリケーションの基盤としてGoogle CloudのCloud Runを利用しています。 そしてCloud Runで実行するコンテナイメージをCloud Build上でビルドしています。 この記事ではCloud Build上でのコンテナイメージのビルド速度を改善した事例を紹介します。

今回やったこと

コミューンでは動作確認用の環境(develop環境と呼ばれています)や本番環境へのデプロイに20分以上かかってしまうことが頻繁に発生していました。 場合によっては29分もの時間がかかってしまう場合もありました。 このような長いデプロイ時間はメインブランチにマージできるPRの数を減少させ、変更のリードタイムを悪化させていると考えています。

これを改善するために現在複数メンバーでデプロイのパイプラインの見直しを行っています。 その中で課題の1つとして、Cloud Build上で行っているNext.jsのコンテナイメージのビルド時間が長いことが挙げられました。 中間レイヤーのキャッシュが全くないような場合には12から13分、場合によっては17分強かかることもありました。

今回その課題に取り組んだ結果、ビルド時のマルチステージビルドをやめることで最長17分強かかる場合もあったビルドを最長8分強にまで改善しました。

改善への道のり

ボトルネックの特定

まずはじめに何が遅いのかを特定する必要がありました。 そこで、DockerfileにRUN命令で記述されている処理の実行時間を調べました。 主な処理は次の3つです。

  1. npm ci(1分30秒前後)
  2. 環境変数をNODE_ENV=productionにしてnpm ci(40秒前後)
  3. next build(3分30秒前後)

これらの処理を合計した時間、つまり純粋にNext.jsのビルドにかかっている時間は5分40秒前後でした。 しかし実際にはビルド全体で12から13分程度かかっています。 このことから実際にNext.jsをビルドするのにかかっている処理とは別の箇所で時間がかかっていることがわかりました。 この理由を調べるためCloud Buildの実行時のログのタイムスタンプをCloud Loggingで確認して実際にはどこに時間がかかっているのか調べました。

すると、DockerfileのRUNやCOPYが終了するたびに実行される中間レイヤー作成ののためのスナップショットで大きく時間が経過している場合があることがわかりました (例えばnpm ci直後のスナップショットに1分かかっている場合がありました)。

このため、これらの問題を改善することの方が個別のコマンド(npm cinext build)のパフォーマンスを改善することよりも効果が大きいのではないかと考えました。

改善のために試したこと

スナップショットのパフォーマンスを改善するKanikoのオプションの利用

スナップショットに時間がかかっていることがわかったためCloud Build上でスナップショットのパフォーマンスを改善する設定等がないか調べました。

調べたところCloud Build上で行うビルドに利用しているKanikoに、スナップショットのパフォーマンスを改善するオプションが存在することがわかったので試してみました。 次の3つです。

  • --snapshot-mode=redo
    • スナップショットを作成する方法を変更するオプションです。デフォルトのfullからredoに変更すると最大50%まで改善する可能性があるようです。
  • --single-snapshot
    • スナップショットの作成をビルドの最後だけにするオプションです。マルチステージビルドの場合はステージの最後だけスナップショットが実行されます。
  • --use-new-run
    • 変更検知の方法を変更するオプションです。

これらのオプションを試した結果、ビルド時間が30秒から1分程度改善されました。しかしこれは12から13分かかるビルド時間の一部でしかありません。 オプションの導入によりスナップショットの回数は減るもののステージの最後にはスナップショットが実行されます。このスナップショットの実行時間があまり改善されなかったため、全体のビルド時間は小さな改善にとどまったのだと考えています。

マルチステージビルドをやめる

これ以上スナップショットを減らすためにはマルチステージビルドをやめる必要があると考え試してみました。マルチステージビルドをやめるため、実行時には不要なビルド用のファイルを削除する処理を追加する必要があります。また、不要なファイルが中間レイヤーに残らないようにするために1つのRUN命令の中でビルド用ファイルの追加・ビルド・ビルド用ファイルの削除を行うか、--single-snapshotオプションを利用する必要があります。今回は--single-snapshotオプションのことを忘れていたので1つのRUN命令の中に全ての処理を書きました。

この結果、5分前後ビルド時間が改善されました。

結果

結果として、マルチステージビルドをやめることで大体7-8分程度でビルドが終了するようになりました。変更による結果の差分は次のグラフの通りです。マルチステージビルドをやめたことでキャッシュが利用されなくなった結果、以前と比べて実行時間のばらつきが減りました。以前は next build の結果までキャッシュを利用できる状態だった場合にはビルド時間は6分ほどだったため、その場合よりは遅くなってしまいますが多くの場面でマルチステージビルドをやめた場合の方が実行時間が短いことがわかります。

また、マルチステージビルドをやめたことで懸念されるイメージサイズの肥大化ですが、中間レイヤーを作成しないことと不要なファイルの削除を行ったことでほぼ変化はありませんでした。

コンテナイメージのビルド時間(分)

まとめ

今回Cloud Build上でのNext.jsのコンテナイメージのビルド速度を改善に取り組みました。 時間がかかっている処理はNext.jsのビルドやnpm ciなどではなく、中間レイヤーを作成する際のスナップショットでした。 スナップショットの回数を減らすためにマルチステージビルドをやめ、中間レイヤーが全く生成されないようにした結果ビルド時間を最大5分前後改善することができました。

終わりに

今回の改善に取り組む前はNext.jsのビルド結果のキャッシュやnode_modulesのキャッシュを行えばビルド速度の改善はうまくいくだろうと想像していたのですが、実際に時間がかかっていたのは中間レイヤー作成のスナップショットでした。このことからも「推測するな、計測せよ」が重要であることを再度確認することができました(大分雑な計測ではありますが)。

また、今回の改善でNext.jsの素のビルド時間に近づいたため当初に想定していたキャッシュを利用した改善にも価値が出てきたと考えています。 今後はこのようなビルドの実作業自体の改善にも取り組んでいきたいと思います。

おわりに

コミューンではエンジニアを募集中です!少しでも興味のある方は気軽にカジュアル面談に申し込んでみてください!

commmune-careers.studio.site