とことんDevOps | 日本仮想化技術のDevOps技術情報メディア

DevOpsに関連する技術情報を幅広く提供していきます。

日本仮想化技術がお届けする「とことんDevOps」では、DevOpsに関する技術情報や、日々のDevOps業務の中での検証結果、TipsなどDevOpsのお役立ち情報をお届けします。
主なテーマ: DevOps、CI/CD、コンテナ開発、IaCなど
読者登録と各種SNSのフォローもよろしくお願いいたします。

GitHub Actionsで並列処理

GitHub Actionsは同じトリガーが設定された別のワークフローファイル.github/workflows/{a,b}.yamlなどを並列で処理します。しかし、1つワークフローで並列処理したいこともありますよね。たとえば、Nodejsのバージョン18,19,20でアプリケーションをビルドしなくてはいけないときなど、直列で処理してしまうとワークフローが終了するまでに3回分のビルド時間を待っている必要があります。

GitHub Actionsにはマトリックスと呼ばれる複数の組み合わせを実行するための機能が備わっていて、これを利用することで並列処理を実現します。

マトリックス

docs.github.com

マトリックスってそもそもなんだ?っていうと、数学のマトリクス?行列演算などと同じようなものですかね。

jobs:
  example_matrix:
    strategy:
      matrix:
        version: [10, 12, 14]
        os: [ubuntu-latest, windows-latest]

このように書いておくとversionとosの全ての組み合わせを実行するジョブが生成されます。

os \ version 10 12 14
ubuntu-latest v10, ubuntu-latest v12, ubuntu-latest v14, ubuntu-latest
windows-latest v10, windows-latest v12, windows-latest v14, windows-latest

もちろん、matrix以下に1つの配列を設定することも可能です。

jobs:
  example_matrix:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]

このように書いておけば、ubuntu-latestとwindows-latestで実行する2つのジョブが生成されます。

もう少し具体的な例を出してみましょう。例えば、いくつかのディレクトリ以下で何らかのテストを実行するとします。

パッと思いつくところで簡単に実装をするなら以下です。

jobs:
  test:
    steps:
      - run: |
          for x in dirs/{a,b,c}; do
            (cd "$x" && make test)
          done

forでdirs以下のa, b, cディレクトリを回し、cdしながらテストを実行しています。処理は1つずつ直列に実行されるので、1つのジョブの実行時間はテスト3つ分かかります。

そこでマトリックスの出番です。マトリックスを使用することで、この3つのテストを並列に処理します。

jobs:
  test:
    strategy:
      matrix:
        dir: [a, b, c]
    steps:
      - run: make test
         working-directory: dirs/${{ matrix.dir }}

${{ matrix.dir }}は、それぞれa, b, cの文字列に展開され、その数分だけジョブが生成されます。

動的に分割

matrixの値を動的に決めたいこともあると思います。例えばモノレポで、変更のあったディレクトリ以下でのみテストを並列で実行したい、などです。

これはmatrixを生成するジョブと、それを読み込むジョブにわけることで実現できます。

jobs:
  updated:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.updated.outputs.matrix }}
    steps:
      - uses: actions/checkout@4
      - id: updated
        run: |
          a=$(git diff --name-only origin/${GITHUB_BASE_REF} HEAD -- . | sed -E 's#^(.*)/[^/]*$#"\1",#' | sort -u | tr -d '\n' | sed -E 's/,$//; s/^(.*)$/[\1]/' | jq -c '{"target-dir": .}')                                      
          echo -E "matrix=$a"                                                                                                                                                                                                      
          echo -E "matrix=$a" >> "$GITHUB_OUTPUT"    

  matrix:
    needs: updated
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target-dir: ${{ fromJSON(needs.updated.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v4
      - run: pwd
        working-directory: dirs/${{ matrix.target-dir }}

updatedジョブのrunの中身が少し複雑になっているので解説します。

  • git diff --name-only origin/${GITHUB_BASE_REF} HEAD -- .

    変更のあったファイル一覧を取得しています。${{ github_base_ref }}はワークフローを実行したときのブランチ名が入ってくるのでgit diff --name-only origin/<branch-name> HEADが実行されたのと同じです。origin/<branch-name>からHEADまでに更新のあったファイルを取得しています。

  • sed -E 's#^(.*)/[^/]*$#"\1",#'

    更新のあったファイル一覧からファイル名の部分を削除し、ダブルクォーテーションで囲いつつ、カンマ区切りで出力しています。

    a/file1
    b/file1
    b/file2
    c/file1
    

    "a",
    "b",
    "b",
    "c",
    

    このフォーマットに整形しています。

  • sort -u

    ディレクトリ名だけになった出力から重複しているディレクトリを削除します。git diffは更新のあったファイルが一覧で取得されるため、1つのディレクトリ内で複数のファイルを変更した場合、同じディレクトリが複数行出力されてしまいます。

  • tr -d '\n'

    改行区切りの値を"<var1>","<var2>",...,のフォーマットで出力します。

  • sed -E 's/,$//'; s/^(.*)$/[\1]/

    最終行の値も"<var_end>",なので改行を削除しただけでは行の最後にカンマが残ってしまいますのでそれを削除し、[]で囲って出力します。

  • jq -c '{"target-dir": .}'

    JSON配列を受け取ってtarget-dirオブジェクトに紐付けます。{"target-dir":["dirs/b","dirs/c"]}このような値を期待します。

  • xargs printf 'matrix=%s\n'

    全体の文字列の前にmatrix=を付与しています。matrix={"target-dir":["dirs/b","dirs/c"]}このような値を期待しています。

ここまでで整形された値はoutputsの${{ steps.updated.outputs.matrix }}で外部のジョブから読み込みできるようにアウトプットしています。

matrixジョブではneedsを使って依存関係を表し、matrixへ生成したJSON文字列を代入しています。

関係のないドキュメントだったのですが、動的にマトリックスを生成している部分があったので参考として載せておきます。

docs.github.com

デモ

まずは単純なマトリックスです。

github.com

Actionsの実行を見てみると、run_in_orderの方は1つのジョブとし実行されているのに対し、matrixの方は3のジョブが並んでいます。ステータスバッチは1つ目だけクリアしている状態で、2、3はまだ実行中ですので、並列で実行されているのがわかります。

matrixで3つのジョブが終了していますが、run_in_orderの方はまだ実行中です。実行時間も見てみてください。

実行ログをみるとrun_in_orderは1つずつ処理しているのに対し、matrixは1つの処理だけを実行しています。

お次は動的に実行します。

github.com

updatedというジョブが実行され、更新されたファイルからマトリックスを生成します。

生成されたマトリックスでジョブが分割され、並列実行されました。

まとめ

異なる複数の環境用にビルドしたいときなどにマトリックスは有用ですが、複数の処理があり、それが並列で実行されても問題ない場合にはマトリックスを使用することで処理時間の短縮を行うことができます。