필자가 개발하고 있는 '개발 한 스푼' 서비스에서는 Github Actions를 활용해 몇 가지 간단한 자동화를 구축해놓았다. 새 버전의 서버를 준비하는 것에서부터 배포 히스토리를 관리하는데까지 드는 공수를 줄이고 휴먼에러를 방지하기 위해 활용되고 있다.
이번 포스트에서는 브랜치 전략에 맞춰 자동화 시스템을 구축한 경험을 공유해보고자 한다.
소개 : 개발 한 스푼 브랜치 전략
개발 한 스푼은 소규모(1~2명)로 운영하고 있다. Git flow와 같은 다수의 개발자가 협업하는 상황에서 필요할 정도의 전략은 현재 상황에서 무게감만 느껴진다고 생각했다. 특히 작업을 병렬로 진행할 가능성이 매우 낮다고 생각하고 상당 부분을 간소화한 브랜치 전략을 사용하였다.
모든 브랜치의 시작점이자 끝점으로 Develop 브랜치를 두고 새 브랜치 생성시 목적에 따라 이름만 다르게 두었다.
- Develop : 모든 브랜치의 시작이자 끝점인 기준 브랜치
- Feature : 새 기능 추가용. Issue Number를 붙인다.
- Release : 새 버전 배포용. 새 버전명을 붙인다.
- Hotfix : 배포된 버전의 에러 수정용. 버전명+Num 을 붙인다.
위 브랜치 전략으로 운영할 때 필연적으로 몇몇 반복과정이 발생하게 된다. 작게는 Release나 Hotfix 브랜치를 생성할 때 버전명을 붙이는 작업이나 크게는 Dev서버나 Prod 서버로 배포, Tag/Release 배포 히스토리를 남기는 작업이 그런 종류이다.
수동으로 작업할 수도 있겠지만 자동화 해달라는 내면의 외침을 외면할 수 없어 Github Actions를 활용해 각 Event 발생 시 작업을 자동화 시켜보기로 했다.
모두 배포와 관련된 자동화로 배포 준비부터 히스토리를 남기는 것까지 총 5가지의 파이프라인으로 구성되어있다.
- Create Release Branch : 릴리즈 브랜치 생성
- Create Hotfix Branch : 핫픽스 브랜치 생성
- Deploy Dev : 데브서버 배포
- Tag & Release : 배포 히스토리 남기기
- Deploy Prod : 프로덕션 서버 배포
하나씩 어떤 방식(How)으로 왜(Why) 만들게 되었는지에 대해 알아보자.
자동화 1 : 새 버전 준비 브랜치 생성
새로운 서버를 배포하기 전 만든 기능을 테스트하기 위해 Release 브랜치를 만든다. 해당 브랜치의 이름에는 배포될 버전명을 함께 가지게 된다.
여기서 버전은 v1.2.3과 같은 형태이다. 추가로 '개발 한 스푼'은 멀티모듈 프로젝트라서 Api, Batch 아티팩트가 존재한다. 따라서, 아티팩트명을 버전의 접두사로 붙여 버전명이 완성된다. 결과적으로 버전명은 `Api-v1.2.3` 의 형태를 가지게 된다.
어떤 아티팩트의 새로운 버전을 만들 때 이전 버전에서 한 단계 올려서 배포한다. patch 버전을 올리면 v1.2.4, minor 버전을 올리면 v1.3.0, major 버전을 올리면 v2.0.0 이 된다.
새롭게 버전을 만들때마다 어떤 버전을 올릴 것인지 선택하고, 이전 버전을 확인하고 버전명을 만든다. 이때 휴면에러가 발생할 가능성이 농후하다. 아티팩트명을 잘못 입력하거나 버전을 잘못 올려서 Release 브랜치를 만들 수도 있다. 첫 번째 자동화는 새로운 버전의 Release 브랜치 생성을 자동화하는 것이다.
아티팩트, 업데이트할 버전만 선택
Release Branch 생성시 아티팩트(Api,Batch) 업데이트 버전(major, minor, patch)만 선택하면 자동으로 이전 버전을 토대로 Release 브랜치가 만들어지도록 구현했다. `create-release-branc.yml` 워크플로우에서 선택 리스트로 두 정보를 받아 브랜치를 만든다. 브랜치 생성은 workflow_dispatch. 버튼을 눌러 생성할 수 있다.
# 새로운 버전의 Release Branch 생성
name: Create Release Branch
on:
workflow_dispatch:
inputs:
artifact:
type: choice
description: 'artifact type'
required: true
default: 'Api'
options:
- 'Api'
- 'Batch'
version-up:
type: choice
description: 'version up type'
required: true
default: 'patch'
options:
- 'major'
- 'minor'
- 'patch'
이벤트로 release 브랜치 생성 워크플로우가 실행되면 다음 순서로 브랜치를 생성한다.
- 최신 Tag 를 가져온다(배포된 버전은 Tag로 기록된다. 자동화 4번에서 설명한다)
- Tag를 기준으로 버전을 올린다.
- 버전을 이용해 Release 브랜치를 생성한다.
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MY_TOKEN }}
fetch-depth: 0
# 최신 Tag 가져오기
- name: Get latest tag
id: get-latest-tag
run: |
latest_tag=$(git describe --tags --abbrev=0 --match "${{ github.event.inputs.artifact }}-v*" 2>/dev/null || echo "")
echo "LATEST_TAG=$latest_tag" >> $GITHUB_OUTPUT
# 최신 Tag 기준으로 버전 업데이트
- name: Version up
id: version-up
run: |
latest_tag=${{ steps.get-latest-tag.outputs.LATEST_TAG }}
if [ -z "$latest_tag" ]; then
new_version="1.0.0"
else
version=$(echo $latest_tag | egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
if [ ${{ github.event.inputs.version-up }} == "major" ]; then
new_version=$(echo $version | awk -F'.' '{printf "%d.%d.%d", $1 + 1, 0, 0}')
elif [ ${{ github.event.inputs.version-up }} == "minor" ]; then
new_version=$(echo $version | awk -F'.' '{printf "%d.%d.%d", $1, $2 + 1, 0}')
else
new_version=$(echo $version | awk -F'.' '{printf "%d.%d.%d", $1, $2, $3 + 1}')
fi
fi
echo "NEW_VERSION=$new_version" >> $GITHUB_OUTPUT
# 새 버전 Release Branch 생성
- name: Create and push release branch
run: |
new_version=${{ steps.version-up.outputs.NEW_VERSION }}
git checkout -b release/${{ github.event.inputs.artifact}}-v$new_version
git push origin release/${{ github.event.inputs.artifact}}-v$new_version
이제 원터치로 간편하게 Release 브랜치를 생성할 수 있게 되었다.
자동화 2 : hotfix 브랜치 생성
Hofix 브랜치는 이미 배포된 버전에서 문제가 발생할 때 만드는 브랜치이다. Api-v1.2.3+1 과 같이 배포된 버전의 뒤에 +fixNum 을 붙여 해당 버전에서 hotfix 되었음을 표시한다.
Release 브랜치 자동화와 마찬가지로 아티팩트명을 input값으로 받으면 Hotfix 브랜치를 생성한다.
- 최신 배포 버전(Tag)를 가져온다.
- Hotfix 버전 생성
- Hotfix 브랜치 생성
# 최신 Tag 기준으로 Hotfix Branch 생성
name: Create Hotfix Branch
on:
workflow_dispatch:
inputs:
artifact:
type: choice
description: 'artifact type'
required: true
default: 'Api'
options:
- 'Api'
- 'Batch'
jobs:
create-branch:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MY_TOKEN }}
fetch-depth: 0
# 최신 Tag 가져오기
- name: Get latest tag
id: get-latest-tag
run: |
latest_tag=$(git describe --tags --abbrev=0 --match "${{ github.event.inputs.artifact }}-v*")
echo "LATEST_TAG=$latest_tag" >> $GITHUB_OUTPUT
# Hotfix 용 버전 생성
- name: Create new hotfix version
id: create-new-version
run: |
tag=${{ steps.get-latest-tag.outputs.LATEST_TAG }}
IFS='+' read -ra version_parts <<< "$tag"
version=${version_parts[0]}
fix_number=${version_parts[1]}
# 픽스넘버가 비어 있는 경우 0으로 설정
if [ -z "$fix_number" ]; then
fix_number=0
fi
# 픽스넘버 증가
fix_number=$((fix_number + 1))
# hotfix 버전
new_version="${version}+${fix_number}"
echo "NEW_VERSION=$new_version" >> $GITHUB_OUTPUT
# tag 기반 Hotfix Branch 생성
- name: Create and push hotfix branch
run: |
tag=${{ steps.get-latest-tag.outputs.LATEST_TAG }}
git checkout -b hotfix/${{ steps.create-new-version.outputs.NEW_VERSION }} tags/$tag
git push origin hotfix/${{ steps.create-new-version.outputs.NEW_VERSION }}
해당 버전으로 hotfix 배포가 없었다면 +1을 그렇지 않다면 이전 fixNum+1을 붙여 버전을 만든다.
자동화 3 : Dev 서버 배포
Release나 Hotfix 브랜치가 생성되거나 해당 브랜치에 Push가 발생한다면 Dev 서버에 배포한다. 현재는 Dev 서버를 직접 띄우지는 않고 프레임만 만들어 두었다. 해당 서비스는 Spring Boot로 서버를 구축했기 때문에 Build 및 배포하는 로직을 담고 있다.
# API - Dev Server에 배포
name: API - Deploy Development Environment
on:
push:
branches:
- release/Api-v*
- hotfix/Api-v*
env:
AWS_REGION: ap-northeast-2
jobs:
dev-api-server-deploy:
if: false
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MY_TOKEN }}
submodules: true
# JDK 환경 셋팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: gradle
# Gradle Permission
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Build
- name: Gradle build
run: |
./gradlew globalCopyConfig
./gradlew adevspoon-api:build -x test
# AWS Config
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-region: ${{ env.AWS_REGION }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Deploy to Lambda
- name: Deploy to lambda
run: |
# Lambda에 배포
자동화 4 : 배포 버전 Merge 시 Tag/Release 생성
Release, Hotfix 브랜치에서 새롭게 배포될 서버에 대한 검증이 끝나면 Develop 브랜치로 Pull Request를 올리고 Merge한다. 해당 PR에는 어떤 내용이 추가/수정 되었는지에 대한 내용이 들어가 있다. PR이 정상적으로 close 되면 이 PR 내용을 태도로 배포 히스토리를 남긴다. 배포 히스토리는 버전명으로 Tag와 Release를 생성하는 것이다.
이 부분도 직접 배포 후 Tag, Release를 수동으로 만들기보다는 Merge 되었을때 자동으로 만들어주도록 자동화하였다.
Github Actions에 merge 이벤트는 따로 존재하지 않아 PR이 close 되었을때 브랜치명을 확인하고 release, hotfix 일때 tag 및 release를 생성하도록 하였다. 이를 위해 브랜치를 확인하는 job, tag 및 release를 생성하는 job으로 분리해서 처리했다.
# Tag 생성 및 Release 생성
name: Versioning - Tagging and Release
on:
pull_request:
branches:
- develop
types:
- closed
jobs:
# 현재 branch 명 가져오는 job
check-branch:
runs-on: ubuntu-latest
outputs:
current_branch: ${{ steps.branch-name.outputs.current_branch }}
steps:
- name: Get branch names
id: branch-name
uses: tj-actions/branch-names@v8
가져온 브랜치명을 확인하여 다음 job을 실행할지를 체크한다.(if 문) `release/` 나 `hotfix/` 브랜치일 경우에만 진행되도록 하였다. 브랜치명 뒤에 적힌 버전명과 PR 내용을 토대로 Tag 및 Release를 생성할 것이다. 생성 로직은 이미 잘 만들어진 Action을 이용하여 쉽게 생성할 수 있었다.
jobs:
check-branch:
# ...
# Release 또는 Hotfix 브랜치가 merge 되었을때만 실행
tagging-and-release:
needs: check-branch
runs-on: 'ubuntu-latest'
if: ${{ github.event.pull_request.merged == true && (startsWith(needs.check-branch.outputs.current_branch, 'release/') || startsWith(needs.check-branch.outputs.current_branch, 'hotfix/')) }}
steps:
- name: Get tag
id: get-tag
run: |
branch=${{ needs.check-branch.outputs.current_branch }}
new_tag="${branch#release/}"
new_tag="${new_tag#hotfix/}"
echo "NEW_TAG=$new_tag" >> $GITHUB_OUTPUT
- name: Check tag
run: |
echo ${{ steps.get-tag.outputs.NEW_TAG }}
- name: Create Release & Tag
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.MY_TOKEN }}
with:
tag_name: ${{ steps.get-tag.outputs.NEW_TAG }}
release_name: ${{ steps.get-tag.outputs.NEW_TAG }}
body: |
${{ github.event.pull_request.body }}
자동화 5 : Production 서버 배포
프로덕션 서버에 새 버전을 배포하는 것은 Tag가 생성되었을때 실행한다. 자동화 4번이 정상적으로 동작하면 뒤이어 자동화 5번이 실행되어 Production 서버에 배포된다.
서버는 CodeDeploy를 이용하여 무중단 배포를 진행하고 있다.(이전 블로그 참고) 따라서, 빌드 후 S3로 아티팩트 업로드, CodeDeploy 실행과 같은 코드들을 담고 있다.
name: API - Deploy Production Environment
on:
push:
tags:
- Api-v*
workflow_dispatch:
env:
AWS_REGION: ---
S3_BUCKET_NAME: ---
CODE_DEPLOY_APPLICATION_NAME: ---
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: ---
jobs:
api-server-deploy:
runs-on: ubuntu-latest
steps:
# Repo checkout
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MY_TOKEN }}
submodules: true
# JDK 환경 셋팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: gradle
# Gradle Permission
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Build App
- name: Gradle build
run: |
./gradlew globalCopyConfig
./gradlew adevspoon-api:build -x test
# AWS Config
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
# S3 Upload
- name: S3 upload
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source ./adevspoon-api
# Code Deploy 실행 (블루/그린 무중단 배포)
- name: CodeDeploy - Blue-Green Deployment
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.OneAtATime \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
마치며
배포 과정과 자동화 로직의 흐름을 요약해보자.
- Feature 브랜치를 통해 새 기능 구현 후 Develop 브랜치에 Merge
- (자동화 1,2) 새로운 버전의 Release나 Hotfix 브랜치 생성 - 버전 자동 생성
- (자동화 3) 새 버전 서버 테스트. 코드 수정 & Push - Dev 서버 자동 배포
- (자동화 4) Release, Hotfix 브랜치 Develop에 Merge - 새로운 버전의 Tag 및 Release 생성
- (자동화 5) Tag 생성 시 Production 서버 자동 배포
필요한 부분들에 대해 자동화를 도입하였지만 위 로직에는 아직 두 가지 문제점이 존재한다. 필요에 따라 추후 수정이 필요해 보이기도 한다.
- Release/Hotfix Branch에서 Push가 한 번도 일어나지 않은 경우
PR이 불가능하기 때문에 README 파일이라도 수정해서 Push를 한 번이라도 해줘야 한다. - Release, Hotfix Branch 머지 전 Feature Branch가 머지 된 경우
Develop 브랜치에서 모든 브랜치가 만들어지고 Merge되기 때문에 충돌이 발생한다. 충돌해결 - Merge로 이어지기 때문에 새 버전에 포함할 기능이 아니어도 배포가 된다. 현재까지는 작업이 병렬로 진행되지는 않기 때문에 아직은 특별히 문제가 되지는 않는다.
Github Actions를 배포할때에만 사용해 보았는데 이번 기회에 여러 용도로 사용해보았다. 결국에는 배포라는 바운더리 안에서 활용했지만 앞으로 더 넓게 사용해 볼 수 있을 것 같다.
전체 코드는 아래 링크에서 확인할 수 있다.
GitHub - kids-ground/adevspoon-backend: CS 질문 - adevspoon-backend
CS 질문 - adevspoon-backend. Contribute to kids-ground/adevspoon-backend development by creating an account on GitHub.
github.com
'Server & DevOps' 카테고리의 다른 글
OS, 어플리케이션에서의 데드락 (2) | 2024.07.12 |
---|---|
경쟁상태와 동기화 매커니즘의 활용 (1) | 2024.07.04 |
Covering Index로 쿼리 성능 개선하기 (0) | 2024.06.14 |
ALB와 CodeDeploy 를 이용해 무중단 배포 파이프라인 구축하기(ft. Terraform) (2) | 2024.06.07 |