yarn berry 적용 및 ECS 배포 방식 변경을 통해 빌드/배포 속도 개선하기

Yeonbeen Park
원티드랩 기술 블로그
20 min readApr 7, 2023

배경

현재 원티드에서는 원아이디 라는 통합인증 서비스를 운영하고 있습니다.

채용, 기업, 긱스 등 원티드의 모든 서비스에 인증 기능을 제공하여 여러 서비스의 회원들을 한번에 관리하고, 로그인/회원가입 기능 작업에 필요한 공수를 줄이는데 도움을 주고 있습니다.

하지만 출시 이후 계정과 관련된 기능이 지속적으로 추가되고 있어 어플리케이션 빌드 및 배포시간이 점차 증가하고 있었습니다.

이에 원티드의 핵심서비스인 원아이디부터 개선을 시도하였습니다.

STEP 1. 배포 소요시간 단축을 위한 aws code pipeline을 제거 및 github actions로 이관하기

STEP 2. 패키지 용량 감소를 위한 무거운 node_modules package 설치에서 벗어나 yarn berry 적용하기

STEP 3. 이미지 크기 감소를 위한 next.js의 standalone 빌드 옵션을 적용하기

기존 원아이디의 배포시간은 8분 가량 이었지만 3가지 작업을 통해 얼마나 개선을 하게 되었는지 설명 드리겠습니다.

작업 과정

STEP 1. code pipeline 을 github actions로 이관

원아이디는 docker와 aws의 ecs 서비스를 이용해 배포를 진행하고 있습니다.

github actions에서 단계가 모두 끝난 후, aws의 ecr에 새로운 docker 이미지를 push 하게 되면 code pipeline 에서 코드 빌드 및 배포하는 단계를 거치고 있었습니다.

총 배포가 완료되는데까지 걸리는 시간은 배포 관련 메시지를 통해 알 수 있듯이 총 8분이 소요되었습니다.

기존 github actions에선 4분 7초정도 소요하고 있었습니다.

4분 7초 소요

이로써 Github actions 이후 단계인 code pipeline 단계가 약 4분이 걸린다는 것을 알 수 있었고, 동료분의 아이디어를 통해 단계 제거를 할 수 있었습니다.

바로 github actions의 aws에서 제공하는 액션을 이용하여 ecs의 서비스를 교체할 수 있도록 변경하는 것 입니다.

하지만 workflow는 devops팀에서 관리하고 있어 devops 팀의 도움을 통해 github actions 상에서 작업 정의 파일을 이용, 컨테이너 update 단계를 추가했습니다.

클러스터의 이름과 서비스 이름을 input으로 전달하면 aws cli를 통해 ECS 서비스에 배포를 해주는 작업입니다.

Docker 이미지를 빌드하여 ECR 리포지토리로 푸시하고, aws 액션을 사용하여 지정된 Amazon ECS 서비스에 새 작업 정의를 배포하게 됩니다.

- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition ${{ needs.set-env.outputs.task_definition }} --query taskDefinition > task-definition.json

- name: Deploy Amazon ECS task definition
id: ecs_deploy
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
timeout-minutes: 10
with:
task-definition: task-definition.json
service: ${{ env.ECS_SERVICE }} # ecs 클러스터 하위의 서비스
cluster: ${{ env.ECS_CLUSTER }} # ecs 클러스터 이름
wait-for-service-stability: true

요약

배포 완료 까지 5분 소요

→ 해당 작업을 통해 배포 시작~완료 까지의 시간이 약 37% 감소되었습니다

그림으로 표현하면 다음과 같습니다.

AS-IS

TO-BE

STEP 2. yarn berry 적용

기존 yarn v1을 이용하여 node_modules 디렉토리에 모든 패키지를 설치하는 방식을 사용했습니다.
설치되는 패키지가 많아질 수록 해당 디렉토리의 크기도 늘어났고, docker를 이용하고 있어 해당 디렉토리를 복사하는데 소요 시간이 오래걸리는 단점이 있습니다. 또한 프로젝트를 클론 받은 후 패키지 설치까지 시간이 꽤 걸립니다.

따라서 node_modules 방식을 사용하지 않는 yarn berry로 업그레이드 하고자 했습니다.
yarn berry는 pnp 방식을 이용하여 node_modules 디렉토리에 모든 패키지를 설치하는 것이 아닌, 패키지들을 zip 파일로 압축하여 사용함에 따라 차지하는 용량이 훨씬 적으므로 특히 docker 이미지를 생성하기 위한 복사 단계 소요시간이 단축될 것이라는 기대를 가졌습니다.

1) 로컬에서 yarn 버전 세팅

package.json에 packageManager 버전이 추가되었습니다.

"scripts": {...},
...
"packageManager": "yarn@3.3.1"

2) .yarnrc.yml 수정

(해당 파일에 yarn config 관련 내용들을 설정할 수 있습니다)

// ./.yarnrc.yml

nodeLinker: pnp
enableScripts: false
compressionLevel: 9

npmRegistries:
'https://npm.pkg.github.com':
npmAlwaysAuth: true
npmAuthToken: ***
...
  • nodeLinker : pnp 방식을 사용하여 zip 파일 형태로 패키지를 관리하기 위함입니다 (node_modules 옵션으로 작성시 기존 node_modules 방식을 사용합니다)
  • enableScripts : unplugged 옵션을 사용중이므로 추가했습니다. (해당 옵션의 자세한 사항은 하단에 기재되어 있습니다.)
  • npmRegistries : 범위별로 레지스트리를 구성할 수 있는 옵션입니다. (내부 패키지에 접근하기 위해 token 값을 함께 넣었습니다)

3) 스크립트 추가

yarn berry를 쉽게 설치/사용할 수 있도록 스크립트도 추가하였습니다
(참고. https://medium.com/wantedjobs/yarn-berry-%EC%A0%81%EC%9A%A9%EA%B8%B0-2-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0-45f1ba67c24c )

4) 명령어 수정

yarn berry로 실행할 수 있도록 관련 명령어들도 수정했습니다.

기존 node_modules 디렉토리 내부의 next를 실행하던 것을 yarn을 이용해 실행하도록 변경했고

node 명령어 또한 yarn이 래핑을 하기 때문에 yarn node로 실행해줬습니다.

// package.json

// AS-IS
{
"scripts" : {
"dev": "yarn ally:load && PORT=3001 node --require ./preload-server.js ./node_modules/.bin/next dev ...",
"start" : "node --require ./preload-server.js ./node_modules/.bin/next start ...",
...
},
...
}


// TO-BE
{
...
"scripts" : {
"dev:port" : "PORT=3001 node --require ./preload-server.js", // datadog 로깅 실행
"prepare:yarn" : "./scripts/yarnV2.sh", // yarn berry 검증
"dev" : "yarn prepare:yarn && yarn ally:load && yarn dev:port & yarn next dev -p 3001 ...",
"start" : "yarn node --require ./preload-server.js & yarn next start & local-ssl-proxy ...",
...
},
...
}

5) docker 파일 수정

기존 /node_modules 를 복사하는 행을 제외하고 yarn berry와 관련된 디렉토리/파일 들을 복사하도록 수정했습니다

#zero-install을 사용하기 위해 필요한 폴더/파일 복사 
COPY .pnp.cjs /app/.pnp.cjs
COPY .yarnrc.yml /app/.yarnrc.yml
COPY .pnp.loader.mjs /app/.pnp.loader.mjs

COPY .yarn/cache /app/.yarn/cache
COPY .yarn/plugins /app/.yarn/plugins
COPY .yarn/releases /app/.yarn/releases
COPY .yarn/sdks /app/.yarn/sdks

결과

docker 이미지 빌드시 크기가 약 22% 감소했습니다

  • 기존
922MB
  • 이후
710MB

STEP 3. Next.js 의 standalone 옵션 적용

현재 원아이디는 next.js 프레임워크를 사용하고 있어 next.js 에서 지원하는 최적화 옵션들을 살펴봤습니다.

standalone 이라는 옵션은 12버전 부터 생긴 옵션으로써 다음과 같은 장점이 있습니다.

(standalone 이란?)

적용 방법은 간단합니다 next.config.js 에 옵션으로 추가해주면 됩니다.

//next.config.js

module.exports = withSentryConfig(
withBundleAnalyzer({
...
output: 'standalone', // 12.3 버전을 사용하고 있어 다음과 같이 추가했습니다
...
);j

해당 옵션을 적용하면 빌드 결과물에 `standalone` 이라는 디렉토리가 생성됩니다. 어플리케이션 실행시 해당 디렉토리의 결과물을 사용하도록 docker 파일도 수정했습니다.

...
#빌드 결과물의 /standalone/.next를 복사
COPY .next/standalone/.next /app/.next
COPY .next/static /app/.next/static
...

결과

해당 작업까지로 인한 결과는 다음과 같습니다

github actions에서 docker 이미지 생성 소요시간약 40초 감소했습니다.

1분 13초 소요
39초 소요

ECR 에서의 docker 이미지 크기는 약 30MB 증가했습니다.

187MB
217MB

ECR 에서의 docker 이미지 크기는 증가했지만 해당 이미지를 생성하는데 걸리는 시간은 단축되었습니다.

STEP 3+1. 추가사항

매주 진행되는 프론트엔드 주간미팅에서 제시된 아이디어가 있었습니다.

dockerignore 파일을 이용하여, 용량이 큰 devDependency 패키지를 제외한다!

원티드의 다른 서비스에서 테스트해본 결과를 공유해주셨고 유의미한 수치가 나와 시도해봤습니다.

docker 빌드시 dockerignore 파일을 참고하여 빌드에 포함하지 않을 디렉토리/파일등을 설정할 수 있습니다.

이를 이용해 production에선 필요하지 않은 dev dependency에 설치된 패키지들을 제외하였습니다.

dockerignore 파일에 실행과 관계없는 패키지들을 추가했습니다.

# production 환경에선 필요하지 않는 패키지 복사 제외
.yarn/cache/typescript-*
.yarn/cache/ts-node*
.yarn/cache/eslint*
...

결과

docker 이미지를 생성하는데 소요시간이 7초 더 단축되었습니다.

39초 → 32초

또한 ECR에 올라간 이미지의 크기가 217MB → 141MB로
35% 감소되었습니다.

해당 여파인진 몰라도 개발환경 배포에 완료되는 시간이 4분이 걸리는 경우도 생겼습니다.

결론

STEP 1~3의 결과를 정리하면 다음과 같습니다.

aws code pipeline을 제거하고, github actions에서 실행하는 것으로 변경을 통해 총 배포 소요시간을 단축했습니다.

다음 작업을 통해 docker 이미지의 크기를 감소시켰고, 이미지 생성시 소요시간을 단축했습니다

  • yarn berry 로 변경
  • 빌드시 standalone 옵션 적용
  • docker 이미지 생성시 개발환경에서 필요한 패키지 제외

수치화 한다면 다음과 같습니다.

도커 이미지 크기는 21% 감소, 도커 이미지 빌드 시간은 56% 감소 되었으며

총 배포 소요시간이 약 30% 단축되었습니다.

마주쳤던 이슈들

  1. yarn run 명령어 실행시 다음과 같은 에러가 발생했습니다.
unplugged 디렉토리가 없다는 에러

yarn qna에서 에러와 관련된 글을 찾을 수 있었습니다.(https://yarnpkg.com/getting-started/qa)

unplugged 디렉토리는 항상 무시해야하는데 무시하면 zero-install이 동작하지 않으니 enableScript 옵션의 값을 false로 주어야 한다.

.yarnrc.yml 에서 enableScripts 옵션을 적용하였고 어플리케이션 실행이 안되는 문제를 해결했습니다.

2. zero-install을 활용할 수 없는 문제가 있습니다.

next 프레임워크를 풀어내기위해 unplugged 옵션을 사용했으며 unplugged 디렉토리는 깃헙에 업로드 되지 않았습니다.

.yarnrc.yml의 enableScript 옵션 값을 false로 주었으나, 최초 클론시 1번은 yarn을 통해 패키지를 설치해야 정상 실행이 가능합니다.

번외 (feat. 데이터독 APM)

저희는 datadog의 APM이라는 서비스를 통해 어떤 요청을 보냈는지, 순서는 어떻게 되는지, 에러는 어느 구간에서 발생하는지 트래킹을 하고 있습니다.

기존엔 여기를 참고해 preload-server.js 라는 apm 연결 파일을 만들어 서비스 실행시 같이 실행될 수 있도록 구성하여 사용했습니다.

하지만 yarn v1 → berry로 업그레이드 작업을 한 이후 데이터독 APM 서비스의 Trace 항목에 요청이 기록되지 않는 현상이 있었습니다.

yarn v1
yarn berry

해당 현상은 브라우저 밖에서의 로그를 확인할 수 없으며 이는 에러 파악에 걸리는 시간을 증가시키게 됩니다. 따라서 해당 현상 해결이 필요했고, 이를 위해 시도한 과정을 설명 드리겠습니다.

1. 실행시 환경변수 넣어주기

기본적으로 어플리케이션 실행시 환경변수를 넣어줄 수가 있습니다.

따라서 환경변수 문제인지 알아보기 위해 먼저 로컬 환경 실행시에 환경변수를 넣어서 테스트했습니다

Before

"dev": "yarn prepare:yarn && yarn ally:load && yarn dev:port & next -p 3001...",
"start": "PORT=3000 yarn node --require ./preload-server.js & yarn next start",

After

"dev": "DD_AGENT_HOST=**** DD_EN=dev DD_SERVICE=**** DEBUG=True DD_ENV=dev PORT=3000 yarn prepare:yarn && yarn ally:load && yarn dev:port & next -p 3001 ...",
"start": "DD_AGENT_HOST=**** DD_EN=dev DD_SERVICE=**** DEBUG=True PORT=3000 yarn node --require ./preload-server.js & yarn next start",

필요한 환경변수 주입을 통해 로컬환경에서 요청들이 기록되는것을 확인했습니다.

하지만 실제 개발환경 배포시에는 요청들이 정상적으로 기록되지 않는 현상이 지속되었습니다.

컨테이너 로그를 확인해본 결과 환경변수들은 문제 없이 주입이 된 상태였습니다.

2. next 패키지의 설치 경로로 접근하여 실행파일 실행하기

1번의 실험을 통해 환경변수 문제는 아니라는 것을 알았습니다.

그렇다면 yarn berry로 업그레이드 하면서 차이점은 무엇인가? 를 생각해본 결과 어플리케이션 실행 방식이 달랐습니다.

yarn v1을 사용할 땐 next가 설치되어있는 `node_modules`로 접근하여 직접 파일을 실행해 사용하고 있었습니다.

따라서 기존 방식처럼 변경을 하여 테스트했습니다.

Before

"dev": "yarn ally:load && PORT=3001 node --require ./preload-server.js ./node_modules/.bin/next dev ...",
"start": "node --require ./preload-server.js ./node_modules/.bin/next start",

After

"dev": "yarn prepare:yarn && yarn ally:load && yarn dev:port .yarn/__virtual__/next-virtual-106f409ea6/0/cache/next-npm-12.3.4-cdaf2db0a7-b8867fba86.zip/node_modules/next/dist/bin/next -p 3001 ...",
"start": "yarn prod:port .yarn/__virtual__/next-virtual-106f409ea6/0/cache/next-npm-12.3.4-cdaf2db0a7-b8867fba86.zip/node_modules/next/dist/bin/next start",

next가 설치되어있는 경로를 찾아가 실행파일을 실행함으로써 개발 서버에서도 정상 동작되는 것을 확인했습니다.

하지만 한가지 걸리는 것이 있었습니다.

next가 설치된 경로의 `next-virtual-106f409ea6` 가 해쉬 형태인 것 입니다.

해당 값이 항상 고정이라는 것을 보장할 수 없다고 판단하여 좀 더 안전한 방법이 무엇이 있을지 고민하다가 다음과 방법으로 해결했습니다.

3. yarn unplugged 이용

unplugged란? 특정 패키지를 분리할 수 있는 기능입니다 .
yarn/cache에 압축된 의존성을 활용하여 .yarn/unplugged 폴더에 해당 패키지를 풀어내는 것을 의미합니다.

해당 기능을 이용하면 unplugged 폴더에 원하는 패키지를 풀어낼 수 있어 안전하고 쉽게 사용할 수 있다고 판단했습니다.

unplugged로 next 패키지를 풀어냈을 때 다음과 같이 분리되었습니다.

next의 설치경로를 직접 입력했을때와 같은 폴더명으로 생성이 되었습니다

.yarn/unplugged

따라서 명령어를 다음과 같이 수정했습니다

AS-IS

"dev" : "yarn ally:load && PORT=3001 node --require ./preload-server.js ./node_modules/.bin/next dev ...",
"start" : "node --require ./preload-server.js ./node_modules/.bin/next start ...",

TO-BE

"dev": "yarn ally:load && yarn dev:port .yarn/unplugged/next-virtual-106f409ea6/node_modules/next/dist/bin/next ...",
"start": ".yarn/unplugged/next-virtual-106f409ea6/node_modules/next/dist/bin/next start",

실행 명령어 수정후 결과는 다음과 같습니다

23.02.25~23.02.27 APM > Trace

결과에서 볼 수 있듯이 현재까지 안정적으로 요청을 기록하고 있습니다.

끝으로 긴 글 읽어주셔서 감사합니다.

--

--