はじめに
こちらは バイセルテクノロジーズ Advent Calendar 2024 の9日目の記事です。 昨日は吉森さんによるバイセル技術広報の2年間の取り組みと、その狙いについてでした。
こんにちは。開発1部の鴨野です。
現在はバックエンドエンジニアとして、リユースプラットフォーム「Cosmos」の出張訪問買取アプリケーション「Visit」の開発に携わっています。
今回は、VisitアプリケーションのGoバックエンドのデプロイ速度を改善するために行った取り組みについて紹介します。
なぜデプロイ速度の改善を行ったのか
私が所属しているVisitチームは6月にプロダクトをリリースし、私自身がチームに参加した7月からは、既に判明しているバグの修正や新機能の開発が進められていました。これまでのゼロからの開発とは異なり、既存のコードを流用して機能を追加していく作業が主となり、それに伴いリリースフローの見直しを行いました。
今まではmainブランチに複数のPR(ローカルで動作確認済み)をマージし、機能として公開できる状態に持っていってからstgに手動デプロイをし、QAをしていました。そして、機能単位で完成した際に本番環境にデプロイを行っていました。
このフローでは、stgのQAで複数の開発者のPRの内容をすべて確認する必要があり、QAにかかる時間とその労力が大きな負担となっていました。また、本番環境へのリリースが属人化しており、リリースの担当者にかかるQAの負担が大きくなっていました。
そこで、リリースフローを現状のチームに合わせたものに変更しました。mainにマージされたものは即時にstgにデプロイされ、その1つのPRに対してのみQAを行うようにしました。そして各開発者がstgでQAをし、問題がなければ本番環境にデプロイするようにしました。
このリリースフローを導入することで、開発者が自分のPRの内容に責任を持つことができ、リリース作業の負担が分散されます。
しかし、当時Visitアプリケーションのデプロイには13分以上かかっており、このリリースフローを導入することで、デプロイの頻度が増え、デプロイにかかる時間が開発者の負担になってしまいました。QAで不具合が見つかった場合も、デプロイにかかる時間が長いため、修正からQAの再開までに30分近くの時間を要してしまいました。
そこで、デプロイの時間が短縮できればリリースを完了するまでの時間が短縮でき、開発者の負担を軽減できると考えたため、デプロイ速度の改善を行うことにしました。
デプロイを改善する
デプロイ速度を改善するために、まず既存のデプロイについて測定を行いました。
デプロイにはCloud Buildを利用しています。
利用したcloudbuild.yamlおよびDockerfileは以下の通りです。
# cloudbuild.yaml # 一部省略、改変しています steps: - id: 'get secret 1' # docker build に使用する secret1の取得 name: 'gcr.io/cloud-builders/gcloud' entrypoint: 'bash' args: - '-c' - | gcloud secrets versions access latest --secret=secret1 > /workspace/secret1.txt - id: 'get secret 2' # docker build に使用する secret1の取得 name: 'gcr.io/cloud-builders/gcloud' entrypoint: 'bash' args: - '-c' - | gcloud secrets versions access latest --secret=secret2 > /workspace/secret2.txt - id: 'docker build' name: 'gcr.io/cloud-builders/docker' entrypoint: 'bash' args: - '-c' - | docker build --build-arg BUILD_SECRET_1=$(cat /workspace/secret1.txt) --build-arg BUILD_SECRET_2=$(cat /workspace/secret2.txt) -t $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_NAME/$_IMAGE_NAME . - id: 'docker push' name: 'gcr.io/cloud-builders/docker' args: ['push', '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_NAME/$_IMAGE_NAME'] - id: 'deploy cloud run' name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' entrypoint: gcloud args: - 'run' - 'deploy' - 'api-container' - '--image' - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_NAME/$_IMAGE_NAME' - '--region' - 'asia-northeast1' images: - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_NAME/$_IMAGE_NAME'
# Dockerfile # 一部省略、改変しています FROM golang:1.23.2-bullseye ARG BUILD_SECRET_1 ARG BUILD_SECRET_2 WORKDIR /app # Install chromium RUN apt update && apt install -y \ wget \ unzip \ chromium # chromiumの設定をしていく RUN ... # その他の設定 COPY go.* ./ RUN go mod download RUN ... # 必要ツールのインストール、自動生成など RUN go build -v -o server CMD ["/app/server"]
計測の方法は、直近5回分のデプロイログを確認し、各ステップの実行時間を計測しました。ネットワークや計算処理によって生じる誤差はあるものの、以下のような結果が得られました。
デプロイには約13分半かかっており、その半分以上がdocker build
によるものだと分かりました。
マルチステージビルドを導入する
計測結果を眺めてみましょう。secret取得ステップの初回のみに時間がかかっています。Cloud Buildは、指定されたコマンドを実行するためのランタイムをdocker imageとしてpullしてくる必要があります。secret取得ステップの初回ではgcr.io/cloud-builders/gcloud
というイメージをArtifact Registryからpullしてきます。
secret取得ステップの2回目では、既にgcr.io/cloud-builders/gcloud
というイメージがpullされているため、pullする必要がなくなります。
つまり、初回のpullにかかっている1分は、純粋にネットワーク速度に依存していると考えられます。
実際にgcr.io/cloud-builders/gcloud
をローカルにpullしてみると、そのサイズは2.95GBであることがわかりました。
Cloud Build上でpullに1分かかっていることから、Cloud BuildとArtifact Registry間での通信速度は400Mbps強であると考えられます。(regionによって変わる場合があると思います)
ネットワーク速度は必要十分ですが、アプリケーションのdocker imageのサイズが大きいためにdocker push
に時間がかかっていると考えられます。docker imageのサイズを1GB小さくできれば、docker push
にかかる時間を約20秒短縮できるのではないかという仮説が立てられました。
そこで、docker imageのサイズを小さくするためにマルチステージビルドを導入しました。
マルチステージビルドとは、複数のFROMステートメントを持つDockerfileを利用して、ビルド環境と実行環境を分けることで、ビルド環境でのみ必要なツールやファイルを含めずに、最終的な実行環境のみを含めることができる手法です。Go言語ではgo build
によって生成されたバイナリがあればアプリケーションを実行できるため、Go言語のランタイムを含めずに実行環境のイメージをビルドできます。
実際にマルチステージビルドを導入したDockerfileは以下の通りです。
# Dockerfile # 一部省略、改変しています # アプリケーションをビルド FROM golang:1.23.3-bullseye AS builder ARG SECRET_1 ARG SECRET_2 RUN go install ... # 必要なツールのインストール WORKDIR /app RUN --mount=type=bind,target=.,rw go build -v -o /output/server # アプリケーションを実行 FROM debian:11.11-slim AS runner WORKDIR /app # Chromiumをインストール RUN apt-get install -y \ --no-install-recommends chromium # chromiumの設定をしていく RUN ... # その他の設定 COPY --from=builder /output/server /app/server CMD ["/app/server"]
builderステージでアプリケーションをビルドし、runnerステージではビルドされたバイナリ(output/server
)のみをコピーして実行するようにしました。アプリケーション内ではChromiumを利用しているため、runnerステージでChromiumをインストールしています。
この変更によって、imageのサイズを3.86GBから867.42MBに減少させることができました。
さらに、マルチステージビルドは可能な限り並列でビルドが行われるため、runnerステージで--from=builder
が出現するまでは、builderステージでのビルドが完了するのを待たずにrunnerステージのビルドが開始されます。その結果、デプロイ時間は10分以下に短縮されました。
影響が大きかったものとしては、docker build
にかかる時間が2分短縮され、docker push
にかかる時間は想定を超えて1分30秒ほど短縮されました。
これは先ほど述べたように、並列化によるdocker build
の効率化と、imageサイズの縮小による通信量の削減が大きく影響していると考えられます。
Cloud Buildランナーのスペックアップ
次にデプロイのボトルネックになっているのはdocker build
のステップであると考えました。docker build
はベースイメージのpullよりも、GoのビルドやChromiumのインストールに時間がかかっているため、docker build
のステップをより高速化するには、処理自体を高速化する必要がありました。そのため、Cloud Buildランナーのスペックをアップしました。
デフォルトで使われているCloud Buildランナーは2コア8GBのマシンです。今回は1つ上の8コア8GBのマシンに変更しました。
https://cloud.google.com/build/pricing?hl=ja
マシンの変更は、cloudbuild.yamlに以下を追記するだけで適用されます。
options: machineType: "N1_HIGHCPU_8"
スペックアップによって、デプロイ時間は5分以下に短縮されました。
secret取得のためのimage pullにかかる時間も削減されていることから、ネットワーク上から取得したimageを展開する際にも、CPUの性能が影響していると考えられます。そして肝心のdocker build
にかかる時間は約4分短縮され、デプロイにかかる時間は約半分に短縮されました。これは非常に大きな変化です。
しかし、Cloud Buildのジョブがキューに入っている時間が20秒程度発生するようになってしまいました。このデメリットはデプロイ時間の短縮に比べれば小さなものであると判断し、このまま運用を続けることにしました。
そして料金面では、デフォルトマシンの2.5倍の料金が時間単位でかかるようになりますが、デプロイ時間が半減したことを考えると、1.25倍の料金を支払うことでデプロイ時間を半減できると考えられ、十分にコストパフォーマンスが高いと判断しました。
secretの取得方法の見直し
デプロイ時間が5分を切ったことで、secretの取得ステップがデプロイ時間に占める割合が大きくなってきました。そこで、secretの取得方法を見直すことにしました。
現在はgcloudコマンドを利用してsecretを取得していますが、gcloudコマンドはCloud Buildランナー上で実行されているため、gcloudコマンドを実行するためのイメージをpullする必要があります。
Cloud Buildのドキュメントを確認すると、cloudbuild.yamlにsecretを取得するオプションがあることがわかりました。これを利用することで、ランナー上でシークレットを直接利用することができるようになります。ただし、Cloud Buildで利用しているサービスアカウントには、シークレットを取得する権限(roles/secretmanager.secretAccessor
)が必要です。
# cloudbuild.yaml steps: - id: "Step: Build deal-api docker image" name: "gcr.io/cloud-builders/docker" env: - DOCKER_BUILDKIT=1 # dockerfileでmountを使うためm,buildkitを有効化 entrypoint: "bash" args: - "-c" - | docker build -t $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPOSITORY_NAME/$_IMAGE_NAME . --build-arg SECRET_1 --build-arg SECRET_2 secretEnv: - SECRET_1 - SECRET_2 # 省略 availableSecrets: secretManager: - versionName: projects/$PROJECT_ID/secrets/secret1/versions/latest env: "SECRET_1" - versionName: projects/$PROJECT_ID/secrets/secret2/versions/latest env: "SECRET_2"
これによってシークレットを取得するステップを削除でき、デプロイ時間をさらに短縮できました。
その結果、デプロイ時間は4分を切るようになりました。
【不採用】docker build
のキャッシュを利用する
Cloud Buildでは、kanikoを利用することで、docker build
のキャッシュを活用することができます。
このkanikoを利用すると、docker build
とdocker push
を行うことができます。docker build
の際には、kanikoがDockerfileを読み込み、各ステップの実行結果をキャッシュとして保存します。次回以降のdocker build
の際には、キャッシュを利用することで、前回のdocker build
の結果を再利用することができます。この機能を難しい設定なしで利用できるため、デプロイ時間の短縮に有効であると考えられます。
結果としては、kanikoのステップに3分以上かかってしまい、デプロイ時間が増加してしまいました。これは、kanikoが利用するキャッシュのダウンロード、展開、そしてキャッシュのpushに時間がかかっていると考えられます。
結果
今回の改善によって、デプロイ時間を短縮することができました。具体的には、
- マルチステージビルド
- Cloud Buildランナーのスペックアップ
- stepの見直し
によって、デプロイ時間を13分半から4分に短縮することができました。各改善の結果をまとめると、以下のようになります。
今回の改善では、最初の状態からデプロイ時間を70%削減でき、料金についても19%削減できました。
今回の改善を通して、Cloud Buildのデプロイ時間短縮のためには、まずはCloud Buildランナーのスペックを上げてもよいかなと個人的には思います。その後に、(導入していない場合は)マルチステージビルドやステップの見直しを行うことで、さらにデプロイ時間を短縮することができます。
まとめ
最後までお読みいただきありがとうございました。
今回は、Visitアプリケーションのデプロイ速度を改善するために行った取り組みについてお話ししました。
デプロイ速度の改善は、開発者の負担を軽減し、開発効率を向上させることができます。手法によっては、費用の削減にもつながるため、やって損はないのではないでしょうか。
最後に、バイセルでは一緒に働くエンジニアを募集しています、興味がある方は、以下よりご応募ください。 herp.careers
明日の バイセルテクノロジーズ Advent Calendar 2024 は、引き続き自分が執筆しました、「log/slogとcontextで妥協しないロギングを実現する」です。お楽しみに!