이 연재글은 AWS 람다(lambda) 실습의 6번째 글입니다.

람다 함수는 함수 자체만으로는 동작할 수 없습니다. 람다의 실행을 촉발시키는 트리거가 필요합니다. 따라서 람다로 Rest api를 만들기 위해서는 트리거로 aws gateway(이하 gateway)를 설정해 주어야 합니다. gateway는 람다 앞단에서 특정 endpoint로 들어오는 요청에 대하여 람다가 처리할 수 있도록 연결해 줍니다.

이번 장에서는 특정 URL로 들어오는 요청에 대하여 람다로 처리하는 방법에 대하여 실습할 것이며 아래와 같이 두 가지 방식을 선택할 수 있습니다.

  1. 하나의 endpoint에 대하여 하나의 람다 함수가 처리
  2. 여러 개의 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

연재글 이동[이전글] aws lambda 개발하기(5) – serverless plugin (offline, prune plugin)
[다음글] aws lambda 개발하기(7) – CircleCI를 이용하여 자동 배포하기