GitHub Actions Matrix Strategy 삽질기

GitHub Actions Matrix Strategy 삽질기

크롤러를 만들었다. URL 목록을 수집하고, 각 URL에서 데이터를 추출하는 2단계 작업.

URL이 수백 개라 순차 처리하면 시간이 오래 걸린다. GitHub Actions에서 병렬로 처리하고 싶었다.


초기 구조

jobs:
  collect:
    runs-on: ubuntu-latest
    steps:
      - name: Collect URLs
        run: python crawl.py urls -o data/urls.json

      - name: Process all URLs
        run: python crawl.py process data/urls.json

300개 URL 처리에 2시간. 너무 느리다.


Matrix Strategy 도입

GitHub Actions의 Matrix Strategy로 병렬 처리.

jobs:
  collect-urls:
    runs-on: ubuntu-latest
    outputs:
      url_count: $
    steps:
      - name: Collect URLs
        id: collect
        run: |
          python crawl.py urls -o data/urls.json
          COUNT=$(python -c "import json; print(len(json.load(open('data/urls.json'))))")
          echo "count=$COUNT" >> $GITHUB_OUTPUT

      - name: Upload URLs artifact
        uses: actions/upload-artifact@v4
        with:
          name: urls
          path: data/urls.json

  process:
    needs: collect-urls
    runs-on: ubuntu-latest
    strategy:
      matrix:
        chunk: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      fail-fast: false
    steps:
      - name: Download URLs
        uses: actions/download-artifact@v4
        with:
          name: urls

      - name: Process chunk
        run: |
          python crawl.py process data/urls.json \
            --chunk $ \
            --total-chunks 10

10개의 job이 병렬로 실행된다. 각 job은 전체의 1/10만 처리.


문제 1: 동적 Matrix

URL 수에 따라 chunk 수를 동적으로 정하고 싶었다.

해결: fromJSON

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: $
    steps:
      - name: Set matrix
        id: set-matrix
        run: |
          # URL 수에 따라 chunk 수 결정
          URL_COUNT=300
          CHUNKS=$((URL_COUNT / 30 + 1))  # 30개씩

          # JSON 배열 생성
          MATRIX=$(python -c "import json; print(json.dumps(list(range($CHUNKS))))")
          echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

  process:
    needs: setup
    strategy:
      matrix:
        chunk: $

fromJSON으로 동적 배열을 matrix에 전달.


문제 2: Artifact 병합

각 job이 결과를 따로 저장한다. 이걸 합쳐야 한다.

해결: 여러 Artifact + 병합 Job

  process:
    # ... matrix job
    steps:
      - name: Process and save
        run: python crawl.py process --chunk $ -o result_$.json

      - name: Upload result
        uses: actions/upload-artifact@v4
        with:
          name: result-$
          path: result_$.json

  merge:
    needs: process
    runs-on: ubuntu-latest
    steps:
      - name: Download all results
        uses: actions/download-artifact@v4
        with:
          pattern: result-*
          merge-multiple: true

      - name: Merge results
        run: |
          python -c "
          import json
          import glob

          all_data = []
          for f in glob.glob('result_*.json'):
              with open(f) as fp:
                  all_data.extend(json.load(fp))

          with open('final_result.json', 'w') as fp:
              json.dump(all_data, fp)
          "

merge-multiple: true로 모든 artifact를 한 폴더에 다운로드.


문제 3: Rate Limit

10개 job이 동시에 같은 사이트를 요청하면 rate limit에 걸린다.

해결: max-parallel

strategy:
  matrix:
    chunk: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  max-parallel: 3
  fail-fast: false

max-parallel: 3으로 동시 실행을 3개로 제한.


문제 4: Self-hosted Runner

크롤링 작업이 무거워서 GitHub-hosted runner 시간이 부족했다.

해결: Self-hosted Runner

jobs:
  process:
    runs-on: self-hosted
    # ...

집에 있는 Mac Mini를 runner로 등록:

# runner 설치
./config.sh --url https://github.com/user/repo --token xxx
./run.sh

무제한 실행 시간 + 더 빠른 네트워크.


최종 워크플로우

name: Crawl

on:
  schedule:
    - cron: '0 6 * * *'
  workflow_dispatch:

jobs:
  # Stage 1: URL 수집
  collect-urls:
    runs-on: ubuntu-latest
    outputs:
      matrix: $
    steps:
      - uses: actions/checkout@v4

      - name: Collect URLs
        run: python crawl.py urls -o data/urls.json

      - name: Set matrix
        id: set-matrix
        run: |
          COUNT=$(python -c "import json; print(len(json.load(open('data/urls.json'))))")
          CHUNKS=$(( (COUNT + 29) / 30 ))
          MATRIX=$(python -c "import json; print(json.dumps(list(range($CHUNKS))))")
          echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

      - uses: actions/upload-artifact@v4
        with:
          name: urls
          path: data/urls.json

  # Stage 2: 병렬 처리
  process:
    needs: collect-urls
    runs-on: ubuntu-latest
    strategy:
      matrix:
        chunk: $
      max-parallel: 5
      fail-fast: false
    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          name: urls

      - name: Process chunk $
        run: |
          python crawl.py process data/urls.json \
            --chunk $ \
            --total-chunks $
            -o result_$.json

      - uses: actions/upload-artifact@v4
        with:
          name: result-$
          path: result_$.json

  # Stage 3: 결과 병합
  merge:
    needs: process
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          pattern: result-*
          merge-multiple: true

      - name: Merge and commit
        run: |
          python crawl.py merge result_*.json -o data/games.jsonl
          git add data/games.jsonl
          git commit -m "Update data" || true
          git push

성능 비교

방식 300 URLs
순차 처리 2시간
Matrix (10 chunks) 15분
Matrix (10) + max-parallel 3 25분 (rate limit 안전)

삽질 정리

문제 해결
순차 처리 느림 Matrix strategy
동적 chunk 수 fromJSON + outputs
Artifact 병합 merge-multiple
Rate limit max-parallel
실행 시간 제한 Self-hosted runner

결론

GitHub Actions Matrix Strategy는 강력하지만, 동적으로 사용하려면 삽질이 필요하다.

핵심:

  1. fromJSON으로 동적 배열
  2. Artifact로 job 간 데이터 전달
  3. max-parallel로 rate limit 관리

무료 CI/CD에서 병렬 처리는 Matrix Strategy가 답이다.

Back