タイトルの通り、CodeBuild がテスト分割と並列処理をサポートしたというアップデート情報がありました!
最近Codeシリーズのアップデートが多くて嬉しいです!

本記事では

  • CodeBuildバッチビルドの説明
  • テスト分割と並列処理の説明
  • テスト分割と並列処理のデモ

を記載いたします!

アップデート内容

https://aws.amazon.com/jp/about-aws/whats-new/2025/01/aws-codebuild-test-splitting-parallelism/

以下、日本語訳です。

AWS CodeBuild がテスト分割と並列処理をサポートするようになりました
テストを分割して、複数の並列実行コンピューティング環境で実行できるようになりました。シャーディング戦略に基づいて、CodeBuild はテストを分割し、指定された数の並列環境で実行します。AWS CodeBuild は、ソースコードをコンパイルし、テストを実行し、すぐにデプロイできるソフトウェアパッケージを作成する、フルマネージドの継続的インテグレーションサービスです。

プロジェクト内のテストの数が増えると、単一のコンピューティングリソースを使用する場合の合計テスト時間も長くなります。複数のコンピューティングリソースでテストを並列実行すると、CI/CD パイプラインの全体的なテスト期間が短縮されます。これにより、フィードバックサイクルが高速化し、開発者の生産性が向上します。

並列テスト機能は、CodeBuild が提供されているすべてのリージョンで利用できます。CodeBuild が利用できる AWS リージョンの詳細については、AWS リージョンのページを参照してください。CodeBuildのテスト分割の詳細については、ドキュメントをご覧ください。CodeBuild の使用開始方法の詳細については、AWS CodeBuild 製品ページをご覧ください。

複数のテストケースを分割して、並列実行することによりテスト実行の所要時間を短縮できるとのことです。

バッチビルドとは

テストの分割と並列処理はCodeBuildのバッチビルド機能に含まれます。
そのため、まずバッチビルド機能について軽く紹介させていただきます。
(ご存知の方は読み飛ばしてください)

CodeBuild のバッチビルドは、複数のビルドを同時に実行する機能です。
多数のテストや、異なる設定でのビルドを効率的に実行したい場合に役立ちます。

バッチビルドには3種類のタイプがあります。

 タイプ   説明 
ビルドグラフ 複数のビルドを依存関係に従って実行
ビルドリスト 複数のビルドを並列に実行
ビルドマトリックス 複数のビルドをパラメーターの組み合わせを網羅して並列に実行

ビルドグラフ

バッチビルド内の各バッチに依存関係を持たせて、順番に実行します。

version: 0.2

batch:
  fast-fail: true
  build-graph:
    - identifier: build1
      env:
        variables:
          BUILD_ID: build1
    - identifier: build2
      env:
        variables:
          BUILD_ID: build2
      depend-on:
        - build1
    - identifier: build3
      env:
        variables:
          BUILD_ID: build3
      depend-on:
        - build2

phases:
  build:
    commands:
       - echo $BUILD_ID start
       - echo `date`
       - echo $BUILD_ID end

上記のビルドコマンドとしてビルドプロジェクトを作成して、実行するとビルドが3つ実行されます。
(実際には4つビルドが実行されますが、そのうちの1つはDOWNLOAD_SOURCEであり、上記のbuildspecでは実質的に何も実行されません。)

残り3つのビルドはビルドコマンドで定義しているbuild1/build2/build3です。

それぞれの実行ログの一部を抜粋します。

[Container] 2025/01/21 13:12:21.105921 Running command echo $BUILD_ID start
build1 start

[Container] 2025/01/21 13:12:21.113703 Running command echo `date`
Tue Jan 21 13:12:21 UTC 2025

[Container] 2025/01/21 13:12:21.124656 Running command echo $BUILD_ID end
build1 end
[Container] 2025/01/21 13:13:26.291785 Running command echo $BUILD_ID start
build2 start

[Container] 2025/01/21 13:13:26.297473 Running command echo `date`
Tue Jan 21 13:13:26 UTC 2025

[Container] 2025/01/21 13:13:26.305316 Running command echo $BUILD_ID end
build2 end
[Container] 2025/01/21 13:14:32.606030 Running command echo $BUILD_ID start
build3 start

[Container] 2025/01/21 13:14:32.613646 Running command echo `date`
Tue Jan 21 13:14:32 UTC 2025

[Container] 2025/01/21 13:14:32.623122 Running command echo $BUILD_ID end
build3 end

dateの結果を見ると、build1->build2->build3で順番に実行されていることがわかります。

ビルドリスト

バッチビルド内の各バッチを並列に同時実行します。

version: 0.2

batch:
  fast-fail: true
  build-list:
    - identifier: build1
      env:
        variables:
          BUILD_ID: build1
      ignore-failure: false
    - identifier: build2
      env:
        variables:
          BUILD_ID: build2
      ignore-failure: true

phases:
  build:
    commands:
       - echo $BUILD_ID start
       - echo `date`
       - echo $BUILD_ID end

上記のビルドコマンドとしてビルドプロジェクトを作成して、実行するとビルドが2つ実行されます。
(ビルドグラフと同じく、最初の1つはDOWNLOAD_SOURCEなので何も実行されません)

それぞれの実行ログの一部を抜粋します。

[Container] 2025/01/21 13:32:26.963560 Running command echo $BUILD_ID start
build1 start

[Container] 2025/01/21 13:32:26.969291 Running command echo `date`
Tue Jan 21 13:32:26 UTC 2025

[Container] 2025/01/21 13:32:26.977413 Running command echo $BUILD_ID end
build1 end
[Container] 2025/01/21 13:32:29.360477 Running command echo $BUILD_ID start
build2 start

[Container] 2025/01/21 13:32:29.367748 Running command echo `date`
Tue Jan 21 13:32:29 UTC 2025

[Container] 2025/01/21 13:32:29.377018 Running command echo $BUILD_ID end
build2 end

dateの結果を見ると、build1とbuild2がほぼ同時に実行されていることがわかります。

ビルドマトリックス

バッチビルド内のdynamicのパラメータの組み合わせを網羅する形で並列実行します。

version: 0.2

batch:
  build-matrix:
    static:
      ignore-failure: true
    dynamic:
      env:
        variables:
          BUILD_ID:
            - build1
            - build2
          MATRIX_ID:
            - matrix1
            - matrix2

phases:
  build:
    commands:
       - echo $BUILD_ID $MATRIX_ID start
       - echo `date`
       - echo $BUILD_ID $MATRIX_ID end

上記の例ではパラメーターとして環境変数が2つ(BUILD_IDとMATRIX_ID)あり、各パラメータは2つの値を持ちます。
パラメーターと値で4つの組み合わせがあるため、ビルドが4つ並行して実行されます。
(BUILD_ID:1/MATRIX_ID:1BUILD_ID:1/MATRIX_ID:2BUILD_ID:2/MATRIX_ID:1BUILD_ID:2/MATRIX_ID:2)

4つビルドが実行されていることがわかります。

それぞれの実行ログを見てみると

[Container] 2025/01/21 13:43:09.578578 Running command echo $BUILD_ID $MATRIX_ID start
build1 matrix1 start

[Container] 2025/01/21 13:43:09.586308 Running command echo `date`
Tue Jan 21 13:43:09 UTC 2025

[Container] 2025/01/21 13:43:09.595499 Running command echo $BUILD_ID $MATRIX_ID end
build1 matrix1 end
[Container] 2025/01/21 13:43:11.176941 Running command echo $BUILD_ID $MATRIX_ID start
build1 matrix2 start

[Container] 2025/01/21 13:43:11.184877 Running command echo `date`
Tue Jan 21 13:43:11 UTC 2025

[Container] 2025/01/21 13:43:11.194257 Running command echo $BUILD_ID $MATRIX_ID end
build1 matrix2 end
[Container] 2025/01/21 13:43:10.689271 Running command echo $BUILD_ID $MATRIX_ID start
build2 matrix1 start

[Container] 2025/01/21 13:43:10.696791 Running command echo `date`
Tue Jan 21 13:43:10 UTC 2025

[Container] 2025/01/21 13:43:10.706041 Running command echo $BUILD_ID $MATRIX_ID end
build2 matrix1 end
[Container] 2025/01/21 13:43:10.689406 Running command echo $BUILD_ID $MATRIX_ID start
build2 matrix2 start

[Container] 2025/01/21 13:43:10.697038 Running command echo `date`
Tue Jan 21 13:43:10 UTC 2025

[Container] 2025/01/21 13:43:10.706504 Running command echo $BUILD_ID $MATRIX_ID end
build2 matrix2 end

dateの結果を見ると、4つともほぼ同時に実行されていることがわかります。

テストの分割と並列処理について

公式ドキュメントを見て、ポイントをまとめます。

https://docs.aws.amazon.com/codebuild/latest/userguide/parallel-test.html
https://docs.aws.amazon.com/codebuild/latest/userguide/parallel-test-splitting.html
https://docs.aws.amazon.com/codebuild/latest/userguide/parallel-test-enable.html
https://docs.aws.amazon.com/codebuild/latest/userguide/sample-parallel-test.html

  • テストの並列実行は、バッチビルドのbuild-fanoutタイプを利用する
  • buildspecでcodebuild-tests-runを呼び出すことで、テストを分割して実行できる
  • テストはファイル単位で分割され、この分割方法をシャーディング戦略と呼ぶ
  • シャーディング戦略には、equal-distribution と stability の2種類がある
  • parallelismというパラメーターで並列度(並列実行できる数)を指定することができる

以下はbuildspecのサンプルです。

batch:
  fast-fail: false 
  build-fanout:
    parallelism: 5
    ignore-failure: false

phases:
  install:
    commands:
      - npm install jest-junit --save-dev
  pre_build:
    commands:
      - echo 'prebuild'
  build:
    commands:
      - |
        codebuild-tests-run \
         --test-command 'npx jest --runInBand --coverage' \
         --files-search "codebuild-glob-search '**/_tests_/**/*.test.js'" \
         --sharding-strategy 'equal-distribution'

  post_build:
    commands:
      - codebuild-glob-search '**/*.xml'  
      - echo "Running post-build steps..."
      - echo "Build completed on `date`"

reports:
  test-reports:
    files:
      - '**/junit.xml'               
    base-directory: .
    discard-paths: yes           
    file-format: JUNITXML

分割と並列処理に関連するのは

batchセクションとphases.buildセクションのcodebuild-tests-runの箇所のようです。

以下テストフレームワークに対応しているようです。(これ以外にも対応しているかもしれません)

  • Django
  • Elixir
  • Go
  • Java (Maven)
  • Javascript (Jest)
  • Kotlin
  • PHPUnit
  • Pytest
  • Ruby (Cucumber)
  • Ruby (RSpec)

実際に試してみる

実際にテストの分割と並列処理をpytestで試してみます。
CodeCommitレポジトリにサンプルプログラムとテストおよびbuildspecを保存して、CodeBuildプロジェクトのソースとして動作を見ていきます。

下準備

まず下準備として、サンプルプログラムを作ります。(calculator.py)

def add(a: int, b: int) -> int:
    return a + b

def subtract(a: int, b: int) -> int:
    return a - b

def multiply(a: int, b: int) -> int:
    return a * b

def divide(a: int, b: int) -> float:
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

そしてテストを作成します。(tests/test_calculator_1.py)

import pytest
import time
from calculator import add, subtract, multiply, divide

def test_add():
    time.sleep(5)
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

def test_subtract():
    time.sleep(5)
    assert subtract(5, 3) == 2
    assert subtract(1, 1) == 0
    assert subtract(0, 5) == -5

def test_multiply():
    time.sleep(5)
    assert multiply(2, 3) == 6
    assert multiply(-2, 3) == -6
    assert multiply(0, 5) == 0

def test_divide():
    time.sleep(5)
    assert divide(6, 2) == 3.0
    assert divide(5, 2) == 2.5
    assert divide(-6, 2) == -3.0

ここではsleepを入れて、意図的にテストの実行時間を伸ばしています。
(テストの分割と並列処理の結果をわかりやすくするため)

さらにこれをコピーしてもう一つテストファイルを作成します。(tests/test_calculator_2.py)
テストの分割と並列処理ではファイル単位でテストを分割して並列実行します。そのため今回はテストファイルを2つ作成しておきます。

そしてテストの分割と並列処理を利用していないbuildspec.ymlを作成します。(これは動作確認用です)

version: 0.2

phases:
  install:
    commands:
      - echo 'Installing Python dependencies'
      - python --version
      - pip3 install --upgrade pip
      - pip3 install pytest
  build:
    commands:
      - echo 'Running Python Tests'
      - python -m pytest -v
  post_build:
    commands:
      - echo "Test execution completed"

これらを新規作成したCodeCommitレポジトリにPushして、それをソースとしてCodeBuildプロジェクトを作成します。
そしてそのプロジェクトを実行したところ、

[Container] 2025/01/22 01:38:39.335268 Running command python -m pytest -v
============================= test session starts ==============================
platform linux -- Python 3.11.9, pytest-8.3.4, pluggy-1.5.0 -- /root/.pyenv/versions/3.11.9/bin/python
cachedir: .pytest_cache
rootdir: /codebuild/output/src1620799403/src/git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/test-build-fanout
collecting ... collected 8 items

tests/test_calculator_1.py::test_add PASSED                              [ 12%]
tests/test_calculator_1.py::test_subtract PASSED                         [ 25%]
tests/test_calculator_1.py::test_multiply PASSED                         [ 37%]
tests/test_calculator_1.py::test_divide PASSED                           [ 50%]
tests/test_calculator_2.py::test_add PASSED                              [ 62%]
tests/test_calculator_2.py::test_subtract PASSED                         [ 75%]
tests/test_calculator_2.py::test_multiply PASSED                         [ 87%]
tests/test_calculator_2.py::test_divide PASSED                           [100%]

============================== 8 passed in 24.03s ==============================

[Container] 2025/01/22 01:39:03.952923 Phase complete: BUILD State: SUCCEEDED

24秒ほどテストの実行にかかっていることがわかります。
1つのテストファイルあたり、sleepで3秒待機しているテストケースが4つあるので、2ファイルで計8テストケースが直列で実行されていることがわかります。

テストの分割と並列処理を試す

buildspec-fanout.ymlという名前でbuildspecを新しく作成します。

version: 0.2

batch:
  fast-fail: false
  build-fanout:
    parallelism: 2
    ignore-failure: false

phases:
  install:
    commands:
      - echo 'Installing Python dependencies'
      - python --version
      - pip3 install --upgrade pip
      - pip3 install pytest
  build:
    commands:
      - echo 'Running Python Tests (fanout)'
      - |
        codebuild-tests-run \
         --test-command 'python -m pytest -v' \
         --files-search "codebuild-glob-search 'tests/test_*.py'" \
         --sharding-strategy 'equal-distribution'
  post_build:
    commands:
      - echo "Test execution completed"

上ではバッチビルドの記述をしています。
build-fanoutを利用することでテストの分割と並列処理について定義できます。
parallelismは並列できる数であり、今回はテストケースを2つ並列で実行させることにします。

batch:
  fast-fail: false
  build-fanout:
    parallelism: 2
    ignore-failure: false

codebuild-tests-runを利用することでテストの分割と並列処理を実行できます。
テストは以下いずれかのシャーディング戦略に基づいて分割されます。

  • equal-distribution : テストファイル名のアルファベット順に基づいて、並列ビルド間でテストを分割する
  • stability : 一貫性のあるハッシュアルゴリズムを使用してシャード間でテストを分割し、ファイル配分が安定していることを保証する

今回はequal-distributionを利用します。

  - |
    codebuild-tests-run \
     --test-command 'python -m pytest -v' \
     --files-search "codebuild-glob-search 'tests/test_*.py'" \
     --sharding-strategy 'equal-distribution'

そしてCodeBuildプロジェクトを編集します。
バッチビルドを有効にして、Buildspecをbuildspec.ymlからbuildspec-fanout.ymlに変更します。

実行結果の確認

バッチビルドにて2つのシャーディングされたビルド実行がありました。

1つ目の結果を見てみると

[Container] 2025/01/22 01:14:41.632389 Running command codebuild-tests-run \
 --test-command 'python -m pytest -v' \
 --files-search "codebuild-glob-search 'tests/test_*.py'" \
 --sharding-strategy 'equal-distribution'

2025/01/22 01:14:41 Executing tests-run command with following inputs: test-command: 'python -m pytest -v', files-search: 'codebuild-glob-search 'tests/test_*.py'', sharding-strategy: 'equal-distribution'
2025/01/22 01:14:41 The following files are assigned for test execution in this shard: '[tests/test_calculator_1.py]'
2025/01/22 01:14:41 Executing command 'python -m pytest -v tests/test_calculator_1.py'
============================= test session starts ==============================
platform linux -- Python 3.11.9, pytest-8.3.4, pluggy-1.5.0 -- /root/.pyenv/versions/3.11.9/bin/python
cachedir: .pytest_cache
rootdir: /codebuild/output/src2131667299/src/git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/test-build-fanout
collecting ... collected 4 items

tests/test_calculator_1.py::test_add PASSED                              [ 25%!](MISSING)
tests/test_calculator_1.py::test_subtract PASSED                         [ 50%!](MISSING)
tests/test_calculator_1.py::test_multiply PASSED                         [ 75%!](MISSING)
tests/test_calculator_1.py::test_divide PASSED                           [100%!](MISSING)

============================== 4 passed in 12.02s ==============================
2025/01/22 01:14:54 Execution completed for command 'python -m pytest -v tests/test_calculator_1.py'

[Container] 2025/01/22 01:14:54.124531 Phase complete: BUILD State: SUCCEEDED

test_calculator_1だけが実行されていることがわかります。
(各テストケースにMISSINGと付いていますが、テストは正常にできていました。MISSINGがつく理由についてはまた調べようと思います。)

2つ目の結果も同様に、test_calculator_2がtest_calculator_1とほぼ同時に実行されていました。

これらの結果から、テストの分割と並列処理ができていることがわかります。

最後に

テストの分割と並列処理を利用すると、簡単にテストの所要時間を短縮できそうです。
CI/CDパイプラインにてテストを実行する場合はぜひ採用したい機能だと思います。
もう少し検証してみて、利用できそうであれば実際にパイプラインに適用してみようと思います。

この投稿が参考になれば幸いです。ご拝読ありがとうございます。