Logo

GitHub Actions의 유용한 작업(job) 설정

지난 포스팅에서는 GitHub Actions의 4가지 핵심적인 개념인 워크플로우(workflow), 작업(job), 단계(step), 액션(action)에 대해서 가볍게 살펴보았는데요.

이번 포스팅에서는 이 중에서도 가장 다양하게 설정할 수 있는 작업(job)에 대해서 좀 더 깊이 알아보도록 하겠습니다.

GitHub Actions에서 작업(job)이란?

먼저 GitHub Actions에서 작업(job)의 역할과 위치에 대해서 간단히 복습을 하고 넘어가겠습니다.

작업(job)은 어떤 이벤트가 발생했을 때 독립된 환경에서 실행되야 하는 일련의 일을 나타내는 매우 중요한 개념인데요. 워크플로우는(workflow)는 작업(job)의 상위 개념이고, 단계(step)는 하위 개념이라고 볼 수 있겠습니다. 즉, 하나 이상의 작업(job)이 하나의 워크플로우(workflow)를 구성하며, 하나의 작업(job)은 순차적으로 수행되는 여러 개의 단계(step)로 이뤄집니다.

워크플로우 YAML 파일 기준으로 보면 작업은 jobs 속성 아래에서 하나의 해시 테이블(hash table)의 타입으로 정의되며, 모든 작업에는 필수적으로 작업 식졀자(ID)가 키(key)로 명시되야하고, 그 밖에 세부 내용(실행 환경, 작업 단계 등)은 값(value)으로 또 다시 해시 테이블 형태로 명시가 됩니다. (중첩 해시 테이블라고 볼 수 있겠죠?)

아래는 2개의 작업으로 이루어진 전형적인 워크플로우 파일의 모습입니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo 'Hello'
          echo 'GitHub Actions'
  job2:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: ls -al

작업(job)의 독립성

GitHub Actions에서 작업 설정을 할 때 염두해야하는 가장 중요한 부분은 바로 하나의 워크플로우 상에서 각각의 작업이 완전히 격리된 환경에서 실행된다는 것입니다. 쉽게 말해 2개의 작업을 실행하면 각 작업은 서로 CI 서버, 즉 다른 컴퓨터에서 돌아가는 것이지요.

이러한 작업의 독립성은 GitHub Actions에서 의도한 설계라고 볼 수 있는데요. 우선 이렇게 작업 간에 완전히 격리되면 병렬 처리가 가능해져 성능 측면에서 유리합니다. 또한 하나의 워크플로우를 다양한 실행 환경에서 실행할 수 있게 됩니다. 다시 말해, 일부 작업은 리눅스에서 돌리고, 다른 작업은 윈도우즈에서 돌릴 수 있는 것이지요.

이러한 작업의 특징을 고려하지 않고 워크플로우를 만들 게 되면 의도치 않은 문제를 겪을 수 있는데요. 예를 들어, 다음과 같이 하나의 파일을 두 개의 작업을 통해서 쓰고 읽는 워크플로우를 생각해보겠습니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  hello1:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'Hello, GitHub Actions!' > hello.txt
  hello2:
    runs-on: ubuntu-latest
    steps:
      - run: cat hello.txt

이 워크플로우를 얼핏 보면 마치 hello.txt 파일에 Hello, GitHub Actions!라는 문자열이 써진 후에, hello.txt 파일의 내용이 콘솔에 출력될 것 같은데요. 실제로 실행 결과 로그를 보면 두 번째 작업에서 hello.txt 파일을 찾을 수 없다는 에러가 발생하는 것을 볼 수 있습니다.

hello1
☑️ Set up Job
☑️ Run echo 'Hello, GitHub Actions!' > hello.txt
☑️ Complete Job
hello2
☑️ Set up Job
☑️ Run cat hello.txt
▶ Run cat hello.txtcat: hello.txt: No such file or directoryError: Process completed with exit code 1.☑️ Complete Job

왜 이런 문제가 발생하는 걸 까요?

hello1 작업과 hello2 작업은 엄밀히 얘기해서 서로 다른 CI 서버에서 돌아가게 됩니다. 그러므로 첫 번째 작업이 생성한 hello.txt 파일에 두 번재 작업이 접근하는 것은 기술적으로 불가능한 것이지요.

따라서 이러한 작업은 두 개의 작업으로 나누지 말고 하나의 작업에서 여러 단계(step)로 처리해야겠습니다. 그러면 하나의 CI 서버에서 일어나는 작업이 되므로 첫 번째 단계에서 생성한 파일을 자연스럽게 두 번째 단계에서 읽을 수 있을 것입니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'Hello, GitHub Actions!' > hello.txt
      - run: cat hello.txt

이제 의도했던데로 hello.txt 파일의 내용이 콘솔에 출력되는 것을 볼 수 있습니다.

hello
☑️ Set up Job
☑️ Run echo 'Hello, GitHub Actions!' > hello.txt
☑️ Run cat hello.txt
▶ Run cat hello.txtHello, GitHub Actions!☑️ Complete Job

작업 간에 실행 순서 제어하기

워크플로우에 여러 개의 작업이 정의되어 있을 경우 기본적으로 모든 작업은 동시에 처리가 됩니다. 작업의 독립성을 상기해보면 어떻게 보면 여러 작업이 병렬 처리가 되는 게 당연하게 느껴지죠? 하나의 컴퓨터를 재사용하는 게 아니고 여러 대의 컴퓨터를 동시에 돌릴 수 있는데 굳이 기다릴 필요가 없는 것이지요.

간단한 실습을 위해 작업 식별자(ID)를 콘솔에 출력하기 위한 동일한 작업 3개로 이루어진 워크플로우를 작성해보겠습니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ github.job }}
  job2:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ github.job }}
  job3:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ github.job }}

이제 GitHub의 Actions 탭에서 Our Jobs 워크플로우를 선택해보면 다음과 같이 3개의 작업이 병렬로 처리되는 것을 볼 수 있습니다.

Jobs
✅ job1
✅ job2
✅ job3
job1
☑️ Set up Job
☑️ Run echo
▶ Run echo job1job1☑️ Complete Job
job2
☑️ Set up Job
☑️ Run echo
▶ Run echo job2job2☑️ Complete Job
job3
☑️ Set up Job
☑️ Run echo
▶ Run echo job3job3☑️ Complete Job

여기서 문제는 우리가 언제나 병렬 처리를 원하는 것은 아니라는 건데요.

대표적인 경우로 CI/CD를 들 수 있죠? 보통 테스트 작업이나 빌드 작업이 작업이 종료된 다음에 배포 작업을 시작할 수 있습니다.

그러면 작업 간에 실행 순서를 제어하고 싶다면 어떻게 해야할까요? 이럴 때는 작업의 needs 속성을 사용해서 작업 간에 의존 관계를 설정해줄 수 있습니다.

예를 들어 job2가 실행되기 전에 job2이 먼저 완료되야 하고, job3가 실행되기 전에 job1job2가 먼저 완료되도록 실습 워크플로우를 수정해보겠습니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ github.job }}
  job2:
    runs-on: ubuntu-latest
    needs: job1    steps:
      - run: echo ${{ github.job }}
  job3:
    runs-on: ubuntu-latest
    needs: [job1, job2]    steps:
      - run: echo ${{ github.job }}

이제 다시 GitHub의 Actions 탭에서 해당 워크플로우를 확인해보면 다음과 같이 작업 간에 의존 관계가 설정되어 순차적으로 실행되는 것을 볼 수 있습니다.

Jobs
✅ job1 - ✅ job2 - ✅ job3

특정 작업을 선택적으로 실행하기

기본적으로 워크플로우는 on 속성을 통해서 언제 작업들이 실행되야하는지를 정의하는데요. 간혹 각각의 작업 수준에서 추가적으로 실행 여부를 결정해야하는 경우가 생깁니다. 이러한 조건은 해당 작업의 if 속성을 통해 명시해주면 되는데요.

예를 들어, 다음과 같이 3개의 작업으로 이루어진 워크플로우를 생각해봅시다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  echo:
    runs-on: ubuntu-latest
    steps:
      - run: echo 'Hello!'
  echo_if:
    runs-on: ubuntu-latest
    if: github.ref_name == 'main'
    steps:
      - run: echo 'Hello, Main!'
  skip_ci:
    runs-on: ubuntu-latest
    if: ${{ !contains(github.event.head_commit.message, 'skip ci') }}
    steps:
      - run: echo 'Hello, Skip CI!'

첫 번째 echo 작업은 무조건 실행이 되고, 두 번째 echo_if 작업은 push 이벤트가 main 브랜치에서 발생했을 때만 실행됩니다. 세 번째 skip_ci 작업은 마지막 커밋(commit) 메시지에 skip ci가 포함되지 않았을 경우에만 실행됩니다.

실제로 main이 아닌 다른 브랜치로 코드를 커밋해보면 두 번째 작업이 생략되어 실행되지 않는 것을 볼 수 있습니다.

echo
🚫 echo_if
✅ skip_ci

동일 작업을 다양한 실행 환경에서 실행하기

만약에 동일한 작업을 다양한 실행 환경에서 돌리고 싶을 때는 어떻게 해야할까요? 보통 범용 라이브러리 프로젝트에서는 특정 운영체제에서만 발생하는 문제를 잡기 위해서 여러 운영체제에서 테스트를 돌리기도 하는데요.

본 포스팅에서는 간단하게 날짜를 출력하는 작업을 여러 운영체제에서 실행하고 싶다고 가정해보겠습니다. 가장 쉽게 생각해낼 수 있는 접근 방법은 워크플로우에 운영체제 별로 동일한 작업을 정의하는 것일 겁니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  date1:
    runs-on: ubuntu-latest
    steps:
      - run: date
  date2:
    runs-on: windows-latest
    steps:
      - run: date
  date3:
    runs-on: macos-latest
    steps:
      - run: date
Jobs
✅ date1
✅ date2
✅ date3
date1
☑️ Set up Job
☑️ Run date
▶ Run dateSun May 15 16:11:26 UTC 2022☑️ Complete Job
date2
☑️ Set up Job
☑️ Run date
▶ Run dateSun May 15 16:11:45 CUT 2022☑️ Complete Job
date3
☑️ Set up Job
☑️ Run date
▶ Run dateSun May 15 16:11:27 UTC 2022☑️ Complete Job

이 방법은 의도한데로 작동은 하지만 같은 작업 내용을 중복해야되기 때문에 최선은 아닌 것 같죠?

사실 작업의 strategy 속성의 matrix 옵션을 활용하면 이러한 중복없이 훨씬 간단하게 동일한 목적을 달성할 수 있습니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  date:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - windows-latest
          - macos-latest
    steps:
      - run: date
Jobs
date (ubuntu-latest)date (windows-latest)date (macos-latest)

마치면서

이상으로 GitHub Actions에서 작업을 설정할 때 흔히 겪을 수 있는 문제와 효과적인 설정 방법에 대해서 알아보았습니다. GitHub Actions를 여러 가지 목적으로 활용하시는데 도움이 되었으면 좋겠습니다.