Nest + Lambda + Docker Image

projaguar·2022년 12월 12일
1
post-thumbnail

MSA(MicroService Architecture)로 백엔드 시스템을 구축할 때 Kubernetes를 이용하는것을 선호 하지만, 서비스 초기에 고정비용에 대한 고민을 하게 되었고,
AWS의 lambda를 이용하여, 사용한 만큼만 비용을 지불 하는 방식을 검토하게 되었습니다.

고려사항

  • 기존 코드와 호환성을 유지하며, 필요시 최소한의 작업으로 실행 플랫폼을 변경(예 Kubernetes로의 변경)
  • 개발 단계에서의 로컬 테스트의 용의성, 신뢰성
  • 개발 및 deploy 편의성

AWS SAM(AWS Serverless Application Model) 과 서드파티 프레임웍인 Serverless 중, 원하는 환경을 구성하기에는 Serverless가 조금 더 편리하여 Serverless 를 선택 하였습니다.

패키지 매니저는 yarn 을 사용하였고,
개인적으로 npm 패키지를 글로벌로 설치하는것을 좋아하지 않아 npx를 이용 하였습니다.
(npx를 사용한 명령은 모두 해당 패키지 document을 참고하여 global 설치 후 사용할 수 있습니다.)

프로젝트 기본 설정

프로젝트를 생성하고, 기본적인(로컬에서 실행할 수 있는) 설정을 합니다.

1. 프로젝트 생성 및 환경 변수 설정

프로젝트 생성

$ npx @nestjs/cli new nest-lambda
$ cd nest-lambda

환경변수 사용을 위한 nestjs 패키지 설치

$ yarn add @nestjs/config

.env 파일 생성

#
# .env
#

NODE_ENV=development
# 로컬 실행시 사용
PORT=3000

2. main.ts 수정

//
// src/main.ts
//

import { Logger, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';

const globalPrefix = 'api';
const versionPrefix = 'v';
const defaultVersion = '1';

export const getApp = async () => {
  const app = await NestFactory.create(AppModule);

  // security
  app.use(helmet());

  // prefix
  app.setGlobalPrefix(globalPrefix);

  // versioning
  app.enableVersioning({
    type: VersioningType.URI,
    prefix: versionPrefix,
    defaultVersion: defaultVersion,
  });

  return app;
};

async function bootstrap() {
  const app = await getApp();
  const port = process.env.PORT || 3000;
  await app.listen(port);
  Logger.log(
    `🚀 Application is running on: http://localhost:${port}/${globalPrefix}/${versionPrefix}${defaultVersion}`,
  );
}

// 로컬 개발환경 경우만 실행
if (process.env.NODE_ENV === 'development') {
  bootstrap();
}

3. app.module.ts 수정

//
// src/app.module.ts
//
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

bootstrap()의 실행은 환경변수 NODE_ENV 가 'development' 일 경우만 실행하도록 설정합니다.
Lambda 등록시, 환경변수 설정을 통해 main의 bootstrap이 실행되지 않도록 설정합니다.
global prefix 설정과 version 설정으로, 기본 domain url 뒤에 /api/v1 을 기본으로 사용합니다.

4. 실행 테스트

nest 어플리케이션 로컬 실행

$ yarn start

접속 테스트

$ curl localhost:3000/api/v1

Serverless 설정 (공통)

사전 준비사항

  1. aws 계정 설정
    https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/cli-configure-files.html
    https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/cli-configure-envvars.html
  2. docker desktop 설치
    https://www.docker.com/products/docker-desktop/

1. 관련 패키지 설치

$ yarn add aws-lambda @vendia/serverless-express
$ yarn add -D serverless-jetpack serverless-offline
  • aws-lambda: 프로비저닝이나 서버관리 없이 코드를 실행하게 해줍니다.
  • @vendia/serverless-express: AWS Lambda 또는 API Gateway의 요청을 node.js 앱이 응답할 수 있도록 해줍니다.
  • serverless-jetpack: 코드 옵티마이저.
  • serverless-offline: 코드를 로컬에서 실행할 수 있게 해줍니다.

2. TypeScript 컴파일 옵션 수정

@vendia/serverless-express실행을 위해 tsconfig.json 수정 합니다.

//
// tsconfig.json
//

{
  "compilerOptions": {
    ...
    "esModuleInterop": true
  }
}

3. Lambda 핸들러 작성

//
// src/lambda.ts
//

import { configure as serverlessExpress } from '@vendia/serverless-express';
import { getApp } from './main';

import { Callback, Context, Handler } from 'aws-lambda';

let server: Handler;

const getExpressApp = async (): Promise<any> => {
  const app = await getApp();
  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
};

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  server = server ?? (await getExpressApp());
  return server(event, context, callback);
};

Nest 앱 deploy (선택1)

별도의 docker 이미지 생성 없이, 바로 AWS Lambda에 deploy.

1. serverless.yaml 작성

service: nest-lambda-serverless

plugins:
  - serverless-jetpack
  - serverless-offline

provider:
  name: aws
  runtime: nodejs18.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
  environment:
    NODE_ENV: production

functions:
  api:
    handler: dist/lambda.handler
    events:
      - http:
          method: any
          path: /{proxy+}
  • provider->region은 앱을 업로드할 aws region으로 변경합니다.
  • 환경변수 NODE_ENV를 설정하여, nest 앱 자체 부트스트랩 실행을 방지 하여야 합니다.

2. deploy 및 test

$ yarn build
$ npx serverless deploy

$ curl https://XXXXXXXXXX.execute-api.ap-northeast-2.amazonaws.com/dev/api/v1

docker image deploy (선택2)

1. serverless.yaml 작성

service: nest-lambda

plugins:
  - serverless-jetpack
  - serverless-offline

provider:
  name: aws
  runtime: nodejs18.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
  ecr:
    images:
      nest-lambda:
        path: ./
#        platform: linux/amd64

functions:
  api:
    architecture: arm64
    image:
      name: nest-lambda
      command:
        - dist/lambda.handler
      entryPoint:
        - '/lambda-entrypoint.sh'
    events:
      - http:
          method: any
          path: /{any+}

provieder-ecr 설정을 통해, 도커 이미지를 빌드하고, ECR (AWS Elastic Container Registry)에 빌드된 이미지를 등록합니다.
functions->api->image 설정에서 등록된 이미지를 사용하여 Lambda 실행합니다.

주의
인텔 CPU에서 도커 빌드 할 경우, functions->api->architecture 항목을 삭제 하여야 합니다. (또는 provider->ecr->images->[이미지이름]->platform 위치에 arm 빌드 설정을 하여야 합니다.)

애플 M1, M2칩에서 도커 빌드를 하면 arm64 이미지가 생성 됩니다.
serverless는 기본으로 x86_64이미지를 사용하여 등록하기 때문에,
docker 빌드 시 x86_64로 빌드 하거나, arm64로 실행하는 Lambda 실행환경을 별도로 설정 하여야 합니다.

애플 M1, M2칩에서 개발 시 x86이미지 생성 및 Labmda 실행을 하고자 할 때는,
위의 설정에서 provider->ecr->images->[이미지이름]->platform에 'linux/amd64' 를 설정하고, functions->api->architecture 항목을 삭제하여야 합니다.

2. Dockerfile 작성

FROM public.ecr.aws/lambda/nodejs:18

COPY package*.json .
RUN npm install

ADD dist ./dist

ENV NODE_ENV='production'

CMD ["dist/lambda.handler"]

3. deploy 및 test

$ yarn build
$ npx serverless deploy

$ curl https://XXXXXXXXXX.execute-api.ap-northeast-2.amazonaws.com/dev/api/v1

Clean Up

$ npx serverless remove

관련된 모든 AWS리소스가 삭제된다고는 하나, 가끔 실패 하는경우가 있어, aws 콘솔에 들어가 모든 리소스(Cloud Formation, Lambda, S3, CloudWatch, ECR 등)가 삭제 되었는지 확인 하는것이 좋습니다.

Conclusion

nestjs 서버를 docker 이미지 빌드 없이 deploy 하는 경우,
Cold Start 짧은 장점이 있지만, 250메가바이트의 한계라는 단점이 있습니다.
docker image로 deploy 할 경우는 10기가바이트의 용량제한으로 비교적 넉넉한 반면, Cold Start시간이 길어져, Cold 상태로 넘어가지 않도록 별도의 트릭이 필요 할것 같습니다.

Reference

https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml
https://docs.nestjs.com/faq/serverless
https://www.serverless.com/examples/aws-node-typescript-nest
https://nishabe.medium.com/nestjs-serverless-lambda-aws-in-shortest-steps-e914300faed5
https://dev.to/aws-builders/deploy-a-nestjs-api-to-aws-lambda-with-serverless-framework-4poo
https://velog.io/@jiffydev/NestJS-AWS-SAM-으로-백엔드-배포하기
https://velog.io/@ghdmsrkd/NestJS-App-deploy-with-lambda-docker-container-support-and-Serverless-Framwork
https://www.serverless.com/blog/keep-your-lambdas-warm/
https://awstip.com/prevent-lambda-cold-starts-using-serverless-framework-c4f9dfe545b3

profile
아직도 개발하고 있는 개발자 입니다

1개의 댓글

comment-user-thumbnail
2023년 9월 26일

좋은 내용 감사합니다! 많은 도움이 될 것 같아요!
작성하신 내용에 대해 질문하고 싶은게 있는데 이메일로 여쭤봐도 될까요?

답글 달기