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 속성 아래에서 하나의 맵핑(mapping)으로 정의되며, 모든 작업에는 필수적으로 키(key)로 작업 식별자(ID)가 명시되야하고, 값(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가 실행되기 전에 job1이 먼저 완료되야 하고, 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

작업 간 출력값 전달

위에서 설명드린 것과 같이 needs 속성을 사용해서 작업 간에 의존 관계를 설정해주면, 이떤 작업에서 발생한 결과물을 outputs 속성을 통해 이후 작업으로 전달하는 것이 가능합니다. 즉, 어떤 단계에서 특정 값을 출력 매개변수로 내보내면 그 단계 이후로 실행되는 모든 단계에서 해당 출력 값을 불러올 수 있습니다.

작업(job) 간 출력값을 전달하려면, 우선 단계(step) 간에 츨력값을 전달하는 방법을 알아야합니다. 이 부분에 대해서는 별도 포스팅에서 자세히 설명하고 있으니 참고 바랍니다.

예를 들어, 다음과 같이 두 개의 작업(job)로 이뤄진 워크플로우를 생각해볼까요?

job1 작업의 step1 단계에서는 word=GitHubstep2 단계에서는 word=Actions를 출력값으로 내보내고 있습니다. 그리고 outputs 속성을 통해서 각 단계에서 내보낸 출력값을 word1word2에 설정해주고 있습니다.

이렇게 해주면 job1에 의존하는 job2 작업에서는 needs.job1.outputs.word1needs.job1.outputs.word2을 불러와서 콘솔에 출력할 수 있습니다.

.github/workflows/jobs.yml
name: Our Jobs
on: push
jobs:
  job1:
    runs-on: ubuntu-latest
    steps:
      - id: step1
        run: echo "word=GitHub" >> $GITHUB_OUTPUT
      - id: step2
        run: echo "word=Actions" >> $GITHUB_OUTPUT
    outputs:
      word1: ${{ steps.step1.outputs.word }}
      word2: ${{ steps.step2.outputs.word }}
  job2:
    runs-on: ubuntu-latest
    needs: job1
    steps:
      - run: echo "${{ needs.job1.outputs.word1 }}"
      - run: echo "${{ needs.job1.outputs.word2 }}"
job1
☑️ Set up Job
☑️ Run echo "word=GitHub" >> $GITHUB_OUTPUT☑️ Run echo "word=Actions" >> $GITHUB_OUTPUT☑️ Complete Job
job2
☑️ Set up Job
☑️ Run echo "GitHub"▶ Run echo "GitHub"GitHub☑️ Run echo "Actions"▶ Run echo "Actions"Actions☑️ Complete Job

단순한 값이 아닌 데이터를 파일에 담아서 다른 작업에서 접근하게 하려면 아티팩트(Artifact)를 사용해야합니다. 이 부분에 대해서는 별도 포스팅에서 자세히 다루고 있습니다.

작업의 선택적 실행

기본적으로 워크플로우는 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이 아닌 다른 브랜치로 코드를 커밋할 때 메시지에 skip ci를 포함시키면 두 번째 작업이 생략되어 실행되지 않는 것을 볼 수 있습니다.

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:
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - windows-latest
          - macos-latest
    runs-on: ${{ matrix.os }}
    steps:
      - run: date
Jobs
date (ubuntu-latest)date (windows-latest)date (macos-latest)

작업 요약

GitHub Actions의 요약 페이지에 어떤 작업에 대한 특정 내용을 마크다운(markdown) 문법으로 추가해줄 수도 있는데요. 일일이 작업 로그에 들어가는 번거로움 없이 요약 페이지에 중요한 내용을 간단하게 보여주고 싶을 때 유용하게 활용할 수 있습니다.

name: Our Jobs
on: push
jobs:
  todos:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "## 할일 목록" >> $GITHUB_STEP_SUMMARY
          echo "- 자기" >> $GITHUB_STEP_SUMMARY
          echo "- 놀기" >> $GITHUB_STEP_SUMMARY
          echo "- 먹기" >> $GITHUB_STEP_SUMMARY

마치면서

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

GitHub Actions 관련 포스팅은 GitHub Actions 태그를 통해서 쉽게 만나보세요!