GitHub Actions ワークフローのステップが並行実行可能になりました
Back to Topはじめに
#先日、GitHub Actions ワークフローで background 機能が実装され、単一のワークフロー内でステップを並行で実行可能になったことが発表されました。
これまでも、Strategy Matrix を使うと複数のランナーを使って並行処理させることはできましたが、今回の機能で、単一のランナーでの並行処理がサポートされたことになります。
公式ドキュメントには、バックエンドとフロントエンドのビルドを並行で実行するサンプルが掲載されています。
steps:
- name: Build frontend
id: build-frontend
run: npm run build:frontend
background: true
- name: Build backend
id: build-backend
run: npm run build:backend
background: true
- name: Run linter while builds run
run: npm run lint
- name: Wait for both builds to finish
wait: [build-frontend, build-backend]
- name: Run tests
run: npm test
並行実行を試す (background 版)
#ステップ毎に background: true を指定する方法です。この属性を付与したステップは、実行開始後即座にフォアグラウンドに処理を戻します。
name: Background Hello World
on:
workflow_dispatch:
jobs:
hello-background:
runs-on: ubuntu-latest
steps:
- name: Background hello 1 #1
id: hello1
run: |
echo "hello1 start: $(date -u +%H:%M:%S)"
sleep 4
echo "hello1 end: $(date -u +%H:%M:%S)"
background: true
- name: Background hello 2 #2
id: hello2
run: |
echo "hello2 start: $(date -u +%H:%M:%S)"
sleep 3
echo "hello2 end: $(date -u +%H:%M:%S)"
background: true
- name: Foreground step (runs while background steps are active) #3
run: |
echo "foreground start: $(date -u +%H:%M:%S)"
sleep 1
echo "foreground end: $(date -u +%H:%M:%S)"
- name: Wait for background steps #4
wait: [hello1, hello2]
- name: Done
run: echo "Both background steps have completed."
- バックグラウンド実行するステップ。4秒間のスリープの開始と終了で時刻を表示します。
background: trueを指定します。 - バックグラウンド実行するステップ2個目。1個目と同様の処理です。スリープは3秒にしてます。
- バックグラウンド実行しながら、フォアグラウンドで実行されるステップ。1秒間スリープします。
- バックグラウンドの2つのステップを待ち受けるステップです。
waitでステップの ID を配列で指定するだけです。
実行結果です。
- hello1 と hello2 が同一時刻に開始され、1秒違いで終了しています。
- フォアグラウンドステップも2つのバックグラウンドステップと同時刻に開始されています。
- wait ステップで2つのバックグラウンドステップの完了を待っています。
上記のサンプルでは単純な sleep と echo をバックグラウンドで実行し、wait で待機しているだけですが、実際の CI/CD パイプラインでは「バックグラウンドで起動したサーバーやプロセスの結果を後から使いたい」というユースケースがよくあります。このようなケースでは、出力は一時ファイルに書き出しておき、wait後のステップでそれを読み込むといった工夫が必要になります。
並行実行を試す (parallel 版)
#parallel キーワード配下にステップを並べるだけで並列化できます。parallel ブロックを抜けると完了するため、wait で待つ必要はありません。
name: Parallel Hello World
on:
workflow_dispatch:
jobs:
hello-parallel:
runs-on: ubuntu-latest
steps:
- parallel: #1
- name: Parallel hello 1
run: |
echo "parallel-1 start: $(date -u +%H:%M:%S)"
sleep 4
echo "parallel-1 end: $(date -u +%H:%M:%S)"
- name: Parallel hello 2
run: |
echo "parallel-2 start: $(date -u +%H:%M:%S)"
sleep 3
echo "parallel-2 end: $(date -u +%H:%M:%S)"
- name: Parallel hello 3
run: |
echo "parallel-3 start: $(date -u +%H:%M:%S)"
sleep 2
echo "parallel-3 end: $(date -u +%H:%M:%S)"
- name: Done after all parallel steps #2
run: |
echo "done step start: $(date -u +%H:%M:%S)"
echo "All parallel steps have completed."
echo "done step end: $(date -u +%H:%M:%S)"
- parallel 配下に3つのステップを配置します。sleep は4秒、3秒、2秒とバリエーションを持たせています。
- 通常のステップです。parallel ステップ完了後に実行されます。
実行結果です。
- hello1, hello2, hello3 が同時刻に開始されています。それぞれ指定通り、4秒、3秒、2秒実行にかかっています。
- 最後のステップは、parallel ステップ完了後の時刻から開始されていることがわかります。
クロスコンパイルで使ってみる
#応用として、すぐに思いつくのは、クロスコンパイルで複数プラットフォーム向けのバイナリ生成を並行で実行することです。例えば、Go 言語では Linux / macOS / Windows の向けのバイナリをクロスコンパイルできます。
- name: Build
run: |
GOOS=linux GOARCH=amd64 go build -o build/linux-amd64/sb2md main.go
GOOS=linux GOARCH=arm64 go build -o build/linux-arm64/sb2md main.go
GOOS=windows GOARCH=amd64 go build -o build/windows/sb2md.exe main.go
GOOS=darwin GOARCH=amd64 go build -o build/macos/sb2md main.go
GOOS=darwin GOARCH=arm64 go build -o build/macos_arm/sb2md main.go
並列化する以前のビルド結果です。5つのバイナリを生成するのに44秒かかっています。
並列化を適用しました。run で複数行書いてましたが、個別のステップに分けて、parallel 配下に置きました。
- parallel:
- name: Build linux amd64
run: GOOS=linux GOARCH=amd64 go build -o build/linux-amd64/sb2md main.go
- name: Build linux arm64
run: GOOS=linux GOARCH=arm64 go build -o build/linux-arm64/sb2md main.go
- name: Build windows amd64
run: GOOS=windows GOARCH=amd64 go build -o build/windows/sb2md.exe main.go
- name: Build darwin amd64
run: GOOS=darwin GOARCH=amd64 go build -o build/macos/sb2md main.go
- name: Build darwin arm64
run: GOOS=darwin GOARCH=arm64 go build -o build/macos_arm/sb2md main.go
トータルは40秒でした、Linux amd64 のビルドは3秒で終わってますが、他のプラットフォーム用のビルドはそれぞれで40秒かかっています。思ったより短縮されませんでした。考えられる原因としては、やはりランナーの CPU コア数でしょうか。
- ランナー(ubuntu-latest)の vCPU が2コアであることから多重度が上がらなかった
- 5つのプロセスが2つのコアを奪い合ってコンテキストスイッチが大きかった
- ランナーのアーキテクチャ自体が Linux amd64 なので、ネイティブのバイナリ生成は瞬時に終わった
CPU コアの多い Larger Runner にすればもっと短縮できそうですが、40秒が3秒程度に短縮されるだけなら、コストパフォーマンスはイマイチですね。今回のユースケースには合わない感じがします。
さいごに
#以上、GitHub Actions ワークフローでの並行ステップ実行を試してみました。
今回試したGoのクロスコンパイルのように、CPUヘビーで互いに独立したタスクであれば、これまで通りStrategy Matrixを使って別々のランナーを立ち上げた方が高速に処理できる可能性が高いです(課金は高くなりますが)。
一方で、今回の並行ステップ実行は公式ドキュメントのサンプルにあるような「バックエンドとフロントエンドのビルド」や「テスト実行と並行しての Lint 実行」など、単一ランナーの空きリソース(I/O待ちの時間など)を効率よく活用したい場面で輝く機能だと言えるのではないかと思います。




