콘텐츠로 이동

Webpack으로 AWS Lambda 번들 크기 줄여 Upload Size Limit 해결하기

AWS Lambda에 올릴 배포 패키지의 unzipped size가 250MB 제한을 넘어 배포에 실패했다.

아래의 로그는 Serverless Framework(sls)로 AWS Lambda를 배포하는 과정에서 나온 에러 메시지다.

Terminal window
Unzipped size must be smaller than 262144000 bytes
  • AWS Lambda는 “압축 해제 기준” 배포 패키지 크기가 약 250MB를 넘을 수 없다.

현재 운영 중인 서비스는 NestJS 프로젝트를 ECS Fargate 와 AWS Lambda 를 통해서 API 서버를 운영하고 있다. ECS Fargate 는 배포 패키지 용량에 영향이 없지만, AWS Lambda 는 배포 패키지의 용량 제한이 있었다.

필요없는 의존성이 업로드되고 있었다.

문제의 핵심은 소스코드가 커진 것보다, 배포할 때 실제로 필요 없는 의존성까지 함께 업로드되고 있었다는 점이었다.

필요한 소스코드가 늘어난 것이라면 전혀 문제가 되지 않는다. 하지만 이번 이슈를 계기로 확인해본 결과, 해당 서비스(API)에는 필요없고 사용되지도 않는 의존성이 공통적으로 업로드 되고 있었다.

API 서버는 하나의 NestJS 프로젝트 안에서 여러 서비스로 나눠 각각의 Lambda 함수로 배포하는 형태였다. 각 서비스는 실제로 필요한 의존성만 import해서 사용하고 있었지만, 업로드하고 있던 node_modules는 공통이었다.

functions:
exampleLambdaIndex:
handler: ${PATH}.handler
package:
patterns:
- '!**'
- dist/** # js transfer result
- node_modules/** # all of dependencies
individually: true

예를 들어 회원 서비스가 실제로는 A, C 의존성만 사용하더라도, 배포 패키지에는 node_modules 전체가 들어가면서 A, B, C가 모두 포함됐다. 서비스가 커지고 의존성이 늘어날수록 이 방식은 금방 한계에 부딪혔다.

Lambda Layer로 공통 의존성을 일부 분리하고 있었지만, 그 정도로는 부족했다. 결국 **Lambda에 실제로 필요한 코드만 남기는 방식(Tree Shaking)**으로 바꿔야 했다.

Webpack으로 Lambda 함수별 번들을 만들자.

해결 방법으로 Webpack 기반 빌드를 선택했다. 목적은 단순했다.

  • Lambda 함수 하나가 실제로 사용하는 코드와 의존성만 묶는다.
  • 결과물을 dist/main.js 같은 단일 번들로 만든다.
  • 배포 시 node_modules 전체를 싣지 않는다.

다양한 번들링 도구가 있었지만, NestJS에서 공식적으로 지원하는 Webpack 옵션을 사용하는 쪽이 가장 안전하다고 판단했다.

package.json에서는 nest build --webpack을 사용하도록 바꾸고, webpack.config.js를 따로 두었다. 좀 더 자세한 설정은 NestJS Webpack 적용 방법에서 따로 정리해 두었다.

  • package.json

    "scripts": {
    "build": "nest build --webpack --webpackPath ${PATH}/webpack.config.js"
    }
  • webpack.config.js

    module.exports = (options, webpack) => {
    return {
    ...options,
    entry: `./src/${NESTJS_APP_PATH}`,
    externals: [],
    output: {
    ...options.output,
    clean: true,
    libraryTarget: 'commonjs2',
    },
    plugins: [
    ...options.plugins,
    new webpack.optimize.LimitChunkCountPlugin({
    maxChunks: 1,
    }),
    ],
    };
    };

Webpack으로 빌드하면 dist/main.js 같은 단일 결과물이 생성된다. 이 파일 안에 Lambda에 필요한 코드와 의존성이 함께 묶인다.

그다음 sls 설정도 이 번들 결과물을 기준으로 바꿨다.

  • package.json

    "scripts": {
    "deploy:example": "nest build --webpack --webpackPath ${PATH}/webpack.config.js && sls deploy -c ./${YAML_PATH}/serverless.yaml"
    }
  • YAML

  • main.js를 그대로 업로드하는 방식이므로 더 이상 node_modules 전체를 패키징하지 않는다.

  • 이번 함수는 번들 결과물만으로 동작할 수 있었기 때문에 Lambda Layer도 제거했다.

  • 원칙적으로는 단일 번들(main.js)만 업로드하는 구성을 목표로 했다. 다만 실제 설정이나 환경에 따라 추가 chunk 파일이 생길 수 있다면, 그 파일도 함께 패키징해야 한다.

    functions:
    exampleLambdaIndex:
    handler: dist/main.handler # main.js 의 handler 함수
    package:
    patterns:
    - '!**'
    - dist/main.js # webpack result
    individually: true

이후에는 아래처럼 항상 빌드 후 배포하도록 스크립트를 고정했다.

Terminal window
$ npm run deploy:example
  • 기존 방식: dist + node_modules 전체 업로드
  • 변경 후: dist/main.js 만 업로드
  • 결과: 약 250MB -> 15MB

배포 패키지 크기 문제도 해결됐고, S3에 쌓이는 배포 아티팩트 크기도 함께 줄일 수 있었다.


1) 왜 번들 크기가 크게 줄었는가

섹션 제목: “1) 왜 번들 크기가 크게 줄었는가”
  • 기존에는 AWS Lambda 함수에 필요한 코드만이 아니라 프로젝트의 공통 node_modules 전체가 함께 들어갔다.
  • Webpack 적용 후에는 AWS Lambda가 실제로 사용하는 코드와 의존성만 단일 파일로 묶었다.
  • 결국 문제는 “소스코드가 너무 많다”보다 “배포 패키지에 필요 없는 내용이 너무 많다”는 쪽에 가까웠다.
  • slspackage.patterns에 지정된 파일을 그대로 패키징해 업로드하므로, 빌드 없이 deploy하면 변경 사항이 반영되지 않을 수 있다.
  • 따라서 build && sls deploy를 하나의 스크립트로 묶어 두는 편이 안전하다.
  • 번들 결과가 항상 main.js 하나로만 끝난다고 단정하지 말고, 실제 빌드 산출물 구성을 한 번 확인하는 편이 좋다.
  • 단점은 빌드 시간이 늘어난다는 점이다. Webpack 적용 후에는 상황에 따라 30초 이상, 길면 몇 분까지도 빌드 시간이 증가했다.