- aws lambda 개발하기(1) – 로컬 개발 환경 구축(node.js + serverless)
- aws lambda 개발하기(2) – hellolambda, Gateway 트리거
- aws lambda 개발하기(3) – node package(모듈)설치 및 개발, 환경변수 적용
- aws lambda 개발하기(4) – serverless로 트리거(trigger), 대상(destination), 실행역할(role), VPC, 기본 설정
- aws lambda 개발하기(5) – serverless plugin (offline, prune plugin)
- aws lambda 개발하기(6) – Multi Endpoint Restful api 개발
- aws lambda 개발하기(7) – CircleCI를 이용하여 자동 배포하기
- aws lambda 개발하기(8) – Asynchronous Tasks by SQS(Simple Queue Service)
- aws lambda 개발하기(9) – Lambda Layer 이용하여 배포 사이즈 줄이기
람다 함수는 함수 자체만으로는 동작할 수 없습니다. 람다의 실행을 촉발시키는 트리거가 필요합니다. 따라서 람다로 Rest api를 만들기 위해서는 트리거로 aws gateway(이하 gateway)를 설정해 주어야 합니다. gateway는 람다 앞단에서 특정 endpoint로 들어오는 요청에 대하여 람다가 처리할 수 있도록 연결해 줍니다.
이번 장에서는 특정 URL로 들어오는 요청에 대하여 람다로 처리하는 방법에 대하여 실습할 것이며 아래와 같이 두 가지 방식을 선택할 수 있습니다.
- 하나의 endpoint에 대하여 하나의 람다 함수가 처리
- 여러 개의 endpoint에 대하여 하나의 람다 함수가 처리
얼핏 보면 1번 방식은 효율적이지 못해 보일 수 있습니다. endpoint마다 람다 함수를 새로 만들면 endpoint가 늘어날 때마다 람다 함수 개수도 늘어나 지속적인 관리 측면에서 불리하기 때문입니다.
하지만 endpoint와 람다 함수를 1:1로 가져가게 되면 장점도 있습니다.
첫 번째로는 endpoint 간에 람다 함수가 독립적이므로 서로 영향을 주지 않아 특정 람다에 문제가 발생하더라도 해당하는 endpoint에만 영향을 미치게 됩니다. 즉 장애 범위가 최소화됩니다.
두 번째로는 람다가 사용하는 자원을 효율적으로 배분할 수 있습니다. 람다는 실행 시 사용하는 메모리나 제한시간, 동시성 등에 대한 설정을 할 수 있는데, 람다가 분리되어 있으면 람다마다 서로 다른 설정값을 세팅할 수 있으며 무거운 작업을 수행하거나 자주 호출하는 람다에 대해서는 더 높은 자원을 할당하고 그 반대인 경우는 낮은 자원을 할당할 수 있습니다.
2번을 선택할 경우 1번의 장점이 단점으로 작용합니다. 하지만 목적에 따라 여러 개의 endpoint를 하나의 람다에서 처리하도록 하면 무분별하게 람다가 생성되는 것을 막을 수 있어 관리적 측면에서는 굉장히 유리해집니다. 또한 공통으로 사용되는 모듈이나 서비스에 대해서 여러 개의 endpoint가 공유를 할 수 있어 효율적이라 볼 수 있습니다.
1번은 여러 개의 람다를 만들면 되는 것이기 때문에 실습에서는 2번에 대해서 어떻게 구현해야 하는지 살펴보겠습니다.
프로젝트 생성
아래와 같이 serverless 프로젝트를 생성합니다.
$ sls create --template aws-nodejs --name restapi Serverless: Generating boilerplate... _______ __ | _ .-----.----.--.--.-----.----| .-----.-----.-----. | |___| -__| _| | | -__| _| | -__|__ --|__ --| |____ |_____|__| \___/|_____|__| |__|_____|_____|_____| | | | The Serverless Application Framework | | serverless.com, v1.67.0 -------' Serverless: Successfully generated boilerplate for template: "aws-nodejs" $ ls README.md handler.js serverless.yml
function명 변경
serverless 명령어로 프로젝트를 생성하면 function명이 기본 hello로 생성되기 때문에 이름을 바꿔줍니다.(그냥 써도 됩니다.)
아래처럼 handler.js, serverless.yml 두개 파일을 수정합니다.
handler.js
'use strict';
module.exports.router = async event => {
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
},
null,
2
),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
serverless.yml
service: restapi
provider:
name: aws
runtime: nodejs12.x
functions:
router:
handler: handler.router
node 라이브러리 설치
npm init으로 node 프로젝트를 초기화하고 lambda-api와 aws-sdk 라이브러리를 설치합니다. lambda-api는 하나의 람다 함수에서 여러 개의 endpoint를 다룰 수 있도록 도와주는 라이브러리입니다. aws-sdk는 람다 함수 개발 시 기본적으로 필요한 라이브러리 이므로 설치합니다.
lambda-api에 대한 자세한 내용은 아래 github를 확인해 주십시요.
https://github.com/jeremydaly/lambda-api
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (lambdarestapi)
version: (1.0.0)
description:
entry point: (handler.js)
test command:
git repository: (https://github.com/codej99/LambdaRestApi.git)
keywords:
author:
license: (ISC)
About to write to /Users/abel/project-lambda/LambdaRestApi/package.json:
{
"name": "lambdarestapi",
"version": "1.0.0",
"description": "AWS Lambda Rest API",
"main": "handler.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/codej99/LambdaRestApi.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/codej99/LambdaRestApi/issues"
},
"homepage": "https://github.com/codej99/LambdaRestApi#readme"
}
Is this OK? (yes) yes
$ npm i lambda-api
npm notice created a lockfile as package-lock.json. You should commit this file.
+ lambda-api@0.10.5
added 1 package from 1 contributor and audited 1 package in 1.661s
found 0 vulnerabilities
$ npm i aws-sdk
+ aws-sdk@2.657.0
added 14 packages from 66 contributors and audited 18 packages in 3.541s
found 0 vulnerabilities
serverless plugin 설치
실습에서는 serverless-prune-plugin을 설치합니다. 배포 내역 중 최근 내역만 남기고 자동으로 삭제시켜주는 플러그인입니다.
$ sls plugin install -n serverless-prune-plugin
serverless.yml에 plugin 설정을 추가합니다.
custom:
prune:
automatic: true
number: 5
endpoint 코드 작성
각각의 endpoint에 대해서 Http Method에 따라 지정한 메서드가 처리하도록 작성합니다. 각각의 메서드에서는 request, response를 인자로 받을 수 있기 때문에 request정보를 기반으로 내부 로직을 처리한 후 response로 결과를 출력합니다.
request정보에서 pathVariable, requestParameter, requestBody정보는 다음과 같이 얻을 수 있습니다.
- pathVariable – path에 ‘~~/:id’로 표기하고 메서드 내부에서 req.params.id로 접근하여 값을 얻을수 있습니다.
- requestParameter – request.params로 접근하여 값을 얻을수 있습니다.
- requestBody – request.body로 접근하여 값을 얻을수 있습니다.
handler.js
'use strict';
const api = require('lambda-api')();
api.get('/v1/membership/user/:id', async(req, res) => {
console.log(`${req.method} - ${req.route} => pathParameter = ${JSON.stringify(req.params.id)} | body = ${JSON.stringify(req.body)}\n`);
res.status(200).json({
"code": 0,
"msg": "success",
"data": {
"id": "id-01",
"name": "happydaddy",
"age": 32,
"job": "programmer"
}
});
});
api.put('/v1/membership/user', async(req, res) => {
console.log(`${req.method} - ${req.route} => queryString = ${JSON.stringify(req.query)} | body = ${JSON.stringify(req.body)}\n`);
res.status(200).json({
"code": 0,
"msg": "success",
"data": {
"id": "id-01",
"name": "happydaddy",
"age": 32,
"job": "programmer"
}
});
});
api.post('/v1/membership/user', async(req, res) => {
console.log(`${req.method} - ${req.route} => queryString = ${JSON.stringify(req.query)} | body = ${JSON.stringify(req.body)}\n`);
res.status(200).json({
"code": 0,
"msg": "success",
"data": req.body
});
});
api.delete('/v1/membership/user', async(req, res) => {
console.log(`${req.method} - ${req.route} => queryString = ${JSON.stringify(req.query)} | body = ${JSON.stringify(req.body)}\n`);
res.status(200).json({
"code": 0,
"msg": "success"
});
});
module.exports.router = async (event, context) => {
return await api.run(event, context)
};
API 테스트
local 개발환경에선 gateway를 람다 함수에 트리거로 설정할 수 없어 테스트가 쉽지 않습니다. 따라서 아래와 같이 request 정보를 json파일로 생성하고 람다 함수에 정보를 주입하여 테스트를 진행합니다. ( serverless-offline 플러그인을 이용해도 되지만 실습에서는 json을 이용한 방법으로 진행합니다. )
test 디렉터리 생성 및 .json 생성
LambdaRestApi
├── README.md
├── handler.js
├── node_modules
│ └── lambda-api
├── package-lock.json
├── package.json
├── serverless.yml
└── test
├── delete-user.json
├── get-user.json
├── post-user.json
└── put-user.json
get-user.json
{
"resource": "/v1/membership/user/id-01",
"path": "/v1/membership/user/id-01",
"httpMethod": "GET",
"headers": {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ..."
},
"queryStringParameters": {
},
"body": {
}
}
put-user.json
{
"resource": "/v1/membership/user",
"path": "/v1/membership/user",
"httpMethod": "PUT",
"headers": {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ..."
},
"queryStringParameters": {
},
"body": {
"id": "id-01",
"name": "happydaddy",
"age": 32,
"job": "programmer"
}
}
post-user.json
{
"resource": "/v1/membership/user",
"path": "/v1/membership/user",
"httpMethod": "POST",
"headers": {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ..."
},
"queryStringParameters": {
},
"body": {
"id": "id-01",
"name": "happydaddy",
"age": 28,
"job": "doctor"
}
}
delete-user.json
{
"resource": "/v1/membership/user",
"path": "/v1/membership/user",
"httpMethod": "DELETE",
"headers": {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) ..."
},
"queryStringParameters": {
"id": "id-01"
},
"body": {
}
}
test json을 이용한 람다 테스트
serverless invoke local 명령을 통해 람다를 로컬에서 실행합니다. 이때 -p 옵션으로 위에서 생성한 json파일을 지정하여 해당 정보가 람다에 주입되도록 합니다.
$ sls invoke local -f router -p test/get-user.json
GET - /v1/membership/user/:id => pathParameter = {"id":"id-01"} | body = {}
{
"headers": {
"content-type": "application/json"
},
"statusCode": 200,
"body": "{\"code\":0,\"msg\":\"success\",\"data\":{\"id\":\"id-01\",\"name\":\"happydaddy\",\"age\":32,\"job\":\"programmer\"}}",
"isBase64Encoded": false
}
$ sls invoke local -f router -p test/put-user.json
PUT - /v1/membership/user => queryString = {} | body = {"id":"id-01","name":"happydaddy","age":32,"job":"programmer"}
{
"headers": {
"content-type": "application/json"
},
"statusCode": 200,
"body": "{\"code\":0,\"msg\":\"success\",\"data\":{\"id\":\"id-01\",\"name\":\"happydaddy\",\"age\":32,\"job\":\"programmer\"}}",
"isBase64Encoded": false
}
$ sls invoke local -f router -p test/post-user.json
POST - /v1/membership/user => queryString = {} | body = {"id":"id-01","name":"happydaddy","age":28,"job":"doctor"}
{
"headers": {
"content-type": "application/json"
},
"statusCode": 200,
"body": "{\"code\":0,\"msg\":\"success\",\"data\":{\"id\":\"id-01\",\"name\":\"happydaddy\",\"age\":28,\"job\":\"doctor\"}}",
"isBase64Encoded": false
}
$ sls invoke local -f router -p test/delete-user.json
DELETE - /v1/membership/user => queryString = {"id":"id-01"} | body = {}
{
"headers": {
"content-type": "application/json"
},
"statusCode": 200,
"body": "{\"code\":0,\"msg\":\"success\"}",
"isBase64Encoded": false
}
람다 배포 및 트리거 연결
로컬에서 테스트가 완료되었으므로 aws에 배포하고 gateway를 트리거로 연결합니다.
sls deploy 명령으로 람다를 배포합니다.
$ sls deploy -s dev Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Creating Stack... Serverless: Checking Stack create progress... ........ Serverless: Stack create finished... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service restapi.zip file to S3 (8.74 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ............ Serverless: Stack update finished... Service Information service: restapi stage: dev region: ap-northeast-2 stack: restapi-dev resources: 5 api keys: None endpoints: None functions: router: restapi-dev-router layers: None Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.
AWS Gateway 페이지에 접속하여 API 생성을 클릭합니다.

REST API 구축을 클릭합니다.

아래와 같이 선택하고 이름 및 설명은 적당한 내용을 입력하고 API 생성을 클릭합니다.

생성이 완료되면 리소스 화면이 보이는데 아직 만들어진 리소스가 없으므로 새로 생성합니다. 실습에서는 /v1/membership/{proxy+} 형태로 리소스를 생성합니다.


위 방법을 반복하여 membership 리소스를 추가로 생성합니다. 그리고 membership 하위에 다시 리소스를 추가하는데 이번에는 아래처럼 프락시 리소스로 구성을 체크하여 {proxy+}를 생성합니다. 프락시 리소스를 추가하면 membership 하위에 어떤 path로 요청이 오더라도 해당 endpoint에서 공통으로 처리할 수 있습니다.(즉 여러 엔드포인트를 한 곳에서 받아서 처리하기 위한 설정입니다.)

여기까지 리소스를 생성하고 나면 아래와 같이 해당 리소스를 처리할 방식을 선택할 수 있는데 실습에서는 람다로 처리할 것이므로 통합 유형 – Lambda 함수 프락시를 선택합니다. Lambda 함수에는 위에서 배포한 함수 이름을 입력합니다. 저장을 클릭하면 권한 추가 화면이 뜨는데 확인을 누르면 Gateway 설정이 완료됩니다.


Gateway 리소스 배포
Gateway에서 설정한 메서드 및 리소스 정보를 반영하려면 배포를 해야 합니다. 아래와 같이 리소스 화면에서 작업 – API 배포를 클릭하여 배포를 진행합니다.

기존에 생성된 스테이지가 없다면 새 스테이지를 선택하고 생성할 스테이지 정보를 입력합니다. 실습에서는 dev환경에 배포할 것이므로 dev로 스테이지를 생성합니다.


배포가 완료되면 dev 스테이지 편집기 화면으로 이동하고 화면 위쪽에서 URL 호출 정보를 확인할 수 있습니다. 위 URL을 기본 베이스로 하여 접속하면 트리거가 발생하고 람다 함수가 실행됩니다.
브라우저에서 람다의 Get 메서드를 호출해 봅니다. Response 정보가 브라우저에 출력되는 것을 확인할 수 있습니다.

이번에는 PUT, POST, DELETE를 호출해 봅니다. 해당 메서드는 브라우저에서 테스트하기 힘드므로 터미널에서 호출하여 테스트합니다. 로컬에서 테스트한 결과와 동일한 결과를 확인할 수 있습니다.
$ curl --header "Content-Type: application/json" \
--request PUT \
--data '{"id": "id-01","name": "happydaddy","age": 32,"job": "programmer"}' \
https://aiw63mofx9.execute-api.ap-northeast-2.amazonaws.com/dev/v1/membership/user
{"code":0,"msg":"success","data":{"id":"id-01","name":"happydaddy","age":32,"job":"programmer"}}
$ curl --header "Content-Type: application/json" \
--request POST \
--data '{"id": "id-01","name": "happydaddy","age": 29,"job": "doctor"}' \
https://aiw63mofx9.execute-api.ap-northeast-2.amazonaws.com/dev/v1/membership/user
{"code":0,"msg":"success","data":{"id":"id-01","name":"happydaddy","age":29,"job":"doctor"}}
$ curl --header "Content-Type: application/json" \
--request DELETE \
https://aiw63mofx9.execute-api.ap-northeast-2.amazonaws.com/dev/v1/membership/user?id=id-01
{"code":0,"msg":"success"}
여기까지 진행하면 실습이 완료됩니다. 실습을 통해 하나의 람다 함수를 통해서도 여러 개의 endpoint를 처리할 수 있음을 확인할 수 있었습니다.
실습에서 사용한 코드는 아래 github에서 확인할 수 있습니다.
https://github.com/codej99/LambdaRestApi














