MSA(MicroService Architecture)로 백엔드 시스템을 구축할 때 Kubernetes를 이용하는것을 선호 하지만, 서비스 초기에 고정비용에 대한 고민을 하게 되었고,
AWS의 lambda를 이용하여, 사용한 만큼만 비용을 지불 하는 방식을 검토하게 되었습니다.
고려사항
AWS SAM(AWS Serverless Application Model) 과 서드파티 프레임웍인 Serverless 중, 원하는 환경을 구성하기에는 Serverless가 조금 더 편리하여 Serverless 를 선택 하였습니다.
패키지 매니저는 yarn 을 사용하였고,
개인적으로 npm 패키지를 글로벌로 설치하는것을 좋아하지 않아 npx를 이용 하였습니다.
(npx를 사용한 명령은 모두 해당 패키지 document을 참고하여 global 설치 후 사용할 수 있습니다.)
프로젝트를 생성하고, 기본적인(로컬에서 실행할 수 있는) 설정을 합니다.
프로젝트 생성
$ npx @nestjs/cli new nest-lambda
$ cd nest-lambda
환경변수 사용을 위한 nestjs 패키지 설치
$ yarn add @nestjs/config
.env 파일 생성
#
# .env
#
NODE_ENV=development
# 로컬 실행시 사용
PORT=3000
//
// 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();
}
//
// 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 을 기본으로 사용합니다.
nest 어플리케이션 로컬 실행
$ yarn start
접속 테스트
$ curl localhost:3000/api/v1
사전 준비사항
$ yarn add aws-lambda @vendia/serverless-express
$ yarn add -D serverless-jetpack serverless-offline
@vendia/serverless-express실행을 위해 tsconfig.json 수정 합니다.
//
// tsconfig.json
//
{
"compilerOptions": {
...
"esModuleInterop": true
}
}
//
// 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);
};
별도의 docker 이미지 생성 없이, 바로 AWS Lambda에 deploy.
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+}
$ yarn build
$ npx serverless deploy
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-2.amazonaws.com/dev/api/v1
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 항목을 삭제하여야 합니다.
FROM public.ecr.aws/lambda/nodejs:18
COPY package*.json .
RUN npm install
ADD dist ./dist
ENV NODE_ENV='production'
CMD ["dist/lambda.handler"]
$ yarn build
$ npx serverless deploy
$ curl https://XXXXXXXXXX.execute-api.ap-northeast-2.amazonaws.com/dev/api/v1
$ npx serverless remove
관련된 모든 AWS리소스가 삭제된다고는 하나, 가끔 실패 하는경우가 있어, aws 콘솔에 들어가 모든 리소스(Cloud Formation, Lambda, S3, CloudWatch, ECR 등)가 삭제 되었는지 확인 하는것이 좋습니다.
nestjs 서버를 docker 이미지 빌드 없이 deploy 하는 경우,
Cold Start 짧은 장점이 있지만, 250메가바이트의 한계라는 단점이 있습니다.
docker image로 deploy 할 경우는 10기가바이트의 용량제한으로 비교적 넉넉한 반면, Cold Start시간이 길어져, Cold 상태로 넘어가지 않도록 별도의 트릭이 필요 할것 같습니다.
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
좋은 내용 감사합니다! 많은 도움이 될 것 같아요!
작성하신 내용에 대해 질문하고 싶은게 있는데 이메일로 여쭤봐도 될까요?