모노레포의 기술적 요구사항 (3) - Deploy / Branch 전략

2022. 8. 7. 17:09Frontend

 

Overview

모노레포의 문화적 의의를 다룬 이전 포스팅에 이어, 이번 포스팅 시리즈에서는 실제로 모노레포를 팀에 도입하기 위해 거쳐왔던 여러 기술적인 고려 사항들을 간단하게 이야기해보려고 합니다. "모노레포는 이렇게 운영하는 것이 좋다"라는 가이드라기보다는 프론트엔드 팀이 모노레포로 전환하는 과정에서 겪은 여러 문제들과, 이를 해결해 나가는 과정에 대한 기록에 가까울 것 같습니다. 글은 다음과 같은 순서로 작성되었습니다.

 

  1. Workspaces & Dependencies
  2. Global Lint & Prettier
  3. Deploy & Branch Management
  4. Plugin
  5. (Optional) Sparse Checkout

 

 

CI / CD Pipeline

 

 

프론트엔드 서비스들을 모노레포로 이전하기 전과 후 CI / CD 파이프라인은 크게 달라진 것이 없었습니다. 웹 서버를 띄워서 요청을 처리하는 SSR 방식의 프로젝트는 클라우드 환경에서 Remote Registry에 빌드된 도커 이미지를 사용해 서비스를 띄우는 방식이기 때문에 모노레포에서 배포되든, 기존 레포에서 배포되든 쿠버네티스 파드 내의 컨테이너가 실행할 이미지만 Registry에 업데이트 되면 문제가 없습니다. 따라서 "어떻게 배포를 트리거할 것인가"와 "도커 이미지를 어떻게 만들 것인가"만 고려하면 되었고, 이에 따라 Dockerfile의 커맨드를 조금 수정하는 정도 (e.g yarn install 하던 것을 focus install 하던것들, 복사하는 파일들의 위치가 조금 변경된 정도) 를 제외하고는 기존과 달라지는 것이 거의 없었습니다.

 

모노레포에서 배포되든, 기존 서비스에서 배포되든 Image Registry에 실행 가능한 이미지만 업로드 되면 문제가 없다.

 

 

웹 서버를 사용하지 않고 정적 빌드 파일을 Cloud Storage(S3, GCS)에 업로드하는 경우에도 마찬가지로 변경할 것이 거의 없었습니다. SSR 배포 과정과 비슷하게 CSR 프로젝트도 정적 빌드파일을 Cloud Storage에 업로드 하면(CDN cache를 invalidate하는 것과 같은 클라우드 환경상의 고려사항들은 여기서는 따로 이야기하지 않겠습니다.) 배포가 끝나기 때문에 빌드하는 스크립트만 조금 수정해서 배포 설정을 마무리했습니다. (콴다 프론트엔드 팀의 프론트엔드 서비스 빌드 파이프라인은 이후 포스팅에서 자세히 다루도록 하겠습니다.)

 

Dockerfile의 일부 (workspaces 관련된 설정만 조금 다르다)
...
COPY .yarn ./.yarn
COPY .yarnrc.yml tsconfig.json ./
COPY package.json yarn.lock ./

...
COPY services/serviceA ./services/serviceB
RUN yarn workspaces focus @mathpresso/qanda-ai-user-web --production

...
ARG BUILD_CMD
RUN yarn serviceA ${BUILD_CMD}

...

 

Tag Based Deployment

앞서 프로젝트를 모노레포로 이전한다 하더라도 Remote Registry에 이미지만 제대로 쌓이거나(SSR), Remote Storage에 build file만 제대로 업로드된다면 (CSR) 서비스를 배포하는 데에 문제가 없다는 것을 살펴보았습니다. 따라서 "어떻게 배포를 트리거할 것인가" 에만 집중하면 모노레포에서도 잘 작동하는 배포 파이프라인을 구축할 수 있을 것입니다. 

 

기존에 사용하던 github flow

 

콴다 프론트엔드 팀은 기존에 "github flow"(git-flow 와는 조금 다른)를 사용하여 각 서비스를 개별 레포에서 배포하였습니다. master (main) 브랜치는 "production" 환경을, develop(dev) 브랜치는 "develop" 환경을 의미하며, 각 기능은 feature 브랜치에서 작업한 후 develop 브랜치에 머지하여 추가합니다. develop(dev) 브랜치에 feature브랜치를 머지하면, develop(dev) 브랜치의 "Push Event"를 받아서 develop 환경에 서비스가 배포되고, develop(dev) 브랜치를 master(main) 브랜치에 머지하면 production 환경에 서비스가 배포됩니다.

 

cloudbuild trigger에서 받을 수 있는 여러 개의 event, tag push / branch push 등 여러 종류릐 event를 받을 수 있다.

 

이러한 배포 전략은 모노레포에서는 사용하기가 어려웠는데, 각 서비스마다 master / develop를 유지하여 master-serviceA, develop-serviceB와 같이 여러개의 배포 브랜치를 사용하게 되면, 브랜치 간 지속적인 통합(Continuous Intergration)이 잘 되지 않는다는 문제가 있었으며, 아래에서 설명할 Trunk Based Development(TBD) 전략과도 잘 맞지 않았습니다.

 

 

TBD를 유지하면서 main에 푸쉬 될때마다 배포를 트리거하게 되면, 의도하지 않게 다른 팀원이 main 브랜치에 푸쉬하면서 다른 서비스의 배포도 trigger하게 될 위험이 있었습니다. (물론 TBD 원칙에 의하면 이렇게 의도치 않게 배포가 되어도 동작에 문제가 없는 feature들만 trunk(main) 브랜치에 머지되어야 합니다.) 따라서 모노레포의 "가장 최신 상태"를 의미하는 Trunk (main 브랜치)를 Single Source of Truth로 두되, 배포는 특정 커밋에 태그를 달아 명시적으로 서비스 오너가 서비스를 배포해야 하는 시점에 태그를 생성하여 배포하도록 하는 Tag Based Deployment 전략을 사용하도록 하였습니다.

 

 

 

 

위에 첨부한 것과 같이 태그는 `${SERVICE_NAME}/${ENVIRONMENT}/${DATE}-${NUMBER}` 의 형식을 띄는데, 태그가 푸쉬되면 배포를 담당하는 Jenkins나 Cloudbuild가 이 태그를 받아서 특정 서비스 / 배포 환경 / 특정 날짜의 몇번째 배포인지 의 정보로 파싱하고, 이에 맞춰 특정 서비스의 특정 환경으로 서비스를 빌드하고 배포합니다. 

Trunk Based Development 

A source-control branching model, where developers collaborate on code in a single branch called ‘trunk’ *, resist any pressure to create other long-lived development branches by employing documented techniques. They therefore avoid merge hell, do not break the build, and live happily ever after.

 

 

앞서 Tag Based Deployment를 설명하면서 Trunk Based Development(이하 TBD)에 대해 언급하였습니다. 모노레포로 전환하면서 하루에도 수십번의 변경사항이 하나의 레포에서 배포되다보니 이를 잘 관리할 수 있는 전략이 필요했고, 이에 따라 기존의 github-flow 방식의 브랜치 전략에서 벗어나 TBD를 도입하게 되었습니다. (TBD에 대한 자세한 내용은 위의 문서를 참고해주세요)

 

 

 

 

TBD는 굉장히 단순한 브랜치 관리 전략으로 쉽게 말해 모든 개발자들이 Trunk(Main)하나에서 협업하는 방식입니다. 이때, Trunk는 "항상 배포가 가능한 상태"여야 하며, 각 개발자는 Trunk에서 feature 브랜치를 따서 작업한 후에 가능한 한 빠르게(trunk와 너무 멀리 떨어지지 않도록) trunk에 머지하고 주기적으로 배포해야 합니다.

 

TBD를 "잘" 사용하기 위해서는 반드시 지켜야 하는 몇 가지의 규칙이 있는데 이는 다음과 같습니다.

 

  1. 모든 변경 사항은 "배포 가능한" 상태여야 한다. (실제로 production에 나가면 안되는 기능들이더라도 이를 feature flag 등을 사용해서 유저에게 보이지 않게 하고, 배포는 할 수 있어야 한다.)
  2. 1번 사항을 잘 지키기 위해서 모든 변경 사항에는 테스트코드가 작성되어야 한다. 

  3. 작업사항이 반영된 PR이 Trunk에 머지되지 않은 상태로 장기간 머물러 있는 경우를 최소화 해야 한다.

 

TBD를 정착하면서 하루에도 수십번씩 모노레포의 main 브랜치에 push가 일어났고, 각각의 변경사항을 담은 PR은 웬만해서는 하루를 넘기지 않은 상태로 리뷰가 되고 머지가 되고 있습니다. 또한 하루가 넘은 PR은 stale한 상태로 판단되어 아래와 같이 Slack 채널에 알림이 오게 됩니다. 이렇게 주기적으로 trunk에 작은 단위의 변경사항들이 푸쉬가 되게 되면 오랜 시간동안 stale한 상태로 개발된 큰 규모의 feature 브랜치를 머지할때 발생하는 어마어마한 merge conflict 문제를 겪지 않을 수 있으며, 큰 프로젝트이더라도 작은 단위로 PR을 생성하여 배포하기 때문에 다른 팀원들의 보다 구체적인 리뷰들을 기대할 수 있습니다.

 

 

Github Settings

TBD를 잘 적용하고 "실수로" 메인 브랜치에 "배포 가능하지 않은" 커밋이 올라오는 것을 막기 위해서 github 에 여러 restriction을 걸어두었습니다. 메인 브랜치에 적용되는 Rules는 github repository의 settings -> branches 에서 설정할 수 있으며, 다음과 같은 설정을 추가했습니다.

 

  • Require status checks to pass before merging 
    • 배포되기 전에 항상 모든 status checks (lint, unit test, integration test) 가 통과되어야 합니다.
  • Require conversation resolution before merging
    • 누군가가 해당 PR에 대해 Comment를 남겼다면, 이를 명시적으로 확인하고 Resolve 해야 합니다
  • Require linear history
    • main 브랜치에는 merge commit을 생성할 수 없으며 squash merge만 허용됩니다.
  • force push와 delete branch는 금지됩니다.

 

 

 

Conclusion

모노레포의 도입이 지닌 기술적 장점으로 인해 모노레포를 팀에 도입하더라도, 결국 이를 사용하는 건 팀원이기 때문에 이를 잘 사용하기 위해 문화적으로 중요한 여러 전략들을 마련하는 것은 굉장히 중요한 요소라는 생각이 듭니다. 다음 포스팅에서는 이러한 사용성을 조금 더 개선하기 위한 yarn plugin에 대한 내용을 살펴보도록 하겠습니다.

반응형