개요
솔리드 커넥션의 웹 파트를 개발하며 HeadVer 버저닝을 기반으로 Github Actions와 Vercel을 사용해 CI/CD 자동화를 이루어낸 기록을 정리합니다.
솔리드 커넥션 웹, 그리고 배포 방식
솔리드 커넥션은 교환학생을 돕기 위한 서비스입니다. 웹 애플리케이션은 Next.js로 개발되었으며, 배포는 Vercel을 통해 이루어지고 있습니다.
Vercel은 정말 편리한 기능들을 많이 제공하고 있습니다. 그 중 하나는 Github과 잘 연계하여 빌드/배포를 매우 편리하게 해주는 것인데요, Github에 올라온 커밋들을 모두 자동으로 빌드해주고, 지정된 배포 브랜치에 올라온 내용을 자동으로 배포해줍니다.
서비스 CD를 위해서 배포용 release 브랜치를 만들고, 해당 브랜치에 PR을 올리는 것으로 배포를 하고 있었는데요, 문제는 배포에 버저닝을 적용하면서 부터 생겨납니다.
버저닝을 적용시킨 후, 배포 과정은 다음과 같은 절차를 거치게 되었는데요,
- main 브랜치의 최신 커밋에 버전 태그 추가(v1.2.3)
- 해당 태그에 대한 깃헙 릴리즈 생성
- release 브랜치로의 PR 생성, 병합
이는 그렇게 복잡한 절차는 아니였지만, 하루에 5번 이상의 배포를 진행하는 일이 생기자 굉장한 피로를 느끼게 되었습니다.
그리고 수동으로 버전을 적용해줘야 했고, release 시마다 브랜치에 PR을 넣어야 하니 PR탭의 절반이 릴리즈 요청으로 뒤덮이는 문제도 있었습니다.
그리하여 이를 개선하기 위해 원클릭으로 배포 가능한 워크플로우를 만들어내기로 합니다.
TO-BE
제가 생각하는 이상은 다음과 같은 요건을 만족하는 시스템입니다.
- 버저닝을 직접 지정해줄 필요가 없어야한다. 또는 최소한으로 인간의 손을 타야한다.
- 버저닝을 직접 지어준다면 실수가 있을 수 있고, 버전을 고민하는데도 스트레스가 오기 때문입니다.
- main 브랜치에 업데이트 된 내용은 바로 stage 환경에 배포되어야 한다.
- main 브랜치에 들어가는 수정사항은 이미 PR로 검증받은 사항이기에 바로 qa 검증을 받기위한 stage환경으로 들어가도 무방하다고 보았습니다.
- main 브랜치에서 자동 빌드되고 qa 테스트를 받은 버전중 하나를 짚어 배포할 수 있어야 한다.
HeadVer
우선 버전 시스템을 정하기로 했고, 제가 도입하기로 결정한 것은 LINE에서 개발한 HeadVer 버저닝 시스템입니다.
일반적으로 많은 분들이 major.minor.patch 구조를 가진 SemVer(Semantic Versioning)에 익숙하실 텐데요. 하지만 웹/앱과 같은 일반 사용자 대상 프로덕트에서는 SemVer보다 HeadVer가 더 적합하다고 판단해 도입했습니다.
왜 SemVer을 사용하지 않았을까요?
SemVer를 사용하면 Major 버전을 언제 올려야 할지가 모호해집니다. 보통 Major 버전은 하위 호환성을 깨는 변경이 있을 때 올리는 것이 일반적이지만, 라이브러리가 아닌 사용자 서비스에서는 ‘하위 호환성’의 개념이 명확하지 않습니다.
그렇다고 Major 버전을 큰 업데이트를 기준으로 올리려 해도, ‘큰 업데이트’의 기준 자체가 애매합니다.
예를 들어, 제가 좋아하는 게임인 Minecraft: Java Edition을 보면,
- 2011년 1.0.0 버전 출시
- 2025년 현재 1.21.4 버전이 최신
으로 14년 동안 Major 버전이 바뀌지 않았습니다. 그렇다면, 이 Major 버전은 도대체 무슨 의미가 있을까요? 🤔
HeadVer의 장점
HeadVer를 도입한 가장 큰 이유는 버전을 올리는 규칙이 명확하다는 것입니다. 버전 번호를 지정할 때 불필요한 고민이 줄어들고, 자동화된 빌드 시스템과 자연스럽게 연동되는 구조를 가지고 있습니다.
HeadVer에 대한 자세한 내용은 아래 링크에서 확인하실 수 있습니다.
HeadVer - 기민한 프로덕트 팀을 위한 새로운 버저닝 시스템
HeadVer - 기민한 프로덕트 팀을 위한 새로운 버저닝 시스템
안녕하세요. ABC Studio 김영재입니다. 저는 LINE이 2020년에 인수한 일본 최대 규모 배달 서비스 데마에칸(出前館, Demaecan) 프로덕트를 담당하고 있습니다. H...
techblog.lycorp.co.jp
GitHub - line/headver: SemVer compatible version specification that has {head}.{yearweek}.{build} system.
SemVer compatible version specification that has {head}.{yearweek}.{build} system. - line/headver
github.com
이러한 이유로, 저는 SemVer가 아닌 HeadVer를 적용하는 것이 더 적절하다고 판단했고, 이를 도입하기로 결정했습니다. 🚀
HeadVer을 적용했을 때
솔리드 커넥션 웹에서는 각 head 넘버(버전)별로 기능을 계획해서 구현하고 각 head 넘버의 마지막 버전이 배포가 된 버전이 되게 했습니다.
즉 다음과 같은 깃 히스토리를 가지게 됩니다.
<추후 이미지 추가>
위 이미지에서 <추후 추가>, <추후 추가> 등이 배포가 이루어진 버전입니다.
그렇기에 해당 버전 다음 커밋부터 head 버전이 올라가 있는 것을 확인할 수 있습니다. 이번 head 버전에서는 모두 기능 개발이 끝나고 배포가 되었으니 뭔가 수정할 것이 있다면 다음 버전에서 진행해야 하는 것입니다.
또한 배포가된 <추후 추가>, <추후 추가> 태그가 있는 커밋은 2개의 버전이 지정되어 있는 것을 확인할 수 있습니다. 이는 stage 환경으로 빌드된 버전 하나, production 환경으로 빌드된 최종 버전 하나가 존재하기 떄문입니다.
이를 같은 버전으로 통합하지 않는 이유는 빌드 넘버는 빌드시마다 하나씩 올라가야 한다는 규칙 때문입니다. 그렇기에 한 커밋에 2개의 버전 태그가 달려 있는 것이 이상한 것은 아닙니다.
Vercel
Vercel은 브랜치에 커밋이 추가 되면 Github에 올라온 코드를 받아와 자동으로 설정해둔 환경변수와 통합하여 빌드를 진행합니다.
이런 자동화 프로세스를 사용하지 않고 직접 수동으로 빌드를 한다면 다음과 같은 과정을 거쳐야합니다.
- Vercel에 설정해둔 환경변수를 로컬로 받아옵니다
- 로컬에서 빌드를 진행합니다
- 빌드한 파일을 Vercel에 업로드해 배포합니다
다음은 Vercel 공식 가이드에서 알려주는 태그 설정으로 수동 배포하는 Github Actions 입니다.
name: Production Tag Deployment
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
push:
# Pattern matched against refs/tags
tags:
- '*' # Push events to every tag not containing /
jobs:
Deploy-Production:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
위의 예시를 참고해서, HeadVer을 자동으로 설정하고 자동으로 빌드해서 Vercel qa(stage) 환경에 배포, 원클릭으로 릴리즈를 생성해 production 환경에 배포할 수 있는 워크플로우를 제작했습니다.
워크플로우
1. headver 생성
# .github/workflow/headver-tagging.yml
name: Generate HeadVer Tag
permissions:
contents: write
on:
workflow_call:
outputs:
version:
description: "Generated HeadVer version"
value: ${{ jobs.generate_tag.outputs.version }}
jobs:
generate_tag:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.compute_version.outputs.version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Compute HeadVer Tag
id: compute_version
run: |
# headver.json에서 head 버전 가져오기
if [ ! -f headver.json ]; then
echo "headver.json 파일이 없습니다. 기본 head=0을 사용합니다."
HEAD=0
else
HEAD=$(jq -r '.head' headver.json)
fi
echo "Head number: $HEAD"
# 현재 연도와 주차(yyww) 가져오기
YYWW=$(date +"%y%V")
echo "YearWeek: $YYWW"
# 최신 태그 검색
LAST_BUILD=$(git tag --list "v*.*.*" | awk -F. '{print $3}' | sort -n | tail -n 1)
if [ -z "$LAST_BUILD" ]; then
BUILD=1
else
BUILD=$((LAST_BUILD + 1))
fi
echo "Build number: $BUILD"
# 최종 태그 생성
VERSION="${HEAD}.${YYWW}.${BUILD}"
echo "Computed version: $VERSION"
# GitHub Actions 환경 변수로 설정
echo "version=$VERSION" >> $GITHUB_ENV
echo "::set-output name=version::$VERSION"
- name: Create Tag
run: |
TAG="v${{ steps.compute_version.outputs.version }}"
echo "Creating tag: $TAG"
git tag $TAG
git push origin $TAG
최신의 HeadVer을 자동으로 생성하는 워크플로우 입니다.
1. 우선 head 버전값을 가져오는데요, 이는 headver.json이라는 파일을 레포지토리에서 관리하는 방식으로 head 버전을 지정할 수 있게 했습니다.
이를 자동화할 수도 있었지만, HeadVer의 철학은 Head말고는 자동화 되는 버저닝 시스템, 다시 말해서 head 버전은 수동으로 지정해줘야 하는 방식이라고 생각했습니다.
그래서 이후에 불편한 점이 있다면 head 버전 지정 방식을 변경하되, 당장은 headver.json 파일에서 수동으로 지정해줄 수 있게 했습니다.
2. 그 다음은 워크플로우가 작동한 시간에서 YYWW 형식의 날짜를 추출합니다.
3. 그리고 레포지토리를 탐색해서 v1.2345.6 형식으로 되어 있는 버저닝 태그를 탐색하고, 이 중 최신의 태그(마지막 build 번호가 가장 높은 것)을 찾아 빌드 번호를 추출합니다. 최신의 버전은 이렇게 추출한 빌드 번호 + 1이 됩니다.
이렇게 추출한 버전을 기반으로 신규 버전을 만들어 태그를 생성합니다.
이렇게 만들어진 워크플로우는 동시에 작동할 경우 빌드 버전이 중복되는 문제가 발생할 수 있습니다.
이를 막기 위해서는 Github Actions 동시 실행 제어를 사용할 수 있습니다.
2. 자동으로 stage 환경 빌드 후 qa 환경에 배포
프론트엔드 개발 시, 예상치 못한 버그들이 발생할 수 있습니다. 이는 특히 보안과 관련한 문제로 localhost 환경에서는 확인하기 어려운 OAuth 관련 사항을 다룰 때에 자주 일어납니다.
버그가 아니더라도 제작된 웹이 기획자/디자이너의 생각이나 디자인대로 제작되지 않을 수 있습니다. 이때 해당 사항들이 프로덕션 환경에 올라가기 전에 이에 대한 피드백을 제공할 수 있게 솔리드 커넥션 팀에서는 qa를 위한 stage 환경을 만들어 qa에 사용하고 있습니다.
# .github/workflow/build.yml
name: Build and Vercel Preview Deployment on Main
permissions:
contents: write
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ENV: preview
on:
push:
branches:
- main
workflow_dispatch:
jobs:
generate_tag:
uses: ./.github/workflows/headver-tagging.yml
with: {}
Deploy-Preview:
runs-on: ubuntu-latest
needs: generate_tag
env:
VERSION_TAG: ${{ needs.generate_tag.outputs.version }}
steps:
- uses: actions/checkout@v3
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --yes --target=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --target=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Output Tag Version
run: echo "Deployment completed for version $VERSION_TAG"
stage 환경은 main에 바로 연결되어, main으로 올라온 커밋은 모두 자동으로 stage 환경에 배포되게 만들어 두었습니다.
main으로 올라온 모든 커밋을 자동으로 빌드하는 이유는, 빌드/배포되지 않은 코드는 잠재적인 부채로 보았기에 최대한 자주 자동으로 배포를 실시하여 기능이 제대로 작동하는지 확인하고자 했습니다.
main에는 개발 구조상 모두 PR에서 검증/리뷰된 코드만 반영되기 떄문에 굳이 stage 환경에 배포하기 전에 한번더 사람의 체크를 하는 것 보다, 자동으로 main의 내용들을 stage 환경에 배포하는 것이 좋다고 생각했습니다.
3. 수동으로 production 환경 빌드후 실제 환경에 배포
# .github/workflow/release.yml
name: Build and Vercel Production Deployment
permissions:
contents: write
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ENV: production
on:
workflow_dispatch:
jobs:
generate_tag:
name: Generate HeadVer Tag
uses: ./.github/workflows/headver-tagging.yml
with: {}
create_release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: generate_tag
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Create Release
id: create_release
uses: ncipollo/release-action@v1
with:
tag: "v${{ needs.generate_tag.outputs.version }}"
release_name: "Release v${{ needs.generate_tag.outputs.version }}"
body: "Automated release created for build v${{ needs.generate_tag.outputs.version }}"
token: ${{ secrets.GITHUB_TOKEN }}
deploy_production:
runs-on: ubuntu-latest
needs: create_release
env:
VERSION_TAG: ${{ needs.generate_tag.outputs.version }}
steps:
- uses: actions/checkout@v3
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --yes --target=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --target=${{ env.VERCEL_ENV }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Output Tag Version
run: echo "Deployment completed for version $VERSION_TAG"
마지막으로 실제로 배포를 진행하는 워크플로우입니다.
워크플로우에서 프로덕션 환경변수를 가져와 직접 빌드하고 실제로 배포하게 했습니다. 기존에는 Vercel 서버에서 빌드를 했는데요, 직접 빌드를 하는 구조를 갖추었기에, 이후 배포 환경을 AWS나 다른 환경으로 변경하여도 좀더 변경이 용이해질 것입니다.
또한 workflow_dispatch를 사용해 버튼 하나로 배포를 진행할 수 있게 했는데요, 이 구조를 취함으로써 배포할 버전/브랜치를 직접 지정할 수도 있게 되었습니다.
문제
문제: Sensitive environment 불러오기 실패 문제
Sensitive environment variables
Sensitive environment variables
Environment variables that cannot be decrypted once created.
vercel.com
빌드시의 워크플로우를 제작하던 중 발생한 문제입니다.
vercel pull 을 통해 Vercel에서 환경변수를 불러올 때 sensitive environment로 설정한 환경변수를 공백값으로 불러와지는 문제가 있었습니다.
이는 sensitive environment는 일반적으로 접근할 수 없고, 빌드 시에만 자동으로 적용되는 값이기 때문입니다.
그렇기에 1. 환경변수를 불러오고, 2. 빌드를 진행하는 것이 아닌, 빌드를 진행하며 환경변수를 자동으로 불러 올 수 있는 vercel build --prod 명령을 통해 시도해보았으나, sensitive environment 값들은 적용이 안되는 것을 확인했습니다.
이런 문제가 발생한 것은 sensitive environment가 비교적 최근에 생긴 기능이기 때문이라고 생각이 드는데요, 해당 문제를 해결하기 위해 그냥 sensitive environment를 사용하지 않는 것으로 방향을 잡았습니다.
이유는 sensitive environment로 설정하지 않더라도 빌드시에 외부에 노출 되는 것이 아니고, sensitive environment는 내부 팀원들에게 환경변수를 보이지 않고 싶을 때 사용하는 설정이기 때문입니다. 환경변수는 어짜피 모두가 접근할 수 있었기에 굳이 sensitive environment를 적용할 필요가 없어 이를 제거했습니다.
사용해보며 느낀점
자동화가 된 부분과 CI/CD 과정중 직접 관리할 수 있는 부분이 늘어난 것은 매우 좋게 느껴졌습니다.
그러나 버전의 head 부분을 관리할 때 문제가 있었는데, 매번 head 버전을 올려주기가 번거로워집니다. 어짜피 head 버전은 배포가 진행된 이후 자동으로 버전 하나가 올라간다는 규칙을 가지고 있기에 자동화해도 문제가 없어 보입니다.
그리고 버전에는 1.2.3-abc 의 구조에서 abc처럼 메타데이터를 포함할 수 있는데요, 이를 활용해 빌드시 사용한 환경에 따라 메타데이터를 붙여주면 좋겠다고 생각했습니다.
예를들면, 10.2513.15-dev 와 10.2513.16-prod 처럼 말이죠.
HeadVer을 사용하면 버전 관리가 쉬워지고, Github Actions 수동 빌드를 이용하면 팀으로 작업해도 Vercel을 무료로 이용할 수 있습니다.
현재 여러분의 프로젝트에 알맞춤이라는 생각이 드신다면 한번 도입해보시는 건 어떨까요?
Reference
HeadVer - 기민한 프로덕트 팀을 위한 새로운 버저닝 시스템
HeadVer - 기민한 프로덕트 팀을 위한 새로운 버저닝 시스템
안녕하세요. ABC Studio 김영재입니다. 저는 LINE이 2020년에 인수한 일본 최대 규모 배달 서비스 데마에칸(出前館, Demaecan) 프로덕트를 담당하고 있습니다. H...
techblog.lycorp.co.jp
How to Deploy Based on Tags or Releases on Vercel
How to Deploy Based on Tags or Releases on Vercel
Learn how to use tags and releases to deploy to Vercel, as well as best practices when deploying using these methods.
vercel.com
Deploying Projects from Vercel CLI
Deploying Projects from Vercel CLI
Learn how to deploy your Vercel Projects from Vercel CLI using the vercel or vercel deploy commands.
vercel.com
Custom environments
Use pre-production environments to preview and test your changes.
vercel.com
Git Configuration
Learn how to configure Git for your project through the vercel.json file.
vercel.com
'인프라 (Infra)' 카테고리의 다른 글
컨테이너 레지스트리 비교 (0) | 2025.03.12 |
---|---|
EC2 T instance VS M instance 비용 비교 (0) | 2024.04.08 |
AWS ELB 구조 간단 정리 (0) | 2024.04.07 |
클라우드 서비스(Azure, AWS, GCP, Oracle) Freetier 비교 (1) | 2022.12.15 |
AWS EC2로 code-server 생성하기 (0) | 2022.08.20 |