서버리스 배포가 너무 느려요!! 속도 10배 개선하기
안녕하세요. 이번 아티클에서는 Serverless Framework와 NxJS 로 구성된 서버리스 모노레포 구조에서 AWS CodeBuild 로 배포하는 배포 파이프라인의 배포 속도를 10배 개선했던 경험과 Serverless Framework가 AWS Lambda 에 소스 코드를 배포하는 과정을 설명하려고 합니다. 우선 AWS Lambda 에 코드가 배포되는 과정부터 천천히 알아가 보겠습니다.
목차
- sls deploy!!
- STAYGELabs 배포 파이프라인 소개
- 배포 파이프라인 마개조
- 성과
- 마무리
sls deploy!!
AWS Lambda를 단순화하여 소개해보겠습니다. 이벤트 요청이 트리거되면 소스 코드를 다운로드받아 로직을 실행시키고 종료되는 작은 단위의 컴퓨팅 자원입니다.
람다는 AWS에서 제공하는 서비스이기 때문에 퍼블릭 클라우드 위에 생성한 이후 사용할 수 있습니다. 람다를 생성하는 방법은 여러 가지가 있는데요.
- AWS 웹 콘솔의 GUI를 통한 생성
- AWS SDK (Software Development Kit) CreateFunction 기능으로 생성
- AWS CDK (Cloud Development Kit) 를 사용한 생성
- Terraform or Pulumi 등의 별도 IaC(Infra as a Code) 도구를 사용한 생성
- AWS CloudFormation 을 사용한 생성
- Serverless Framework 를 사용한 생성
간단히 생각한 것인데 람다를 생성할 수 있는 방법은 7가지나 되며 터미널에서 AWS CLI(Command Line Interface)를 사용하거나, 제가 모르는 다른 IaC 도구를 사용할 수도 있습니다. 이런 도구들은 사실 람다를 생성할 때 사용될 뿐 아니라 AWS EC2 와 같은 AWS의 모든 인프라 서비스를 생성하고 수정, 삭제할 수 있습니다. 이번에는 이 중에서 Serverless Framework가 람다 함수에 코드를 어떻게 배포하는지에 대하여 이야기해보겠습니다.
서버리스 프레임워크는 CloudFormation을 기반으로 동작하며 서버리스와 관련된 Lambda, S3, SQS, DynamoDB 등을 구성하는데 특화된 IaC 도구이며 엔터프라이즈 급이 아닌 작고 빠르게 시작하기에 적합합니다.
아래의 프로젝트에는 작은 단위로 구성된 API 서버 역할을 하는 람다 함수가 코드로 정의되어 있습니다. serverless.yml 파일에서는 인프라에 대한 정의를 선언하고, index.js 파일에 이벤트를 처리하는 로직을 넣게 됩니다.
“sls package” 명령어를 실행시키면 node_modules를 포함한 소스 코드는 빌드되어 하나의 파일로 압축되고 serverless.yml 파일은 CloudFormation 설정 파일로 변환되어 클라우드에 인프라를 생성할 수 있는 상태가 됩니다. 마지막으로 “sls deploy — package .serverless” 명령어로 패키지를 배포하면 Lambda로 구성된 API 서버(링크)가 생겨나게 됩니다.
이 과정을 도식화 하여 정리해보겠습니다.
- 여러 개의 타입스크립트로 작성된 파일이 자바스크립트 코드로 변환되고 한 개의 파일로 번들링되는 등의 빌드 과정을 거친 뒤 압축됩니다. serverless.yml 파일이 CloudFormation 을 위한 코드로 변환 됩니다.
- CloudFormation 파일은 CloudFormation 스택을 만들게 됩니다.
- CloudFormation 스택은 소스 코드를 업로드할 S3 버킷을 만들고 API 서버를 위한 Lambda와 API Gateway를 생성합니다.
- 소스 코드는 S3 버킷에 업로드하고 람다 함수에 다운로드받을 소스 코드의 위치를 알려줍니다.
- API Gateway 에 HTTP 요청이 들어오고 람다에 이벤트를 트리거시키면.
- 람다는 S3에 소스 코드를 다운로드받고 코드의 로직을 실행시킵니다.
우리는 Serverless Framework가 인프라를 생성하고 코드를 배포하는 과정에 대해 알게 되었습니다. 다른 종류의 IaC 도구들도 이와 비슷하게 동작할 것입니다.
STAYGELabs 배포 파이프라인 소개
람다는 작지만 대단한 친구입니다. 다양한 AWS 서비스로부터 이벤트를 트리거 받을 수 있고, 최대 250MB 사이즈까지 소스 코드를 업로드 하거나 컨테이너 이미지로는 10GB 까지도 소스 코드를 업로드 할 수 있습니다. 여러 종류의 이벤트를 트리거로 설정할 수 있고, 큰 사이즈의 소스 코드까지 업로드 할 수 있는 특성 때문에 람다를 다양한 일을 하는 큰 하나의 서버로 사용 하는 것이 가능 합니다. 하지만, 이것은 다음과 같은 이유로 권장 되지 않는 패턴입니다.
- 소스 코드 사이즈 증가로 인한 콜드스타트 시간 증가 (cold-start: 로직이 실행 되기 까지 Lambda 가 준비 되는 기간)
- 수행 작업에 대한 최적화 설정의 어려움
- 배포 시간 증가
- 디버깅 복잡도 증가
이러한 이유들 때문에 한 개의 람다를 모노리스화 시키는 것이 아닌, 도메인 또는 이벤트 트리거 등의 기준으로 나누어 적당한 사이즈의 여러개의 람다를 사용 하는 패턴을 권장합니다.
STAYGELabs의 Mnet Plus 서비스는 2021년 부터 현재 까지 약 3년간 개발 되면서 99개의 람다 함수와 38개의 애플리케이션(serverless.yml 1개에 1애플리케이션) 운영 되고 있습니다. 그에 따라서 깃 레포지토리도 도메인 별로 여러개를 운영하고 있었는데요. 몇가지의 불편함과 문제점을 맞이하게 됩니다.
- 공통으로 사용 되는 코드에 대한 관리 어려움
- Lint(컨벤션), Prettier(포멧) 설정 파일의 파편화
- 의존성 버전 관리 (레포별 다른 라이브러리 버전)
- 통합 배포의 어려움
서비스의 확장으로 인해 발생하는 문제들을 해결하기 위해서 저희는 여러 종료의 모노레포 도구 중 NxJS 를 선택하여 멀티래포에서 모노레포로 전환하기로 결정 했습니다. 모노레포로 전환하게 되면서 멀티래포에서 발견한 문제점을 해결할 수 있었습니다. Nx 를 사용하게 되면서 배포 시에 가장 좋았던 점은 여러개의 서버리스 어플리케이션을 배포 하기 위해서 배포 명령어(sls deploy)를 여러번 실행 시켜야 했는데, 하나의 명령어로 묶어서 통합적으로 배포할 수 있다는 것 입니다.
// api 폴더 하위에 api 애플리케이션을 모두 배포해라!
npx nx run-many --target=deployApi --projects="api/*"
그리고, 여러개의 서버리스 애플리케이션을 개발과 운영 환경에 더 자주 그리고 더 안정적으로 배포 하기 위해서 AWS CodeBuild 를 사용하여 클라우드 상에 배포 파이프라인을 만들게 되었습니다. AWS CodeBuild 는 GitHub Actions와 유사한 제품이며 빌드 및 배포를 할 때 사용 되는 서비스로 빌드에 걸린 시간 만큼 비용이 청구되는 빌드용 서버입니다. 최종적으로 CodeBuild 에서 Nx 를 사용하여 서버리스 애플리케이션을 배포하는 배포 파이프라인을 구성 하게 되었습니다. 해당 구조는 사실 1년 전에 만들어 졌고 느린 배포 속도에 대한 이슈와 튜닝을 거치게 되면서 10배 빨라지게 됩니다.
Mnet Plus 팀은 2주 스프린트를 거쳐 2주마다 정기 배포를 하고 있습니다. 배포의 프로세스는 릴리즈 되는 기능에 따라 달라 질 수 있지만 다음의 과정을 거치게 됩니다.
배포가 시작 되면 우선 백엔드 개발자가 DB 테이블에 변경된 내용을 적용 시키고, CodeBuild 를 실행하여 서버리스 애플리케이션을 람다에 배포 합니다. 서버 배포가 완료 되면 프론트엔드 개발자가 Web과 App을 배포하게 되고 QA 엔지니어의 검수 이후 배포가 종료 되게 됩니다.
버그는 항상 잘 돌아가던 코드가 변경되면 우리를 찾아오죠. 새로운 기능이 릴리즈 되고 테스트 환경에서 미쳐 발견되지 못한 버그에 신속히 대응하기 위해서 배포를 담당 하는 개발자와 새로운 기능에 기여한 개발자 그리고 QA 엔지니어와 서비스 운영자 까지 많은 사람들이 배포의 시작부터 종료까지 과정을 함께하게 됩니다.
앞서 말했듯 배포는 프로세스의 순서대로 순차적으로 진행되기 때문에, 앞선 작업의 시간이 오래 걸리면 전체 시간도 늘어나게 됩니다. 문제는 서버리스 애플리케이션 배포 과정에서 발생하게 되는데요. 처음에는 평균 20분정도 소요 되던 시간이 서버리스 애플리케이션의 개수가 늘어나 무려 1시간을 넘어서게 됩니다. 그에 따라서 전체 배포 시간도 늘어나게 되고 아까운 인력의 낭비가 발생하게 되죠.
배포 파이프라인 마개조
1시간이 넘게 걸리는 배포 파이프라인을 더 이상 지켜 보고만 있을 수는 없었기 때문에 배포 속도를 개선 해야 했고, 병목이 생기는 지점을 분석해 보았습니다. 서버리스 애플리케이션은 API 그룹이 배포된 후 배치 작업이나 비동기 처리에 사용되는 워커 그룹이 배포되게 구성 되어 있습니다. 각 그룹에 포함 된 서버리스 애플리케이션들은 한줄을 서서 순차적으로 배포 되었기 때문에 “하나의 서버리스 애플리케이션이 배포 되는 시간” x “개수” 만큼 시간이 걸리는 구조이었습니다.
이 구조를 여러개의 줄을 만들어 동시에 여러개의 서버리스 애플리케이션을 배포 하는 것이 배포 시간을 줄일 수 있는 Key 이었습니다. 이것을 병렬(Parallel) 구조로 바꾸는 것은 Nx의 — parallel 설정으로 간단히 해결할 수 있었습니다.
// api 폴더 하위에 api 애플리케이션을 모두 배포해라! 그런데 한번에 15개씩!!
npx nx run-many --target=deployApi --projects="api/*" --parallel=15
그러나, 또 다른 문제가 발생하게 되는데요. 저희팀은 자바스크립트 빌드 도구인 webpack 을 사용하는데 빌드를 하는 과정에 메모리를 많이 사용하고 하나의 서버리스 애플리케이션을 빌드하데 약 2GB 정도의 메모리가 필요 했습니다. 즉 병렬로 15개의 서버리스 애플리케이션을 배포하기 위해서는 최소 30GB 이상 메모리의 빌드 서버가 준비 되어 있어야 했습니다. 다행히 Codebuild의 최고 사양 옵션을 사용하면 충족할 수 있었습니다.
결과적으로 최대 자원 사용량 VCPU 18개와 Memory 34GB 를 사용하여 약 1시간 30분 정도 소요 되던 배포 시간을 약 15분 까지 개선 시킬 수 있었습니다. Codebuild 의 비용을 계산 했을 때 “15분 x 0.25$ = 3.75$” 로 한번의 빌드를 위해 비용을 지불하기 비싸다고 판단하여 15개씩 병렬 배포하는 것은 운영 환경에만 적용하는 것으로 마무리 하였습니다. (개발 환경 배포는 여전히 1시간…😭)(시간은 돈이니까 일단은 덮어두자…😭)
이렇게 몇개월을 사용하다가 새로운 기능을 위해 람다가 더 추가되고 운영 환경 배포 시간이 20분에서 30분을 넘어 서려고 했습니다. 이것을 해결하기 위해 덮어 두었던 근본적인 해결책을 다시 꺼내게 되었습니다. 근본적인 해결책은 바로 자바스크립트 빌드 도구를 webpack 에서 esbuild 로 교체 하는 것 이었습니다.
webpack은 자바스크립트로 만들어졌고 싱글 스레드로 동작하며 다른 플러그인과의 확장성에 초점이 맞춰진 초창기 빌드 도구입니다. esbuild 는 Go 로 만들어 졌고 멀티 스레드로 동작하며 빌드 속도에 초점이 맞춰진 차세대 빌드 도구입니다. 공식 문서에 따르면 esbuild는 webpack 보다 100배 빠르다고 소개 합니다. 싱글 스레드를 즐기는 NodeJS 개발자로서는 신선한 충격으로 다가왔습니다.
실제로 코드가 빌드 되는 시간은 9333ms(webpack)에서 146ms(esbuild)로 63배 빨라졌고, 1개의 람다 함수를 가진 서버리스 애플리케이션을 패키징 하는데 소요되는 시간은 18s 에서 9s 으로 2배 빨라졌습니다. 최종적으로 운영 환경에 배포 하는 배포 파이프라인은 VCPU 17개와 1.2GB Memory 를 사용하여 약 9분 안에 끝낼 수 있게 되었고, 1차 개선과 비교해서 3배 더 빨라졌습니다.
성과
여기까지 중간 정리를 해보겠습니다. 우리는 Serverless Framework를 사용해 AWS Lambda가 배포되는 과정을 살펴보았습니다. 또한, 저희 팀의 배포 파이프라인을 몇 가지 튜닝을 통해 배포 속도를 개선한 사례를 소개했습니다. 이러한 튜닝으로 배포 시간 뿐만 아니라 예기치 않은 다른 성과도 얻을 수 있었습니다.
배포 속도 개선
개선 전 배포 파이프라인의 배포 시간은 최대 90분이 소요되었습니다. 1차 개선 후 최대 25분, 2차 개선 후 9분까지 단축했습니다. 결과적으로 배포 시간이 10배 빨라졌고, 개발자들이 느끼는 주간 배포의 부담감과 피로를 크게 줄일 수 있었습니다.
Codebuild 비용 감소
앞서 설명했듯이 AWS Codebuild는 서버의 가동 시간에 따라 비용이 청구됩니다. 배포 속도가 빨라지고, 서버 사양을 낮춰 조정했으며, 개발과 테스트 환경의 Codebuild 사양도 조정했기 때문에 비용이 크게 감소했습니다. 1차 개선 전 월 539.5$에서 2차 개선 후 월 36.65$로, 비용을 14배 절감할 수 있었습니다.
콜드스타트 속도 개선
Lambda는 초기화 과정에서 소스 코드를 다운로드하기 때문에, 코드 크기가 콜드 스타트 시간에 큰 영향을 미칩니다. Tree Shaking은 빌드 과정에서 사용되지 않는 코드를 제거하는 기술입니다. webpack을 사용할 때는 Tree Shaking이 배포 시간을 늘려 사용하지 못했습니다. esbuild로 전환하면서 Tree Shaking을 적용할 수 있었고, 그 결과 코드 크기가 6.5MB에서 1.2MB로 5배 줄었습니다. 이로 인해 콜드 스타트 시간이 약 2초에서 1초로 단축되어 시스템의 전반적인 성능이 향상되었습니다.
마무리
“주변을 둘러보면 앞으로 나아가는데 방해하는 시스템이 있는데, 약간의 관심을 주어 개선 한다면 원동력이 될것이다.” 서비스가 시작 되고 지속적으로 기능이 추가 되다 보면 프로젝트는 구조가 복잡해지고 크기가 커지게 됩니다. 그러다 보면 과거에는 합리 적이던 시스템이 시간이 지나 우리의 발목을 잡게 되는 순간이 오게 될 것입니다. 그 때 주변을 둘러 보아 저희의 배포 파이프라인과 같은 문제가 있는 시스템을 외면하지 말고 찾아내서 개선한다면 더 큰 효과로 돌아 올 것입니다.
저희 팀의 배포 파이프라인은 아직 여정 안에 있습니다. CI(지속적 통합)과 CD(지속적 배포) 에서 CD에만 치중 되어 있습니다. CI 가 되기 위해서는 커버리지가 높은 테스트 코드를 더 작성 해야 하고 빌드와 배포 과정이 별도의 시스템에서 분리 되어 동작해야 해야 합니다. 저희 팀은 앞으로의 과제에 도전 할 것입니다.