|
| 1 | +# Load Balancing |
| 2 | + |
| 3 | +Orchestrator supports multiple providers, and you can use standard GitHub Actions scripting to route |
| 4 | +builds across them. This lets you distribute builds based on platform, project size, runner |
| 5 | +availability, or any custom logic. |
| 6 | + |
| 7 | +``` |
| 8 | + ┌──────────────────────┐ |
| 9 | + │ Incoming Build │ |
| 10 | + │ Request │ |
| 11 | + └──────────┬─────────────┘ |
| 12 | + │ |
| 13 | + ┌──────────▼─────────────┐ |
| 14 | + │ Router Logic │ |
| 15 | + │ (GitHub Actions / │ |
| 16 | + │ script / matrix) │ |
| 17 | + └──┬───────┬──────────┬───┘ |
| 18 | + │ │ │ |
| 19 | + ┌──────────────▼┐ ┌───▼────────┐ ┌▼──────────────┐ |
| 20 | + │ aws │ │ k8s │ │ local-docker │ |
| 21 | + │ │ │ │ │ │ |
| 22 | + │ Fargate │ │ Cluster │ │ Self-hosted │ |
| 23 | + │ (scalable) │ │ (flexible) │ │ (no cost) │ |
| 24 | + └───────────────┘ └────────────┘ └───────────────┘ |
| 25 | +``` |
| 26 | + |
| 27 | +## Why Load Balance? |
| 28 | + |
| 29 | +- **Cost control** — Route small or test builds to free self-hosted runners and reserve cloud |
| 30 | + providers for production builds. |
| 31 | +- **Availability** — Fall back to a cloud provider when your self-hosted runner is busy or offline. |
| 32 | +- **Platform routing** — Send Linux builds to AWS and Windows builds to a local runner. |
| 33 | +- **Parallel scaling** — Split a multi-platform matrix across providers to finish faster. |
| 34 | + |
| 35 | +## Basic Provider Routing |
| 36 | + |
| 37 | +Use a GitHub Actions matrix or conditional logic to route builds to different providers. |
| 38 | + |
| 39 | +### Route by Platform |
| 40 | + |
| 41 | +```yaml |
| 42 | +name: Platform-Based Routing |
| 43 | + |
| 44 | +on: |
| 45 | + push: |
| 46 | + branches: [main] |
| 47 | + |
| 48 | +jobs: |
| 49 | + build: |
| 50 | + name: Build ${{ matrix.targetPlatform }} |
| 51 | + runs-on: ubuntu-latest |
| 52 | + strategy: |
| 53 | + fail-fast: false |
| 54 | + matrix: |
| 55 | + include: |
| 56 | + # Linux builds go to AWS (fast, scalable) |
| 57 | + - targetPlatform: StandaloneLinux64 |
| 58 | + provider: aws |
| 59 | + # Windows builds go to self-hosted runner |
| 60 | + - targetPlatform: StandaloneWindows64 |
| 61 | + provider: local-docker |
| 62 | + # WebGL builds go to Kubernetes |
| 63 | + - targetPlatform: WebGL |
| 64 | + provider: k8s |
| 65 | + steps: |
| 66 | + - uses: actions/checkout@v4 |
| 67 | + with: |
| 68 | + lfs: true |
| 69 | + |
| 70 | + - uses: game-ci/unity-builder@v4 |
| 71 | + with: |
| 72 | + providerStrategy: ${{ matrix.provider }} |
| 73 | + targetPlatform: ${{ matrix.targetPlatform }} |
| 74 | + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} |
| 75 | +``` |
| 76 | +
|
| 77 | +### Route by Branch |
| 78 | +
|
| 79 | +Send production builds to a high-resource cloud provider and development builds to a cheaper option. |
| 80 | +
|
| 81 | +```yaml |
| 82 | +jobs: |
| 83 | + build: |
| 84 | + runs-on: ubuntu-latest |
| 85 | + steps: |
| 86 | + - uses: actions/checkout@v4 |
| 87 | + with: |
| 88 | + lfs: true |
| 89 | + |
| 90 | + - name: Select provider |
| 91 | + id: provider |
| 92 | + run: | |
| 93 | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then |
| 94 | + echo "strategy=aws" >> "$GITHUB_OUTPUT" |
| 95 | + echo "cpu=4096" >> "$GITHUB_OUTPUT" |
| 96 | + echo "memory=16384" >> "$GITHUB_OUTPUT" |
| 97 | + else |
| 98 | + echo "strategy=local-docker" >> "$GITHUB_OUTPUT" |
| 99 | + echo "cpu=1024" >> "$GITHUB_OUTPUT" |
| 100 | + echo "memory=3072" >> "$GITHUB_OUTPUT" |
| 101 | + fi |
| 102 | +
|
| 103 | + - uses: game-ci/unity-builder@v4 |
| 104 | + with: |
| 105 | + providerStrategy: ${{ steps.provider.outputs.strategy }} |
| 106 | + targetPlatform: StandaloneLinux64 |
| 107 | + containerCpu: ${{ steps.provider.outputs.cpu }} |
| 108 | + containerMemory: ${{ steps.provider.outputs.memory }} |
| 109 | + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} |
| 110 | +``` |
| 111 | +
|
| 112 | +## Fallback on Runner Availability |
| 113 | +
|
| 114 | +If your self-hosted runner is offline or busy, fall back to a cloud provider. Use a short initial |
| 115 | +job to check runner health, then pick the provider accordingly. |
| 116 | +
|
| 117 | +```yaml |
| 118 | +name: Fallback Routing |
| 119 | + |
| 120 | +on: |
| 121 | + push: |
| 122 | + branches: [main] |
| 123 | + |
| 124 | +jobs: |
| 125 | + check-runner: |
| 126 | + name: Check self-hosted availability |
| 127 | + runs-on: ubuntu-latest |
| 128 | + outputs: |
| 129 | + provider: ${{ steps.pick.outputs.provider }} |
| 130 | + steps: |
| 131 | + - name: Check if self-hosted runner is available |
| 132 | + id: pick |
| 133 | + env: |
| 134 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 135 | + run: | |
| 136 | + # Query runner status via the GitHub API |
| 137 | + RUNNERS=$(gh api repos/${{ github.repository }}/actions/runners --jq '[.runners[] | select(.status == "online")] | length') |
| 138 | + if [[ "$RUNNERS" -gt 0 ]]; then |
| 139 | + echo "provider=local-docker" >> "$GITHUB_OUTPUT" |
| 140 | + echo "Self-hosted runner available — using local-docker" |
| 141 | + else |
| 142 | + echo "provider=aws" >> "$GITHUB_OUTPUT" |
| 143 | + echo "No runners online — falling back to AWS" |
| 144 | + fi |
| 145 | +
|
| 146 | + build: |
| 147 | + name: Build |
| 148 | + needs: check-runner |
| 149 | + runs-on: ubuntu-latest |
| 150 | + steps: |
| 151 | + - uses: actions/checkout@v4 |
| 152 | + with: |
| 153 | + lfs: true |
| 154 | + |
| 155 | + - uses: game-ci/unity-builder@v4 |
| 156 | + with: |
| 157 | + providerStrategy: ${{ needs.check-runner.outputs.provider }} |
| 158 | + targetPlatform: StandaloneLinux64 |
| 159 | + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} |
| 160 | +``` |
| 161 | +
|
| 162 | +## Async Mode for Load Balancing |
| 163 | +
|
| 164 | +The [`asyncOrchestrator`](../api-reference#github-integration) parameter works well with load |
| 165 | +balancing. When enabled, the GitHub Action dispatches the build and returns immediately without |
| 166 | +waiting for it to finish. This means your routing job completes in seconds regardless of which |
| 167 | +provider handles the actual build. |
| 168 | + |
| 169 | +``` |
| 170 | + Router Job (seconds) Provider A Provider B |
| 171 | + ┌───────────────────┐ ┌──────────────┐ ┌──────────────┐ |
| 172 | + │ 1. Check runners │ │ │ │ │ |
| 173 | + │ 2. Pick provider │ │ │ │ │ |
| 174 | + │ 3. Dispatch build ├──────►│ 3a. Building │ OR ─►│ 3b. Building │ |
| 175 | + │ 4. Return (done) │ │ ... │ │ ... │ |
| 176 | + └───────────────────┘ │ ... │ │ ... │ |
| 177 | + Completes instantly │ 5. Complete │ │ 5. Complete │ |
| 178 | + └──────┬───────┘ └──────┬───────┘ |
| 179 | + │ │ |
| 180 | + ┌──────▼───────────────────────▼──────┐ |
| 181 | + │ GitHub Check updated │ |
| 182 | + │ (monitor from PR page) │ |
| 183 | + └─────────────────────────────────────┘ |
| 184 | +``` |
| 185 | +
|
| 186 | +Benefits of combining async mode with load balancing: |
| 187 | +
|
| 188 | +- **No wasted runner minutes** — The router job finishes in seconds. You don't pay for a |
| 189 | + GitHub-hosted runner to sit idle while the build runs in the cloud. |
| 190 | +- **Parallel dispatch** — Launch multiple builds to different providers simultaneously. Each |
| 191 | + dispatches and returns immediately. |
| 192 | +- **Monitor everything from one place** — Enable `githubCheck: true` and all builds report status |
| 193 | + back to the pull request Checks tab, regardless of which provider ran them. |
| 194 | +
|
| 195 | +```yaml |
| 196 | +jobs: |
| 197 | + build: |
| 198 | + name: Build ${{ matrix.targetPlatform }} |
| 199 | + runs-on: ubuntu-latest |
| 200 | + strategy: |
| 201 | + fail-fast: false |
| 202 | + matrix: |
| 203 | + include: |
| 204 | + - targetPlatform: StandaloneLinux64 |
| 205 | + provider: aws |
| 206 | + - targetPlatform: StandaloneWindows64 |
| 207 | + provider: k8s |
| 208 | + steps: |
| 209 | + - uses: actions/checkout@v4 |
| 210 | + with: |
| 211 | + lfs: true |
| 212 | +
|
| 213 | + - uses: game-ci/unity-builder@v4 |
| 214 | + with: |
| 215 | + providerStrategy: ${{ matrix.provider }} |
| 216 | + targetPlatform: ${{ matrix.targetPlatform }} |
| 217 | + gitPrivateToken: ${{ secrets.GITHUB_TOKEN }} |
| 218 | + asyncOrchestrator: true |
| 219 | + githubCheck: true |
| 220 | +``` |
| 221 | + |
| 222 | +Each matrix job dispatches its build and exits immediately. The builds continue running on their |
| 223 | +respective providers and report progress via GitHub Checks. |
| 224 | + |
| 225 | +## Weighted Distribution |
| 226 | + |
| 227 | +For teams with both cloud and self-hosted capacity, distribute builds based on a ratio. This example |
| 228 | +sends 70% of builds to AWS and 30% to a local runner using a simple hash. |
| 229 | + |
| 230 | +```yaml |
| 231 | +- name: Weighted provider selection |
| 232 | + id: provider |
| 233 | + run: | |
| 234 | + # Hash the run ID to get a stable pseudo-random number |
| 235 | + HASH=$(echo "${{ github.run_id }}" | md5sum | cut -c1-8) |
| 236 | + DECIMAL=$((16#$HASH % 100)) |
| 237 | + if [[ $DECIMAL -lt 70 ]]; then |
| 238 | + echo "strategy=aws" >> "$GITHUB_OUTPUT" |
| 239 | + else |
| 240 | + echo "strategy=local-docker" >> "$GITHUB_OUTPUT" |
| 241 | + fi |
| 242 | +``` |
| 243 | +
|
| 244 | +## Tips |
| 245 | +
|
| 246 | +- **Start simple** — A platform-based matrix with different providers per entry is the easiest |
| 247 | + starting point. Add dynamic routing only when you need it. |
| 248 | +- **Use async for long builds** — Combining `asyncOrchestrator: true` with `githubCheck: true` keeps |
| 249 | + your routing job fast and gives you build status on the PR page. |
| 250 | +- **Cache keys are provider-independent** — The [`cacheKey`](../api-reference#caching) parameter |
| 251 | + works the same across all providers, so builds routed to different providers can still share |
| 252 | + caches if they use the same storage backend. |
| 253 | +- **Test fallback logic** — Temporarily disable your self-hosted runner to verify that your fallback |
| 254 | + routing works before you need it in production. |
| 255 | +- **Custom providers** — The same routing patterns work with |
| 256 | + [custom providers](../providers/custom-providers). Set `providerStrategy` to a GitHub repo or NPM |
| 257 | + package and Orchestrator loads it dynamically. |
0 commit comments