본문으로 바로가기

docs.aws.amazon.com/ko_kr/lambda/latest/dg/with-ddb.html

 

Using AWS Lambda with Amazon DynamoDB - AWS Lambda

Using AWS Lambda with Amazon DynamoDB AWS Lambda 함수를 사용하여 Amazon DynamoDB 데이터 스트림의 레코드를 처리할 수 있습니다. DynamoDB 스트림를 사용하여 DynamoDB가 업데이트될 때마다 추가 작업을 수행하는 L

docs.aws.amazon.com

 

차례

 

1. 어떤 버전의 sdk를 사용할 것인가?

2. DocumentClient, DynamoDB, AWS.Request

3. DynamoDB 접근을 위한 IAM 정책, 역할 생성

4. GW를 통해 접근 가능한 End Point 만들기

5. GW CORS 구성


 

DynamoDB도 serverless라서인지, lambda와 함께 쓰이는 케이스가 많다.

serverless 프레임웤에서도 dynamoDB를 사용하기 위한 보일러플레이트도 제공하고 있다.

 

https://www.serverless.com/

 

dynamoDB에 대한 간략한 설명, 사용법은 공식 문서와 darrengwon.tistory.com/1340 를 참고하도록하고, 여기선 lambda에서 dynamoDB에 접근하고, 활용하는 방식에 대해서 알아보도록하자.

 

DynamoDB 간단히 사용해보기

설명이 매우 잘 되어 있다! docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/Introduction.html Amazon DynamoDB이란 무엇입니까? - Amazon DynamoDB 이 페이지에 작업이 필요하다는 점을 알려..

darrengwon.tistory.com

 

step 1. 어떤 버전의 sdk를 사용할 것인가?

 

labmda가 aws 내 다른 서비스에 접근하기 위해서는 aws-sdk를 사용하면 된다. (너무 당연한가?)

그런데 aws-sdk가 2020년 12월부터 v3이 되면서 여러 패키지로 쪼개지게 되었고, 이에 따라 사용법도 v2에 비해 달라지게 되었다.

그래서 v3를 문서를 읽어서 쿼리를 날려봤는데... 안된다? 모듈 주입 에러가 났다.

 

lambda면 aws 내부 서비스일텐데 sdk도 기본 탑재가 안 되어 있지는 않을터, 뭐가 문제인지 찾아봤는데 알고보니,

현재(2021년 4월) 람다에서 최신 aws-sdk를 지원하고 있지 않기 때문이었다!

 

물론 최신 sdk를 사용하는 방법을 안내하고 있지만 이렇게 되면

배포되는 함수의 용량을 늘리게 된다 => 메모리가 많이 소요 된다 => 람다비용이 많이 책정된다는 이유로 사용하고 싶지 않았다.

물론 v3이 코딩하는게 훨씬 좋긴하다. Typescript로 만들어졌고, async/await를 사용하게끔 권장하고 있어서이다.

aws.amazon.com/ko/premiumsupport/knowledge-center/lambda-layer-aws-sdk-latest-version/

 

일단 현재 무슨 버전의 sdk를 사용하고 있는지 먼저 체크해보았다. 현재 버전은 2.804.0를 사용하고 있더라. 

// lambda에서 사용하고 있는 sdk의 버전 체크
const AWS = require('aws-sdk')

exports.handler = async(event) => {
  return AWS.VERSION;
};

 

좋다. 그렇다면 v2의 문서 정보들을 확인해서 사용해보자.

 

- 문서

docs.aws.amazon.com/ko_kr/AWSJavaScriptSDK/latest/

 

- 개발자 가이드

docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/sdk-code-samples.html

 

 

step 2. DocumentClient, DynamoDB, AWS.Request

- dynamoDB

docs.aws.amazon.com/ko_kr/AWSJavaScriptSDK/latest/AWS/DynamoDB.html

 

- DocumentClient

docs.aws.amazon.com/ko_kr/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html

 

거 DocumentClient가 js에서 좀 더 편리하게 사용할 수 있는 방법이라니 DocumentClient를 사용하겠습니다.

 

- AWS.Request는 또 뭐야??

docs.aws.amazon.com/ko_kr/AWSJavaScriptSDK/latest/AWS/Request.html

 

메서드를 사용하려고 보니 반환하는 값들이 우리가 원하는 값을 반환하는게 아니라 AWS.Request란 객체를 반환하고 있다. 

 

이 녀석을 다룬 문서 중 딱 첫 줄만 읽어보았다.

 

All requests made through the SDK are asynchronous and use a callback interface. Each service method that kicks off a request returns an AWS.Request object that you can use to register callbacks.

 

즉, SDK를 통한 모든 요청은 비동기적이며 콜백을 사용한다고 한다. 각 서비스의 메서드는 AWS.Request객체를 반환하는데 여기에 콜백을 등록할 수 있다고 한다.

 

예시를 들자면 다음과 같다. ec2의 정보를 describe하는 메서드는 AWS.Request 객체를 가져오고 해당 객체에 콜백을 달아서 사용하는 꼴이다. 

// request is an AWS.Request object
var request = ec2.describeInstances();

// register callbacks on request to retrieve response data
request.on('success', function(response) {
  console.log(response.data);
});

 

최종적으로, 등록하고자 하는 모든 콜백이 세팅되면 send 메서드를 통해서 보낼 수 있단다.

request.send();

 

따라서, 아래처럼 사용하는 것이 가장 의도된 모습이라고 할 수 있겠다.

request.
  on('success', function(response) {
    console.log("Success!");
  }).
  on('error', function(error, response) {
    console.log("Error!");
  }).
  on('complete', function(response) {
    console.log("Always!");
  }).
  send();

 

그러나, javascript적인 관점으로 보았을 때 이러한 코드 작성보다는 try/catch를 활용하는게 더 직관적이다. 

AWS.Request 객체는 다음과 같은 추가 메서드를 통해 요청이나 조작을 가할 수 있는데 여기서 promise를 활용하겠다.

Promise를 반환하게 만들어서 기존 js에서 처리하듯 작성하는게 가장 직관적일 터이다.

참고로 v3에서는 콜백을 지양하고 처음부터 js 지향적이게 바뀐 편이다. 어서 빨리 lambda의 sdk 버전이 v3로 올라갔으면 좋겠다.

 

 

드디어. dynamoDB를 scan한 결과를 받아볼 수 있게 되었다!

const AWS = require("aws-sdk");
AWS.config.update({ region: "ap-northeast-2" });

const dbclient = new AWS.DynamoDB.DocumentClient({
  apiVersion: "2012-08-10",
});

exports.handler = async (event) => {
  let response;
  const params = {
    TableName: "Cards",
  };

  try {
    const cards = await dbclient.scan(params).promise();

    response = {
      statusCode: 200,
      body: JSON.stringify(cards),
    };
  } catch (error) {
    console.error("Error", error);
    response = {
      statusCode: 500,
      body: JSON.stringify({ Message: error }),
    };
  }

  return response;
};

 

참고로, 아래는 sdk v3의 query 코드이다. 앞서 살펴본 v2에 비해 훨씬 간결해진 것을 살펴볼 수 있다.

// Import required AWS SDK clients and commands for Node.js
const { DynamoDBClient, QueryCommand } = require("@aws-sdk/client-dynamodb");

// Set the AWS Region
const REGION = "region"; //e.g. "us-east-1"

// Set the parameters
const params = {
  TableName: "EPISODES_TABLE",
};

// Create DynamoDB service object
const dbclient = new DynamoDBClient({ region: REGION });
const run = async () => {
  try {
    const results = await dbclient.send(new QueryCommand(params));
    results.Items.forEach(function (element, index, array) {
      console.log(element.Title.S + " (" + element.Subtitle.S + ")");
    });
  } catch (err) {
    console.error(err);
  }
};
run();

 

 

step 3. DynamoDB 접근을 위한 IAM 정책, 역할 생성

IAM 설정의 위계는 권한 < 역할 < 사용자 < 그룹이다.

다른 개발자와 협력해서 사용자 권한을 넘겨줘야 하는 일은 (적어도 이 일에선) 없을터이니 lambda가 사용할 역할까지만 작성하면 된다.

 

위 코드를 기반으로 lambda를 돌려보면 AccessDeniedException가 난다. 권한이 부족하다는 말이다.

기본 람다의 권한 정책만 설정되어 있음을 확인할 수 있고, dynamoDB 관련 권한은 가지고 있지 않음을 확인할 수 있습니다.

Statement를 살펴보자면, cloudwatch의 LogGroup을 만들 수 있는 권한, 그리고 해당 log-group(여기서 이름은 /aws/lambda/getCards라고 하네요)에 log 이벤트를 넣을 수 있고, 로그 스트림을 생성할 수 있는 권한까지 가지고 있음을 확인할 수 있습니다.

{
    "Version": "2012-10-17", // iam version
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-2:xxxxxxxxx:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-2:xxxxxxxxx:log-group:/aws/lambda/getCards:*"
            ]
        }
    ]
}

 

cloudwatch에 접근하는 현 권한에 dynamoDB 접근 및 조작 권한을 허용하도록하겠습니다.

사용할 api인 scan, putitem 등을 허용한 후, 다루고자 하는 table의 ARN을 지정한 후 정책을 생성하였습니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:DeleteItem",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:ap-northeast-2:xxxxxxxxx:table/Card"
        },
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-2:xxxxxxxxx:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-2:xxxxxxxxx:log-group:/aws/lambda/getCards:*"
            ]
        }
    ]
}

 

이렇게 만들어진 정책을 기반으로 역할을 만들거나, 사용자를 만들 수 있습니다.

 

이 정책을 기반으로 lambda가 사용할 역할을 생성해보도록 하면 됩니다.

 

lambda에서 생성한 Role을 등록해주고, 접근 권한이 있는 리소스도 확인할 수 있습니다.

 

 

 

step 4. GW를 통해 접근 가능한 End Point 만들기

lambda의 문서 중 GW와 함께 쓰기 문서를 참고하면 좋습니다.

docs.aws.amazon.com/ko_kr/lambda/latest/dg/services-apigateway.html?icmpid=docs_lambda_console

 

너무 쉬워서 그냥 생략...하려고 했는데 에러를 좀 만나서 해겨해보았습니다.

 

 

주의점 1) GW 테스트 기능 어디갔음?

 

문서에서는 있댔는데 aws가 UI를 업데이트하면서 콘솔 내에 GW를 테스트하는 기능을 제거해버렸다.

cli나 postman을 이용해 테스트하도록하자. 그리고 가급적 body에 정보는 raw로 작성하자.

이상하게 다른 녀석들은 안되더라. (개인적인 실수일 수도 있습니다...)

raw로...

 

aws-cli 쓰실거면 아래 문서를 참고해봅시다.

docs.aws.amazon.com/cli/latest/reference/apigateway/test-invoke-method.html

 

 

주의점 2) GW에서의 HTTP API와 REST API의 차이가 무엇이지?

 

docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/http-api-vs-rest.html

 

HTTP API는 AWS Lambda 및 HTTP 엔드포인트를 기반으로 작동하는 백엔드 + AWS 서비스와의 통합에 적합함

무엇보다  100만건 요청당 1.00$의 저렴한 비용 REST API 보다 최대 71% 저렴하다는 점!

단점으론 RESTful API를 구축할 순 있지만, API 프록시 기능만 제공한다는 것.

 

REST API는 정말로 REST API를 만드는 데 사용하면 적합합니다. 가격은 HTTP API보다 좀 더 비쌉니다. 

 

 

주의점 3) Lambda 통합이 있는 내 API가 {"message":"Internal Server Error"}를 반환한다? 로그 좀 보자

 

흔하게 발생하는 에러인데, 단순히 엔드 포인트에 접근한다면 왜 에러가 났는지를 알 수가 없습니다.

aws에선 에러메세지를 외부인에게 노출하지 않기로 했나봅니다. 로깅을 통해서 이유를 확인해봅시다.

일반 labmda 내에서 발생하는 에러야 자동 생성된 CloudWatch에서 확인해보면 되겠지만 GW와 접합됨으로서 발생하는 IntegrationErrorMessage는 별도로 로깅을 찍어줘야 합니다.

 

docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/http-api-troubleshooting-lambda.html

 

 

로그를 확인해보니 제 경우엔 

"The response from the Lambda function doesn't match the format that API Gateway expects. Lambda body contains the wrong type for field "body"" 에러를 받았습니다. 즉, API Gateway가 기대하는 lambda의 반환 형식을 지켜주지 않았다는 거죠.

 

알아본 결과 넘겨주는 값의 타입이 문제였습니다. body의 값은 obj가 아니라 string 값으로 넘겨주도록 합시다.

response = {
  statusCode: 200,
  body: cards, // 이렇게 하니 에러가 됨
  body: JSON.stringify(cards), // stringify로 처리하여 string으로 반환할 것
};

 

 

주의점 4)  GW를 통과하면서 lambda의 event 객체에 달려오는 것들을 한 번 확인해볼까요?

 

  • 유용하게도 stage, time, timeEpoch, 각종 header들이 들어 있습니다. 나중에 접근 통계를 낼 때에 좋을지도?
  • requestContext 내부의 requestId는 고유한 식별자로 사용하기에 유용합니다.
  • URL에 전달된 ? 이후의 쿼리들은 자동으로 파싱되어 queryStringParameters에 담겨 있습니다. parse할 필요 없습니다.
  • path에 넣은 값은 pathParameters에 담겨 있습니다. /users/{id} 꼴로 동적인 path를 넘겨서 사용하실 때 이용하세요. parse할 필요 없습니다. 출력해보면 앎 ㅎ
  • labmda 테스트와는 달리, GW를 거쳐 온 form body는 event의 "body"라는 인자에 담겨 있습니다. 뽑아 쓰세요.
  • postman으로 테스트하신다면 body에 넣는 정보는 그냥 raw로 작성하세요. 그게 마음이 편합니다... 

 

{
  version: '2.0',
  routeKey: 'GET /getCards',
  rawPath: '/production/getCards',
  rawQueryString: '',
  headers: {
    accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
    'content-length': '0',
    host: 'cbk9cgyrvg.execute-api.ap-northeast-2.amazonaws.com',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'cross-site',
    'sec-fetch-user': '?1',
    'sec-gpc': '1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36',
    'x-amzn-trace-id': 'Root=1-6071caa4-74b14f3b63297a7167ef37d6',
    'x-forwarded-for': '125.141.4.208',
    'x-forwarded-port': '443',
    'x-forwarded-proto': 'https'
  },
  requestContext: {
    accountId: '137503059838',
    apiId: 'cbk9cgyrvg',
    domainName: 'cbk9cgyrvg.execute-api.ap-northeast-2.amazonaws.com',
    domainPrefix: 'cbk9cgyrvg',
    http: {
      method: 'GET',
      path: '/production/getCards',
      protocol: 'HTTP/1.1',
      sourceIp: '125.141.4.208',
      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
    },
    requestId: 'dkyZti8vIE0EM_A=',
    routeKey: 'GET /getCards',
    stage: 'production',
    time: '10/Apr/2021:15:56:20 +0000',
    timeEpoch: 1618070180438
  },
  isBase64Encoded: false
}

 

 

주의점 5) 배포 시 Burst와 Rate를 조절할 것

 

쉽게 말해 요청 횟수 제한을 걸라는 겁니다. 과도하게 api 요청을 보내지 않도록.

애초에 프론트에서 중복 요청을 보낼 수 없도록 쓰로틀링을 해주는 게 좋겠죠?

 

Rate는 초당 요청 가능 수

Burst는 동시 요청 가능 수

 

당연히 Rate가 Burst보다 높게 설정해야 한다. 논리적으로 생각해보면 당연하다

1초에 1000개 처리 가능한데 burst가 2000개로 설정해놓는다는 건 논리적으로 이상하다는 것을 알 수 있을 것이다.

 

Rate/Burst는 각 api 경로별로도 설정할 수 있고, 계정별로도 설정할 수 있다.

 

 

 

step 5. GW CORS 구성

가급적 요청을 보내는 녀석도 fetching handler를 만들어놓고,

lambda에서 반환하는 형식도 일관되게 작성하는 것이 좋다. 왜인지는 아래 글로 갈음한다.

그리고 HOST도 애초에 GW를 달면서 매우매우 길어져서, 이렇게 관리를 할수밖에 없다.

 

darrengwon.tistory.com/830

 

중앙 집중식 API 에러 핸들링 (탑레벨 fetching handler를 만들어라)

어떤 식으로 서버와의 통신을 하는지, 어떤 data가 들어오는 지에 따라 적절한 탑레벨 http api handler를 만들어주는 것이 좋다. 일관된 처리를 위해서 필요하다. 1. 응답 객체는 일관성이 있어야 관

darrengwon.tistory.com

 

GW CORS 설정

 

여튼, 연결된 GW에 요청을 보내보면 CORS에러가 발생함을 알 수 있다. 예전에 이 에러 때문에 고생한 기억이 난다...

네트워크 요청을 받았을 때 Origin이 다를 경우, 실제 요청에 앞서 브라우저에서 자동으로 CORS Preflight Request를 전송하여 응답으로 대상 서버가 CORS를 허용하는지 확인하는데 그게 막혔다는 것이다. OPTION 메서드로 날아간 preflight가 사전 요청에 해당합니다.

 

MDN 한 번 읽어주세요~

developer.mozilla.org/ko/docs/Glossary/Preflight_request

 

결론적으론, preflight 응답의 헤더에 Access-Control-Allow-Origin (cors를 허용할 origin)에 해당 사항이 없어서 못받겠다는 겁니다. 

이런 경우엔 express 같은 경우에는 cors로 설정해주고, nginx에선 jsonobject.tistory.com/245 를 참고하셔서 nginx.conf를 구성하시면 됩니다. 그런데 GW에선?

 

매우 간편하게 설정해줄 수 있습니다. 설명서도 짧으니까 읽어보면서 설정해봅시다.

docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html

 

잘 모르겠다면 * (와일드카드)를 썼다가, 나중에 도메인 올리고 그 부분만 origin을 구성해주면 됩니다.

header랑 methods도 무.조.건. 설정해줍시다.

 

 

 

 

 

 

 

 

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체