Back to blog
Aug 14, 2024
5 min read

배포 GitHub Actions 개선하기

빌드는 한 번에, 코드 변경은 순서대로?

현재 회사에서는 GitHub Actions를 통해 Docker 이미지를 만들고 배포하고 있습니다.
하지만 제가 합류하기 이전에 사용하고 있었던 배포 워크플로우에는 약간의 문제가 있었는데요, 이를 개선한 후기를 적어 보려 합니다.

배경

현재 저희 백엔드 소스 코드는 단일 저장소에 여러 개의 폴더가 있고, 각각의 폴더가 Kubernetes 상에서 독립된 서버로 구동되도록 나뉘어 있습니다. 이들을 독립적으로 배포하기 위해, 기존에는 코드 변경이 일어났을 때 경로를 감지해서 관련 파일이 변경된 경우에만 각각의 워크플로우가 작동되도록 설정되어 있었습니다.

예를 들면, 이러한 파일이 여러 개 있는 것이죠.

name: foo app Actions
on:
push:
branches:
# ...
paths:
- 'apps/foo/**/*'
- 'common/**/*'
jobs:
build-and-push-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
# ...
(Build... Push...)
- name: Checkout manifest repository
# ...
- name: Change deployment.yaml
# ...
- name: Pushes to the repository
# ...

이렇게 사용할 경우 코드 변경에 영향을 받는 서버만 선택적으로 빌드가 가능하고, 각각의 과정이 병렬 실행이 되는 장점이 있습니다. 하지만 워크플로우의 마지막 부분에 이미지 태그 업데이트를 위해 Git Push를 하는데, 이 시점이 겹치면 충돌로 인해 에러가 발생하고, 실패한 워크플로우를 다시 수동으로 돌려야 하는 문제가 있었습니다.

기존 워크플로우

그래서 개선된 워크플로우에서는 선택적 빌드와 병렬성이라는 장점은 가져가면서, Git Push에서의 에러를 없애는 것이 주된 목적이었습니다.

새로운 GitHub Actions 만들기

재사용 워크플로우

사전에 각각의 워크플로우들을 살펴보았을 때, 변수 몇 개만 설정한다면 재사용이 가능해 보였습니다. 워크플로우를 재사용하는 법에는 크게 두 가지가 있는데, 하나는 별도의 저장소에 워크플로우를 구성하는 것이고 다른 하나는 저장소 내에 파일을 두고 참조하여 사용하는 것입니다. 전자의 경우가 확장성 면에서 더 뛰어나지만, 여기서는 하나의 저장소에서만 사용하면 되기 때문에 후자를 선택했습니다.

공통으로 사용할 파일의 대략적인 형태는 다음과 같습니다.

on:
workflow_call:
inputs:
module-name:
required: true
type: string
registry:
required: true
type: string
outputs:
image-tag:
description: Docker image tag
branch:
description: Branch name
secrets:
# ...
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
# ...
(Build... Push...)
- name: Checkout manifest repository
# ...
- name: Change deployment.yaml
# ...
- run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
git switch -c manifest-${{ inputs.module-name }}
git add .
git commit -m "Update ${{ inputs.module-name }} deployment.yaml"
git push origin manifest-${{ inputs.module-name }}

여기서 뒷부분이 살짝 다른데, 그 이유는 이어서 적도록 하겠습니다.

메인 워크플로우

이제 위의 공통 워크플로우를 활용해 전체 배포 프로세스를 구현해야 합니다.
핵심 아이디어는 3가지였습니다.

  1. 선택적 빌드를 위한 필터링

    조건이 없다면 어떤 파일을 변경해도 전체 빌드가 다 돌아갈 테니, 필터링을 해 줄 필요가 있었습니다.
    다행히 dorny/paths-filter 라는 워크플로우를 찾았고, 어떤 모듈이 변경되었는지 판단할 수 있었습니다.

  2. 병렬 처리를 위한 Matrix Strategy

    동일한 워크플로우를 여러 변수를 입력하여 실행하기 위해 Matrix Strategy를 사용했습니다. 기본적으로 병렬로 실행되기 때문에 목적에도 부합했고, 일부 설정을 조건부로 제외하는 것도 가능해서 위에서 얻은 필터 값을 통해 빌드할 대상을 지정할 수 있었습니다. 여러 개의 변수를 정의하고 이를 조합하여 사용하는 것도 가능하지만, 저는 1개의 변수만 필요해서 그렇게 하지 않았습니다.

  3. Git Push 충돌 방지하기

    이 부분이 가장 고민을 많이 했던 부분이었습니다. 일반적인 방법으로는 병렬 프로세스에서 각각 코드 변경을 했을 경우 충돌을 피하기 어려워 보였습니다. 그래서 생각한 방법은 각각의 워크플로우에서 브랜치를 하나씩 만들어 변경사항을 저장하고, 마지막에 이를 모두 합치는 것이었습니다. 이 합치는 단계는 Matrix Strategy가 설정된 단계가 완료된 후 진행되도록 하여 필요한 과정이 모두 실행되었음을 보장하도록 하고, 위에서 구한 필터 조건을 기반으로 생성된 브랜치를 합쳤습니다.

완성된 최종 메인 워크플로우의 형태는 다음과 같습니다.

name: 전체 Workflow
on:
push:
branches:
# ...
paths:
- "apps/**/*"
- "common/**/*"
jobs:
get-filter:
runs-on: ubuntu-latest
outputs:
foo: ${{ steps.filter.outputs.foo }}
bar: ${{ steps.filter.outputs.bar }}
# ...
steps:
- uses: actions/checkout@v2
- uses: dorny/paths-filter@v2
id: filter
with:
base: ${{ github.ref }}
filters: |
...
ci-cd:
permissions:
contents: read
packages: write
needs: [get-filter]
strategy:
matrix:
module:
- foo
- bar
# ...
exclude:
- module: ${{ needs.get-filter.outputs.foo != 'true' && 'foo' }}
- module: ${{ needs.get-filter.outputs.bar != 'true' && 'bar' }}
# ...
uses: ./.github/workflows/common_workflow.yml
with:
module-name: ${{ matrix.module }}
registry: ghcr.io
secrets: inherit
push-manifest:
runs-on: ubuntu-latest
needs: [get-filter, ci-cd]
steps:
- name: Checkout manifest repository
# ...
- run: |
git fetch origin
git branch -a
git checkout main
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
- name: Merge foo branch
if: needs.get-filter.outputs.foo == 'true'
- name: Merge bar branch
if: needs.get-filter.outputs.bar == 'true'
# ...
- name: Push to main branch
# ...

후기

새로운 워크플로우 팀원들의 반응

몇 가지 시행착오를 더 거쳐서, 현재는 충분히 테스트가 완료되었고 운영 환경에서도 저의 워크플로우를 사용하고 있습니다. 팀원들의 반응도 꽤나 좋았습니다.

한도 초과

다만 이번 달 초 GitHub Actions 사용량 한도를 모두 사용해서, 잠시 배포를 멈추게 된 소소한 이슈가 있었습니다. 아무리 생각해도 제 지분이 좀 있는 것 같네요 😅

회사 정보가 들어 있기 때문에 모든 코드를 공개할 수는 없지만, 궁금하신 점은 최대한 답해 드리겠습니다 :)

참고 자료

Tags

  • Business Project
  • DevOps
  • CI/CD
  • GitHub Actions