
GitHub Actions Job 관리 및 유지보수를 좀 더 효율적으로 진행하기 위해 Reusable Workflow, Composite Action을 사용하여 모듈화를 진행했던 내용을 정리해보았다.
현재 CI / CD 파이프라인을 비롯해 가벼운 배치성 Job까지 GitHub Actions로 운영하고 있었지만,
팀·서비스·리포지토리 단위로 관리되다 보니 워크플로우와 Action이 여기저기 파편화된 상태였다.
물론 특정 비즈니스 로직에 강하게 의존하는 Job이라면 재사용이 어렵기 때문에 큰 문제가 되지 않는다.
그러나 CI / CD 파이프라인처럼 어느 정도 템플릿화가 가능한 Job이나, 슬랙 채널로 메시지를 전송하는 Action과 같이 공통으로 사용되는 구성 요소들이 각 리포지토리에 흩어져 있는 상황은 장기적으로 유지보수 비용을 크게 증가시킬 수밖에 없었다.
특히 공통 로직에 대한 수정이 필요한 경우, 여러 리포지토리를 순회하며 동일한 변경을 반복 적용해야 하는 구조는 작업 효율과 안정성 측면에서 분명한 한계를 가지고 있었다.
여기에 더해, 올해 안으로 CI / CD 파이프라인 고도화 작업을 계획하고 있었기 때문에 본격적인 개선에 앞서 구조를 정리하고 모듈화하는 선행 작업이 반드시 필요했다.
또한 이후 고도화 과정에서 새롭게 추가될 Job이나 Step들 역시 초기부터 재사용을 염두에 둔 형태로 관리할 필요가 있었다.
이러한 배경에서 GitHub Actions가 Job 또는 Step 단위의 모듈화를 지원하는지 검토하게 되었고, 그 과정에서 Reusable Workflow와 Composite Action이라는 개념을 알게 되었다.
Reusable Workflow와 Composite Action은 GitHub Action에 제공하는 기능으로 Job 또는 Step을 묶어서 쓸 수 있는 기능이다.
두 기능은 묶을 수 있는 단위와 사용 방법이 다르며 이에 따라 용례도 다르다.
Reusable Workflow는 Job 또는 여러 개의 Job을 가진 Workflow를 하나로 묶을 수 있는 기능으로, Job / Workflow 단위로 모듈화를 할 수 있다.
on.worflow_call 속성 안에 해당 Workflow를 사용할 때 필요한 인자값을 지정할 수 있다.
# 공통으로 사용할 workflow (.github/workflows/build.yml)
name: Reusable Build Workflow
on:
workflow_call:
# workflow 실행 시 받아야 할 input값
inputs:
java-version:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
- run: ./gradlew build
이렇게 지정한 Workflow는 다른 곳에서 사용할 수 있다.
name: CI
on: [push]
jobs:
build:
# workflow 주소 지정
uses: mrcocoball/test-repo/.github/workflows/build.yml@main
# input 값 지정
with:
java-version: "17"
Job들을 모듈화시킨 것이기 때문에 호출을 하게 될 경우 Job 단위로 사용을 하게 된다.
즉, 하나의 커다란 작업을 공통화시켜서 사용해야 할 경우 Reusable Workflow가 용이하다.
Composite Action은 Step 또는 여러 개의 Step을 하나의 Action으로 묶을 수 있는 기능으로, Step / Action 단위로 모듈화를 할 수 있다.
# 공통으로 사용할 action (.github/actions/setup-java/action.yml)
name: Setup Java
description: Setup Java and Gradle cache
inputs:
java-version:
required: true
runs:
using: "composite"
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
- run: echo "Java setup complete"
shell: bash
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mrcocoball/test-repo/.github/actions/setup-java@main
with:
java-version: "17"
- run: ./gradlew build
Step들을 모듈화시킨 것이기 때문에 호출하게 될 경우 Step 단위로 사용을 하게 된다.
여러 개의 Step을 공통화시켜서 사용해야 할 경우 Composite Action가 용이하다.
일단 같은 계정 내 어느 리포지토리에서도 사용할 수 있게끔, Reusable Workflow와 Composite Action만 모아두는 리포지토리를 구성한다.
이 때 중요한 것은 해당 리포지토리의 Settings > Action에서 Access 부분을 Not accessible에서 Accessible fromrepositories owned by the user 로 바꿔야 한다는 점이다. 이걸 바꾸지 않으면, 다른 리포지토리에서 해당 리포지토리에 존재하는 Workflow, Action을 찾을 수 없다.
그리고 나서 .github 디렉터리에 각각 workflows, actions 디렉터리를 구성하고 그 밑에 yml 파일을 작성해둔다.
.github
ㄴ workflows
ㄴ common-workflow.yml
ㄴ actions
ㄴ common-action
ㄴ action.yml
다른 리포지토리의 Workflow에서 사용이 가능한데, 주의할 점은 uses의 경로는 상대 경로가 아니라 절대 경로여야 하는 점이다.
...
jobs:
common-job:
# workflow 주소 지정 (절대경로)
uses: mrcocoball/test-repo/.github/workflows/common-job.yml@main
extra-job:
runs-on: ubuntu-latest
steps:
- name: common-step
# action 주소 지정 (절대경로)
uses: mrcocoball/test-repo/.github/actions/common-action@main
처음에는 Reusable Workflow로 CI / CD 파이프라인 자체를 모듈화하려고 했었다.
그러나 배포 환경별, 서비스별로 공통화 할 수 없는 지점이 분명히 존재하였고, 이를 분기 처리하면서까지 Job 단위로 모듈화를 시키느니 Composite Action으로 모듈화를 우선 진행하고 차후 Reusable Workflow로 변경하는 것이 낫겠다는 판단을 하였다.
그래서 일단 유닛 별로 모듈화를 진행하고, 이러한 유닛들을 한 번 더 통합하여 통합 모듈화를 진행하였다.
.github
ㄴ actions
ㄴ backend # 백엔드용 Action 모음
ㄴ unit # 유닛 Action
ㄴ build
ㄴ image-build-and-push
ㄴ render-task-definition
ㄴ deploy-task-definition
ㄴ register-task-definition
ㄴ integration # 통합 Action
ㄴ cicd-dev
ㄴ cicd-stg
ㄴ cicd-prod
ㄴ frontend # 프론트엔드용 Action 모음
ㄴ unit
ㄴ integration
ㄴ common # 공통 Action 모음
ㄴ unit
ㄴ slack-notify
ㄴ integration
먼저 가장 단위인 유닛을 모듈화하는 기준은 다음과 같았다.
그리고 배포 환경이나 서비스 별로 달라지는 부분 등을 케이스로 만들어 케이스별로 유닛 모듈을 묶는 통합 모듈을 구성하였다.
이 때 추후 확장성을 대비하여 Reusable Workflow가 아닌 Composite Action으로 만들었는데, 통합 모듈 내에서 Step을 추가할 수도 있지만 통합 모듈 바깥에서 통합 모듈과 연계해야 하는 Step을 추가해야 하는 경우도 분명 있기 때문에 서두르지 않았다.
당장 아래와 같은 시나리오를 보면, 모듈화에 익숙치 않은 상태라면 안전하게 Composite Action으로 먼저 통합 모듈을 만들고 어느 정도 공통화할 수 있는 부분이 생긴다면 그 때 Reusable Workflow로 넘어가는 것이 낫지 않을까 싶다.
# 통합 모듈 현황 (A, B는 Step 구성이 다름)
- 개발환경 통합 모듈 A
- 개발환경 통합 모듈 B
- 운영환경 통합 모듈 A
- 운영환경 통합 모듈 B
# 추가 요구사항 (예시)
- 추가 기능 1, 2가 들어가야 하는데 추가 기능 1은 환경 변수에 따라 다르게 들어가야 함 => 모듈 자체에서 추가
- 추가 기능 2는 모듈을 사용하는 서비스 별로 분기 처리 해야 함 => ??
# 통합 모듈을 Reusable Workflow로 만든 경우
- 호출하는 서비스의 Job에서 추가 기능 2를 붙여야 하는데 이미 Workflow 단위이므로
새로운 Job을 만들어 Step으로 붙이거나, 추가 기능 2 자체를 Reusable Workflow로 만들어 Job 단위로 붙여야 함
# 통합 모듈을 Composite Action으로 만든 경우
- 호출하는 서비스의 Job에서 추가 기능 2를 붙이며, Step 단위로 붙일 수 있음
- 추가 기능 2를 Composite Action으로 만들 수 있음
확실히 여기저기 파편화되어 있던 Workflow를 input값만 다르게 해서 깔끔하게 만들 수 있었고, 흐름 중에 바뀌어야 하는 부분은 부가적인 Step을 붙이는 식으로 할 수 있어서 편리해졌다.
# 기존
- step1
- step2
- step3
... 이러한 job이 서비스별 / 환경별로 N개 존재 -> step1이 바뀌면 N번 수정
# 변경 후
- composite action (step1, step2, step3) 딸깍
... 서비스별 / 환경별로 N개 존재하지만 step1이 바뀌더라도 composite action 모듈 1번만 수정하면 됨
이 과정에서 Reusable Workflow와 Composite Action이 Job 간, Step 간 스코프를 어떻게 나누는지, 환경 변수와 Secret이 어떤 방식으로 전달되는지, 그리고 상태값(output)가 어디까지 전파되는지 등 미묘하지만 중요한 차이들 때문에 시행착오도 적지 않게 겪었다.
다만 이러한 시행착오 덕분에 GitHub Actions의 실행 모델과 설계 의도를 더 깊이 이해할 수 있었고, 단순히 “동작하는 CI”를 넘어 “유지보수 가능한 CI / CD 구조”에 대해 고민해볼 수 있는 계기가 되었다.
무엇보다도 이번에 모듈화를 선행해 두었기 때문에, 앞으로 예정된 CI / CD 파이프라인 고도화 작업에서도 변경 사항을 국소화하고 영향 범위를 예측하기 쉬워졌다는 점에서 개인적으로 큰 만족감을 느끼고 있다.