GitHub Actions에서 반복되는 코드 재사용하기 - Composite Actions

GitHub Actions의 Composite Actions를 사용하여 CI/CD 파이프라인을 효율적으로 구성하는 방법을 알아봅니다.

잡설
약 2년만에 작성해보는 글입니다. 글또라는 커뮤니티를 하며 드디어 글을 쓰게 되었어요.
누군가에게 도움이 되길 바라며 작성해봅니다.

Github Actions로 할 수 있는 것

개발자가 아니더라도 여타 모든 사람들은 반복되는 일을 하고 싶지 않을 것 같아요.
정확하게는 불필요하게 반복되는 일을 하기 싫겠죠.

기억력은 유한하고 반복되는 일은 지루합니다.
더불어 이는 실수를 유발할 가능성이 높고, 그 시간에 다른 생산적인 일들을 할 수 있기에 개발자들은 자동화를 추구하는 것 같아요..

이미 사용하시는 분들이 많을 것이라 예상합니다만, Github를 사용하는 회사나 개발자분들은 편리하게도 Github Actions를 통해 많은 것들을 자동화할 수 있어요.

현재 개발 중인 서비스에서 아래의 작업들을 자동화 해두었는데요.

  • 스토리북 배포 / 버저닝
  • 패키지 배포 / 버저닝
  • 단위, E2E 테스트
  • 리뷰어 지정

etc

  • PR의 타이틀을 보고 라벨링
  • cron을 통해 특정 시점에 배포

왜 재사용이 필요할까?

상기 자동화 리스트의 워크플로우에서 매번 필요한 것들이 있어요.
아래와 같은 작업들인데요.

  • node.js 설치
  • 패키지 매니저 설정 및 의존성 설치
  • 캐싱
  • telemetry 수집 비활성화

그럼 자동화 리스트의 모든 워크플로우에 코드를 복붙해야 할까요?
워크플로우가 적다면 복붙도 맞을 수 있어요. 다만 점점 워크플로우가 많아지면 관리하기 쉽지 않아요.

만약 복붙한 코드에서 패키지 매니저가 변경된다면 무엇을 어떻게 해야할까요?
모든 워크플로우를 들어가 관련 코드를 변경해야할 수도 있겠죠.

실제로 저는 디자인 시스템에서 패키지 매니저를 yarn에서 pnpm으로 변경했었는데, 곧 소개드릴 내용을 통해 공통 워크플로우 동작은 한 파일의 코드만 변경하면 됐었어요.

이런 경험 있지 않으신가요?

간단한 코드 작업으로 예상했는데 바꿔야 하는 곳이 엄청 많았던 경험.

Github Actions에서도 동일한데요, 이와 같이 반복되는 코드를 적절하게 나누어 한 곳에서 관리하여 전반적으로 유지보수를 쉽게 도와주는 Composite Actions를 소개해드립니다.

Reusable Workflow

엥 갑자기 Reusable Workflows가 왜 나올까요?
Github Actions는 워크플로우 코드 중복을 방지하기 위해서 2가지 해결책을 제시했어요.

  • Reusable Workflow
  • Composite Actions

저희의 상황에선 Composite Actions가 더 적합해서 사용했고, Reusable Workflow는 간단하게 살펴만 보고 넘어갈게요.

Reusable Workflow는 말 그대로 다시 사용 가능한 워크플로우에요.

우선 간단하게 아래의 내용을 이해해볼까요?

Workflow란
워크플로우는 GitHub Actions에서 자동화된 프로세스로, 하나 이상의 Job으로 구성됩니다.

Job이란
Job은 워크플로우 내에서 독립적으로 실행되는 작업 단위입니다. 기본적으로 병렬로 실행되며, 각각 별도의 실행 환경(예: 가상 머신)을 가집니다.
필요에 따라 Job 간 의존성을 설정하거나 artifacts를 통해 데이터를 공유할 수 있습니다.

Step이란
Step은 Job 내에서 순차적으로 실행되는 개별 작업입니다. 각 Step은 쉘 스크립트를 실행하거나 Action을 사용할 수 있습니다.
Step들은 같은 실행 환경을 공유하므로 이전 Step의 결과를 다음 Step에서 사용할 수 있습니다.

Reusable WorkflowJob 단위로 재사용이 가능하여 각각 별도의 실행 환경을 갖습니다.
그리하여 해당 워크플로우를 호출한 워크플로우에서 결과를 사용하려면 artifacts를 통해 공유해야 해요.

어떤 경우에 사용하면 좋을까요?

  • 여러 프로젝트나 저장소에서 공통으로 사용되는 복잡한 CI/CD 프로세스를 표준화할 때
    ex) 마이크로서비스의 빌드, 도커 이미지 생성, 레지스트리 푸시, 배포 등의 과정

Composite Actions

Composite Actionsstep 수준에서 재사용이 가능하고, 어러 개의 액션을 하나의 재사용 가능한 단위로 묶은 것이에요.
이는 Job 내부의 Step에서 사용하기에 같은 실행 환경을 공유해요.

말이 조금 복잡한데요, 아까 설명 드렸던 부분을 다시 가져와볼게요.
아래와 같이 여러가지 워크플로우에서 필수로 해야하는 것들이 있어요.

  • node.js 설치
  • 패키지 매니저 설정 및 의존성 설치
  • 캐싱
  • telemetry 수집 비활성화

이러한 작업들은 하나의 재사용 가능한 단위가 될 수 있을 것 같아요.
예를 들면 setup-node-dependencies라는 이름으로요.

그리고 이를 여러 워크플로우에서 재사용할 수 있지 않을까요?

어떤 경우에 사용하면 좋을까요?

  • 여러 워크플로우에서 사용되는 작은 작업 단위를 재사용할 때
    ex) 의존성 설치, 코드 품질 검사

왜 사용했어요?

Composite Actions를 사용한 이유는 아까 설명드린 Workflow의 특성 때문이에요.
Job의 경우 별도의 실행 환경을 갖는다는 것에 집중해야해요.

별도의 실행 환경은 격리성이나 병렬 처리, 환경 특화, 유연한 설계, 장애 격리 등에서 여러가지 이점을 갖지만, 동일한 의존성을 매번 설치해야 한다는 점이 있어요.
(Job간의 의존과 artifacts를 통해 처리할 수 있을 것 같기도 하네요)

저의 예시와 같이 간단한 형태인 node와 의존성 설치에는 Composite Actions처럼 Job 내부의 같은 실행 환경을 공유하는 것이 맞다고 생각해요.

Example

# .github/actions/{some-composite-actions}.yml
name: 'Setup Node.js and Dependencies'/
description: 'Set up Node.js and install dependencies with caching'
inputs:
  node-version:
    description: 'Node.js version to set up'
    required: true
    default: '18.x'
runs:
  using: 'composite'
  steps:
    - name: Install pnpm
      uses: pnpm/action-setup@v4
      with:
        version: 9
 
    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'pnpm'
 
    - name: Install dependencies
      run: pnpm install
      shell: bash
 
# .github/workflows/Release.yml
name: Release
 
concurrency: ${{ github.workflow }}-${{ github.ref }}
 
jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: Setup Node.js and Dependencies
        uses: ./.github/actions/{some-composite-actions}.yml
        with:
          node-version: '18.x'
 
      - name: Check if there are changesets
        run: pnpm changeset status --since=main
 
      - name: Create Release Pull Request or Publish to npm
        id: changesets
        uses: changesets/action@v1
        with:
          version: pnpm changeset version
          publish: pnpm release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

지금 예시엔 Release 워크플로우만 있는데요, 만약 5개, 10개로 늘어나면 어떻게 될까요?
적절하게 잘 분리해놓았다면 조금만 변경하면 될 것 같아요.

Reusable Workflow, Composite Actions 간단 비교하기

Reusable Workflows는요!

  • 전체 워크플로우 수준에서 재사용 가능해요.
  • workflows/{some-reusable-workflow}.yml로 정의해요.
  • 내부에서 secrets 사용이 가능해요.
  • Workflow 내부의 여러 Job 혹은 여러 Step이 있더라도 모든 로그가 잘 분리되어 보기 좋게 나와요.

Composite Actions는요!

  • 액션 수준에서 재사용 가능해요.
  • actions/{some-composite-action}/action.yml로 정의해요.
  • 여러가지 step을 하나의 액션으로 만들고, 워크플로우 내의 단일 step으로 사용해요.
  • 여러 개의 액션을 하나로 묶다보니 로깅과 같은 부분이 단일 step으로 찍혀서 조금 보기 어렵다는 단점이 있어요.
  • 내부에서 secrets를 사용할 수 없다보니 내부에서 필요한 변수들은 매개변수로 꼭 입력해줘야 한다는 점도 있죠.

References

잠깐 등장한 actions