- 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