CodeBuild のリモート Docker サーバの挙動と料金を見てみる

記事タイトルとURLをコピーする

こんにちは、末廣です。 「CodeBuild サンドボックスを使用したビルドのデバッグ」を試しながら「リモート Docker サーバ」の挙動を確認し、どういった開発をしている場合に使えそうか、料金面に着目して妄想してみました。 それではどうぞ。

関連アップデート

aws.amazon.com

この仕組みを使うと Docker コマンドの実行時(docker build 等)にリモートサーバーを使用するようビルド環境を設定し、複数のビルドにわたって永続的なキャッシュを維持できるようになります。 これにより、一度ビルドしたレイヤーはキャッシュとして保持され、次回以降のビルドが早く完了します。

もともと CodeBuild はローカルキャッシュや、S3 を用いたキャッシュを使うことができました。 しかし、ローカルキャッシュはホストが変わってしまうと使えなくなってしまったり、S3 を用いたキャッシュはS3からのダウンロードと復元が必要なため、キャッシュの量が多い場合に時間がかかってしまうこともありました。

blog.serverworks.co.jp

blog.serverworks.co.jp

そしてもう一つ、デバッグ作業に役立つアップデートです。

aws.amazon.com

これにより、ビルドのデバッグ作業のためのサンドボックス環境を作成し、Linux コマンド等を使ってファイルを修正しながらビルドエラーを調査することができます。

Docker サーバの設定

公式ドキュメントには AWS CLI でビルドプロジェクトを設定する方法がのっています。

docs.aws.amazon.com

公式ブログにマネジメントコンソールからの設定手順もあります。こちら参考に私もやってみました。

aws.amazon.com

ビルドプロジェクトの作成

github.com

AWS CodeBuild が公開している公式 Docker イメージのリポジトリをソースとして使います。(公式ブログ同様)

ソースリポジトリ

環境の追加設定の箇所に「Docker server configuration」があるのでチェックを入れ、メモリと CPU の程度を選択します。

環境

Docker サーバの設定ができるのは

  • プロビジョニングモデルがオンデマンド
  • コンピューティングが EC2
  • 実行モードがコンテナ
  • OS が Linux(Ubuntu)

あたりの条件があるようです。 例えばコンピューティングを Lambda にした時は Docker サーバのチェック項目がそもそも出てこないのですが、実行モードを「インスタンス」にして作成しようとすると以下のようなエラーが出てプロジェクト作成ができない点、注意が必要です。

Docker サーバ有効エラー

ビルドプロジェクトの変更(6/13時点の検証結果

プロジェクト作成後、プロジェクトの詳細タブからこのような感じでみれます。

作成後の様子

「環境を編集」からサイズの変更できる?と思いきや、

環境を編集

できませんでした。

変更できない

どうやら一度 Docker サーバを無効化してから再度有効化する必要があるようで、プロジェクトを作成したときと違うメモリ、CPU タイプのものに変更することもできませんでした。 同じコンピュートタイプであれば 有効 / 無効 を切り替えれるので実際に発生した料金の計算はしていないですが、しばらくキャッシュがいないときは無効にしてしまってもいいかもしれません。

また、ビルドプロジェクトを一度 Docker サーバを有効化せずに作成した後、環境を変更から有効化しようとするとエラーがでるため、アップデート前に作成した既存のプロジェクトに適用するのも難しそうです。

有効化エラー

エラーメッセージも奇妙なものが出てるので今後の修正に期待です。

デバッグビルド

サンドボックス環境を作成してデバッグビルドをして中身をみていきます。

デバッグビルド

マネジメントコンソールからそのままコマンドを打ち込む、SSH クライアントや IDE を使った接続、セッションマネージャーで接続する方法がありますが、今回はコマンドをそのまま打ち込んでいきます。

サンドボックス環境の起動

まず初めに適当なコマンドを実行すると、サンドボックス環境が起動します。

適当なコマンドを実行

1分くらいで起動終わりました。

サンドボックスのフェーズ

このサンドボックスのセッション自体はビルドプロジェクトのタイムアウト時間と同じになっているので、長い時間デバッグ作業する時は注意が必要です。

また、サンドボックスが起動中はプロジェクトの詳細タブ等を押しても見に行けません。デバッグセッションタブしか見れません;;

プロジェクトの詳細を見に行けない

コマンド叩いてみる

上記で適当に ls を叩いていたので結果です。 ソースにしたリポジトリのルートにあるディレクトリが見れます。

ls の結果

このように CloudWatch Logs を有効化にしていると、実行コマンド毎にログストリームが作られてログの保存確認も行うことができます。

ビルドする

実際にビルドしていきます。 ビルドプロジェクトの環境を amazonlinux-x86_64-standard:5.0 で作成したので、リポジトリのディレクトリal/x86_64/standard/5.0にある Dockerfile でビルドします。

ディレクトリ移動

docker build します。

docker build

ログがでました。"codebuild-docker-server" instance using remote driver でリモート Docker サーバを使っていることがわかります。

#0 building with "codebuild-docker-server" instance using remote driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 28.44kB done
#1 DONE 0.0s

#2 [internal] load metadata for public.ecr.aws/amazonlinux/amazonlinux:2023
#2 DONE 1.6s

#3 [internal] load .dockerignore
#3 transferring context: 2B done
⋮

ちなみに、Docker サーバを有効化せずにビルドすると、

#0 building with "default" instance using docker driver

と出力されます。

Docker サーバの力

なんとこのビルド、失敗しました。公式なのになぜ…

#22 [tools 15/17] RUN set -ex     && curl -L "https://download.mozilla.org/?product=firefox-latest&os=linux64" --output /tmp/FirefoxSetup.tar.bz2     && tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/     && ln -s /opt/firefox/firefox /usr/local/bin/firefox     && rm -rf /tmp/*     && firefox --version
#22 0.224 + curl -L 'https://download.mozilla.org/?product=firefox-latest&os=linux64' --output /tmp/FirefoxSetup.tar.bz2
#22 0.228   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
#22 0.230                                  Dload  Upload   Total   Spent    Left  Speed
#22 0.231 
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   136  100   136    0     0    336      0 --:--:-- --:--:-- --:--:--   335
#22 1.375 
 63 71.1M   63 45.0M    0     0  39.2M      0  0:00:01  0:00:01 --:--:-- 39.2M
100 71.1M  100 71.1M    0     0  45.9M      0  0:00:01  0:00:01 --:--:-- 65.2M
#22 1.779 + tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/
#22 1.786 bzip2: (stdin) is not a bzip2 file.
#22 1.786 tar: Child returned status 2
#22 1.786 tar: Error is not recoverable: exiting now
#22 ERROR: process "/bin/sh -c set -ex     && curl -L \"https://download.mozilla.org/?product=firefox-latest&os=linux64\" --output /tmp/FirefoxSetup.tar.bz2     && tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/     && ln -s /opt/firefox/firefox /usr/local/bin/firefox     && rm -rf /tmp/*     && firefox --version" did not complete successfully: exit code: 2
------
 > [tools 15/17] RUN set -ex     && curl -L "https://download.mozilla.org/?product=firefox-latest&os=linux64" --output /tmp/FirefoxSetup.tar.bz2     && tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/     && ln -s /opt/firefox/firefox /usr/local/bin/firefox     && rm -rf /tmp/*     && firefox --version:
0.224 + curl -L 'https://download.mozilla.org/?product=firefox-latest&os=linux64' --output /tmp/FirefoxSetup.tar.bz2

100   136  100   136    0     0    336      0 --:--:-- --:--:-- --:--:--   335

100 71.1M  100 71.1M    0     0  45.9M      0  0:00:01  0:00:01 --:--:-- 65.2M
1.779 + tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/
1.786 bzip2: (stdin) is not a bzip2 file.
1.786 tar: Child returned status 2
1.786 tar: Error is not recoverable: exiting now
------
Dockerfile:225
--------------------
 224 |     # Install Mozilla Firefox
 225 | >>> RUN set -ex \
 226 | >>>     && curl -L "https://download.mozilla.org/?product=firefox-latest&os=linux64" --output /tmp/FirefoxSetup.tar.bz2 \
 227 | >>>     && tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/ \
 228 | >>>     && ln -s /opt/firefox/firefox /usr/local/bin/firefox \
 229 | >>>     && rm -rf /tmp/* \
 230 | >>>     && firefox --version
 231 |     
--------------------
ERROR: failed to solve: process "/bin/sh -c set -ex     && curl -L \"https://download.mozilla.org/?product=firefox-latest&os=linux64\" --output /tmp/FirefoxSetup.tar.bz2     && tar xjf /tmp/FirefoxSetup.tar.bz2 -C /opt/     && ln -s /opt/firefox/firefox /usr/local/bin/firefox     && rm -rf /tmp/*     && firefox --version" did not complete successfully: exit code: 2

原因としては現在、Mozilla が提供する product=firefox-latest&os=linux64 のURLは、.tar.bz2 ファイルではなく、.tar.gz (gzip形式) のファイルをダウンロードするようになっており、bzip2形式 (j) で圧縮されたアーカイブファイルを解凍 (x) することができなくなっているからでした。

そして私はこのエラーを修正している最中にサンドボックスのタイムアウトが発生してしまいました。サンドボックスはデバッグセッション中に永続的なファイルシステムを維持してくれる、すなわち Docker のキャッシュも持ってくれますが、停止するとまたフェーズ21 までのビルド作業を待つ必要があります。

でもこれは Docker サーバを有効化していない時の話です。

再度サンドボックス環境を立ち上げて docker build すると、キャッシュされています!

#0 building with "codebuild-docker-server" instance using remote driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 28.44kB 0.0s done
#1 DONE 0.0s
#2 [internal] load metadata for public.ecr.aws/amazonlinux/amazonlinux:2023
#2 DONE 1.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [core 1/3] FROM public.ecr.aws/amazonlinux/amazonlinux:2023@sha256:4e1648e3917aea2ecc13ae65ec45a9479380c38a4c525e224aa2d1936f6c9693
#4 resolve public.ecr.aws/amazonlinux/amazonlinux:2023@sha256:4e1648e3917aea2ecc13ae65ec45a9479380c38a4c525e224aa2d1936f6c9693 0.0s done
#4 DONE 0.0s
#5 [tools 11/17] RUN yum -y install 'dnf-command(config-manager)'     && yum config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo     && yum -y install gh --repo gh-cli
#5 CACHED
#6 [tools 12/17] RUN set -ex     && curl -L https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm --output /tmp/google-chrome-stable_current_x86_64.rpm     && yum install -y -q /tmp/google-chrome-stable_current_x86_64.rpm     && rm -rf /tmp/*     && google-chrome --version
#6 CACHED
#7 [tools  1/17] RUN wget -nv https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip -O /tmp/samcli.zip     && unzip -q /tmp/samcli.zip -d /opt/samcli     && /opt/samcli/install --update -i /usr/local/sam-cli -b /usr/local/bin     && rm -rf /opt/samcli /tmp/*     && sam --version
#7 CACHED

今回に限ってはデバッグ作業中のセッション切れによる時間短縮が一度あったのみですが、これが本番ビルドでおこなわれるということです。

Docker サーバを使うと、ローカルキャッシュを使える状態が常にプロビジョニングされたインスタンスで最大 1 か月間残ってくれているという感じですね。

料金と用途

Docker サーバの料金

aws.amazon.com

Docker サーバは秒ごとに料金が発生し、クールダウン期間というものが設定されているようです。

1 秒あたりの永続キャッシュの保存時の料金

Docker イメージサーバーにはクールダウン期間があり、サーバーはアクティブなままで、イメージビルドリクエストの受信のために最大 1 時間待機します。クールダウン期間内にビルドリクエストが届かない場合、サーバーは停止し、保存時のキャッシュ料金が下がります。この機能を無効にするまで、引き続き料金が発生します。

Docker サーバーは秒ごとに料金が発生し、1時間ビルドリクエストが届かないと「クールダウン期間」に入り、料金が安くなります。 0が多すぎるので $1 = ¥140 で単位を整理します。

単位 料金(USD) 料金(日本円)
1秒あたり $0.000000037 ¥0.00000518
1時間あたり $0.0001332 ¥0.018648
1日(24時間)あたり $0.0031968 ¥0.447552
1ヶ月(30日)あたり $0.095904 ¥13.42656

クールダウン期間になっていれば、思ったより料金が発生しないですね。

料金表そのままですが、アクティブな状態での料金は以下です。

アーキテクチャ メモリ vCPU 料金 / 秒 (USD) 料金 / 時間 (USD) 料金 / 秒 (円) 料金 / 時間 (円)
ARM 4 GiB 2 $0.00004 $0.144 ¥0.0056 ¥20.16
x86 4 GiB 2 $0.00004 $0.144 ¥0.0056 ¥20.16
ARM 8 GiB 4 $0.00007 $0.252 ¥0.0098 ¥35.28
x86 8 GiB 4 $0.00008 $0.288 ¥0.0112 ¥40.32
ARM 16 GiB 8 $0.00015 $0.540 ¥0.0210 ¥75.60
x86 16 GiB 8 $0.00016 $0.576 ¥0.0224 ¥80.64
ARM 64 GiB 32 $0.00059 $2.124 ¥0.0826 ¥297.36
x86 64 GiB 32 $0.00063 $2.268 ¥0.0882 ¥317.52
ARM 128 GiB 64 $0.00118 $4.248 ¥0.1652 ¥594.72
x86 128 GiB 64 $0.00126 $4.536 ¥0.1764 ¥635.04

一番小さいコンピューティングタイプでもアクティブな時とクールダウン期間の時で 1000 倍程度料金の差があります。

比較して用途を考えてみる

では、どのような時に使えそうでしょうか?これまでのキャッシュ方法と比較します。

比較項目 Docker サーバー機能 (新機能) ローカルキャッシュ S3キャッシュ
キャッシュの場所 永続的なリモートDockerサーバー ビルドを実行するホストのローカルディスク ユーザーが指定したS3バケット
永続性・信頼性 非常に高い
(最大1ヶ月保持)
低い(ベストエフォート)
(ビルドホストが変わるとキャッシュが消える)
非常に高い
(S3に保存されている限り永続)
パフォーマンス 非常に高い
(キャッシュヒット率が非常に高く、ネットワーク転送もない)
非常に高い
(キャッシュヒット率が非常に高く、ネットワーク転送もない)
低い
(毎回S3からキャッシュをダウンロード/アップロード、展開するため、遅い)
料金 Docker サーバの追加料金がかかる かからない S3 への保存料金がかかる (少額)

料金面を考えなければ、ローカルキャッシュの「不安定さ」とS3 キャッシュの「遅さ」という課題を解決できる優れモノに見えます。 用途を考えると、複数人、または CI/CD で頻繁にビルドが実行されるようなプロジェクトで、ビルド時間をボトルネックにせず、キャッシュを利用した早いビルドを毎回確実に行いたい時とかでしょうか?

料金バランスを考える

Docker サーバを使う時と使わない時、ほとんど料金が変わらないような条件で料金を出してみました。

前提条件
  • 休日はビルドしない(Docker サーバは常にクールダウン状態)
  • 営業日は Docker サーバは 10時間稼働状態(general1.small: $0.005 / 分)
  • 1回のビルドに20分かかる
  • キャッシュが
  • 1日25回ビルドする
Docker サーバを使わない料金計算

キャッシュヒットが不安定なため、平均ビルド時間を17分とします

  • 月間合計ビルド時間:
    • 500回 × 17分/回 = 8,500分
  • 月額コンピューティング料金:
    • USD: 8,500分 × $0.005/分 = $42.50
    • 日本円: $42.50 × 140円/ドル = ¥5,950
  • 合計月額料金:
    • 合計: 約 $42.50 / 5,950円
Docker サーバを使う料金計算

平均ビルド時間を3分に短縮できると仮定します。

コンピューティング料金の計算
  • 月間合計ビルド時間:

    • 500回 × 3分/回 = 1,500分
  • 月額コンピューティング料金:

    • USD: 1,500分 × $0.005/分 = $7.50
    • 日本円: $7.50 × 140円/ドル = ¥1,050
Dockerサーバー機能の料金計算
  • 料金単価:
    • 稼働時間: $0.00004 / 秒
    • クールダウン時間: $0.000000037 / 秒
  • 平日1日あたりの料金 (稼働10時間 + クールダウン14時間):
    • 稼働料金: $0.00004 × 3,600秒 × 10時間 = $1.44
    • クールダウン料金: $0.000000037 × 3,600秒 × 14時間 = $0.0018648
    • 小計: $1.4418648 / 日
  • 休日1日あたりの料金 (クールダウン24時間):
    • クールダウン料金: $0.000000037 × 3,600秒 × 24時間 = $0.0031968 / 日
  • 月額のDockerサーバー料金:
    • USD: ($1.4418648 × 20日) + ($0.0031968 × 10日) = $28.837... + $0.0319... ≈ $28.87
    • 日本円: $28.87 × 140円/ドル ≈ ¥4,042
合計月額料金
  • 合計 (USD): $7.50 (コンピューティング) + $28.87 (サーバー) = $36.37
  • 合計 (日本円): ¥1,050 (コンピューティング) + ¥4,042 (サーバー) = ¥5,092
  • 合計: 約 $36.37 / 5,092円
AI が物語を作りました(参考)

設定: 開発者Aさん、金曜日の朝。新機能「ユーザー推薦機能」の開発を担当。Dockerイメージのビルドには通常20分かかる。

シナリオ1:Dockerサーバーを「使わない」世界(ローカルキャッシュのみ) 午前9:30 Aさんは作業を開始し、最初のコミットをプッシュします。CodeBuildが起動しますが、新しいビルドホストが割り当てられ、キャッシュは効きません。

ビルド時間:20分 Aさんの思考: 「さて、ビルドが終わるまでコーヒーを淹れて、別のタスクの資料でも読んでおくか…」 待ち時間が発生し、集中が一度途切れます。

午前11:00 機能に修正を加え、再度プッシュ。運悪く、また別のホストに割り当てられてしまいました。

ビルド時間:20分 Aさんの思考: 「え、また20分か。午前のうちにプルリクエストを出したかったのに…」 ビルドの「ガチャ」に外れ、計画に遅れが生じ始めます。

午後4:00 コードレビューの指摘を修正し、本日3回目のプッシュ。今度は運良く同じホストにヒットしました。

ビルド時間:3分 Aさんの思考: 「お、速い!ラッキー。よし、すぐ確認してマージしよう」 偶然の幸運に安堵するも、この不安定さが日々のストレスになっています。

【この世界の1ヶ月後】 チームはビルドの遅延に振り回され、開発者の貴重な時間が待ち時間に消えていきました。この不安定な開発体験にかかっているCodeBuildの合計コストは、月額 約5,950円 でした。

シナリオ2:Dockerサーバーを「使う」世界 午前9:30 Aさんは作業を開始し、最初のコミットをプッシュ。これはチームでその日最初のビルドなので、Dockerサーバーのキャッシュはまだウォームアップされておらず、時間がかかります。

ビルド時間:20分 Aさんの思考: 「よし、朝イチのビルドはこんなもんだな。これでチーム全員のキャッシュが温まったはず」 1日の最初の「投資」と割り切り、 predictable な待ち時間として受け入れています。

午前11:00 機能に修正を加え、再度プッシュ。Dockerサーバーの永続キャッシュが即座に応答します。

ビルド時間:3分 Aさんの思考: 「よし、すぐ終わった。これなら午前中にレビューまで出せるな」 確実な高速フィードバックにより、開発がスムーズに進みます。

午後4:00 コードレビューの指摘を修正し、プッシュ。当然のようにキャッシュが効きます。

ビルド時間:3分 Aさんの思考: 「修正完了。CIもすぐ通ったし、マージ依頼しよう」 ビルドの待ち時間を意識することなく、コーディング作業そのものに集中できています。

【この世界の1ヶ月後】 チームはビルドの待ち時間というストレスから解放され、開発サイクルは劇的に高速化しました。この快適な開発体験を実現しているCodeBuildの合計コストは、月額 約5,100円。ローカルキャッシュのみの場合より安く、かつ圧倒的に快適で生産性の高い環境が手に入っています。

結論

具体的な料金計算から、Docker サーバーを使って料金の上がり幅が大きくなってしまう条件も見えてきました。 それは、「Dockerレイヤーのキャッシュがあまり効かない」状況で、かつ「毎日ビルドが実行され、サーバーが常にアクティブ状態になる」場合です。この場合、ビルド時間の短縮メリットが得られないままサーバーのアクティブ料金がかさむため、導入によるコストの上がり幅が最も大きくなると考えられます。このようなケースでは、導入は慎重に検討した方が良いでしょう。

逆に、私が特に効率が良いと感じたのは、「開発を行う日は集中的にビルドする一方、全くビルドしない日もある」というメリハリのある開発スタイルの場合です。 ビルドしない日は安価なクールダウン料金でコストを抑えつつ、ビルドする日には永続的なキャッシュの恩恵を最大限に受けることができるため、費用対効果の高い使い方ができるのではないかと考えました。

おまけ:GitHub Actions (メモ程度)

docs.docker.com

Github Actions にはそもそも無料枠があるため、そこを超過しない限りは料金が発生しない上に、キャッシュについても無料枠で使うこともできるため、料金を安くしたい場合はそもそも Codebuild を選ぶことが少ないと思います。

無料枠として、Registry cache は 500MB、 GitHub cache は7日間で自動削除などと制限があるようで、実行時間自体も2,000分/月 となっています。

GitHub Action ランナーによって Codebuild を動かすこともできるため、今回の Docker サーバも使うことができますが、すでに無料枠内で使えているようなビルドであればそのままで十分でしょう。

aws.amazon.com

まとめ

本ブログでは「リモート Docker サーバ」の設定方法、挙動を確認し、料金やメリットについて考えてみました。 実際に利用するかどうかは開発フローやキャッシュの使い方、速度の問題と料金を天秤にかけ、検討は必要そうです。 しかし、アクティブなプロジェクトにおいては「簡単に」「思ったより高くなく」「ストレスフリーな Docker ビルド環境」が手に入りそう、というイメージでまとめさせていただこうと思います。

末廣 満希(執筆記事の一覧)

2022年新卒入社です。ここに何かかっこいい一言を書くことができるエンジニアになれるように頑張ります。