Github Ops 로 Mono Repo 배포를 더욱 쉽게

Github Ops 로 Mono Repo 배포를 더욱 쉽게

하나의 레포에서 여러 컴포넌트 배포, 어떻게 더 잘할 수 있을까?

Changkyu Kim — 김창규 @glenn-kim
안녕하세요. 데이터 파이프라인 팀에서 광고 데이터 처리를 담당하는 김창규입니다.
우리 팀에서는 목적에 따라 여러 컴포넌트를 유지보수하고 있는데, 가능한 언어별로 하나의 깃 레포지토리(Mono Repo)로 관리합니다. Mono Repo를 채택하는 결정에는 여러 이유가 있겠지만, 변경이 드문 컴포넌트를 오랫동안 방치되지 않도록 잘 관리하고자 하였습니다.
1년 이상 변경이 없었던 컴포넌트는 언어와 의존성 버전이 오래된 버전으로 남아있고, 심지어 CI/CD 또한 옛날 그대로 남아있어 수정을 위해서는 컴포넌트의 모든 것을 최신화해야 하는 문제가 종종 발생했습니다. 아무래도 눈에서 멀어지면 관심이 덜 가는 게 사람의 마음이기 때문에, 자주 챙기지 못하는 컴포넌트를 Mono Repo로 구성하여 자주 변경되는 컴포넌트를 챙길 때 한 번이라도 더 챙기자는 의미에서 Mono Repo로 구성하여 사용 중입니다.
그런데 Mono Repo로 구성하고 보니, 기존에 만들어두었던 파이프라인들이 MonoRepo 환경에 도입하자니, 방식마다 문제점이 있어 이번 기회에 모든 배포 경험을 통일도 할 겸 더 나은 Practice를 탐색하게 되었습니다.

기존 배포 파이프라인에 대한 탐구

저희는 CI/CD를 Github Actions로 옮긴 이래로 여러 버전의 배포 파이프라인 버전이 있었습니다. Mono Repo에 적용했던 버전도 있고, 단일레포에서 적용했던 버전도 있었는데, 각 방식의 장점과 단점을 탐구해 봅니다.

방법1: 특정 브렌치에 push 되면 배포

Git을 통해 배포하는 가장 기본적인 방식입니다. 환경별로 특정 브렌치가 있고, 그 브렌치에 push가 되면 배포 파이프라인이 트리거 되고, 매핑된 환경으로 배포되는 방식입니다.
보통은 main브렌치는 production환경, dev 브렌치는 dev환경으로 배포되고, 컴포넌트에 따라 staging, qa 환경 등이 추가로 존재하는 경우가 있습니다.
Mono Repo에 적용했을경우, push 가 발생했을때 변경된 코드에 따라 배포가 필요한 컴포넌트를 판단하여 필요한 컴포넌트만 배포하도록 구성 할 수 있습니다.

장점

branch의 head 가 환경의 배포된 버전의 코드를 잘 반영합니다.

단점

공유하는 코드가 변경되었을때 관련 컴포넌트가 일시에 배포됩니다.
배포 순서가 중요하거나, 하나씩 배포 후 모니터링이 필요한 경우 컨트롤하기 어렵습니다.
공유하는 코드가 변경되어도 배포가 필요 없는 경우(예: 파일은 참조하지만, 변경이랑 관련 없는 경우)에는 불필요한 배포가 일어날 수 있습니다.
배포를 롤백하기 어렵습니다.
code를 revert 하여 배포하거나, 이전 배포된 이미지를 찾아 수동으로 배포해 주어야 합니다.

방법2: main branch에서 Github Release를 생성하여 배포

Github에서는 git tag에 release를 생성할 수 있습니다. 생성할 때에 release 노트에 어떤 점에 변경되었는지 작성이 가능합니다. release 생성을 Github Actions의 트리거로 사용하여 배포 파이프라인을 실행할 수 있는데, 저희는 다음과 같은 워크플로우를 구성했습니다.
1.
release 가 생성된 태그로 Docker Image를 생성
2.
ArgoCD API를 통해 Prod에 image tag 파라미터만 변경 후 생성자에게 Slack 노티
3.
생성자는 배포를 원하는 시간에 ArgoCD에서 Sync를 눌러 배포
또한 actions의 workflow_dispatch 트리거를 이용하여 생성된 릴리즈를 특정환경에 직접 배포할 수 있도록 작성하였습니다. 과거의 버전으로 다시 릴리즈할 수 있는것을 다시 말해 stable 버전으로 롤백이 가능하다는 뜻입니다.

장점

어떤 버전이 어떤 변경 사항을 가지고 있고, 어떤 변경이 있었는지 문서화가 잘 됩니다.
롤백을 git push 없이 할 수 있습니다
여러 변경 사항을 한꺼번에 배포할 수 있습니다.

단점

merge후, 릴리즈 노트 작성하여 릴리즈를 생성하는 것이 매우 번거롭습니다.
여러개의 컴포넌트가 있을때에는 release 생성 과 배포기준이 모호해집니다.
하나의 버전을 만들어 모든 컴포넌트에 배포 → 변경사항 없는데 불필요한 배포 발생
컴포넌트마다 다른 버전규칙 (예: prefix)을 만들지 않고 배포 → 릴리즈 생성을 여러번 반복하여 더 번거로움
main branch head 와 production 배포버전의 불일치가 발생할 수 있습니다.

방법3: Pull request에 comment를 달아 배포후 main branch merge

Github Actions를 이용하여 Issue Ops를 통한 배포방식입니다. Github에서 제공하는 액션을 이용하는데, 본문의 내용을 요약하면 다음과 같습니다.
Issue Ops 는 일종의 ChatOps 와 비슷한건데, Github Issue에서 댓글로 하는 Ops를 의미합니다. (PR은 Issue의 한 종류로 취급됨)
Branch Deploy 는 머지가 되기 전의 변경사항을 production에 배포 후 머지하는 방식을 의미합니다.
main branch 는 stable 하고 배포가능한 branch입니다.
main branch에 머지 되기전에 모든 변경사항은 배포되어야합니다.
롤백 할때에는 main branch를 배포할 수 있습니다.
merge후 배포를 롤백하기 위해서는 revert 하고, 테스트, 빌드, 배포가 되어야하는것에 비해 branch deploy는 main이 stable 하기 때문에 테스트 모든 과정을 생략하고 배포만 할 수 있습니다.

장점

배포 후 문제가 없는것을 확인 한 이후 main branch에 머지할 수 있습니다.
배포가 실패하거나, 배포 후 초기 문제 발견시 PR에 이어서 수정 후 배포할 수 있습니다.
방법 1에서는 배포 후 바로 문제가 생길 경우 별로의 핫픽스를 준비해야합니다.

단점

하나의 레포에 여러 컴포넌트가 있을때 무엇을 배포해야할지 결정이 어렵습니다.
배포가 오래 걸릴경우, merge를 잊어 버리는경우가 있을 수 있습니다.

우리 팀에서 생각하는 배포 파이프라인의 핵심 가치

여러 배포방식을 경험 했을때 우리 팀에서는 다음의 가치는 모두 챙겨졌으면 좋겠다는 의견이 있었습니다.
release(tag)을 생성하여 versioning을하고, 해당버전으로 빠르게 롤백(재배포) 할 수 있다
IssueOps를 통해 배포할 수 있다
배포할 때에는 꼭 배포가 필요한 컴포넌트만 배포할 수 있어야 한다.
여러 컴포넌트를 배포해야할 때 하나씩 배포할 수 있어야한다.

배포 경험 설계 그리고 구현

먼저 구현된 Github Ops을 소개합니다
배포를 하기 위해서는 다음의 절차로 이루어집니다.
1.
PR을 작성합니다. Body의 내용은 어떤 변경점이 있는지 상세히 기록합니다.
2.
PR에서 lint 와 test 가 통과하길 기다립니다.
3.
PR에 comment로 .deploy <component_code>_<환경>을 남깁니다. (예: .deploy pp_dev )
4.
comment를 남기면 Github Actions 가 해당 컴포넌트의 해당 환경에 배포를 진행합니다.
PR의 변경사항에 따라 꼭 배포가 필요한 환경이 있습니다.
label에 배포가 필요한 환경 목록이 표시되고 comment로 배포 현황이 표시됩니다.
모든 배포가 완료되면 merge가 허용됩니다.
만약 작성자의 판단하에 굳이 배포하지 않아도 되는 환경은 label 제거를 통해 요구사항에서 제외할 수 있습니다.
요구하는 모든 환경을 배포한 뒤에는 머지 할 수 있습니다.
머지하게 되면 마지막 배포버전으로 릴리즈가 생성됩니다.
해당릴리즈는 Manual Deploy 워크플로우를 통해 언제든지 다시 배포할 수 있습니다.
(과거 릴리즈로 롤백할 수 있습니다)
머지가 되면 Dev환경에도 최신 버전이 함께 배포가 됩니다.

어떤 고민으로 이렇게 만들었을까

저희는 branch-deploy action를 토대로 배포 파이프라인을 구성하기로 했습니다. branch-deploy는 여러개의 environment를 골라서 배포할 수 있는데, 이 environment 목록에 “production” “dev” 가 아닌 component의 목록을 넣어서 배포 타깃을 정할 수 있겠다는 생각이 들었습니다. 예를들어 componentA-prod componentA-dev componentB-prod componentB-dev 가 목록으로 있고, 배포하고자 하는 component와 environment를 골라서 .deploy componentA-prod 식으로 PR에 comment를 남기면 Github Actions 가 해당 환경의 컴포넌트를 배포 해 줍니다.
name: on-deploy-comment on: issue_comment: types: [created] permissions: pull-requests: write deployments: write contents: write checks: read statuses: read jobs: trigger: if: ${{ github.event.issue.pull_request }} # only run on pull request comments runs-on: self-hosted steps: - uses: github/branch-deploy@v9.8.1 id: branch-deploy with: environment: componentA-dev # .deploy 만 입력했을때 배포되는 환경 environment_targets: componentA-dev,componentB-dev,componentA-prod,component-B-prod # 배포 가능한 환경을 열거 해 줍니다. production_environments: componentA-prod,component-B-prod - uses: ./.github/action/deploy-componentA if: steps.branch-deploy.outputs.environment == 'componentA-prod' with: environment: prod - uses: ./.github/action/deploy-componentB if: steps.branch-deploy.outputs.environment == 'componentB-prod' with: environment: prod ...
YAML
복사
이렇게 를 이용하여 workflow를 구현한다면 comment에 따라 다양한 컴포넌트와 환경에 배포할 수 있도록 구성할 수 있습니다.

다른 두개의 PR에서 동시에 production에 배포를 한다면?

branch deploy를 이용한다면, 자동으로 환경에 대해 lock을 걸어주게 됩니다. (Github 자체를 state저장소로 사용해서 작동합니다) 기본 락은 “배포가 진행되는 동안”에만 lock을 걸어 동일한 환경에 다른 PR에서 배포할 수 없도록 막는 역할을 하고 배포가 종료되면 lock을 해제 하게 됩니다.
그렇다면 하나의 환경에 서로 다른 두가지 PR이 서로 배포하는 상황이 연출 될 수 있습니다. 이 문제를 해결하기 위해 branch deploy 는 sticky_locks 옵션을 제공합니다.
이 옵션을 활성화 한다면, workflow 가 종료 될 때 unlock을 해 주는 스텝을 건너뛰게 되어 계속해서 lock이 남아있게 됩니다. 이 lock 은 .unlock <environment> comment를 달아서 수동으로 해제를 시켜줄 수 있지만, PR 이 main branch에 merge 될 때 자동으로 해제되는것이 가장 좋을 경험이 될것입니다. 이 문서는 PR이 닫혔을때 모든 lock을 해제하는 방법을 소개합니다. 별도의 workflow를 분리하여 동일하게 branch-deploy를 사용하여 unlock 할 수 있습니다.
name: Unlock On Merge on: pull_request: types: [closed] permissions: contents: write jobs: unlock-on-merge: runs-on: ubuntu-latest # Gate this job to only run when the pull request is merged (not closed) # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#running-your-pull_request-workflow-when-a-pull-request-merges if: github.event.pull_request.merged == true steps: - name: unlock on merge uses: github/branch-deploy@vX.X.X id: unlock-on-merge with: unlock_on_merge_mode: "true" # <-- indicates that this is the "Unlock on Merge Mode" workflow
YAML
복사

Github Environment를 이용하여 변수를 가져다 쓰도록 만들자

다양한 컴포넌트 구성할때 마다 각각의 스크립트를 만들게 된다면 workflow 파일이 매우 거대해 질것입니다.
다행히 Github에서 environment를 사용한다면, environment에 환경변수와 Secret을 넣을 수 있습니다.
각 환경마다 달라지는 빌드 변수들을 environment 에 넣어 동일한 workflow로 빌드 및 배포를 하도록 할 수 있습니다.
저희는 environment에 다음과 같은 값을들 넣어서 활용하고 있습니다.
프로젝트 코드값 - release 생성 할 때 prefix로 붙이기 위함
Docker build args 값
ECR Repository 이름
ArgoCD Application 이름
배포 알림을 보낼 Slack 채널 이름
배포 알림을 보낼때 사용할 프로필 이름
배포 알림을 보낼때 사용할 프로필 사진 URL
jobs: trigger: if: ${{ github.event.issue.pull_request }} # only run on pull request comments runs-on: self-hosted outputs: continue: ${{ steps.branch-deploy.outputs.continue }} noop: ${{ steps.branch-deploy.outputs.noop }} environment: ${{ steps.branch-deploy.outputs.environment }} steps: - uses: github/branch-deploy@v9.8.1 id: branch-deploy with: environment: componentA-dev environment_targets: componentA-dev,componentB-dev,componentA-prod,component-B-prod deploy: if: ${{ needs.trigger.outputs.continue == 'true' && needs.trigger.outputs.noop != 'true' }} needs: ["trigger"] environment: ${{ needs.trigger.outputs.environment }} steps: - uses: ./.github/action/deploy with: target: ${{var.TARGET}} environment: ${{var.ENVIRONMENT}}
Shell
복사
그런데 이렇게 작성하게 되면 문제가 하나 발생합니다. deploy는 완료되지 않았는데, PR에는 배포가 성공적으로 완료 되었다고 표시가 되는 의아한 상황이 발생합니다.
그 이유는 branch-deploy의 작동방식를 잘 살펴봐야하는데, Github Actions의 step 은 정의된 순서에 한번, job 이 종료될 때 finalizer로 한번(optional) 실행됩니다. branch-deploy는 첫 실행때 comment의 내용을 보고 진행 여부를 판단하고, job 이 종료될때 정상적으로 종료되었다면 정상종료 comment를 달아주게 됩니다.
하지만 위 예시에서는 branch deploy 는 trigger job에 속해있고, 실제 배포는 deploy job에서 진행하고 있으니, branch-deploy 가 성공했다고 comment를 단 뒤 진짜 deploy 가 실행되게 됩니다.
이 문제를 해결하기 위해서 branch deploy 문서의 (Advanced) Multi Job Example를 참고 할 수 있습니다. 요약하자면 branch deploy 파라미터중 skip_completing 가 있는데, 이것을 ‘true’로 설정하면, branch-deploy 의 finalize 작업을 진행하지 않게 됩니다. 그리고 모든 job 이 종료 된 이후 수동으로 comment를 달아주고 lock을 해제해주는 작업을 추가합니다.
jobs: trigger: if: ${{ github.event.issue.pull_request }} # only run on pull request comments runs-on: self-hosted outputs: continue: ${{ steps.branch-deploy.outputs.continue }} noop: ${{ steps.branch-deploy.outputs.noop }} environment: ${{ steps.branch-deploy.outputs.environment }} ... steps: - uses: github/branch-deploy@v9.8.1 id: branch-deploy with: environment: componentA-dev environment_targets: componentA-dev,componentB-dev,componentA-prod,component-B-prod deploy: if: ${{ needs.trigger.outputs.continue == 'true' && needs.trigger.outputs.noop != 'true' }} needs: ["trigger"] runs-on: self-hosted environment: ${{ needs.trigger.outputs.environment }} steps: - uses: ./.github/action/deploy with: target: ${{var.TARGET}} environment: ${{var.ENVIRONMENT}} result: needs: [trigger, deploy] if: ${{ always() && needs.trigger.outputs.continue == 'true' }} runs-on: self-hosted steps: - ... # example에서 복사,붙여넣기 하면 됩니다.
Shell
복사
multi job 으로 나누지 않더라도 Github API를 사용하면 environment의 variable에 접근가능합니다. 아래의 스크립트로 env에 추가하여 사용할 수 있습니다.
gh api "repos/{owner}/{repo}/environments/<environment>/variables" \ -q '.variables[] | .name, .value' \ | xargs printf '%s=%s\n' >> $GITHUB_ENV
Shell
복사
하지만 이 방법은 2024년 현재 아직 Github Action에서 사용하기 어려운데, 해당 API는 variable scope 의 read 권한이 있어야 하는데, Github Actions에서 제공하는 permission에는 variable을 취득하도록 허용하고 있지 않기때문입니다.
권한이 있는 유저의 PAT를 활용하거나, GithubApp의 Token을 GH_TOKEN 으로 사용한다면 사용이 가능은 합니다.

어떻게 배포가 필요한 환경인지 구분할것인가

방법 1에서는 Github Actions trigger를 통해 컴포넌트별로 다른 trigger를 통해 특정 파일 변경을 배포할 수 있었습니다. 아래는 workflow 예시입니다.
name: '[ComponentA] Deploy On Merge' on: push: branches: - main paths: - 'cmd/component-a/**' - 'pkg/**' - 'go.mod' - 'go.sum' --- name: '[ComponentB] Deploy On Merge' on: push: branches: - main paths: - 'cmd/component-b/**' - 'pkg/pkg-only-b-using/**' - 'go.mod' - 'go.sum'
YAML
복사
하지만 comment로 여러 컴포넌트를 배포하는 이번 practice에서는 무엇을 배포 해야하는지 인지하기 어렵습니다. 그래서 어떤 배포가 필요한지 인지하기 위해서 Github Actions로 PR에 label을 달아 어떤 component가 배포가 되어야하는지 작성자에게 알려주고, 또한 PR 목록에서 PR이 어떤 component를 변경했는지 알려주는 역할을 하게 만들고자 했습니다.
그것을 구현하기 위해 labeler 라는 actions을 활용하였습니다. actions/labeler 는 코드에 저장된 규칙파일을 읽어 PR에 자동으로 label을 달아주는 action 입니다.
# .github/labeler.yml deploy/component-a: - changed-files: - any-glob-to-any-file: - 'cmd/component-a/**' - 'pkg/**' - 'go.mod' - 'go.sum' deploy/component-b: - changed-files: - any-glob-to-any-file: - 'cmd/component-b/**' - 'pkg/pkg-only-b-using/**' - 'go.mod' - 'go.sum'
YAML
복사
# .github/workflows/labler name: "PR Labeler" on: - pull_request jobs: labeler: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v5
YAML
복사
위와 같이 작성한다면 PR에 push 하거나 변경될 때 마다 label이 갱신되어 어떤 컴포넌트가 배포되어야 하는지 label을 통해 확인할 수 있습니다.

배포가 필요한 환경이 배포가 되지 않았을때 어떻게 Merge를 막을 수 있을까

위 과정을 통해 어떤 환경을 배포해야하는지 label을 통해 식별 할 수 있게되었습니다. 필요한 모든 환경을 배포했을 때에만 main에 merge 하도록 강제 하려면 어떻게 해야할까요
Github에서는 Settings > Rules에서 branch merge 규칙을 정할 수 있습니다. 여기에서 merge를 하기 위해 PR Review 조건, Checks 통과조건, Deployment 성공조건 등을 추가할 수 있습니다. 하지만 Deployment 조건은 PR마다 다르게 설정할 수 있는게 아니라 모든 PR에 대해 적용되는 규칙이다보니, 배포 요구사항에 따라 동적으로 변경할 수 없어 componentA-prod 만 배포되어야하는 PR과 compoenentB-prod 만 배포되어야 하는 PR 두가지를 커버 할 수 없습니다.
그래서 저희는 Github Actions workflow로 요구되는 모든 환경에 성공적으로 배포가 되었는지 확인되면 merge를 허용해주고자 했습니다. 아래는 배포 요구사항을 체크하는 workflow job 코드입니다.
Github cli를 사용하여 pr의 label을 읽어 요구사항을 확인하고, deployment api를 이용하여 마지막 commit에 성공한 deployment 목록을 불러와 비교합니다.
name: check-deploy-requirement on: pull_request: types: [ opened, synchronize, unlabeled ] permissions: contents: read pull-requests: write deployments: write env: GH_TOKEN: ${{ github.token }} jobs: check-deploy-requirement: runs-on: self-hosted steps: # PR의 label로 부터 배포가 필요한 환경목록을 불러옵니다 - name: check label id: check-label run: | target_pr="${{ github.event.pull_request.number }}" required_env="" while read label; do env=$(echo $label | cut -d/ -f2) if [[ -z $required_env ]]; then required_env=$env else required_env="$required_env,$env" fi done < <(gh pr view $target_pr --repo ${{github.repository}} --json labels -q '.labels[] | .name' | grep "^deploy/") echo "required :: $required_env" echo "required_env=$required_env" >> $GITHUB_OUTPUT # deployment 상태를 체크합니다 - name: check deployment id: check-deploy-requirement run: | result_comment="" failed=false target_sha="${{ github.event.pull_request.head.sha }}" deployed_env=$(gh api "repos/${{github.repository}}/deployments?sha=$target_sha" | jq -r '.[] | .environment') for env in $(echo "${{ steps.check-label.outputs.required_env }}" | tr ',' '\n'); do if [[ -z $(echo $deployed_env | grep $env) ]]; then echo "$env not deployed" result_comment="$result_comment- \`${env}\` not deployed \n" failed=true else echo "$env :white_check_mark: deployed" result_comment="$result_comment- \`${env}\` :white_check_mark: deployed\n" fi done echo "result_comment<<EOF" >> $GITHUB_OUTPUT echo -e $result_comment >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT if [[ $failed == true ]]; then exit 1 fi echo "all required env deployed" # required env 가 배포 되었는지 댓글로 알려줍니다. - name: comment on the pull request if : always() uses: hasura/comment-progress@v2.3.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} number: ${{ steps.get-pr-number.outputs.pr-number }} id: check-deploy-requirement recreate: true message: | ## Deploy Requirement Check :white_check_mark: required env : ${{ steps.check-label.outputs.required_env || 'none' }} ${{ steps.check-deploy-requirement.outputs.result_comment }}
YAML
복사
이제 모든 환경이 배포되었는지 체크를 할 수 있게 되었습니다. 이제 체크에 실패했을때 PR merge 만 막을 수 있으면 됩니다.
PR의 merge를 막을 수 있는 방법은 크게 Checks를 이용하는 방법과 Deployment 가 성공했는지 여부를 보는것 두가지 입니다. Checks 는 마지막 commit이 test, lint 등을 성공했을때 actions 혹은 api를 통해 성공을 기록 해 줄 수 있고, 특정 check를 required로 두어 해당 check 가 성공했을때에만 merge 가 가능하도록 제한할 수 있습니다. 이 경우에는 check-deploy-requirement job을 required로 두고, 이 job이 성공해야지 merge 하도록 막을 수 있습니다.
하지만 이 기능은 branch-deploy 와 함께 사용하기에는 적절하지 않았습니다. branch-deploy 가 배포를 진행해도 되는지 여부를 판단하기 위해서 사용하는 정보 중 하나가 checks 가 모두 통과 했는지 여부인데, 배포 완료 체크를 check로 만들게 되면 check를 하기 위해 deploy 가 필요하고 deploy를 위해 check 가 필요한 순환오류에 빠지게 됩니다.
그래서 저희는 deployment로 merge를 막기로 했습니다. 가상의 environment인 deployment-check를 생성하여 required로 설정하고, 모든 환경이 배포되었을때 api를 통해 api로 성공한 deployment를 생성 해 주었습니다.
- name: check deployment id: check-deploy-requirement run: | ... if [[ $failed == true ]]; then exit 0 fi echo "all required env deployed" # deployment를 deployment-check환경에 대해 생성합니다. deploy_id=$(gh api --method POST repos/${{github.repository}}/deployments -q '.id' \ -f ref=$target_sha \ -f environment=deployment-check \ -F auto_merge=false \ -F 'required_contexts[]' \ -f description="all required env deployed" ) # deployment의 상태를 성공으로 변경합니다. gh api --method POST repos/${{github.repository}}/deployments/$deploy_id/statuses \ -f state=success \ -f description="all required env deployed"
YAML
복사
추가적으로 위 check deployment를 PR 갱신과 comment로 배포된 이후에 각각 호출해 주게 된다면, PR label에 달려있는 모든 환경이 배포 될때 deployment-check 도 배포 완료가 되고, 정상적으로 merge 할 수 있도록 merge 버튼이 활성화 됩니다

Branch-deploy를 활용해서 어떻게 Release를 생성하고 롤백할 수 있을까

이제 거의 다 되었습니다! 마지막으로 남은 기능은 버저닝을 해두고 언제든지 특정버전으로 배포할 수 있게 만들기만 하면 됩니다.
먼저 버저닝 규칙부터 정하였습니다. semantic version (v1.0.0 같은것) 은 자동화된 versioning에 적절하지 않기 때문에 저희는 단순한 시간 기반 버저닝을 사용하기로했습니다 v{yyyymmddHH} 형식으로 만들고, 동일 시간에 여러번의 버전이 있다면 v{yyyymmddHH}.{idx}로 두어 서로 겹치지 않도록 했습니다. 여러 컴포넌트를 하나의 레포에서 지원해야하기 때문에 {component code}_v{yyyymmddHH} 형식으로 realease(git tag)를 생성하여 구분하였습니다.
아래는 태그를 만들고 해당 태그로 docker build 및 image 빌드, release 생성, argocd deploy 하는 step입니다.
... jobs: release: steps: ... # 태그명을 준비합니다. - name: tag-prepare id: tag-prepare # todo check if image exists, add idx run: | base_tag=v$(date +'%Y%m%d%H') idx=0 tag_name=$base_tag while git rev-parse "${{ env.TARGET_CODE }}_$tag_name" >/dev/null 2>&1; do # 중복되는 태그가 있을 경우 .idx를 붙여서 새로운 태그 이름 생성 idx=$((idx + 1)) tag_name="${base_tag}.${idx}" done echo "dkr-release-version=$tag_name" >> $GITHUB_OUTPUT echo "gh-release-version=${{ env.TARGET_CODE }}_$tag_name" >> $GITHUB_OUTPUT - name: build-and-push id: build-and-push uses: docker/build-push-action@v2 with: push: true tags: ${{ env.ECR_REPOSITORY }}:${{ steps.tag-prepare.outputs.dkr-release-version }} ... - name: make release id: make-release env: GH_TOKEN: ${{ github.token }} run: | # release body를 PR body를 이용해 작성 gh pr view ${{inputs.ref}} --json title,body -t '# {{.title}} {{.body}} ' | sed '/<!--- checklist --->/,$d' >> note.md echo '---' >> note.md echo "* \`${{env.ECR_REPOSITORY}}:${{steps.tag-prepare.outputs.dkr-release-version }}\`" >> note.md # release를 prerelase로 생성 gh release create ${{ steps.tag-prepare.outputs.gh-release-version }} \ --title "${{ steps.tag-prepare.outputs.gh-release-version }}" \ --notes-file note.md \ --target "${{ needs.trigger.outputs.sha }}" \ --prerelease - name: Deploy to ArgoCD uses: ./.github/actions/argocd-deploy id: argocd_deploy with: image-tag: ${{ steps.tag-prepare.outputs.dkr-release-version }} argocd-application: ${{ env.ARGOCD_APPLICATION }}
YAML
복사
branch deploy를 통해 deploy를 하다보면 하나의 PR에 여러 버전이 생길 수 있습니다만 master에 머지가 되는 버전은 컴포넌트 별로 하나가 되면 좋겠죠.
그렇게 하기 위해 최초에 release를 생성을 할 때에 prerelease로 작성하고 PR이 merge 가 되었을때 마지막 prerelease 만 release로 승격 시키고 나머지는 삭제해 주는 방법을 채택하였습니다.
on: pull_request: types: [closed] permissions: contents: write jobs: release-latest: runs-on: self-hosted-arm64-nano env: GH_TOKEN: ${{ github.token }} steps: # 모든 git 히스토리를 가져옵니다. - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: release latest if: github.event.pull_request.merged == true id: release-latest run: | # 마지막 커밋에 있는 tag를 찾아내 release를 수정해 줍니다. git tag --contains ${{ github.event.pull_request.head.sha }} | while read tag; do echo "release $tag as latest" gh release edit \ --prerelease=false \ --draft=false \ $tag done - name: remove unreleased id: remove-unreleased run: | # PR에 남아있는 prerelease들을 모두 제거해줍니다. first_sha=$(git rev-list ${{github.event.pull_request.base.sha}}..${{github.event.pull_request.head.sha}} --reverse | head -n 1) git tag --contains $first_sha | while read tag; do if [[ -n $(gh release view $tag --json isDraft,isPrerelease,name -q'select(.isDraft or .isPrerelease)') ]]; then echo "delete $tag" gh release delete --cleanup-tag -y $tag || true fi done
YAML
복사
이제 release 가 생성 되었음 으로, 롤백을 할때 과거에 생성된 release를 기준으로 workflow_dispatch 트리거를 이용해 배포를 수동으로 실행 시킬 수 있습니다
name: Manual Deploy on: workflow_dispatch: inputs: environment: description: environment name to deploy to required: true type: environment environment_confirm: description: "[confirm] environment name to deploy to" required: true type: string jobs: deploy: env: TARGET_CODE: ${{ vars.TARGET_CODE }} runs-on: self-hosted environment: ${{ inputs.environment }} steps: # 실수를 방지하기 위해 confirm 절차를 추가하였습니다. - name: Check Environment Input if: github.event_name == 'workflow_dispatch' run: | if [[ '${{ inputs.environment }}' != '${{ inputs.environment_confirm }}' ]]; then exit 1 fi # release로 실행된게 맞는지확인하고, release tag를 output으로 저장합니다. - id: tag name: tag check run: | if [[ "$GITHUB_REF" == "refs/tags/"* ]]; then tag="$(echo $GITHUB_REF | sed 's/refs\/tags\///')" else tag=${{ inputs.tag }} fi if [[ $tag == "${{ env.TARGET_CODE }}_"* ]]; then echo "tag=$tag" >> $GITHUB_OUTPUT echo "docker_tag=$(echo $tag | sed 's/${{ env.TARGET_CODE }}_//' )" >> $GITHUB_OUTPUT else echo "tag=$tag is not a valid tag for ${{ env.TARGET_CODE }}" exit 1 fi # 알아낸 tag를 기준으로 배포를 시작합니다. - name: manual deploy uses: ./.github/actions/manual-deploy with: tag: ${{ steps.tag.outputs.docker_tag }} environment: ${{ inputs.environment }}
YAML
복사
위와 같이 workflow_dispatch로 trigger를 작성 할 시 아래왜 같이 Actions > Manual Deploy > Run workflow를 눌러 배포할 수 있습니다.
cli를 통해 배포할 수도 있는데, gh cli를 사용하여 다음 명령어로 트리거 시킬 수 있습니다.
gh workflow run <workflow name | workflow id> \ --repo <owner>/<reponame> \ -r <tag-name> \ -f environment=<environment> \ -f environment_confirm=<environment>
YAML
복사

마치며

Github Actions 는 매우 자유도가 높아 다양한 액션을 구성할 수 있습니다. 하지만 디버깅하기 어렵다는 점과 아직 actions에서 사용하기 어려운 beta기능들(deployment 같은) 도 많아 작업이 다소 어려움이 있었습니다.
저희가 원하는 기능을 잘 구현해 둔 action가 별로 없다 보니 shell script 와 Github CLI (gh명령어)를 사용하여 직접 Github REST API를 사용하여 구현한 것이 많았습니다. 이런 practice를 모아서 node 나 python 같은 언어로 하나의 모듈을 새로 만들지 생각도 해 봤지만, Actions 작업을 할 때마다 새로운 기능이 생기고 개선되고 있어 금방 못쓰게 될 것 같다는 생각이 먼저 들었습니다.
하나의 예시로 처음엔 private repo에 정의된 actions를 못 쓰길래 Github OAuth App 토큰을 가져와 다른 레포의 공용 action을 가져다 쓰게 만들었는데 몇 개월 뒤 private repo를 가져다 쓸 수 있도록 풀린 적이 있고, branch-deploy 와 같이 모르던 좋은 action을 발견해 조금 더 편해진 사례가 있습니다.
이 글을 읽게 되시는 분이 언제 이 글을 읽으실지는 모르겠지만, 저희가 한 방법보다 더 근사한 방법이 생겼거나 발견하실 수 있다고 생각합니다. 저희보다 더 근사한 방법을 찾으셨다면, 또한 기술 공유 블로그를 작성해 주시는 건 어떨까요?
ᴡʀɪᴛᴇʀ
Changkyu Kim @glenn-kim Backend Engineer @AB180
유니콘부터 대기업까지 쓰는 제품. 같이 만들어볼래요? 에이비일팔공에서 함께 성장할 다양한 직군의 동료들을 찾고 있어요! → 더 알아보기