스터디에서 혹은 개인, 팀을 이뤄서 프로젝트를 하는 경우가 종종 있습니다. 그럴 때 고민되는 게 API 서버를 어디에 어떻게 올릴 것인가에 대한 문제이죠.
테스트, 개인용 프로젝트인데 AWS에서 저렴하게 EC2 하나를 생성하더라도 한 달에 최소 몇 만원 정도는 비용이 발생합니다.
이 비용조차 아까워지기 마련이죠. 그래서 더 저렴한 방법은 없을까 고민해 보았어요.
API 서버를 띄우기 위해 AWS EC2를 받았다고 가정해 봅시다.
메모리가 2기가 정도는 되어야 하기에 t4g.small
사양으로 할당받았다고 가정해 보면,
한 달 사용료는 0.0208 * 24 * 30 = $14.976 가 발생합니다. 대략 2만원 정도 발생합니다.
하루에 1~2번 호출될 서버에 2만원도 과하다고 생각할 수 있어요.
그럼, AWS Lambda 가격도 살펴보겠습니다.
lambda 는 무기한 프리티어를 제공합니다.
즉, 월별 100만건 요청까지는 무료이고, 그 이후로는 100건당 $0.20 가 발생합니다.
($0.20 per 1 million requests thereafter, or $0.0000002 per request)
이는 사이드프로젝트에서 사용하기에 충분한 요청량으로 보이고 또한 월 100만건 요청이 들어오고, 추가 요청까지 들어온다면 충분히 요금을 더 낼 의사가 있습니다. (사이드프로젝트의 성공일까요)
요청이 1건이던 100건이던 상관없이 항상 서버 비용을 내야하는 EC2와 비교해서, 실행 횟수에 따라 요금을 책정하는 Lambda는 사이드프로젝트에서 사용하기 더 적절해보입니다.
물론 Lambda의 요금 책정은 단순히 요청 횟수 뿐 아니라, 호출에 대한 실행시간도 비용이 영향을 미칩니다.
실행 시간은 요청이 들어오고, 리턴이 나가는 시간까지의 비용입니다. 아래에서 더 자세히 설명합니다.
월별 10,000건의 호출이 호출당 2초가 걸린다고 가정해보고 계산해보아도 월 0원이 발생하는걸 볼 수 있습니다.
그럼 위에서 설명한 Lambda는 뭐하는 서비스이길래 EC2보다 더 저렴하게 사용할 수 있을까요?
Lambda Service에 대해 AWS 공식 홈페이지에는 아래와 같이 설명하고 있습니다.
Lambda는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 컴퓨팅 서비스입니다.
Lambda는 고가용성 컴퓨팅 인프라에서 코드를 실행하고 서버와 운영 체제 유지 관리, 용량 프로비저닝 및 자동 조정, 코드 및 보안 패치 배포, 로깅 등 모든 컴퓨팅 리소스 관리를 수행합니다.
Lambda를 사용하면 거의 모든 유형의 애플리케이션 또는 백엔드 서비스에 대한 코드를 실행할 수 있습니다.
즉, serverless로 함수를 실행할 수 있도록 해주는 AWS 서비스입니다.
(cloud에 많은 serverless라고하는 서비스들은 실제로 서버가 없다는 뜻은 아닙니다. 사용자가 서버를 관리하지 않아도 된다는 의미지요)
여기서 중요한건 함수
를 실행하는 것입니다.
exports.handler = async function(event, context) {
console.log("EVENT: \n" + JSON.stringify(event, null, 2))
return context.logStreamName
}
handler 함수를 통해 event를 받아 어떠한 작업을 한 후 return값을 돌려줄 수 있습니다.
하지만, 우리가 실행하고싶은건 단순히 함수 1개가 아니라 서버였습니다. (endpoint가 무수히 많은 api 서버 1대 말이죠.)
그래서 예전에는 실제로 한개의 웹/앱 서비스를 구성하기위해 무수히 많은 lambda 함수를 사용하기도 했었습니다.
본 글에서는 이 방법이 아닌 Serverless npm 모듈을 사용해서 함수가 아닌 NestJS 프로젝트를 Lambda 한개의 서비스로 담아 배포하는 방법에 대해 설명하려 합니다.
결론부터 말씀드리면 꼭 그렇지는 않습니다.
이런 Lambda역시 단점이 존재합니다. 바로 Cold Start의 문제입니다.
Lambda 의 실행순서를 보면 다음과 같습니다.
Lambda 함수 호출되면 ECR 혹은 S3에서 소스코드를 읽어와서 실행을 준비합니다. 그리고 설정된 서버구성(RAM 등)을 읽어와 서버를 구성합니다. 마지막으로 Hander코드를 통해 함수가 실행된 후 결과를 리턴하죠.
즉, 소스코드를 읽어와 서버를 구성하고 실행하는데까지 어느정도의 시간이 필요합니다. 이렇게 실행되는것을 Cold Start
라고 하고, 이 과정으로 인해 Latency 가 발생합니다.
Lambda는 함수 호출이 끝난 다음에는 해당 서버를 즉시 내리지 않고 잠시 기다립니다. 서버가 완전히 내려가기 전에, 해당 함수가 다시한번 더 호출되면 이번엔 소스코드를 읽어오는 과정이 생략되고 서버를 재사용하여 함수가 즉시 실행될 수 있습니다. 그리고 이렇게 실행되는걸 Warm Start
라고 부릅니다.
이 때에는 서버구성하는 과정이 빠지기 때문에 Cold Start
보다 더 빠르고, Latency 가 발생하지 않습니다.
이처럼 자주 사용되는 함수는 Warm Start로 빠르게 응답을 주는 반면, 가끔 호출되는 함수의 경우 Cold Start로 실행되어 첫 호출에는 결과받는데 시간이 조금 걸립니다 (1초 이상 걸릴 수 있습니다)
그리고 Warm Start로 게속 실행되는 함수 (주기적으로 호출되어서)라고 하더라도 lambda에 새로운 코드가 배포되면 Cold Start로 동작합니다.
그 외에도 동시성에 대한 호출 역시 Cold Start로 실행될 수 있는데, 아래 링크의 AWS 공식문서를 참고 바랍니다.
NestJS는 TypeScript 언어를 사용해 API 서버를 손쉽게 구성할 수 있는 Framework입니다.
Serverless 는 어떤 프로젝트를 serverless 로 실행할 수 있도록 구성해주는 프레임워크 입니다. (여기서는 NestJS 를 사용하지만 express와 같은 다른 프로젝트도 모두 사용가능합니다.)
이 npm 모듈을 사용해서 NestJS를 serverless로 실행할 수 있는 구성을 하고, Lambda로 배포할 수 있습니다.
nest cli 명령어를 통해 프로젝트를 생성해줍니다.
nest new aws-sample
만약 nest 명령어가 없다면 nest cli 를 먼저 설치합니다.
npm install -g @nestjs/cli
nest start
롤 통해 생성된 서버를 실행할 수 있고, curl http://localhost:3000
으로 잘 실행되는지 확인합니다.
코드 배포를 위해 AWS 사용자를 생성합니다. AWS의 IAM 서비스로 들어가 새로운 사용자를 생성하고, keyId, secretId 를 잘 저장합니다.
사용자를 추가하고, 필요한 권한을 추가합니다.
(본 글에서 Serverless를 통해 AWS Lambda 서비스로 배포하기 위해서는, S3, Lambda, API Gateway 등의 권한이 필요합니다)
마지막으로 NestJS 프로젝트에 AWS Lambda로 배포하기위해 Serverless Framework 를 세팅합니다.
우선 serverless를 설치합니다.
$npm i -g serverless
이후 설치된 serverless 는 sls 라는 명령어를 통해 실행이 가능합니다.
구성을 위해 몇가지 파일을 추가합니다.
service: sample-project
plugins:
- serverless-plugin-typescript
- serverless-plugin-warmup
- serverless-offline
provider:
name: aws
region: ap-northeast-2
stage: dev
runtime: nodejs16.x
ecr:
images:
appimage:
path: ./
functions:
main:
handler: src/lambda.handler
events:
- http:
method: any
path: /{any+}
서울로 배포하기위해 region 설정을 ap-northeast-2
를 사용합니다.
stage 설정을 통해 env를 분리해서 사용할 수 있습니다.
"tsBuildInfoFile": ".tsbuildinfo",
"esModuleInterop": false
(아래 자세한 오류 내용 설명)
import { Handler, Context } from 'aws-lambda';
import { Server } from 'http';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const express = require('express');
const binaryMimeTypes: string[] = [];
let cachedServer: Server;
async function bootstrapServer(): Promise<Server> {
if (!cachedServer) {
const expressApp = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
);
nestApp.use(eventContext());
await nestApp.init();
cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
}
return cachedServer;
}
export const handler: Handler = async (event: any, context: Context) => {
cachedServer = await bootstrapServer();
return proxy(cachedServer, event, context, 'PROMISE').promise;
};
Running "serverless" from node_modules
Compiling with Typescript...
Using local tsconfig.json - tsconfig.json
{
file: undefined,
start: undefined,
length: undefined,
messageText: "Option '--incremental' can only be specified using tsconfig, emitting to single file or when option '--tsBuildInfoFile' is specified.",
category: 1,
code: 5074,
reportsUnnecessary: undefined,
reportsDeprecated: undefined
}
Environment: darwin, node 16.13.2, framework 3.25.1 (local) 3.25.1v (global), plugin 6.2.2, SDK 4.3.2
Docs: docs.serverless.com
Support: forum.serverless.com
Bugs: github.com/serverless/serverless/issues
Error:
TypeError: Cannot read properties of undefined (reading 'getLineAndCharacterOfPosition')
tsconfig.json에 "tsBuildInfoFile": ".tsbuildinfo",
설정을 통해 해결합니다.
ANY /dev/test (λ: main)
✖ Unhandled exception in handler 'main'.
✖ TypeError: (0 , express_1.default) is not a function
at bootstrapServer (/Users/byun/Desktop/program/github/time-manager/api/.build/src/lambda.js:14:50)
at handler (/Users/byun/Desktop/program/github/time-manager/api/.build/src/lambda.js:23:26)
at InProcessRunner.run (file:///Users/byun/Desktop/program/github/time-manager/api/node_modules/serverless-offline/src/lambda/handler-runner/in-process-runner/InProcessRunner.js:87:20)
at async MessagePort.<anonymous> (file:///Users/byun/Desktop/program/github/time-manager/api/node_modules/serverless-offline/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js:24:14)
✖ (0 , express_1.default) is not a function
tsconfig.json에 "esModuleInterop": false
설정을 통해 해결합니다.
serverless 배포를 위해 위 AWS에서 만든 profile key정보를 설정합니다.
명령어를 통해 프로필을 설정할 수 있습니다.
sls config credentials --provider aws --key {keyId} --secret ${secretId}
실행하고나면 ~/.aws
경로에 credentials
파일이 생성됩니다. 열어보면 default 가 생겨있을거에요.
ex)
[default]
aws_access_key_id=...
aws_secret_access_key=...
만약 이미 파일이 있어서 위 명령어 실행이 안된다면, -o 옵션을 붙여주면 됩니다. (override option)
마지막으로 sls deploy
명령어를 통해 배포할 수 있습니다.
AWS Lambda로 가보면 잘 배포된걸 확인할 수 있습니다.
전체가 동작하는 과정은 아래 그림과 같습니다.