최근 진행하고 있는 쿠버네티스 기반의 서비스 플랫폼 프로젝트에서 각 서비스들의 빌드 과정을 자동화하기 위한 과정이 있었다.
현재 관리하는 서비스들은 4개로, 각각 별도로 고유한 컨테이너 이미지가 존재한다.
여기에서 Github Actions를 이용해 각 서비스 별로 변경점의 push가 발생하면, 그에 맞는 서비스 이미지를 새로 빌드해서 허브에 push하도록 구성하고자 했었다.
여기서 들었던 의문점은 '이 4개의 서비스를 어떻게 워크플로우로 빌드 과정을 작성할 수 있을까' 라는 것이였다.
물론 각 서비스별로 빌드 Job을 생성해서 코드로 작성하면 구현은 되는 것인데, 그러면 중복이 생기게 되고 워크플로우 코드와 추후의 유지보수가 복잡해진다는 점이 다가오게 되었다.
name: CI - Build & Push to Docker Hub
on:
push:
branches:
- main
paths:
- 'src/backtest/**'
- 'src/news/**'
- 'src/stocks/**'
- 'src/ui/**'
....
이 과정에서 워크플로우 코드를 재사용해서 중복된 워크플로우를 줄이는 방법인 `use`와 `workflow_call`을 알게되었고, 이를 적용함으로써 문제를 해결한 내용을 알아보도록 한다.
use
다른 워크플로우를 호출하는 키워드
동일한 repo의 다른 워크플로우나 다른 이의 repo 속 워크플로우를 호출할 수 있다.
이를 통해 워크플로우를 복사해서 기존 워크플로우로 가져오는 대신에, 이미 작성되어 있는 워크플로우를 호출함으로써 사용할 수 있게 된다.
jobs:
call-workflow-passing-data:
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
with:
config-path: .github/labeler.yml
workflow_call
다른 워크플로우로부터 호출될 수 있는 키워드
워크플로우 파일에서 on 절에 workflow_call을 작성함으로써 "이 워크플로우는 다른 워크플로우로부터 트리거 된다"를 선언하게 된다.
공식 Docs에서는 다른 워크플로우를 호출하는 것은 Caller, 호출당한 것은 Called workflow로 구분한다.
on:
workflow_call:
함수처럼 Called 워크플로우에서 사용할 입력값이나 Secret 값들을 정의할 수 있다.
on:
workflow_call:
inputs:
config-path:
required: true
type: string
secrets:
personal_access_token:
required: true
사용자와 재사용할 워크플로우에 접근할 권한이 있는 사용자들이 다른 워크플로우에서 호출할 수 있다.
이러한 워크플로우들의 연결은 최대 4단계 깊이까지만 지원하며, 단일 워크플로우 파일에서는 최대 20개까지 호출할 수 있는 제한이 존재한다.
워크플로우 호출과 재사용
이를 이용해서 나의 상황에 맞게 아래와 같이 repo에서 서비스 별로 변경점을 감지하는 워크플로우와 해당 서비스를 빌드하는 워크플로우로 파일을 분리했다.
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
backtest: ${{ steps.changes.outputs.backtest }}
news: ${{ steps.changes.outputs.news }}
stocks: ${{ steps.changes.outputs.stocks }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
# 변경점이 발생한 서비스만 파악
- name: Detect changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
backtest:
- 'src/backtest/**'
news:
- 'src/news/service/**'
stocks:
- 'src/stocks/app/**'
build-backtest:
needs: detect-changes
if: needs.detect-changes.outputs.backtest == 'true'
uses: ./.github/workflows/build.yml
with:
service: backtest
build-news:
needs: detect-changes
if: needs.detect-changes.outputs.news == 'true'
uses: ./.github/workflows/build.yml
with:
service: news
build-stocks:
needs: detect-changes
if: needs.detect-changes.outputs.stocks == 'true'
uses: ./.github/workflows/build.yml
with:
service: stocks
그리고 Called 워크플로우에서는 Caller 워크플로우로부터 전달받은 input(여기서는 빌드할 서비스의 종류 이름)을 바탕으로 서비스 이미지를 빌드하도록 구성할 수 있었다.
name: Build & Push Docker Image
# '이 workflow는 다른 workflow에서 호출되어서 실행된다'를 선언
on:
workflow_call:
inputs:
service:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
env:
TAG: ${{ github.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
...
# 빌드 context (서비스 or scraper)
# inputs.service에 따라 해당하는 서비스 이미지 빌드
- name: Set Build Context
id: ctx
run: |
echo "name=kubestock-${{ inputs.service }}" >> $GITHUB_OUTPUT
echo "path=src/${{ inputs.service }}/service" >> $GITHUB_OUTPUT
echo "kube_path=deploy/kubernetes/${{ inputs.service }}/deployment.yml" >> $GITHUB_OUTPUT
# 이미지 빌드 & push
- name: Build and Push Docker Image
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ steps.ctx.outputs.name }}:${{ steps.tag.outputs.sha_short }} \
-f ${{ steps.ctx.outputs.path }}/Dockerfile ${{ steps.ctx.outputs.path }}
docker push ${{ secrets.DOCKER_USERNAME }}/${{ steps.ctx.outputs.name }}:${{ steps.tag.outputs.sha_short }}
Issue #1 - 중첩된 워크플로우에서의 Permission
이렇게 설정한 워크플로우 구조에서 동작 테스트를 위해 Application 코드 변경 커밋과 push를 생성했다.
하지만, 정작 workflow부터 트리거가 되지않았고, 확인해보니 워크플로우 간 권한 문제가 있어서 트리거되지 않은 것 처럼 표기되었던 것이다.
이렇게 하위(Called) 워크플로우에서 git push하는 과정이 추가적인 Permission이 필요한 상태였다.
# Manifest push
- name: Commit & Push Manifest
working-directory: deploy/kubernetes/${{ inputs.service }}
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
if [[ "${{ inputs.scraper }}" == "true" ]]; then
git commit -m "Update ${{ inputs.service }}-scraper image tag to ${{ steps.tag.outputs.sha_short }}"
else
git commit -m "Update ${{ inputs.service }} image tag to ${{ steps.tag.outputs.sha_short }}"
fi
git push origin main
Caller 워크플로우가 호출할때 추가적인 권한을 주지 않아서 Called 워크플로우가 읽기 권한만 기본적으로 가져가고, 여기에서 실행이 막힌 것을 알 수 있었다.
build-backtest:
needs: detect-changes
permissions:
contents: write # 워크플로우 호출 시, 추가 권한 지정
if: needs.detect-changes.outputs.backtest == 'true'
uses: ./.github/workflows/build.yml
with:
service: backtest
Issue #2 - 중첩된 워크플로우에서의 Secret
권한까지 확인한 뒤에 워크플로우를 다시 트리거 했지만, 이번에는 Called workflow에서 docker hub에 로그인하지 못하는 것을 발견했다.
분명 secret에 username과 password를 넣어주었지만, 로그 상에서 username과 password값이 전달되지 않은 모습을 볼 수 있었다.
디버깅 결과, 역시나 Called workflow가 Secret 값을 참조하지 못해서 비어있는 것을 확인할 수 있었다.
중첩된 워크플로우에서는 Caller workflow가 다른 워크플로우를 호출할때, secret을 명시적으로 전달해주어야한다는 것을 Docs에서 찾게 되었다.
이러한 Secret 값 전달에는 `inherit` 키워드를 통해 모두 한번에 전달하는 방법과 전달할 값만 지정해서 넘겨주는 방법이 존재한다.
이렇게 workflow_call을 바탕으로 워크플로우를 재사용하며 불필요한 코드를 줄일 수 있었다.
권한이나 Secrets 전달과 같은 몇가지 이슈들이 있었지만, 병렬로 실행될 수 있는 빌드 자동화 파이프라인을 구성해 볼 수 있었다.